Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57367b75c8 | |||
| f8e6daa084 |
1
.gitignore
vendored
@@ -6,6 +6,7 @@ __pycache__/
|
||||
# Local environment files
|
||||
.env
|
||||
backend/.env
|
||||
perl_mail.txt
|
||||
|
||||
# Local SQLite databases
|
||||
data/*.db
|
||||
|
||||
BIN
assets/news_consulenza.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/news_cucciolo.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/news_gatto.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/ortovet.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/services_dermatologia.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/services_ecografia.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
assets/services_endoscopia.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/services_endoscopia.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/services_laboratorio.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/services_laparoscopia.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
assets/services_oculistica.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
assets/services_radiologia.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/services_visite_cliniche.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
@@ -3,3 +3,11 @@ APP_ENV=development
|
||||
APP_HOST=127.0.0.1
|
||||
APP_PORT=8000
|
||||
DATABASE_URL=sqlite:///C:/devel/clinica_veterinaria_formiginese/data/clinica.db
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_USE_TLS=true
|
||||
BOOKING_EMAIL_FROM=
|
||||
BOOKING_EMAIL_TO=
|
||||
BOOKING_EMAIL_SUBJECT_PREFIX=[Clinica Veterinaria Formiginese]
|
||||
|
||||
27
backend/app/api/booking.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from app.schemas.booking import BookingRequestCreate, BookingRequestResponse
|
||||
from app.services.mailer import MailConfigurationError, send_booking_request_email
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["booking"])
|
||||
|
||||
|
||||
@router.post("/booking-request", response_model=BookingRequestResponse)
|
||||
def create_booking_request(payload: BookingRequestCreate) -> BookingRequestResponse:
|
||||
try:
|
||||
send_booking_request_email(payload)
|
||||
except MailConfigurationError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Il servizio email non è ancora configurato.",
|
||||
) from exc
|
||||
except Exception as exc: # pragma: no cover - mail delivery path
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Si è verificato un problema durante l'invio della richiesta.",
|
||||
) from exc
|
||||
|
||||
return BookingRequestResponse(
|
||||
message="La richiesta è stata inviata correttamente e sarà presa in carico al più presto dal team.",
|
||||
)
|
||||
@@ -6,9 +6,11 @@ from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
PROJECT_ROOT = BASE_DIR.parent
|
||||
ENV_FILE = PROJECT_ROOT / ".env"
|
||||
ENV_FILE = BASE_DIR / ".env"
|
||||
ROOT_ENV_FILE = PROJECT_ROOT / ".env"
|
||||
|
||||
load_dotenv(ENV_FILE)
|
||||
load_dotenv(ROOT_ENV_FILE)
|
||||
load_dotenv(ENV_FILE, override=True)
|
||||
|
||||
|
||||
class Settings:
|
||||
@@ -19,6 +21,17 @@ class Settings:
|
||||
self.app_port = int(os.getenv("APP_PORT", "8000"))
|
||||
default_sqlite_path = (PROJECT_ROOT / "data" / "clinica.db").resolve()
|
||||
self.database_url = os.getenv("DATABASE_URL", f"sqlite:///{default_sqlite_path.as_posix()}")
|
||||
self.smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
|
||||
self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
|
||||
self.smtp_username = os.getenv("SMTP_USERNAME", "")
|
||||
self.smtp_password = os.getenv("SMTP_PASSWORD", "")
|
||||
self.smtp_use_tls = os.getenv("SMTP_USE_TLS", "true").lower() in {"1", "true", "yes", "on"}
|
||||
self.booking_email_from = os.getenv("BOOKING_EMAIL_FROM", self.smtp_username)
|
||||
self.booking_email_to = os.getenv("BOOKING_EMAIL_TO", "")
|
||||
self.booking_email_subject_prefix = os.getenv(
|
||||
"BOOKING_EMAIL_SUBJECT_PREFIX",
|
||||
"[Clinica Veterinaria Formiginese]",
|
||||
)
|
||||
|
||||
@property
|
||||
def sqlite_file_path(self) -> str | None:
|
||||
@@ -27,5 +40,18 @@ class Settings:
|
||||
return self.database_url.removeprefix(sqlite_prefix)
|
||||
return None
|
||||
|
||||
@property
|
||||
def booking_mail_enabled(self) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.smtp_host,
|
||||
self.smtp_port,
|
||||
self.smtp_username,
|
||||
self.smtp_password,
|
||||
self.booking_email_from,
|
||||
self.booking_email_to,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.api.booking import router as booking_router
|
||||
from app.api.health import router as health_router
|
||||
from app.config import settings
|
||||
from app.db.init_db import init_db
|
||||
@@ -9,3 +10,4 @@ init_db()
|
||||
|
||||
app = FastAPI(title=settings.app_name)
|
||||
app.include_router(health_router)
|
||||
app.include_router(booking_router)
|
||||
|
||||
17
backend/app/schemas/booking.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BookingRequestCreate(BaseModel):
|
||||
name: str = Field(min_length=2, max_length=120)
|
||||
phone: str = Field(min_length=5, max_length=40)
|
||||
pet_name: str = Field(default="", max_length=120)
|
||||
pet_type: str = Field(default="cane", max_length=40)
|
||||
service: str = Field(min_length=2, max_length=120)
|
||||
date: str = Field(min_length=8, max_length=20)
|
||||
time: str = Field(default="", max_length=20)
|
||||
notes: str = Field(default="", max_length=2000)
|
||||
|
||||
|
||||
class BookingRequestResponse(BaseModel):
|
||||
success: bool = True
|
||||
message: str
|
||||
78
backend/app/services/mailer.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from email.message import EmailMessage
|
||||
import smtplib
|
||||
|
||||
from app.config import settings
|
||||
from app.schemas.booking import BookingRequestCreate
|
||||
|
||||
|
||||
class MailConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _build_booking_subject(payload: BookingRequestCreate) -> str:
|
||||
return f"{settings.booking_email_subject_prefix} Nuova richiesta visita - {payload.name}"
|
||||
|
||||
|
||||
def _build_booking_html(payload: BookingRequestCreate) -> str:
|
||||
submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M")
|
||||
return f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.6;">
|
||||
<h2 style="color: #1B4F72;">Nuova richiesta di prenotazione visita</h2>
|
||||
<p>È stata inviata una nuova richiesta di prenotazione non vincolante dal sito web.</p>
|
||||
<table cellpadding="8" cellspacing="0" border="0" style="border-collapse: collapse;">
|
||||
<tr><td><strong>Nome e cognome</strong></td><td>{payload.name}</td></tr>
|
||||
<tr><td><strong>Telefono</strong></td><td>{payload.phone}</td></tr>
|
||||
<tr><td><strong>Nome animale</strong></td><td>{payload.pet_name or "-"}</td></tr>
|
||||
<tr><td><strong>Tipo animale</strong></td><td>{payload.pet_type}</td></tr>
|
||||
<tr><td><strong>Tipo di visita</strong></td><td>{payload.service}</td></tr>
|
||||
<tr><td><strong>Data preferita</strong></td><td>{payload.date}</td></tr>
|
||||
<tr><td><strong>Orario preferito</strong></td><td>{payload.time or "Qualsiasi orario"}</td></tr>
|
||||
<tr><td><strong>Note</strong></td><td>{payload.notes or "-"}</td></tr>
|
||||
<tr><td><strong>Inviata il</strong></td><td>{submitted_at}</td></tr>
|
||||
</table>
|
||||
<p style="margin-top: 20px;">
|
||||
Questa richiesta <strong>non costituisce conferma automatica dell'appuntamento</strong> e richiede
|
||||
presa in carico da parte dello staff.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def _build_booking_text(payload: BookingRequestCreate) -> str:
|
||||
submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M")
|
||||
return (
|
||||
"Nuova richiesta di prenotazione visita\n\n"
|
||||
f"Nome e cognome: {payload.name}\n"
|
||||
f"Telefono: {payload.phone}\n"
|
||||
f"Nome animale: {payload.pet_name or '-'}\n"
|
||||
f"Tipo animale: {payload.pet_type}\n"
|
||||
f"Tipo di visita: {payload.service}\n"
|
||||
f"Data preferita: {payload.date}\n"
|
||||
f"Orario preferito: {payload.time or 'Qualsiasi orario'}\n"
|
||||
f"Note: {payload.notes or '-'}\n"
|
||||
f"Inviata il: {submitted_at}\n\n"
|
||||
"La richiesta non costituisce conferma automatica dell'appuntamento."
|
||||
)
|
||||
|
||||
|
||||
def send_booking_request_email(payload: BookingRequestCreate) -> None:
|
||||
if not settings.booking_mail_enabled:
|
||||
raise MailConfigurationError("Configurazione SMTP incompleta.")
|
||||
|
||||
message = EmailMessage()
|
||||
message["Subject"] = _build_booking_subject(payload)
|
||||
message["From"] = settings.booking_email_from
|
||||
message["To"] = settings.booking_email_to
|
||||
message.set_content(_build_booking_text(payload))
|
||||
message.add_alternative(_build_booking_html(payload), subtype="html")
|
||||
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=30) as server:
|
||||
if settings.smtp_use_tls:
|
||||
server.starttls()
|
||||
server.login(settings.smtp_username, settings.smtp_password)
|
||||
server.send_message(message)
|
||||
@@ -4,3 +4,4 @@ sqlalchemy
|
||||
pydantic
|
||||
python-dotenv
|
||||
aiosqlite
|
||||
pydantic[email]
|
||||
|
||||
@@ -12,9 +12,5 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script
|
||||
defer
|
||||
src="%VITE_ANALYTICS_ENDPOINT%/umami"
|
||||
data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
clinica-app/client/public/images/news_consulenza.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
clinica-app/client/public/images/news_cucciolo.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
clinica-app/client/public/images/news_gatto.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
clinica-app/client/public/images/ortovet.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
clinica-app/client/public/images/services_dermatologia.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
clinica-app/client/public/images/services_ecografia.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
clinica-app/client/public/images/services_endoscopia.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
clinica-app/client/public/images/services_endoscopia.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
clinica-app/client/public/images/services_laboratorio.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
clinica-app/client/public/images/services_laparoscopia.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
clinica-app/client/public/images/services_oculistica.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
clinica-app/client/public/images/services_radiologia.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
clinica-app/client/public/images/services_visite_cliniche.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
@@ -1,6 +1,9 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import CookiePolicyPage from "@/pages/CookiePolicyPage";
|
||||
import LegalNotesPage from "@/pages/LegalNotesPage";
|
||||
import NotFound from "@/pages/NotFound";
|
||||
import PrivacyPolicyPage from "@/pages/PrivacyPolicyPage";
|
||||
import { Route, Switch } from "wouter";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
@@ -10,6 +13,9 @@ function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={"/"} component={Home} />
|
||||
<Route path={"/privacy-policy"} component={PrivacyPolicyPage} />
|
||||
<Route path={"/cookie-policy"} component={CookiePolicyPage} />
|
||||
<Route path={"/note-legali"} component={LegalNotesPage} />
|
||||
<Route path={"/404"} component={NotFound} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function AboutSection() {
|
||||
{/* Immagine principale */}
|
||||
<div className="relative rounded-2xl overflow-hidden shadow-2xl">
|
||||
<img
|
||||
src="https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/hero_dog_cat_30af7cf4.jpg"
|
||||
src="/images/hero_dog_cat.jpg"
|
||||
alt="Cane e gatto insieme — la nostra missione"
|
||||
className="w-full h-[480px] object-cover"
|
||||
/>
|
||||
|
||||
@@ -1,65 +1,76 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione registrazione/login: tabs con form eleganti
|
||||
* Sfondo bianco, accenti blu petrolio e verde acqua
|
||||
* Sezione registrazione/login visibile ma non ancora attiva.
|
||||
* Gli elementi restano leggibili, ma ogni interazione mostra
|
||||
* il messaggio "Servizio in corso di attivazione".
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useInView } from "framer-motion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { User, Mail, Lock, Eye, EyeOff, CheckCircle2, PawPrint } from "lucide-react";
|
||||
import { User, Mail, Lock, Eye, CheckCircle2, PawPrint, Clock3 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function AuthSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
const [tab, setTab] = useState<"login" | "register">("register");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [registered, setRegistered] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (tab === "register") {
|
||||
setRegistered(true);
|
||||
toast.success("Registrazione completata!", {
|
||||
description: "Benvenuto nella Clinica Veterinaria Formiginese.",
|
||||
});
|
||||
} else {
|
||||
toast.success("Accesso effettuato!", {
|
||||
description: "Bentornato nella tua area personale.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const benefits = [
|
||||
const benefits = [
|
||||
"Storico visite e referti digitali",
|
||||
"Promemoria vaccinazioni automatici",
|
||||
"Prenotazioni online prioritarie",
|
||||
"Comunicazioni dirette con il veterinario",
|
||||
"Gestione di più animali domestici",
|
||||
];
|
||||
"Gestione di piu animali domestici",
|
||||
];
|
||||
|
||||
function showActivationToast() {
|
||||
toast.info("Servizio in corso di attivazione", {
|
||||
description: "L'area riservata sara disponibile a breve.",
|
||||
});
|
||||
}
|
||||
|
||||
function DisabledInput({
|
||||
icon,
|
||||
type,
|
||||
placeholder,
|
||||
}: {
|
||||
icon: React.ComponentType<{ size?: number; className?: string }>;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const Icon = icon;
|
||||
|
||||
return (
|
||||
<section id="registrazione" className="py-20 md:py-28 bg-white">
|
||||
<div className="relative">
|
||||
<Icon size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
disabled
|
||||
className="w-full cursor-not-allowed rounded-lg border border-gray-200 bg-gray-50/90 py-2.5 pl-9 pr-3 text-sm text-gray-400 opacity-90"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuthSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
|
||||
return (
|
||||
<section id="registrazione" className="bg-white py-20 md:py-28">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
{/* Colonna sinistra: benefici */}
|
||||
<div className="grid grid-cols-1 items-center gap-16 lg:grid-cols-2">
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-0.5 w-12 bg-[#4ECDC4]" />
|
||||
<span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
|
||||
Area Personale
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
className="text-[#1B4F72] mb-6"
|
||||
className="mb-6 text-[#1B4F72]"
|
||||
style={{
|
||||
fontFamily: "'Cormorant Garamond', serif",
|
||||
fontSize: "clamp(2rem, 4vw, 3rem)",
|
||||
@@ -67,210 +78,148 @@ export default function AuthSection() {
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Registrati e gestisci{" "}
|
||||
Accedi e gestisci{" "}
|
||||
<span className="italic">la salute del tuo animale</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600 leading-relaxed mb-8 text-base">
|
||||
Crea il tuo profilo personale per accedere a tutti i servizi digitali della clinica.
|
||||
Tieni traccia della storia clinica del tuo animale, ricevi promemoria e prenota
|
||||
le visite in pochi click.
|
||||
<p className="mb-8 text-base leading-relaxed text-gray-600">
|
||||
L'area riservata e la registrazione online sono in fase di attivazione.
|
||||
Qui i clienti potranno gestire i propri dati, consultare lo storico e accedere
|
||||
ai servizi digitali della clinica in uno spazio personale dedicato.
|
||||
</p>
|
||||
|
||||
{/* Benefits list */}
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="mb-8 space-y-3">
|
||||
{benefits.map((benefit) => (
|
||||
<div key={benefit} className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-[#4ECDC4]/15 flex items-center justify-center flex-shrink-0">
|
||||
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-[#4ECDC4]/15">
|
||||
<CheckCircle2 size={14} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<span className="text-gray-700 text-sm">{benefit}</span>
|
||||
<span className="text-sm text-gray-700">{benefit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Decorazione */}
|
||||
<div className="bg-[#F5F0E8] rounded-2xl p-6 flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-[#1B4F72] flex items-center justify-center flex-shrink-0">
|
||||
<div className="flex items-center gap-4 rounded-2xl bg-[#F5F0E8] p-6">
|
||||
<div className="flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full bg-[#1B4F72]">
|
||||
<PawPrint size={24} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[#1B4F72] font-semibold text-sm">Già più di 500 famiglie</p>
|
||||
<p className="text-gray-600 text-xs mt-0.5">
|
||||
si affidano alla nostra clinica per la cura dei loro animali
|
||||
<p className="text-sm font-semibold text-[#1B4F72]">Servizio in attivazione</p>
|
||||
<p className="mt-0.5 text-xs text-gray-600">
|
||||
La clinica sta completando la configurazione dell'area riservata per offrirti
|
||||
un accesso semplice e sicuro.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Colonna destra: form */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
{registered ? (
|
||||
<div className="bg-[#F5F0E8] rounded-2xl p-8 text-center shadow-sm">
|
||||
<div className="w-16 h-16 rounded-full bg-[#4ECDC4]/15 flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle2 size={32} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-2"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.8rem" }}
|
||||
>
|
||||
Benvenuto!
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-6">
|
||||
La tua registrazione è avvenuta con successo. Ora puoi accedere a tutti i servizi
|
||||
della tua area personale.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-[#1B4F72] hover:bg-[#163d5a] text-white"
|
||||
onClick={() => setRegistered(false)}
|
||||
>
|
||||
Vai all'area personale
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
|
||||
{/* Tabs */}
|
||||
<div className="relative overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-lg">
|
||||
<div className="flex border-b border-gray-100">
|
||||
{(["register", "login"] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
className={`flex-1 py-4 text-sm font-semibold transition-all duration-200 ${
|
||||
tab === t
|
||||
? "text-[#1B4F72] border-b-2 border-[#4ECDC4] bg-[#F5F0E8]/50"
|
||||
: "text-gray-400 hover:text-gray-600"
|
||||
}`}
|
||||
type="button"
|
||||
disabled
|
||||
className="flex-1 cursor-not-allowed border-b-2 border-[#4ECDC4] bg-[#F5F0E8]/50 py-4 text-sm font-semibold text-[#1B4F72]"
|
||||
>
|
||||
{t === "register" ? "Registrati" : "Accedi"}
|
||||
Registrati
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="flex-1 cursor-not-allowed py-4 text-sm font-semibold text-gray-400"
|
||||
>
|
||||
Accedi
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-4">
|
||||
{tab === "register" && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="mb-5 flex items-center gap-3 rounded-xl border border-[#E4D7C6] bg-[#FFF9F1] px-4 py-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-[#A95F3A]/12">
|
||||
<Clock3 size={18} className="text-[#A95F3A]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<p className="text-sm font-semibold text-[#1B4F72]">Servizio in corso di attivazione</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
Registrazione e accesso saranno disponibili a breve.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 opacity-75">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Nome
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Mario"
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<DisabledInput icon={User} type="text" placeholder="Mario" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Cognome
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Rossi"
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
/>
|
||||
<DisabledInput icon={User} type="text" placeholder="Rossi" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="mario.rossi@email.it"
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<DisabledInput icon={Mail} type="email" placeholder="mario.rossi@email.it" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-9 pr-10 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
disabled
|
||||
className="w-full cursor-not-allowed rounded-lg border border-gray-200 bg-gray-50/90 py-2.5 pl-9 pr-10 text-sm text-gray-400 opacity-90"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? <EyeOff size={15} /> : <Eye size={15} />}
|
||||
</button>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<Eye size={15} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tab === "register" && (
|
||||
<div className="flex items-start gap-2 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
required
|
||||
id="privacy"
|
||||
className="mt-0.5 accent-[#4ECDC4]"
|
||||
disabled
|
||||
className="mt-0.5 cursor-not-allowed accent-[#4ECDC4]"
|
||||
/>
|
||||
<label htmlFor="privacy" className="text-xs text-gray-500 leading-relaxed">
|
||||
Accetto la{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#4ECDC4] hover:underline"
|
||||
onClick={() => toast.info("Privacy policy in arrivo")}
|
||||
>
|
||||
Privacy Policy
|
||||
</button>{" "}
|
||||
e i{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#4ECDC4] hover:underline"
|
||||
onClick={() => toast.info("Termini di servizio in arrivo")}
|
||||
>
|
||||
Termini di Servizio
|
||||
</button>
|
||||
</label>
|
||||
<p className="text-xs leading-relaxed text-gray-500">
|
||||
Accetto la Privacy Policy e i Termini di Servizio.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-[#1B4F72] hover:bg-[#163d5a] text-white font-bold py-3 text-base transition-all duration-300"
|
||||
type="button"
|
||||
disabled
|
||||
className="w-full cursor-not-allowed bg-[#1B4F72] py-3 text-base font-bold text-white opacity-70"
|
||||
>
|
||||
{tab === "register" ? "Crea il tuo account" : "Accedi all'area personale"}
|
||||
Crea il tuo account
|
||||
</Button>
|
||||
|
||||
{tab === "login" && (
|
||||
<p className="text-center text-xs text-gray-400">
|
||||
<p className="text-center text-xs text-gray-400">Password dimenticata?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#4ECDC4] hover:underline"
|
||||
onClick={() => toast.info("Recupero password in arrivo")}
|
||||
>
|
||||
Password dimenticata?
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
aria-label="Servizio in corso di attivazione"
|
||||
onClick={showActivationToast}
|
||||
className="absolute inset-0 z-10"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,56 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione prenotazione: form elegante su sfondo blu petrolio
|
||||
* Layout: testo a sinistra + form a destra
|
||||
* Sezione prenotazione: form to mail con conferma non vincolante.
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useInView } from "framer-motion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar, Clock, User, Phone, PawPrint, CheckCircle2 } from "lucide-react";
|
||||
import { Calendar, Clock, User, Phone, PawPrint, CheckCircle2, ShieldCheck } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const services = [
|
||||
"Visita generale",
|
||||
"Radiologia / Ecografia",
|
||||
"Chirurgia (consulenza)",
|
||||
"Laboratorio analisi",
|
||||
"Visita clinica generale",
|
||||
"Ecografia",
|
||||
"Radiologia",
|
||||
"Laboratorio",
|
||||
"Vaccinazione",
|
||||
"Dermatologia",
|
||||
"Odontoiatria",
|
||||
"Oncologia",
|
||||
"Dermatologia",
|
||||
"Oculistica",
|
||||
"Nutrizione",
|
||||
"Ortopedia",
|
||||
"Endoscopia",
|
||||
"Laparoscopia",
|
||||
];
|
||||
|
||||
const timeSlots = [
|
||||
"09:00", "09:30", "10:00", "10:30", "11:00", "11:30",
|
||||
"14:30", "15:00", "15:30", "16:00", "16:30", "17:00",
|
||||
"09:00",
|
||||
"09:30",
|
||||
"10:00",
|
||||
"10:30",
|
||||
"11:00",
|
||||
"11:30",
|
||||
"14:30",
|
||||
"15:00",
|
||||
"15:30",
|
||||
"16:00",
|
||||
"16:30",
|
||||
"17:00",
|
||||
];
|
||||
|
||||
export default function BookingSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
type BookingFormState = {
|
||||
name: string;
|
||||
phone: string;
|
||||
petName: string;
|
||||
petType: string;
|
||||
service: string;
|
||||
date: string;
|
||||
time: string;
|
||||
notes: string;
|
||||
};
|
||||
|
||||
const initialForm: BookingFormState = {
|
||||
name: "",
|
||||
phone: "",
|
||||
petName: "",
|
||||
@@ -40,29 +59,74 @@ export default function BookingSection() {
|
||||
date: "",
|
||||
time: "",
|
||||
notes: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
export default function BookingSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submittedName, setSubmittedName] = useState("");
|
||||
const [form, setForm] = useState<BookingFormState>(initialForm);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!form.name || !form.phone || !form.service || !form.date) {
|
||||
toast.error("Compila tutti i campi obbligatori");
|
||||
return;
|
||||
}
|
||||
setSubmitted(true);
|
||||
toast.success("Richiesta inviata!", {
|
||||
description: "Ti contatteremo entro 24 ore per confermare l'appuntamento.",
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
|
||||
const response = await fetch("/api/booking-request", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: form.name,
|
||||
phone: form.phone,
|
||||
pet_name: form.petName,
|
||||
pet_type: form.petType,
|
||||
service: form.service,
|
||||
date: form.date,
|
||||
time: form.time,
|
||||
notes: form.notes,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorPayload = await response.json().catch(() => null);
|
||||
throw new Error(errorPayload?.detail || "Invio non riuscito");
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
setSubmittedName(form.name);
|
||||
setSubmitted(true);
|
||||
setForm(initialForm);
|
||||
toast.success("Richiesta inviata", {
|
||||
description: payload.message,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Si è verificato un problema durante l'invio.";
|
||||
toast.error("Invio non riuscito", {
|
||||
description: message,
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="prenotazione" className="py-20 md:py-28 bg-[#1B4F72] relative overflow-hidden">
|
||||
{/* Decorazioni di sfondo */}
|
||||
<div className="absolute top-0 right-0 w-96 h-96 rounded-full bg-white/5 -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-64 h-64 rounded-full bg-[#4ECDC4]/10 translate-y-1/2 -translate-x-1/2" />
|
||||
<section id="prenotazione" className="relative overflow-hidden bg-[#1B4F72] py-20 md:py-28">
|
||||
<div className="absolute right-0 top-0 h-96 w-96 translate-x-1/2 -translate-y-1/2 rounded-full bg-white/5" />
|
||||
<div className="absolute bottom-0 left-0 h-64 w-64 -translate-x-1/2 translate-y-1/2 rounded-full bg-[#4ECDC4]/10" />
|
||||
|
||||
<div className="container relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
|
||||
{/* Colonna sinistra: testo */}
|
||||
<div className="grid grid-cols-1 items-start gap-16 lg:grid-cols-2">
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
@@ -70,15 +134,15 @@ export default function BookingSection() {
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-white"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-0.5 w-12 bg-[#4ECDC4]" />
|
||||
<span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
|
||||
Prenotazioni
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
className="text-white mb-6"
|
||||
className="mb-6 text-white"
|
||||
style={{
|
||||
fontFamily: "'Cormorant Garamond', serif",
|
||||
fontSize: "clamp(2rem, 4vw, 3rem)",
|
||||
@@ -86,93 +150,118 @@ export default function BookingSection() {
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Prenota la tua visita{" "}
|
||||
Richiedi una visita{" "}
|
||||
<span className="italic text-[#4ECDC4]">online</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-white/80 leading-relaxed mb-8 text-base">
|
||||
Compila il modulo per richiedere un appuntamento. Ti contatteremo entro 24 ore
|
||||
per confermare la data e l'orario. Per urgenze, chiama direttamente il numero
|
||||
dedicato disponibile 24 ore su 24.
|
||||
<p className="mb-8 text-base leading-relaxed text-white/80">
|
||||
Compila il modulo per inviare una richiesta di appuntamento. La richiesta
|
||||
non è vincolante e dovrà essere confermata dallo staff della clinica, che ti
|
||||
ricontatterà il prima possibile.
|
||||
</p>
|
||||
|
||||
{/* Info box urgenze */}
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-5 border border-white/20 mb-8">
|
||||
<p className="text-[#4ECDC4] font-semibold text-sm mb-2 uppercase tracking-wide">
|
||||
<div className="mb-8 rounded-xl border border-white/20 bg-white/10 p-5 backdrop-blur-sm">
|
||||
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-[#4ECDC4]">
|
||||
Urgenze 24h
|
||||
</p>
|
||||
<a
|
||||
href="tel:3205322439"
|
||||
className="text-white text-2xl font-bold hover:text-[#4ECDC4] transition-colors"
|
||||
className="text-2xl font-bold text-white transition-colors hover:text-[#4ECDC4]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif" }}
|
||||
>
|
||||
320 532.24.39
|
||||
</a>
|
||||
<p className="text-white/60 text-xs mt-1">Disponibile 7 giorni su 7</p>
|
||||
<p className="mt-1 text-xs text-white/60">Disponibile 7 giorni su 7</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 rounded-2xl border border-[#4ECDC4]/30 bg-[#14384F] p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-[#4ECDC4]/15">
|
||||
<ShieldCheck size={18} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Richiesta non vincolante</p>
|
||||
<p className="mt-1 text-sm leading-relaxed text-white/75">
|
||||
L'invio del modulo non costituisce conferma automatica dell'appuntamento.
|
||||
Il team verificherà disponibilità, tipologia di visita e urgenza del caso prima
|
||||
di confermare data e orario.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orari */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/70 text-sm font-semibold uppercase tracking-wide mb-3">
|
||||
<p className="mb-3 text-sm font-semibold uppercase tracking-wide text-white/70">
|
||||
Orari di apertura
|
||||
</p>
|
||||
{[
|
||||
{ days: "Lunedì — Venerdì", hours: "09:00 — 12:30 · 14:30 — 19:00" },
|
||||
{ days: "Sabato", hours: "09:00 — 12:30" },
|
||||
{ days: "Lunedi - Venerdi", hours: "09:00 - 12:30 · 14:30 - 19:00" },
|
||||
{ days: "Sabato", hours: "09:00 - 12:30" },
|
||||
{ days: "Domenica", hours: "Solo urgenze" },
|
||||
].map((slot) => (
|
||||
<div key={slot.days} className="flex justify-between items-center text-sm">
|
||||
<div key={slot.days} className="flex items-center justify-between text-sm">
|
||||
<span className="text-white/70">{slot.days}</span>
|
||||
<span className="text-white font-medium">{slot.hours}</span>
|
||||
<span className="font-medium text-white">{slot.hours}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Colonna destra: form */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
{submitted ? (
|
||||
<div className="bg-white rounded-2xl p-8 text-center shadow-2xl">
|
||||
<div className="w-16 h-16 rounded-full bg-[#4ECDC4]/15 flex items-center justify-center mx-auto mb-4">
|
||||
<div className="rounded-2xl bg-white p-8 text-center shadow-2xl">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-[#4ECDC4]/15">
|
||||
<CheckCircle2 size={32} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-2"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.8rem" }}
|
||||
className="mb-2 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.9rem" }}
|
||||
>
|
||||
Richiesta inviata!
|
||||
Grazie {submittedName || "per la tua richiesta"}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-6">
|
||||
Abbiamo ricevuto la tua richiesta di appuntamento. Ti contatteremo entro 24 ore
|
||||
per confermare data e orario.
|
||||
<p className="mb-3 text-sm leading-relaxed text-gray-600">
|
||||
La tua richiesta di prenotazione è stata inviata correttamente.
|
||||
</p>
|
||||
<p className="mb-6 text-sm leading-relaxed text-gray-600">
|
||||
Il team della Clinica Veterinaria Formiginese la prenderà in carico il prima
|
||||
possibile e ti ricontatterà per confermare disponibilità, data e orario.
|
||||
</p>
|
||||
<div className="mb-6 rounded-xl border border-[#E4D7C6] bg-[#FFF9F1] px-4 py-3 text-left text-sm text-gray-600">
|
||||
<strong className="text-[#1B4F72]">Nota importante:</strong> la richiesta inviata
|
||||
non equivale a una prenotazione già confermata.
|
||||
</div>
|
||||
<Button
|
||||
className="bg-[#1B4F72] hover:bg-[#163d5a] text-white"
|
||||
className="bg-[#1B4F72] text-white hover:bg-[#163d5a]"
|
||||
onClick={() => setSubmitted(false)}
|
||||
>
|
||||
Nuova prenotazione
|
||||
Invia una nuova richiesta
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-white rounded-2xl p-6 md:p-8 shadow-2xl space-y-4"
|
||||
className="space-y-4 rounded-2xl bg-white p-6 shadow-2xl md:p-8"
|
||||
>
|
||||
<div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-2"
|
||||
className="mb-2 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.5rem", fontWeight: 600 }}
|
||||
>
|
||||
Richiedi un appuntamento
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
Compila i campi richiesti e inviaci una proposta di data: sarà lo staff a
|
||||
confermare la visita.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Nome e telefono */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Nome e Cognome *
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -183,12 +272,12 @@ export default function BookingSection() {
|
||||
placeholder="Mario Rossi"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Telefono *
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -199,17 +288,16 @@ export default function BookingSection() {
|
||||
placeholder="333 123 4567"
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animale */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
Nome dell'animale
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Nome dell'animale
|
||||
</label>
|
||||
<div className="relative">
|
||||
<PawPrint size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
@@ -218,18 +306,18 @@ export default function BookingSection() {
|
||||
placeholder="Fido"
|
||||
value={form.petName}
|
||||
onChange={(e) => setForm({ ...form, petName: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Tipo di animale
|
||||
</label>
|
||||
<select
|
||||
value={form.petType}
|
||||
onChange={(e) => setForm({ ...form, petType: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all bg-white"
|
||||
className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
>
|
||||
<option value="cane">Cane</option>
|
||||
<option value="gatto">Gatto</option>
|
||||
@@ -238,28 +326,28 @@ export default function BookingSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Servizio */}
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Tipo di visita *
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
value={form.service}
|
||||
onChange={(e) => setForm({ ...form, service: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all bg-white"
|
||||
className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
>
|
||||
<option value="">Seleziona un servizio</option>
|
||||
{services.map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
<option key={s} value={s}>
|
||||
{s}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Data e ora */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Data preferita *
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -270,12 +358,12 @@ export default function BookingSection() {
|
||||
value={form.date}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
onChange={(e) => setForm({ ...form, date: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Orario preferito
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -283,20 +371,21 @@ export default function BookingSection() {
|
||||
<select
|
||||
value={form.time}
|
||||
onChange={(e) => setForm({ ...form, time: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all bg-white"
|
||||
className="w-full rounded-lg border border-gray-200 bg-white py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
>
|
||||
<option value="">Qualsiasi orario</option>
|
||||
{timeSlots.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Note aggiuntive
|
||||
</label>
|
||||
<textarea
|
||||
@@ -304,19 +393,20 @@ export default function BookingSection() {
|
||||
placeholder="Descrivi brevemente il motivo della visita..."
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm({ ...form, notes: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all resize-none"
|
||||
className="w-full resize-none rounded-lg border border-gray-200 px-3 py-2.5 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-[#4ECDC4] hover:bg-[#3ab5ad] text-white font-bold py-3 text-base shadow-lg shadow-[#4ECDC4]/30 transition-all duration-300 hover:shadow-xl"
|
||||
disabled={submitting}
|
||||
className="w-full bg-[#4ECDC4] py-3 text-base font-bold text-white shadow-lg shadow-[#4ECDC4]/30 transition-all duration-300 hover:bg-[#3ab5ad] hover:shadow-xl disabled:cursor-wait disabled:opacity-80"
|
||||
>
|
||||
Invia Richiesta di Appuntamento
|
||||
{submitting ? "Invio in corso..." : "Invia richiesta di prenotazione"}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
* Campi obbligatori. Ti contatteremo entro 24 ore per confermare.
|
||||
<p className="text-center text-xs text-gray-500">
|
||||
* Campi obbligatori. La richiesta sarà valutata e confermata dallo staff.
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
* Sfondo blu petrolio scuro, testo bianco/grigio chiaro
|
||||
*/
|
||||
import { MapPin, Phone, Mail, Facebook, Clock, ArrowRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const quickLinks = [
|
||||
{ label: "Chi Siamo", href: "#chi-siamo" },
|
||||
{ label: "Radiologia", href: "#servizi" },
|
||||
{ label: "Chirurgia", href: "#servizi" },
|
||||
{ label: "Laboratorio", href: "#servizi" },
|
||||
{ label: "Il Team", href: "#team" },
|
||||
{ label: "News & Blog", href: "#news" },
|
||||
{ label: "Prenota Visita", href: "#prenotazione" },
|
||||
{ label: "Area Personale", href: "#registrazione" },
|
||||
{ label: "Chi Siamo", href: "/#chi-siamo" },
|
||||
{ label: "Radiologia", href: "/#servizi" },
|
||||
{ label: "Chirurgia", href: "/#servizi" },
|
||||
{ label: "Laboratorio", href: "/#servizi" },
|
||||
{ label: "Il Team", href: "/#team" },
|
||||
{ label: "News & Blog", href: "/#news" },
|
||||
{ label: "Prenota Visita", href: "/#prenotazione" },
|
||||
{ label: "Area Personale", href: "/#registrazione" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
@@ -158,24 +157,15 @@ export default function Footer() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => toast.info("Privacy Policy in arrivo")}
|
||||
className="hover:text-white/70 transition-colors"
|
||||
>
|
||||
<a href="/privacy-policy" className="hover:text-white/70 transition-colors">
|
||||
Privacy Policy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toast.info("Cookie Policy in arrivo")}
|
||||
className="hover:text-white/70 transition-colors"
|
||||
>
|
||||
</a>
|
||||
<a href="/cookie-policy" className="hover:text-white/70 transition-colors">
|
||||
Cookie Policy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toast.info("Note legali in arrivo")}
|
||||
className="hover:text-white/70 transition-colors"
|
||||
>
|
||||
</a>
|
||||
<a href="/note-legali" className="hover:text-white/70 transition-colors">
|
||||
Note Legali
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-white/25 text-xs mt-3 text-center md:text-left">
|
||||
|
||||
83
clinica-app/client/src/components/LegalPageLayout.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type LegalPageLayoutProps = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
intro: string;
|
||||
updatedAt: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function LegalPageLayout({
|
||||
eyebrow,
|
||||
title,
|
||||
intro,
|
||||
updatedAt,
|
||||
children,
|
||||
}: LegalPageLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F4EC] text-[#18364A]">
|
||||
<header className="border-b border-[#d8cec0] bg-white/95 backdrop-blur">
|
||||
<div className="container flex items-center justify-between py-4">
|
||||
<a href="/" className="inline-flex items-center gap-2 text-sm font-semibold text-[#1B4F72] hover:text-[#4ECDC4] transition-colors">
|
||||
<ArrowLeft size={16} />
|
||||
Torna alla home
|
||||
</a>
|
||||
<div className="text-right">
|
||||
<div
|
||||
className="leading-tight text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem", fontWeight: 700 }}
|
||||
>
|
||||
Clinica Veterinaria
|
||||
</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[#4ECDC4]">
|
||||
Formiginese
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section className="border-b border-[#e4d8c9] bg-white">
|
||||
<div className="container py-14 md:py-18">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-0.5 w-12 bg-[#4ECDC4]" />
|
||||
<span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
|
||||
{eyebrow}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)] lg:items-end">
|
||||
<div>
|
||||
<h1
|
||||
className="text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2.2rem, 5vw, 4rem)", fontWeight: 600, lineHeight: 1.06 }}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-base leading-relaxed text-gray-600">
|
||||
{intro}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-[#e6ddcf] bg-[#F8F4EC] px-5 py-4 text-sm text-gray-600 shadow-sm">
|
||||
<p className="font-semibold text-[#1B4F72]">Ultimo aggiornamento</p>
|
||||
<p className="mt-1">{updatedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="container py-12 md:py-16">
|
||||
<div className="rounded-[28px] border border-[#e9dece] bg-white px-6 py-8 shadow-sm md:px-10 md:py-10">
|
||||
<div className="legal-content prose prose-slate max-w-none prose-headings:text-[#1B4F72] prose-headings:font-semibold prose-p:text-gray-600 prose-li:text-gray-600 prose-strong:text-[#1B4F72]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown, Menu, X, Phone, MapPin } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type NavLink = {
|
||||
label: string;
|
||||
@@ -55,6 +56,12 @@ export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const showActivationToast = () => {
|
||||
toast.info("Servizio in corso di attivazione", {
|
||||
description: "L'area riservata sara disponibile a breve.",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 80);
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
@@ -150,7 +157,7 @@ export default function Navbar() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-[#1B4F72] text-[#1B4F72] hover:bg-[#1B4F72] hover:text-white transition-all duration-200"
|
||||
onClick={() => document.getElementById("registrazione")?.scrollIntoView({ behavior: "smooth" })}
|
||||
onClick={showActivationToast}
|
||||
>
|
||||
Accedi
|
||||
</Button>
|
||||
@@ -206,7 +213,7 @@ export default function Navbar() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-[#1B4F72] text-[#1B4F72] w-full"
|
||||
onClick={() => { setMobileOpen(false); document.getElementById("registrazione")?.scrollIntoView({ behavior: "smooth" }); }}
|
||||
onClick={() => { setMobileOpen(false); showActivationToast(); }}
|
||||
>
|
||||
Accedi / Registrati
|
||||
</Button>
|
||||
|
||||
@@ -19,7 +19,7 @@ const news = [
|
||||
"Con l'arrivo della primavera è il momento ideale per aggiornare il piano vaccinale del tuo cane o gatto. Scopri quali vaccini sono essenziali e perché.",
|
||||
date: "15 Marzo 2026",
|
||||
readTime: "4 min",
|
||||
image: "https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=600&q=80",
|
||||
image: "/images/news_gatto.jpg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -30,7 +30,7 @@ const news = [
|
||||
"La dieta è fondamentale per la salute del tuo felino. I nostri veterinari spiegano come scegliere il cibo giusto e le quantità raccomandate per ogni fase della vita.",
|
||||
date: "8 Marzo 2026",
|
||||
readTime: "6 min",
|
||||
image: "https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=600&q=80",
|
||||
image: "/images/news_consulenza.jpg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -41,7 +41,7 @@ const news = [
|
||||
"La sterilizzazione è una delle procedure più comuni nella medicina veterinaria. Ecco perché è importante, quando eseguirla e cosa aspettarsi nel post-operatorio.",
|
||||
date: "1 Marzo 2026",
|
||||
readTime: "5 min",
|
||||
image: "https://images.unsplash.com/photo-1548767797-d8c844163c4c?w=600&q=80",
|
||||
image: "/images/news_cucciolo.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,32 +1,58 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione servizi con 6 card principali allineate al menu della clinica.
|
||||
* Layout: immagine in alto, bordo superiore colorato, hover lift.
|
||||
* Sezione servizi con due livelli:
|
||||
* - Servizi Clinici
|
||||
* - Visite Specialistiche
|
||||
*/
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { Activity, ArrowRight, FlaskConical, Scan, Stethoscope, Video } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
Apple,
|
||||
ArrowRight,
|
||||
Bone,
|
||||
Eye,
|
||||
FlaskConical,
|
||||
Scan,
|
||||
Sparkles,
|
||||
Stethoscope,
|
||||
Video,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const services = [
|
||||
type ServiceItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
image: string;
|
||||
imageClassName?: string;
|
||||
imageHoverClassName?: string;
|
||||
icon: React.ComponentType<{ size?: number; className?: string }>;
|
||||
color: string;
|
||||
features: string[];
|
||||
tone?: "default" | "specialist";
|
||||
};
|
||||
|
||||
const clinicalServices: ServiceItem[] = [
|
||||
{
|
||||
id: "visite-cliniche",
|
||||
title: "Visite Cliniche",
|
||||
subtitle: "Medicina preventiva",
|
||||
description:
|
||||
"Percorsi di prevenzione e visite cliniche complete per monitorare lo stato di salute di cani e gatti in ogni fase della vita. Un approccio attento, continuativo e personalizzato.",
|
||||
image: "https://images.unsplash.com/photo-1628009368231-7bb7cfcb0def?w=900&q=85",
|
||||
"Percorsi di prevenzione e visite cliniche complete per monitorare lo stato di salute di cani e gatti in ogni fase della vita.",
|
||||
image: "/images/services_visite_cliniche.jpg",
|
||||
icon: Stethoscope,
|
||||
color: "#1B4F72",
|
||||
features: ["Check-up periodici", "Vaccinazioni", "Profilassi antiparassitaria", "Piani prevenzione senior"],
|
||||
features: ["Check-up periodici", "Vaccinazioni", "Profilassi antiparassitaria", "Controlli senior"],
|
||||
},
|
||||
{
|
||||
id: "ecografia",
|
||||
title: "Ecografia",
|
||||
subtitle: "Diagnostica non invasiva",
|
||||
description:
|
||||
"Indagini ecografiche per approfondire in modo rapido e non invasivo organi addominali, apparato urinario, apparato riproduttivo e principali sospetti clinici.",
|
||||
image: "https://images.unsplash.com/photo-1579684385127-1ef15d508118?w=900&q=85",
|
||||
"Indagini ecografiche rapide per approfondire apparato addominale, urinario e riproduttivo con maggiore precisione clinica.",
|
||||
image: "/images/services_ecografia.jpg",
|
||||
icon: Activity,
|
||||
color: "#4ECDC4",
|
||||
features: ["Ecografia addominale", "Controlli gravidanza", "Valutazioni urinarie", "Follow-up terapeutici"],
|
||||
@@ -36,8 +62,8 @@ const services = [
|
||||
title: "Radiologia",
|
||||
subtitle: "Diagnostica per immagini",
|
||||
description:
|
||||
"Radiografie digitali e diagnostica per immagini a supporto della visita clinica, utili per valutare apparato scheletrico, torace, addome e urgenze.",
|
||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/radiology_39785f88.jpg",
|
||||
"Radiografie digitali e diagnostica per immagini per apparato scheletrico, torace, addome e approfondimenti d'urgenza.",
|
||||
image: "/images/services_radiologia.jpg",
|
||||
icon: Scan,
|
||||
color: "#2E86AB",
|
||||
features: ["Radiografia digitale", "Studi toracici", "Valutazioni ortopediche", "Diagnostica d'urgenza"],
|
||||
@@ -47,8 +73,8 @@ const services = [
|
||||
title: "Laboratorio",
|
||||
subtitle: "Analisi interne",
|
||||
description:
|
||||
"Laboratorio interno per esami rapidi e mirati, fondamentale per supportare diagnosi tempestive, monitoraggi terapeutici e controlli pre-operatori.",
|
||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/laboratory_5a1c4119.jpg",
|
||||
"Laboratorio interno per esami rapidi e mirati, utile per diagnosi tempestive, monitoraggi terapeutici e controlli pre-operatori.",
|
||||
image: "/images/services_laboratorio.jpg",
|
||||
icon: FlaskConical,
|
||||
color: "#1B4F72",
|
||||
features: ["Ematologia", "Biochimica", "Urine", "Citologia", "Coagulazione"],
|
||||
@@ -58,8 +84,8 @@ const services = [
|
||||
title: "Endoscopia",
|
||||
subtitle: "Esplorazione mini-invasiva",
|
||||
description:
|
||||
"Tecniche endoscopiche per esplorare apparato digerente e vie respiratorie, riducendo invasivita e tempi di recupero quando il quadro clinico lo consente.",
|
||||
image: "https://images.unsplash.com/photo-1530026405186-ed1f139313f8?w=900&q=85",
|
||||
"Tecniche endoscopiche per esplorare apparato digerente e vie respiratorie riducendo invasivita e tempi di recupero.",
|
||||
image: "/images/services_endoscopia.webp",
|
||||
icon: Video,
|
||||
color: "#4ECDC4",
|
||||
features: ["Valutazioni gastroenteriche", "Corpi estranei", "Biopsie mirate", "Esplorazioni respiratorie"],
|
||||
@@ -69,33 +95,112 @@ const services = [
|
||||
title: "Laparoscopia",
|
||||
subtitle: "Chirurgia mini-invasiva",
|
||||
description:
|
||||
"Procedure mini-invasive che permettono interventi piu delicati, incisioni ridotte e recuperi piu confortevoli rispetto alla chirurgia tradizionale.",
|
||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/surgery_e92904ed.jpg",
|
||||
"Procedure mini-invasive con incisioni ridotte, recuperi piu confortevoli e maggiore delicatezza nei passaggi operatori.",
|
||||
image: "/images/services_laparoscopia.jpg",
|
||||
icon: Activity,
|
||||
color: "#2E86AB",
|
||||
features: ["Procedure mini-invasive", "Biopsie laparoscopiche", "Riduzione del dolore", "Recupero post-operatorio"],
|
||||
},
|
||||
];
|
||||
|
||||
function ServiceCard({ service, index }: { service: typeof services[0]; index: number }) {
|
||||
const specialistVisits: ServiceItem[] = [
|
||||
{
|
||||
id: "oncologia",
|
||||
title: "Oncologia",
|
||||
subtitle: "Percorsi dedicati",
|
||||
description:
|
||||
"Valutazioni specialistiche per definire iter diagnostici e strategie terapeutiche personalizzate nei casi oncologici.",
|
||||
image: "/images/hero_cat.jpg",
|
||||
icon: Stethoscope,
|
||||
color: "#A95F3A",
|
||||
features: ["Inquadramento clinico", "Piani terapeutici", "Monitoraggi periodici"],
|
||||
tone: "specialist",
|
||||
},
|
||||
{
|
||||
id: "dermatologia",
|
||||
title: "Dermatologia",
|
||||
subtitle: "Cute e mantello",
|
||||
description:
|
||||
"Approfondimenti per prurito, alopecie, otiti ricorrenti e alterazioni cutanee che richiedono un percorso mirato.",
|
||||
image: "/images/services_dermatologia.jpg",
|
||||
icon: Sparkles,
|
||||
color: "#B76E79",
|
||||
features: ["Allergie", "Citologie cutanee", "Otiti croniche"],
|
||||
tone: "specialist",
|
||||
},
|
||||
{
|
||||
id: "oculistica",
|
||||
title: "Oculistica",
|
||||
subtitle: "Vista e benessere oculare",
|
||||
description:
|
||||
"Controlli specialistici per disturbi oculari, lacrimazione, arrossamenti e valutazioni funzionali della vista.",
|
||||
image: "/images/services_oculistica.jpg",
|
||||
icon: Eye,
|
||||
color: "#4C7A9F",
|
||||
features: ["Esame del segmento anteriore", "Pressione oculare", "Follow-up oculari"],
|
||||
tone: "specialist",
|
||||
},
|
||||
{
|
||||
id: "nutrizione",
|
||||
title: "Nutrizione",
|
||||
subtitle: "Equilibrio alimentare",
|
||||
description:
|
||||
"Consulenze per impostare piani nutrizionali su misura in base a eta, patologie, stile di vita e obiettivi clinici.",
|
||||
image: "/images/hero_dog.jpg",
|
||||
imageClassName: "object-[center_22%]",
|
||||
icon: Apple,
|
||||
color: "#7EA55A",
|
||||
features: ["Diete personalizzate", "Gestione del peso", "Supporto nutrizionale"],
|
||||
tone: "specialist",
|
||||
},
|
||||
{
|
||||
id: "ortopedia",
|
||||
title: "Ortopedia",
|
||||
subtitle: "Movimento e postura",
|
||||
description:
|
||||
"Valutazioni specialistiche per zoppie, dolore articolare, traumi e impostazione del corretto iter ortopedico.",
|
||||
image: "/images/ortovet.webp",
|
||||
imageClassName: "scale-[0.5]",
|
||||
imageHoverClassName: "group-hover:scale-[0.7]",
|
||||
icon: Bone,
|
||||
color: "#6D7B8C",
|
||||
features: ["Valutazioni zoppia", "Controlli articolari", "Percorsi post-trauma"],
|
||||
tone: "specialist",
|
||||
},
|
||||
];
|
||||
|
||||
function ServiceCard({
|
||||
service,
|
||||
index,
|
||||
className = "",
|
||||
}: {
|
||||
service: ServiceItem;
|
||||
index: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-50px" });
|
||||
const specialist = service.tone === "specialist";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: index * 0.12 }}
|
||||
className="group bg-white rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-400 hover:-translate-y-1"
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
className={`group overflow-hidden rounded-2xl transition-all duration-400 hover:-translate-y-1 ${className} ${
|
||||
specialist
|
||||
? "border border-[#D7C1A8] bg-gradient-to-b from-[#fffaf2] to-white shadow-[0_18px_45px_rgba(169,95,58,0.12)] hover:shadow-[0_22px_50px_rgba(169,95,58,0.18)]"
|
||||
: "bg-white shadow-md hover:shadow-xl"
|
||||
}`}
|
||||
>
|
||||
<div className="h-1" style={{ backgroundColor: service.color }} />
|
||||
<div className="h-1.5" style={{ backgroundColor: service.color }} />
|
||||
|
||||
<div className="relative h-52 overflow-hidden">
|
||||
<img
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
className={`h-full w-full object-cover transition-transform duration-700 ${service.imageHoverClassName ?? "group-hover:scale-105"} ${service.imageClassName ?? ""}`}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-4 right-4 w-12 h-12 rounded-full flex items-center justify-center shadow-lg"
|
||||
@@ -103,37 +208,42 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
|
||||
>
|
||||
<service.icon size={22} className="text-white" />
|
||||
</div>
|
||||
{specialist && (
|
||||
<div className="absolute left-4 top-4 rounded-full bg-white/90 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-[#7d5233] shadow-sm">
|
||||
Alta specializzazione
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="text-xs uppercase tracking-widest font-semibold mb-1" style={{ color: service.color }}>
|
||||
<div className="mb-1 text-xs font-semibold uppercase tracking-widest" style={{ color: service.color }}>
|
||||
{service.subtitle}
|
||||
</div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-3"
|
||||
className="mb-3 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.75rem", fontWeight: 600 }}
|
||||
>
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed mb-4">{service.description}</p>
|
||||
<p className="mb-4 text-sm leading-relaxed text-gray-600">{service.description}</p>
|
||||
|
||||
<ul className="space-y-1.5 mb-5">
|
||||
<ul className="mb-5 space-y-1.5">
|
||||
{service.features.map((feat) => (
|
||||
<li key={feat} className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ backgroundColor: service.color }} />
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full" style={{ backgroundColor: service.color }} />
|
||||
{feat}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-sm font-semibold transition-all duration-200 group/btn"
|
||||
className="group/btn flex items-center gap-1.5 text-sm font-semibold transition-all duration-200"
|
||||
style={{ color: service.color }}
|
||||
onClick={() => {
|
||||
toast("Sezione in arrivo", { description: `La pagina dedicata a ${service.title} sara disponibile a breve.` });
|
||||
}}
|
||||
>
|
||||
Scopri di piu
|
||||
Approfondisci
|
||||
<ArrowRight size={15} className="transition-transform duration-200 group-hover/btn:translate-x-1" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -143,7 +253,9 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
|
||||
|
||||
export default function ServicesSection() {
|
||||
const ref = useRef(null);
|
||||
const specialistRef = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
const specialistInView = useInView(specialistRef, { once: true, margin: "-80px" });
|
||||
|
||||
return (
|
||||
<section id="servizi" className="py-20 md:py-28" style={{ backgroundColor: "#F5F0E8" }}>
|
||||
@@ -155,38 +267,74 @@ export default function ServicesSection() {
|
||||
transition={{ duration: 0.7 }}
|
||||
className="mb-14"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-0.5 w-12 bg-[#4ECDC4]" />
|
||||
<span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
|
||||
Specializzazioni
|
||||
</span>
|
||||
</div>
|
||||
<h2
|
||||
className="text-[#1B4F72] mb-4"
|
||||
className="mb-4 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 600 }}
|
||||
>
|
||||
Servizi Specialistici
|
||||
Servizi Clinici
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-xl text-base leading-relaxed">
|
||||
<p className="max-w-xl text-base leading-relaxed text-gray-600">
|
||||
Sei aree di eccellenza clinica per accompagnare prevenzione, diagnosi e trattamento
|
||||
dei tuoi animali domestici, con tecnologie avanzate e professionisti esperti.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
{services.map((service, index) => (
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-3">
|
||||
{clinicalServices.map((service, index) => (
|
||||
<ServiceCard key={service.id} service={service} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
ref={specialistRef}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={specialistInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.7, delay: 0.2 }}
|
||||
className="mt-20"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-0.5 w-12 bg-[#A95F3A]" />
|
||||
<span className="text-sm font-semibold uppercase tracking-widest text-[#A95F3A]">
|
||||
Alta specializzazione
|
||||
</span>
|
||||
</div>
|
||||
<h3
|
||||
className="mb-4 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(1.9rem, 3.7vw, 2.8rem)", fontWeight: 600 }}
|
||||
>
|
||||
Visite Specialistiche
|
||||
</h3>
|
||||
<p className="max-w-2xl text-base leading-relaxed text-gray-600">
|
||||
Un secondo livello di consulenza clinica per casi che richiedono competenze dedicate,
|
||||
approfondimenti mirati e percorsi di valutazione ad alto valore aggiunto.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-6">
|
||||
{specialistVisits.map((service, index) => (
|
||||
<ServiceCard
|
||||
key={service.id}
|
||||
service={service}
|
||||
index={index}
|
||||
className={index < 3 ? "xl:col-span-2" : "xl:col-span-3"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isInView ? { opacity: 1 } : {}}
|
||||
animate={specialistInView ? { opacity: 1 } : {}}
|
||||
transition={{ duration: 0.7, delay: 0.6 }}
|
||||
className="text-center mt-12"
|
||||
className="mt-12 text-center"
|
||||
>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 text-[#1B4F72] font-semibold border-2 border-[#1B4F72] px-8 py-3 rounded-full hover:bg-[#1B4F72] hover:text-white transition-all duration-300"
|
||||
className="inline-flex items-center gap-2 rounded-full border-2 border-[#1B4F72] px-8 py-3 font-semibold text-[#1B4F72] transition-all duration-300 hover:bg-[#1B4F72] hover:text-white"
|
||||
onClick={() => {
|
||||
toast("Tutti i servizi", { description: "La pagina completa dei servizi sara disponibile a breve." });
|
||||
}}
|
||||
|
||||
@@ -1,153 +1,243 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione team: griglia di 6 professionisti con card eleganti
|
||||
* Sfondo sabbia calda, foto placeholder con iniziali, specializzazioni colorate
|
||||
* Sezione team: due aree distinte per team medico e collaborazioni
|
||||
* Stessa grammatica visiva delle card precedenti, con schede pronte
|
||||
* ad accogliere progressivamente i profili completi.
|
||||
*/
|
||||
import { motion } from "framer-motion";
|
||||
import { useInView } from "framer-motion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
|
||||
const team = [
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
specialization?: string;
|
||||
color: string;
|
||||
bio?: string[];
|
||||
};
|
||||
|
||||
const medicalTeam: TeamMember[] = [
|
||||
{
|
||||
name: "Dott. Paolo Parmeggiani",
|
||||
role: "Direttore Sanitario",
|
||||
specialization: "Oncologia Veterinaria",
|
||||
role: "Direttore sanitario",
|
||||
specialization: "Oncologia veterinaria",
|
||||
color: "#1B4F72",
|
||||
initials: "PP",
|
||||
bio: "Master in Oncologia Veterinaria. Direttore sanitario della clinica con oltre 20 anni di esperienza.",
|
||||
bio: [
|
||||
"Laureato a pieni voti presso l'Universita degli Studi di Milano, ha conseguito un Master di II livello in Oncologia Veterinaria presso l'Universita di Pisa e ha svolto un tirocinio di chirurgia e medicina interna presso la facolta di Sao Joao da Boa Vista, in Brasile.",
|
||||
"Iscritto all'Ordine dei Medici Veterinari di Modena (n. 715), e socio SCIVAC e SIONCOV.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Elena Rossi",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Chirurgia",
|
||||
name: "Dott.ssa Irene Paganelli",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Ecografia ed esotici",
|
||||
color: "#4ECDC4",
|
||||
initials: "ER",
|
||||
bio: "Specializzata in chirurgia ortopedica e dei tessuti molli. Referente per gli interventi d'urgenza.",
|
||||
bio: [
|
||||
"Laureata a pieni voti con lode presso l'Universita di Bologna, ha svolto un tirocinio in medicina interna e chirurgia presso l'Ospedale Veterinario dell'Universita di Copenhagen.",
|
||||
"Iscritta all'Ordine dei Medici Veterinari di Modena (n. 749), e socia SCIVAC e SIVAE. Ha seguito diversi corsi di perfezionamento in ecografia addominale ed ecocardiografia del cane e del gatto.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dott. Marco Bianchi",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Radiologia",
|
||||
name: "Dott. Simone Tinti",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#2E86AB",
|
||||
initials: "MB",
|
||||
bio: "Esperto in diagnostica per immagini, ecografia e radiologia digitale veterinaria.",
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Sara Ferrari",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Medicina Interna",
|
||||
color: "#E8A838",
|
||||
initials: "SF",
|
||||
bio: "Specializzata in medicina interna felina e canina, con focus su malattie metaboliche.",
|
||||
name: "Dott.ssa Michela Sghedoni",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#B76E79",
|
||||
},
|
||||
{
|
||||
name: "Dott. Luca Moretti",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Dermatologia",
|
||||
name: "Dott. Luca Pietri",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#6B8F71",
|
||||
initials: "LM",
|
||||
bio: "Esperto in dermatologia e allergologia veterinaria. Referente per le patologie cutanee.",
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Anna Conti",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Odontoiatria",
|
||||
color: "#9B5DE5",
|
||||
initials: "AC",
|
||||
bio: "Specializzata in odontoiatria veterinaria e chirurgia orale per cani e gatti.",
|
||||
name: "Dott.ssa Sara Casali",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#C58C63",
|
||||
},
|
||||
];
|
||||
|
||||
function TeamCard({ member, index }: { member: typeof team[0]; index: number }) {
|
||||
const collaborators: TeamMember[] = [
|
||||
{
|
||||
name: "Dott. Andrea Arrigoni",
|
||||
role: "Collaborazione specialistica",
|
||||
specialization: "Endoscopia, citologia e medicina interna",
|
||||
color: "#A95F3A",
|
||||
bio: [
|
||||
"Laureato presso l'Universita degli Studi di Bologna, ha trascorso due anni come studente interno nel dipartimento di Clinica Chirurgica della stessa universita.",
|
||||
"Dal 2004 al 2017 ha collaborato con cliniche veterinarie del territorio modenese e romagnolo in medicina interna, endoscopia e citologia. Dal 2012 opera come freelance di endoscopia e citologia in numerose strutture veterinarie dell'Emilia Romagna. Dal 2017 entra a far parte di ENDOVET.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dott. Alessandro Poli",
|
||||
role: "Collaborazione specialistica",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#7EA55A",
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Elena Venturelli",
|
||||
role: "Collaborazione specialistica",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#4C7A9F",
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Cinzia Pellegrini",
|
||||
role: "Collaborazione specialistica",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#8E6C88",
|
||||
},
|
||||
];
|
||||
|
||||
function getInitials(name: string) {
|
||||
return name
|
||||
.replaceAll(".", "")
|
||||
.split(" ")
|
||||
.filter((part) => part.length > 2)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase())
|
||||
.join("");
|
||||
}
|
||||
|
||||
function TeamCard({
|
||||
member,
|
||||
index,
|
||||
}: {
|
||||
member: TeamMember;
|
||||
index: number;
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-40px" });
|
||||
const hasBio = Boolean(member.bio?.length);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
className="group bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
|
||||
transition={{ duration: 0.6, delay: index * 0.08 }}
|
||||
className="group h-full overflow-hidden rounded-2xl bg-white shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
{/* Bordo superiore */}
|
||||
<div className="h-1" style={{ backgroundColor: member.color }} />
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="p-6 pb-4">
|
||||
<div className="p-6 pb-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-xl flex-shrink-0 shadow-md"
|
||||
className="flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full text-xl font-bold text-white shadow-md"
|
||||
style={{ backgroundColor: member.color, fontFamily: "'Cormorant Garamond', serif" }}
|
||||
>
|
||||
{member.initials}
|
||||
{getInitials(member.name)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3
|
||||
className="text-[#1B4F72] font-semibold leading-tight"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.15rem" }}
|
||||
className="leading-tight text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.15rem", fontWeight: 600 }}
|
||||
>
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className="text-gray-500 text-xs mt-0.5">{member.role}</p>
|
||||
<p className="mt-0.5 text-xs text-gray-500">{member.role}</p>
|
||||
{member.specialization && (
|
||||
<span
|
||||
className="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full mt-2"
|
||||
className="mt-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold"
|
||||
style={{ backgroundColor: `${member.color}18`, color: member.color }}
|
||||
>
|
||||
{member.specialization}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed mt-4">{member.bio}</p>
|
||||
{hasBio ? (
|
||||
<div className="mt-4 space-y-3">
|
||||
{member.bio?.map((paragraph) => (
|
||||
<p key={paragraph} className="text-sm leading-relaxed text-gray-600">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 rounded-xl border border-dashed border-gray-200 bg-[#F9F6F1] px-4 py-5">
|
||||
<p className="text-sm italic leading-relaxed text-gray-400">
|
||||
Profilo in aggiornamento.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamSection() {
|
||||
function TeamGroup({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
members,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
members: TeamMember[];
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
|
||||
return (
|
||||
<section id="team" className="py-20 md:py-28 bg-white">
|
||||
<div className="container">
|
||||
{/* Header */}
|
||||
<div className="mt-14 first:mt-0">
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.7 }}
|
||||
className="mb-14"
|
||||
className="mb-10"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
Il Nostro Team
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<div className="h-0.5 w-10 bg-[#4ECDC4]" />
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.2em] text-[#4ECDC4]">
|
||||
{eyebrow}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<h2
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
||||
<h3
|
||||
className="text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 600 }}
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(1.8rem, 3.2vw, 2.5rem)", fontWeight: 600 }}
|
||||
>
|
||||
Sei professionisti,{" "}
|
||||
<span className="italic">un'unica passione</span>
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-sm text-sm leading-relaxed">
|
||||
Ogni membro del nostro team porta competenze specialistiche uniche,
|
||||
unite dalla stessa dedizione al benessere animale.
|
||||
</p>
|
||||
{title}
|
||||
</h3>
|
||||
<p className="max-w-xl text-sm leading-relaxed text-gray-600">{description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Grid team */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{team.map((member, index) => (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{members.map((member, index) => (
|
||||
<TeamCard key={member.name} member={member} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamSection() {
|
||||
return (
|
||||
<section id="team" className="bg-white py-20 md:py-28">
|
||||
<div className="container">
|
||||
<TeamGroup
|
||||
eyebrow="Team medico"
|
||||
title="Team medico"
|
||||
description="I professionisti che seguono l'attivita clinica quotidiana della struttura, con competenze diverse e percorsi di aggiornamento continui."
|
||||
members={medicalTeam}
|
||||
/>
|
||||
|
||||
<TeamGroup
|
||||
eyebrow="Collaborazioni"
|
||||
title="Collaboratori"
|
||||
description="Una rete di competenze specialistiche che amplia le possibilita diagnostiche e terapeutiche disponibili per i pazienti della clinica."
|
||||
members={collaborators}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
78
clinica-app/client/src/pages/CookiePolicyPage.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import LegalPageLayout from "@/components/LegalPageLayout";
|
||||
|
||||
export default function CookiePolicyPage() {
|
||||
return (
|
||||
<LegalPageLayout
|
||||
eyebrow="Informativa cookie"
|
||||
title="Cookie Policy"
|
||||
intro="Questa pagina descrive l'uso di cookie e di altri strumenti tecnici da parte del sito della Clinica Veterinaria Formiginese."
|
||||
updatedAt="23 maggio 2026"
|
||||
>
|
||||
<h2>1. Cosa sono i cookie</h2>
|
||||
<p>
|
||||
I cookie sono piccoli file di testo che i siti visitati possono salvare sul dispositivo
|
||||
dell'utente per consentire il funzionamento del sito, migliorare la navigazione o memorizzare
|
||||
preferenze e impostazioni.
|
||||
</p>
|
||||
|
||||
<h2>2. Tipologie di strumenti attualmente utilizzati</h2>
|
||||
<p>Allo stato attuale il sito utilizza esclusivamente strumenti tecnici o assimilabili ai tecnici, necessari a:</p>
|
||||
<ul>
|
||||
<li>erogare correttamente le pagine e i contenuti richiesti;</li>
|
||||
<li>gestire eventuali elementi tecnici indispensabili al funzionamento dell'interfaccia;</li>
|
||||
<li>garantire sicurezza, stabilità e corretta erogazione del servizio.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Cookie tecnici</h2>
|
||||
<p>
|
||||
I cookie tecnici sono quelli strettamente necessari alla navigazione o alla fornitura di un
|
||||
servizio esplicitamente richiesto dall'utente. Per tali strumenti, secondo la normativa
|
||||
applicabile, non è richiesto il consenso preventivo dell'utente, fermo restando l'obbligo
|
||||
di fornire un'informativa adeguata.
|
||||
</p>
|
||||
|
||||
<h2>4. Cookie di profilazione e analytics</h2>
|
||||
<p>
|
||||
Alla data di aggiornamento di questa pagina il sito non utilizza cookie di profilazione,
|
||||
strumenti di marketing né sistemi di analytics attivi configurati per il monitoraggio del
|
||||
comportamento degli utenti a fini non tecnici.
|
||||
</p>
|
||||
<p>
|
||||
Qualora in futuro venissero introdotti strumenti di tracciamento non tecnici, la presente
|
||||
policy verrà aggiornata e, ove necessario, sarà implementato un meccanismo di raccolta del
|
||||
consenso conforme alla normativa applicabile.
|
||||
</p>
|
||||
|
||||
<h2>5. Risorse di terze parti</h2>
|
||||
<p>
|
||||
Il sito può richiamare risorse esterne di supporto, come librerie o font distribuiti da terzi.
|
||||
In tali casi il browser dell'utente può effettuare richieste ai rispettivi server per
|
||||
scaricare le risorse necessarie alla visualizzazione della pagina.
|
||||
</p>
|
||||
|
||||
<h2>6. Futuri servizi con autenticazione</h2>
|
||||
<p>
|
||||
In occasione della futura attivazione dell'area riservata potranno essere utilizzati cookie
|
||||
o identificatori tecnici di sessione strettamente necessari all'autenticazione e alla gestione
|
||||
sicura dell'accesso. Anche tali strumenti, se limitati alla funzione tecnica richiesta
|
||||
dall'utente, rientrano normalmente tra quelli esenti da consenso preventivo.
|
||||
</p>
|
||||
|
||||
<h2>7. Come gestire i cookie</h2>
|
||||
<p>
|
||||
L'utente può controllare e cancellare i cookie attraverso le impostazioni del proprio browser.
|
||||
La disattivazione dei cookie tecnici potrebbe tuttavia compromettere il corretto funzionamento
|
||||
di alcune funzionalità del sito.
|
||||
</p>
|
||||
|
||||
<h2>8. Contatti</h2>
|
||||
<p>
|
||||
Per qualsiasi chiarimento relativo all'uso dei cookie e degli altri strumenti tecnici è possibile
|
||||
contattare la Clinica Veterinaria Formiginese all'indirizzo{" "}
|
||||
<a href="mailto:clinicaveterinariaformiginese@gmail.com">
|
||||
clinicaveterinariaformiginese@gmail.com
|
||||
</a>.
|
||||
</p>
|
||||
</LegalPageLayout>
|
||||
);
|
||||
}
|
||||
78
clinica-app/client/src/pages/LegalNotesPage.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import LegalPageLayout from "@/components/LegalPageLayout";
|
||||
|
||||
export default function LegalNotesPage() {
|
||||
return (
|
||||
<LegalPageLayout
|
||||
eyebrow="Note legali"
|
||||
title="Note Legali"
|
||||
intro="Le presenti note disciplinano l'accesso e l'utilizzo del sito della Clinica Veterinaria Formiginese e forniscono informazioni generali sull'uso dei contenuti pubblicati."
|
||||
updatedAt="23 maggio 2026"
|
||||
>
|
||||
<h2>1. Informazioni generali</h2>
|
||||
<p>
|
||||
Il presente sito è dedicato alla presentazione della Clinica Veterinaria Formiginese,
|
||||
dei suoi servizi e delle modalità di contatto con la struttura. I contenuti pubblicati
|
||||
hanno finalità prevalentemente informative e organizzative.
|
||||
</p>
|
||||
|
||||
<h2>2. Utilizzo del sito</h2>
|
||||
<p>
|
||||
L'utente si impegna a utilizzare il sito in modo conforme alla legge, al buon costume e
|
||||
alle finalità per cui esso è messo a disposizione, evitando qualsiasi uso improprio,
|
||||
fraudolento o lesivo dei diritti del titolare o di terzi.
|
||||
</p>
|
||||
|
||||
<h2>3. Contenuti informativi</h2>
|
||||
<p>
|
||||
Le informazioni presenti sul sito, incluse quelle relative ad attività cliniche, visite,
|
||||
servizi e aggiornamenti divulgativi, non sostituiscono in alcun modo una valutazione
|
||||
veterinaria diretta e personalizzata. Per esigenze cliniche specifiche è sempre necessario
|
||||
rivolgersi direttamente alla struttura o al professionista competente.
|
||||
</p>
|
||||
|
||||
<h2>4. Prenotazioni e richieste online</h2>
|
||||
<p>
|
||||
Eventuali richieste di registrazione o di prenotazione visita inoltrate attraverso il sito
|
||||
hanno natura informativa o pre-organizzativa e non costituiscono, salvo diversa conferma da
|
||||
parte della clinica, appuntamento automaticamente confermato né accettazione vincolante della
|
||||
prestazione richiesta.
|
||||
</p>
|
||||
|
||||
<h2>5. Proprietà intellettuale</h2>
|
||||
<p>
|
||||
Salvo diversa indicazione, testi, immagini, elementi grafici, marchi, struttura del sito e
|
||||
altri contenuti presenti nel sito sono protetti dalle norme applicabili in materia di proprietà
|
||||
intellettuale e non possono essere copiati, riprodotti, diffusi o riutilizzati senza preventiva
|
||||
autorizzazione del titolare o dei rispettivi aventi diritto.
|
||||
</p>
|
||||
|
||||
<h2>6. Link a siti esterni</h2>
|
||||
<p>
|
||||
Il sito può contenere collegamenti a risorse esterne, gestite da soggetti terzi. La presenza di
|
||||
tali link non implica approvazione o controllo costante sui relativi contenuti. La clinica non
|
||||
risponde dei contenuti, delle policy o del funzionamento dei siti esterni.
|
||||
</p>
|
||||
|
||||
<h2>7. Limitazione di responsabilità</h2>
|
||||
<p>
|
||||
Pur ponendo la massima attenzione nell'aggiornamento dei contenuti, la clinica non garantisce
|
||||
che tutte le informazioni presenti sul sito siano sempre complete, prive di errori o aggiornate
|
||||
in tempo reale. Nei limiti consentiti dalla legge, il titolare non potrà essere ritenuto
|
||||
responsabile per danni derivanti dall'utilizzo del sito o dall'affidamento riposto nelle
|
||||
informazioni ivi contenute.
|
||||
</p>
|
||||
|
||||
<h2>8. Modifiche</h2>
|
||||
<p>
|
||||
Il titolare si riserva il diritto di aggiornare, modificare o rimuovere in qualsiasi momento
|
||||
contenuti, servizi, condizioni d'uso e presenti note legali, senza obbligo di preavviso.
|
||||
</p>
|
||||
|
||||
<h2>9. Legge applicabile</h2>
|
||||
<p>
|
||||
L'utilizzo del sito è regolato dalla legge italiana. Restano salve le tutele eventualmente
|
||||
previste dalla normativa inderogabile applicabile agli utenti.
|
||||
</p>
|
||||
</LegalPageLayout>
|
||||
);
|
||||
}
|
||||
108
clinica-app/client/src/pages/PrivacyPolicyPage.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import LegalPageLayout from "@/components/LegalPageLayout";
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
<LegalPageLayout
|
||||
eyebrow="Informativa privacy"
|
||||
title="Privacy Policy"
|
||||
intro="Questa informativa descrive come vengono trattati i dati personali degli utenti che consultano il sito della Clinica Veterinaria Formiginese, contattano la struttura o utilizzano i servizi online via via attivati."
|
||||
updatedAt="23 maggio 2026"
|
||||
>
|
||||
<h2>1. Titolare del trattamento</h2>
|
||||
<p>
|
||||
Il titolare del trattamento dei dati personali trattati attraverso questo sito è la
|
||||
Clinica Veterinaria Formiginese, con sede operativa in Via Quattro Passi, 16 -
|
||||
41043 Formigine (MO), contattabile all'indirizzo email{" "}
|
||||
<a href="mailto:clinicaveterinariaformiginese@gmail.com">
|
||||
clinicaveterinariaformiginese@gmail.com
|
||||
</a>{" "}
|
||||
e al numero <a href="tel:0598396263">059 839.62.63</a>.
|
||||
</p>
|
||||
<p>
|
||||
Eventuali ulteriori dati identificativi del titolare e i riferimenti amministrativi
|
||||
potranno essere integrati in questa pagina in occasione dei successivi aggiornamenti.
|
||||
</p>
|
||||
|
||||
<h2>2. Tipologie di dati trattati</h2>
|
||||
<p>Il sito può trattare le seguenti categorie di dati:</p>
|
||||
<ul>
|
||||
<li>dati di navigazione e dati tecnici necessari al funzionamento del sito;</li>
|
||||
<li>dati comunicati spontaneamente tramite email, telefono o moduli di contatto;</li>
|
||||
<li>dati inseriti nei servizi online di registrazione e richiesta di prenotazione;</li>
|
||||
<li>dati necessari alla gestione delle richieste informative e organizzative.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Finalità del trattamento</h2>
|
||||
<p>I dati possono essere trattati per:</p>
|
||||
<ul>
|
||||
<li>consentire la navigazione e la sicurezza del sito;</li>
|
||||
<li>rispondere a richieste di informazioni inviate dagli utenti;</li>
|
||||
<li>gestire richieste di registrazione all'area riservata;</li>
|
||||
<li>gestire richieste di prenotazione visita non vincolanti e i relativi contatti di follow-up;</li>
|
||||
<li>adempiere a obblighi di legge, amministrativi o di tutela dei diritti del titolare.</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Base giuridica</h2>
|
||||
<p>
|
||||
Il trattamento è effettuato, a seconda dei casi, sulla base dell'art. 6, par. 1, lett. b)
|
||||
del Regolamento (UE) 2016/679 per l'esecuzione di misure precontrattuali richieste
|
||||
dall'interessato, dell'art. 6, par. 1, lett. c) per obblighi di legge, e dell'art. 6,
|
||||
par. 1, lett. f) per il legittimo interesse del titolare alla sicurezza del sito e alla
|
||||
gestione organizzativa delle richieste.
|
||||
</p>
|
||||
|
||||
<h2>5. Modalità di trattamento</h2>
|
||||
<p>
|
||||
I dati sono trattati con strumenti elettronici e organizzativi idonei a garantirne
|
||||
riservatezza, integrità, disponibilità e aggiornamento, nel rispetto dei principi di
|
||||
liceità, correttezza, trasparenza e minimizzazione.
|
||||
</p>
|
||||
|
||||
<h2>6. Conservazione dei dati</h2>
|
||||
<p>
|
||||
I dati sono conservati per il tempo strettamente necessario alle finalità per cui sono
|
||||
raccolti e, successivamente, nei limiti previsti dalle norme applicabili o comunque per
|
||||
il tempo necessario alla tutela dei diritti del titolare.
|
||||
</p>
|
||||
<p>
|
||||
Le richieste di contatto o prenotazione possono essere conservate per il tempo necessario
|
||||
alla loro gestione e per l'eventuale definizione dei successivi rapporti organizzativi o
|
||||
amministrativi con l'utente.
|
||||
</p>
|
||||
|
||||
<h2>7. Destinatari dei dati</h2>
|
||||
<p>
|
||||
I dati possono essere trattati da personale autorizzato della clinica e, ove necessario,
|
||||
da fornitori di servizi tecnici e informatici che operano quali responsabili o soggetti
|
||||
autorizzati al trattamento, nei limiti delle rispettive competenze.
|
||||
</p>
|
||||
|
||||
<h2>8. Trasferimenti verso Paesi terzi</h2>
|
||||
<p>
|
||||
Alcune risorse tecniche di terze parti eventualmente richiamate dal sito, come servizi di
|
||||
distribuzione di font o contenuti esterni, possono comportare il trattamento di dati tecnici
|
||||
da parte dei relativi fornitori. Tali trattamenti avvengono secondo le condizioni e le
|
||||
garanzie previste dai rispettivi operatori.
|
||||
</p>
|
||||
|
||||
<h2>9. Diritti degli interessati</h2>
|
||||
<p>
|
||||
Gli interessati possono esercitare i diritti previsti dagli articoli 15 e seguenti del
|
||||
Regolamento (UE) 2016/679, tra cui accesso, rettifica, cancellazione, limitazione del
|
||||
trattamento, opposizione e, ove applicabile, portabilità dei dati, contattando il titolare
|
||||
ai recapiti sopra indicati.
|
||||
</p>
|
||||
<p>
|
||||
Resta ferma la possibilità di proporre reclamo al Garante per la protezione dei dati
|
||||
personali, qualora si ritenga che il trattamento avvenga in violazione della normativa
|
||||
applicabile.
|
||||
</p>
|
||||
|
||||
<h2>10. Natura del conferimento</h2>
|
||||
<p>
|
||||
Il conferimento dei dati eventualmente richiesti nei moduli del sito è facoltativo, ma il
|
||||
mancato conferimento può impedire alla clinica di dare seguito alla richiesta dell'utente.
|
||||
</p>
|
||||
</LegalPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -11,13 +11,37 @@
|
||||
- endoscopia
|
||||
- laparoscopia
|
||||
|
||||
- viite specialistiche
|
||||
- visite specialistiche
|
||||
- oncologia
|
||||
- dermatologia
|
||||
- oculistica
|
||||
- nutrizione
|
||||
- ortopedia
|
||||
|
||||
- team medico
|
||||
- Team medico
|
||||
|
||||
Dott. Paolo Parmeggiani
|
||||
|
||||
|
||||
|
||||
Dott.ssa Irene Paganelli
|
||||
Dott. Simone Tinti
|
||||
Dott.ssa Michela Sghedoni
|
||||
Dott. Luca Pietri
|
||||
|
||||
-Collaborazioni
|
||||
|
||||
Dott. Andrea Arrigoni
|
||||
Dott. Alessandro Poli
|
||||
Dott.ssa Elena Venturelli
|
||||
Dott.ssa Cinzia Pellegrini
|
||||
|
||||
-Team Tecnici Veterinari
|
||||
|
||||
Alessia Succi
|
||||
Francesca Polato
|
||||
Cristina Vecchi
|
||||
Giada Palumbo
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$path = '%CADDYFILE%';" ^
|
||||
"$content = Get-Content -Raw $path;" ^
|
||||
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %LIVE_ROOT%' + [Environment]::NewLine + ' try_files {path} /index.html' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' route {' + [Environment]::NewLine + ' handle /api/* {' + [Environment]::NewLine + ' reverse_proxy 127.0.0.1:8000' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' handle {' + [Environment]::NewLine + ' root * %LIVE_ROOT%' + [Environment]::NewLine + ' try_files {path} /index.html' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' }' + [Environment]::NewLine + '}';" ^
|
||||
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||
if errorlevel 1 (
|
||||
@@ -34,7 +34,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$path = '%CADDYFILE%';" ^
|
||||
"$content = Get-Content -Raw $path;" ^
|
||||
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %COMING_SOON_ROOT%' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' route {' + [Environment]::NewLine + ' handle /api/* {' + [Environment]::NewLine + ' reverse_proxy 127.0.0.1:8000' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' handle {' + [Environment]::NewLine + ' root * %COMING_SOON_ROOT%' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' }' + [Environment]::NewLine + '}';" ^
|
||||
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||
if errorlevel 1 (
|
||||
|
||||