2 Commits

Author SHA1 Message Date
57367b75c8 versione 1.0 2026-05-23 22:50:13 +02:00
f8e6daa084 Aggiunge visite specialistiche e aggiorna sezione servizi 2026-05-23 11:43:28 +02:00
50 changed files with 1272 additions and 465 deletions

1
.gitignore vendored
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/news_cucciolo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/news_gatto.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/ortovet.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -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]

View 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.",
)

View File

@@ -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()

View File

@@ -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)

View 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

View 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)

View File

@@ -4,3 +4,4 @@ sqlalchemy
pydantic
python-dotenv
aiosqlite
pydantic[email]

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -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>

View File

@@ -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"
/>

View File

@@ -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 = [
"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&apos;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&apos;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>

View File

@@ -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&apos;invio del modulo non costituisce conferma automatica dell&apos;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&apos;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>
)}

View File

@@ -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">

View 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>
);
}

View File

@@ -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>

View File

@@ -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",
},
];

View File

@@ -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." });
}}

View File

@@ -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>
);
}

View 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&apos;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&apos;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&apos;utente. Per tali strumenti, secondo la normativa
applicabile, non è richiesto il consenso preventivo dell&apos;utente, fermo restando l&apos;obbligo
di fornire un&apos;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 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&apos;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&apos;area riservata potranno essere utilizzati cookie
o identificatori tecnici di sessione strettamente necessari all&apos;autenticazione e alla gestione
sicura dell&apos;accesso. Anche tali strumenti, se limitati alla funzione tecnica richiesta
dall&apos;utente, rientrano normalmente tra quelli esenti da consenso preventivo.
</p>
<h2>7. Come gestire i cookie</h2>
<p>
L&apos;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&apos;uso dei cookie e degli altri strumenti tecnici è possibile
contattare la Clinica Veterinaria Formiginese all&apos;indirizzo{" "}
<a href="mailto:clinicaveterinariaformiginese@gmail.com">
clinicaveterinariaformiginese@gmail.com
</a>.
</p>
</LegalPageLayout>
);
}

View 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&apos;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 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&apos;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&apos;utilizzo del sito o dall&apos;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&apos;uso e presenti note legali, senza obbligo di preavviso.
</p>
<h2>9. Legge applicabile</h2>
<p>
L&apos;utilizzo del sito è regolato dalla legge italiana. Restano salve le tutele eventualmente
previste dalla normativa inderogabile applicabile agli utenti.
</p>
</LegalPageLayout>
);
}

View 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&apos;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&apos;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&apos;art. 6, par. 1, lett. b)
del Regolamento (UE) 2016/679 per l&apos;esecuzione di misure precontrattuali richieste
dall&apos;interessato, dell&apos;art. 6, par. 1, lett. c) per obblighi di legge, e dell&apos;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&apos;eventuale definizione dei successivi rapporti organizzativi o
amministrativi con l&apos;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&apos;utente.
</p>
</LegalPageLayout>
);
}

View File

@@ -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

View File

@@ -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 (