17 Commits

84 changed files with 2332 additions and 695 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Python local environments
backend/.venv/
__pycache__/
*.py[cod]
# Local environment files
.env
backend/.env
perl_mail.txt
# Local SQLite databases
data/*.db
data/*.db-*
# Logs and temporary files
*.log
*.tmp

BIN
assets/cavia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
assets/coniglio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
assets/logo_high.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
assets/logo_low.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

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: 1.6 MiB

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

13
backend/.env.example Normal file
View File

@@ -0,0 +1,13 @@
APP_NAME=Clinica Veterinaria Formiginese API
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,22 @@
@echo off
setlocal
set "BACKEND_DIR=C:\devel\clinica_veterinaria_formiginese\backend"
set "VENV_ACTIVATE=%BACKEND_DIR%\.venv\Scripts\activate.bat"
if not exist "%VENV_ACTIVATE%" (
echo ERRORE: virtual environment non trovato.
echo Crea prima il venv con:
echo py -3.13 -m venv .venv
exit /b 1
)
cd /d "%BACKEND_DIR%"
call "%VENV_ACTIVATE%"
echo.
echo Ambiente backend attivo in %BACKEND_DIR%
echo Per uscire usa: deactivate
echo.
cmd /k

1
backend/app/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Backend package for the Clinica Veterinaria Formiginese project.

View File

@@ -0,0 +1 @@
# API routers package.

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

29
backend/app/api/health.py Normal file
View File

@@ -0,0 +1,29 @@
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.config import settings
from app.db.session import get_db
router = APIRouter(prefix="/api", tags=["health"])
@router.get("/health")
def healthcheck() -> dict[str, str]:
return {
"status": "ok",
"environment": settings.app_env,
"app_name": settings.app_name,
}
@router.get("/db-health")
def database_healthcheck(db: Session = Depends(get_db)) -> dict[str, str | int]:
result = db.execute(text("SELECT 1")).scalar_one()
return {
"status": "ok",
"database": "connected",
"result": result,
"sqlite_file": settings.sqlite_file_path or "not-applicable",
}

57
backend/app/config.py Normal file
View File

@@ -0,0 +1,57 @@
from pathlib import Path
import os
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_ROOT = BASE_DIR.parent
ENV_FILE = BASE_DIR / ".env"
ROOT_ENV_FILE = PROJECT_ROOT / ".env"
load_dotenv(ROOT_ENV_FILE)
load_dotenv(ENV_FILE, override=True)
class Settings:
def __init__(self) -> None:
self.app_name = os.getenv("APP_NAME", "Clinica Veterinaria Formiginese API")
self.app_env = os.getenv("APP_ENV", "development")
self.app_host = os.getenv("APP_HOST", "127.0.0.1")
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:
sqlite_prefix = "sqlite:///"
if self.database_url.startswith(sqlite_prefix):
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

@@ -0,0 +1 @@
# Database package.

5
backend/app/db/base.py Normal file
View File

@@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass

24
backend/app/db/init_db.py Normal file
View File

@@ -0,0 +1,24 @@
import hashlib
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.db.base import Base
from app.db.session import SessionLocal, engine
from app.models import TestUser
def init_db() -> None:
Base.metadata.create_all(bind=engine)
seed_test_data()
def seed_test_data() -> None:
with SessionLocal() as db:
existing_user = db.scalar(select(TestUser).where(TestUser.username == "admin_test"))
if existing_user:
return
password_hash = hashlib.sha256("change_me_123".encode("utf-8")).hexdigest()
db.add(TestUser(username="admin_test", password_hash=password_hash))
db.commit()

25
backend/app/db/session.py Normal file
View File

@@ -0,0 +1,25 @@
from collections.abc import Generator
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from app.config import settings
if settings.sqlite_file_path:
Path(settings.sqlite_file_path).parent.mkdir(parents=True, exist_ok=True)
engine = create_engine(
settings.database_url,
connect_args={"check_same_thread": False} if settings.database_url.startswith("sqlite") else {},
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()

13
backend/app/main.py Normal file
View File

@@ -0,0 +1,13 @@
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
init_db()
app = FastAPI(title=settings.app_name)
app.include_router(health_router)
app.include_router(booking_router)

View File

@@ -0,0 +1,3 @@
from app.models.test_user import TestUser
__all__ = ["TestUser"]

View File

@@ -0,0 +1,12 @@
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class TestUser(Base):
__tablename__ = "test_users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
password_hash: Mapped[str] = mapped_column(String(128), nullable=False)

View File

@@ -0,0 +1 @@
# Data access layer package.

View File

@@ -0,0 +1 @@
# Pydantic schemas package.

View File

@@ -0,0 +1,18 @@
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)
doctor: str = Field(min_length=2, max_length=120)
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 @@
# Business services package.

View File

@@ -0,0 +1,80 @@
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>Medico richiesto</strong></td><td>{payload.doctor}</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"Medico richiesto: {payload.doctor}\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)

7
backend/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
sqlalchemy
pydantic
python-dotenv
aiosqlite
pydantic[email]

17
backend/run_backend.bat Normal file
View File

@@ -0,0 +1,17 @@
@echo off
setlocal
set "BACKEND_DIR=C:\devel\clinica_veterinaria_formiginese\backend"
set "PYTHON_EXE=%BACKEND_DIR%\.venv\Scripts\python.exe"
if not exist "%PYTHON_EXE%" (
echo ERRORE: python del virtual environment non trovato.
echo Crea prima il venv con:
echo py -3.13 -m venv .venv
echo Poi installa le dipendenze con:
echo pip install -r requirements.txt
exit /b 1
)
cd /d "%BACKEND_DIR%"
"%PYTHON_EXE%" -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload

View File

@@ -12,9 +12,5 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script
defer
src="%VITE_ANALYTICS_ENDPOINT%/umami"
data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
</body> </body>
</html> </html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

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: 1.6 MiB

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 { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import CookiePolicyPage from "@/pages/CookiePolicyPage";
import LegalNotesPage from "@/pages/LegalNotesPage";
import NotFound from "@/pages/NotFound"; import NotFound from "@/pages/NotFound";
import PrivacyPolicyPage from "@/pages/PrivacyPolicyPage";
import { Route, Switch } from "wouter"; import { Route, Switch } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary"; import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext"; import { ThemeProvider } from "./contexts/ThemeContext";
@@ -10,6 +13,9 @@ function Router() {
return ( return (
<Switch> <Switch>
<Route path={"/"} component={Home} /> <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 path={"/404"} component={NotFound} />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>

View File

@@ -97,7 +97,7 @@ export default function AboutSection() {
{/* Immagine principale */} {/* Immagine principale */}
<div className="relative rounded-2xl overflow-hidden shadow-2xl"> <div className="relative rounded-2xl overflow-hidden shadow-2xl">
<img <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" alt="Cane e gatto insieme — la nostra missione"
className="w-full h-[480px] object-cover" className="w-full h-[480px] object-cover"
/> />
@@ -105,19 +105,6 @@ export default function AboutSection() {
<div className="absolute inset-0 bg-gradient-to-t from-[#1B4F72]/30 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-[#1B4F72]/30 to-transparent" />
</div> </div>
{/* Badge flottante */}
<div className="absolute -bottom-6 -left-6 bg-white rounded-2xl shadow-xl p-5 max-w-[200px]">
<div
className="text-[#1B4F72] font-bold mb-1"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "2.5rem" }}
>
15+
</div>
<div className="text-gray-600 text-xs leading-tight">
Anni di esperienza nella cura degli animali
</div>
</div>
{/* Decorazione */} {/* Decorazione */}
<div className="absolute -top-4 -right-4 w-24 h-24 rounded-full bg-[#4ECDC4]/10 -z-10" /> <div className="absolute -top-4 -right-4 w-24 h-24 rounded-full bg-[#4ECDC4]/10 -z-10" />
<div className="absolute -bottom-8 right-8 w-16 h-16 rounded-full bg-[#1B4F72]/10 -z-10" /> <div className="absolute -bottom-8 right-8 w-16 h-16 rounded-full bg-[#1B4F72]/10 -z-10" />

View File

@@ -1,65 +1,76 @@
/* /*
* DESIGN: "Clinical Warmth" * DESIGN: "Clinical Warmth"
* Sezione registrazione/login: tabs con form eleganti * Sezione registrazione/login visibile ma non ancora attiva.
* Sfondo bianco, accenti blu petrolio e verde acqua * Gli elementi restano leggibili, ma ogni interazione mostra
* il messaggio "Servizio in corso di attivazione".
*/ */
import { useState } from "react"; import { motion, useInView } from "framer-motion";
import { motion } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef } from "react"; import { useRef } from "react";
import { Button } from "@/components/ui/button"; 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"; import { toast } from "sonner";
export default function AuthSection() { const benefits = [
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", "Storico visite e referti digitali",
"Promemoria vaccinazioni automatici", "Promemoria vaccinazioni automatici",
"Prenotazioni online prioritarie", "Prenotazioni online prioritarie",
"Comunicazioni dirette con il veterinario", "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 ( 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="container">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center"> <div className="grid grid-cols-1 items-center gap-16 lg:grid-cols-2">
{/* Colonna sinistra: benefici */}
<motion.div <motion.div
ref={ref} ref={ref}
initial={{ opacity: 0, x: -30 }} initial={{ opacity: 0, x: -30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}} animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
> >
<div className="flex items-center gap-3 mb-4"> <div className="mb-4 flex items-center gap-3">
<div className="w-12 h-0.5 bg-[#4ECDC4]" /> <div className="h-0.5 w-12 bg-[#4ECDC4]" />
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest"> <span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
Area Personale Area Personale
</span> </span>
</div> </div>
<h2 <h2
className="text-[#1B4F72] mb-6" className="mb-6 text-[#1B4F72]"
style={{ style={{
fontFamily: "'Cormorant Garamond', serif", fontFamily: "'Cormorant Garamond', serif",
fontSize: "clamp(2rem, 4vw, 3rem)", fontSize: "clamp(2rem, 4vw, 3rem)",
@@ -67,210 +78,148 @@ export default function AuthSection() {
lineHeight: 1.2, lineHeight: 1.2,
}} }}
> >
Registrati e gestisci{" "} Accedi e gestisci{" "}
<span className="italic">la salute del tuo animale</span> <span className="italic">la salute del tuo animale</span>
</h2> </h2>
<p className="text-gray-600 leading-relaxed mb-8 text-base"> <p className="mb-8 text-base leading-relaxed text-gray-600">
Crea il tuo profilo personale per accedere a tutti i servizi digitali della clinica. L&apos;area riservata e la registrazione online sono in fase di attivazione.
Tieni traccia della storia clinica del tuo animale, ricevi promemoria e prenota Qui i clienti potranno gestire i propri dati, consultare lo storico e accedere
le visite in pochi click. ai servizi digitali della clinica in uno spazio personale dedicato.
</p> </p>
{/* Benefits list */} <div className="mb-8 space-y-3">
<div className="space-y-3 mb-8">
{benefits.map((benefit) => ( {benefits.map((benefit) => (
<div key={benefit} className="flex items-center gap-3"> <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]" /> <CheckCircle2 size={14} className="text-[#4ECDC4]" />
</div> </div>
<span className="text-gray-700 text-sm">{benefit}</span> <span className="text-sm text-gray-700">{benefit}</span>
</div> </div>
))} ))}
</div> </div>
{/* Decorazione */} <div className="flex items-center gap-4 rounded-2xl bg-[#F5F0E8] p-6">
<div className="bg-[#F5F0E8] rounded-2xl p-6 flex items-center gap-4"> <div className="flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full bg-[#1B4F72]">
<div className="w-14 h-14 rounded-full bg-[#1B4F72] flex items-center justify-center flex-shrink-0">
<PawPrint size={24} className="text-[#4ECDC4]" /> <PawPrint size={24} className="text-[#4ECDC4]" />
</div> </div>
<div> <div>
<p className="text-[#1B4F72] font-semibold text-sm">Già più di 500 famiglie</p> <p className="text-sm font-semibold text-[#1B4F72]">Servizio in attivazione</p>
<p className="text-gray-600 text-xs mt-0.5"> <p className="mt-0.5 text-xs text-gray-600">
si affidano alla nostra clinica per la cura dei loro animali La clinica sta completando la configurazione dell&apos;area riservata per offrirti
un accesso semplice e sicuro.
</p> </p>
</div> </div>
</div> </div>
</motion.div> </motion.div>
{/* Colonna destra: form */}
<motion.div <motion.div
initial={{ opacity: 0, x: 30 }} initial={{ opacity: 0, x: 30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}} animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
> >
{registered ? ( <div className="relative overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-lg">
<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="flex border-b border-gray-100"> <div className="flex border-b border-gray-100">
{(["register", "login"] as const).map((t) => (
<button <button
key={t} type="button"
onClick={() => setTab(t)} disabled
className={`flex-1 py-4 text-sm font-semibold transition-all duration-200 ${ className="flex-1 cursor-not-allowed border-b-2 border-[#4ECDC4] bg-[#F5F0E8]/50 py-4 text-sm font-semibold text-[#1B4F72]"
tab === t
? "text-[#1B4F72] border-b-2 border-[#4ECDC4] bg-[#F5F0E8]/50"
: "text-gray-400 hover:text-gray-600"
}`}
> >
{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> </button>
))}
</div> </div>
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-4"> <div className="p-6 md:p-8">
{tab === "register" && ( <div className="mb-5 flex items-center gap-3 rounded-xl border border-[#E4D7C6] bg-[#FFF9F1] px-4 py-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <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> <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 Nome
</label> </label>
<div className="relative"> <DisabledInput icon={User} type="text" placeholder="Mario" />
<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>
</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">
Cognome Cognome
</label> </label>
<div className="relative"> <DisabledInput icon={User} type="text" placeholder="Rossi" />
<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"
/>
</div> </div>
</div> </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 Email
</label> </label>
<div className="relative"> <DisabledInput icon={Mail} type="email" placeholder="mario.rossi@email.it" />
<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>
</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">
Password Password
</label> </label>
<div className="relative"> <div className="relative">
<Lock size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" /> <Lock size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input <input
type={showPassword ? "text" : "password"} type="password"
required
placeholder="••••••••" 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 <span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
type="button" <Eye size={15} />
onClick={() => setShowPassword(!showPassword)} </span>
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>
</div> </div>
</div> </div>
{tab === "register" && (
<div className="flex items-start gap-2 pt-1"> <div className="flex items-start gap-2 pt-1">
<input <input
type="checkbox" type="checkbox"
required disabled
id="privacy" className="mt-0.5 cursor-not-allowed accent-[#4ECDC4]"
className="mt-0.5 accent-[#4ECDC4]"
/> />
<label htmlFor="privacy" className="text-xs text-gray-500 leading-relaxed"> <p className="text-xs leading-relaxed text-gray-500">
Accetto la{" "} Accetto la Privacy Policy e i Termini di Servizio.
<button </p>
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>
</div> </div>
)}
<Button <Button
type="submit" type="button"
className="w-full bg-[#1B4F72] hover:bg-[#163d5a] text-white font-bold py-3 text-base transition-all duration-300" 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> </Button>
{tab === "login" && ( <p className="text-center text-xs text-gray-400">Password dimenticata?</p>
<p className="text-center text-xs text-gray-400"> </div>
</div>
<button <button
type="button" type="button"
className="text-[#4ECDC4] hover:underline" aria-label="Servizio in corso di attivazione"
onClick={() => toast.info("Recupero password in arrivo")} onClick={showActivationToast}
> className="absolute inset-0 z-10"
Password dimenticata? />
</button>
</p>
)}
</form>
</div> </div>
)}
</motion.div> </motion.div>
</div> </div>
</div> </div>

View File

@@ -1,68 +1,166 @@
/* /*
* DESIGN: "Clinical Warmth" * DESIGN: "Clinical Warmth"
* Sezione prenotazione: form elegante su sfondo blu petrolio * Sezione prenotazione: form to mail con conferma non vincolante.
* Layout: testo a sinistra + form a destra
*/ */
import { useState } from "react"; import { useRef, useState } from "react";
import { motion } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { useInView } from "framer-motion"; import {
import { useRef } from "react"; Calendar,
import { Button } from "@/components/ui/button"; CheckCircle2,
import { Calendar, Clock, User, Phone, PawPrint, CheckCircle2 } from "lucide-react"; Clock,
PawPrint,
Phone,
ShieldCheck,
User,
} from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button";
const services = [ const services = [
"Visita generale", "Visita clinica generale",
"Radiologia / Ecografia", "Ecografia",
"Chirurgia (consulenza)", "Radiologia",
"Laboratorio analisi", "Laboratorio",
"Vaccinazione", "Vaccinazione",
"Dermatologia",
"Odontoiatria",
"Oncologia", "Oncologia",
"Dermatologia",
"Oculistica",
"Nutrizione",
"Ortopedia",
"Endoscopia",
"Laparoscopia",
];
const doctors = [
"Dott. Paolo Parmeggiani",
"Dott.ssa Irene Paganelli",
"Dott. Simone Tinti",
"Dott.ssa Michela Sghedoni",
"Dott. Luca Pietri",
"Dott.ssa Sara Casali",
"Dott. Riccardo Suffritti",
"Dott.ssa Elena Venturelli",
"Dott.ssa Cinzia Pellegrini",
];
const openingHours = [
{ days: "Visite: Lunedi - Venerdi", hours: "09:00 - 19:30" },
{ days: "Visite: Sabato", hours: "09:00 - 17:00" },
{ days: "Urgenze: Lunedi - Venerdi", hours: "08:00 - 22:30" },
{ days: "Urgenze: Sabato e Festivi", hours: "09:00 - 20:00" },
]; ];
const timeSlots = [ const timeSlots = [
"09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "09:00",
"14:30", "15:00", "15:30", "16:00", "16:30", "17:00", "09:30",
"10:00",
"10:30",
"11:00",
"11:30",
"14:30",
"15:00",
"15:30",
"16:00",
"16:30",
"17:00",
]; ];
type BookingFormState = {
name: string;
phone: string;
petName: string;
petType: string;
doctor: string;
service: string;
date: string;
time: string;
notes: string;
};
const initialForm: BookingFormState = {
name: "",
phone: "",
petName: "",
petType: "cane",
doctor: "",
service: "",
date: "",
time: "",
notes: "",
};
export default function BookingSection() { export default function BookingSection() {
const ref = useRef(null); const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-80px" }); const isInView = useInView(ref, { once: true, margin: "-80px" });
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const [form, setForm] = useState({ const [submitting, setSubmitting] = useState(false);
name: "", const [submittedName, setSubmittedName] = useState("");
phone: "", const [form, setForm] = useState<BookingFormState>(initialForm);
petName: "",
petType: "cane",
service: "",
date: "",
time: "",
notes: "",
});
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!form.name || !form.phone || !form.service || !form.date) {
if (!form.name || !form.phone || !form.doctor || !form.service || !form.date) {
toast.error("Compila tutti i campi obbligatori"); toast.error("Compila tutti i campi obbligatori");
return; return;
} }
setSubmitted(true);
toast.success("Richiesta inviata!", { try {
description: "Ti contatteremo entro 24 ore per confermare l'appuntamento.", 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,
doctor: form.doctor,
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 e verificato un problema durante l'invio.";
toast.error("Invio non riuscito", {
description: message,
});
} finally {
setSubmitting(false);
}
}; };
return ( return (
<section id="prenotazione" className="py-20 md:py-28 bg-[#1B4F72] relative overflow-hidden"> <section id="prenotazione" className="relative overflow-hidden bg-[#1B4F72] py-20 md:py-28">
{/* Decorazioni di sfondo */} <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 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 h-64 w-64 -translate-x-1/2 translate-y-1/2 rounded-full bg-[#4ECDC4]/10" />
<div className="absolute bottom-0 left-0 w-64 h-64 rounded-full bg-[#4ECDC4]/10 translate-y-1/2 -translate-x-1/2" />
<div className="container relative z-10"> <div className="container relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start"> <div className="grid grid-cols-1 items-start gap-16 lg:grid-cols-2">
{/* Colonna sinistra: testo */}
<motion.div <motion.div
ref={ref} ref={ref}
initial={{ opacity: 0, x: -30 }} initial={{ opacity: 0, x: -30 }}
@@ -70,15 +168,15 @@ export default function BookingSection() {
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
className="text-white" className="text-white"
> >
<div className="flex items-center gap-3 mb-4"> <div className="mb-4 flex items-center gap-3">
<div className="w-12 h-0.5 bg-[#4ECDC4]" /> <div className="h-0.5 w-12 bg-[#4ECDC4]" />
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest"> <span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
Prenotazioni Prenotazioni
</span> </span>
</div> </div>
<h2 <h2
className="text-white mb-6" className="mb-6 text-white"
style={{ style={{
fontFamily: "'Cormorant Garamond', serif", fontFamily: "'Cormorant Garamond', serif",
fontSize: "clamp(2rem, 4vw, 3rem)", fontSize: "clamp(2rem, 4vw, 3rem)",
@@ -86,93 +184,139 @@ export default function BookingSection() {
lineHeight: 1.2, lineHeight: 1.2,
}} }}
> >
Prenota la tua visita{" "} Richiedi una visita <span className="italic text-[#4ECDC4]">online</span>
<span className="italic text-[#4ECDC4]">online</span>
</h2> </h2>
<p className="text-white/80 leading-relaxed mb-8 text-base"> <p className="mb-8 text-base leading-relaxed text-white/80">
Compila il modulo per richiedere un appuntamento. Ti contatteremo entro 24 ore Compila il modulo per inviare una richiesta di appuntamento. La richiesta
per confermare la data e l'orario. Per urgenze, chiama direttamente il numero non e vincolante e dovra essere confermata dallo staff della clinica, che ti
dedicato disponibile 24 ore su 24. ricontattera il prima possibile.
</p> </p>
{/* Info box urgenze */} <div className="mb-8 rounded-xl border border-white/20 bg-white/10 p-5 backdrop-blur-sm">
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-5 border border-white/20 mb-8"> <p className="mb-2 text-sm font-semibold uppercase tracking-wide text-[#4ECDC4]">
<p className="text-[#4ECDC4] font-semibold text-sm mb-2 uppercase tracking-wide">
Urgenze 24h Urgenze 24h
</p> </p>
<a <a
href="tel:3205322439" 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" }} style={{ fontFamily: "'Cormorant Garamond', serif" }}
> >
320 532.24.39 320 532.24.39
</a> </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>
{/* Orari */} <div className="mb-8 rounded-2xl border border-[#4ECDC4]/30 bg-[#14384F] p-5">
<div className="space-y-3"> <div className="flex items-start gap-3">
<p className="text-white/70 text-sm font-semibold uppercase tracking-wide mb-3"> <div className="mt-0.5 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-[#4ECDC4]/15">
Orari di apertura <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 verifichera disponibilita, tipologia di visita e urgenza del caso prima
di confermare data e orario.
</p> </p>
{[ </div>
{ days: "Lunedì — Venerdì", hours: "09:00 — 12:30 · 14:30 — 19:00" }, </div>
{ days: "Sabato", hours: "09:00 — 12:30" }, </div>
{ days: "Domenica", hours: "Solo urgenze" },
].map((slot) => ( <div className="max-w-[39rem]">
<div key={slot.days} className="flex justify-between items-center text-sm"> <div className="rounded-[26px] border border-white/20 bg-white/10 px-6 py-6 shadow-[0_18px_45px_rgba(0,0,0,0.18)] backdrop-blur-md sm:px-7 sm:py-7">
<span className="text-white/70">{slot.days}</span> <h4
<span className="text-white font-medium">{slot.hours}</span> className="mb-5 text-white/95 uppercase tracking-[0.28em]"
style={{
fontFamily: "'Nunito Sans', sans-serif",
fontSize: "0.8rem",
fontWeight: 700,
}}
>
Orari di apertura
</h4>
<div className="space-y-4">
{openingHours.map((slot) => (
<div key={slot.days} className="flex items-start gap-3.5">
<div className="mt-0.5 rounded-full bg-[#4ECDC4]/20 p-2">
<Clock size={14} className="text-[#7ce3dc]" />
</div>
<div>
<p className="text-sm font-semibold text-white/92 sm:text-[0.98rem]">
{slot.days}
</p>
<p className="mt-0.5 text-sm text-white/72 sm:text-[0.96rem]">
{slot.hours}
</p>
</div>
</div> </div>
))} ))}
</div> </div>
</div>
</div>
</motion.div> </motion.div>
{/* Colonna destra: form */}
<motion.div <motion.div
initial={{ opacity: 0, x: 30 }} initial={{ opacity: 0, x: 30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}} animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
> >
{submitted ? ( {submitted ? (
<div className="bg-white rounded-2xl p-8 text-center shadow-2xl"> <div className="rounded-2xl bg-white 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="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-[#4ECDC4]/15">
<CheckCircle2 size={32} className="text-[#4ECDC4]" /> <CheckCircle2 size={32} className="text-[#4ECDC4]" />
</div> </div>
<h3 <h3
className="text-[#1B4F72] mb-2" className="mb-2 text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.8rem" }} style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.9rem" }}
> >
Richiesta inviata! Grazie {submittedName || "per la tua richiesta"}
</h3> </h3>
<p className="text-gray-600 text-sm mb-6"> <p className="mb-3 text-sm leading-relaxed text-gray-600">
Abbiamo ricevuto la tua richiesta di appuntamento. Ti contatteremo entro 24 ore La tua richiesta di prenotazione e stata inviata correttamente.
per confermare data e orario.
</p> </p>
<p className="mb-6 text-sm leading-relaxed text-gray-600">
Il team della Clinica Veterinaria Formiginese la prendera in carico il prima
possibile e ti ricontattera per confermare disponibilita, 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 gia confermata.
</div>
<Button <Button
className="bg-[#1B4F72] hover:bg-[#163d5a] text-white" className="bg-[#1B4F72] text-white hover:bg-[#163d5a]"
onClick={() => setSubmitted(false)} onClick={() => setSubmitted(false)}
> >
Nuova prenotazione Invia una nuova richiesta
</Button> </Button>
</div> </div>
) : ( ) : (
<form <form
onSubmit={handleSubmit} 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 <h3
className="text-[#1B4F72] mb-2" className="mb-2 text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.5rem", fontWeight: 600 }} style={{
fontFamily: "'Cormorant Garamond', serif",
fontSize: "1.5rem",
fontWeight: 600,
}}
> >
Richiedi un appuntamento Richiedi un appuntamento
</h3> </h3>
<p className="text-sm leading-relaxed text-gray-500">
Compila i campi richiesti e inviaci una proposta di data: sara lo staff a
confermare la visita.
</p>
</div>
{/* Nome e telefono */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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">
Nome e Cognome * Nome e Cognome *
</label> </label>
<div className="relative"> <div className="relative">
@@ -183,12 +327,13 @@ export default function BookingSection() {
placeholder="Mario Rossi" placeholder="Mario Rossi"
value={form.name} value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })} 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> </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 * Telefono *
</label> </label>
<div className="relative"> <div className="relative">
@@ -199,17 +344,16 @@ export default function BookingSection() {
placeholder="333 123 4567" placeholder="333 123 4567"
value={form.phone} value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })} 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> </div>
</div> </div>
{/* Animale */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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">
Nome dell'animale Nome dell&apos;animale
</label> </label>
<div className="relative"> <div className="relative">
<PawPrint size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" /> <PawPrint size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
@@ -218,18 +362,19 @@ export default function BookingSection() {
placeholder="Fido" placeholder="Fido"
value={form.petName} value={form.petName}
onChange={(e) => setForm({ ...form, petName: e.target.value })} 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> </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 Tipo di animale
</label> </label>
<select <select
value={form.petType} value={form.petType}
onChange={(e) => setForm({ ...form, petType: e.target.value })} 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="cane">Cane</option>
<option value="gatto">Gatto</option> <option value="gatto">Gatto</option>
@@ -238,28 +383,47 @@ export default function BookingSection() {
</div> </div>
</div> </div>
{/* Servizio */}
<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">
Medico richiesto *
</label>
<select
required
value={form.doctor}
onChange={(e) => setForm({ ...form, doctor: e.target.value })}
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 medico</option>
{doctors.map((doctor) => (
<option key={doctor} value={doctor}>
{doctor}
</option>
))}
</select>
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
Tipo di visita * Tipo di visita *
</label> </label>
<select <select
required required
value={form.service} value={form.service}
onChange={(e) => setForm({ ...form, service: e.target.value })} 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> <option value="">Seleziona un servizio</option>
{services.map((s) => ( {services.map((service) => (
<option key={s} value={s}>{s}</option> <option key={service} value={service}>
{service}
</option>
))} ))}
</select> </select>
</div> </div>
{/* Data e ora */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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">
Data preferita * Data preferita *
</label> </label>
<div className="relative"> <div className="relative">
@@ -270,12 +434,13 @@ export default function BookingSection() {
value={form.date} value={form.date}
min={new Date().toISOString().split("T")[0]} min={new Date().toISOString().split("T")[0]}
onChange={(e) => setForm({ ...form, date: e.target.value })} 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> </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 Orario preferito
</label> </label>
<div className="relative"> <div className="relative">
@@ -283,20 +448,21 @@ export default function BookingSection() {
<select <select
value={form.time} value={form.time}
onChange={(e) => setForm({ ...form, time: e.target.value })} 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> <option value="">Qualsiasi orario</option>
{timeSlots.map((t) => ( {timeSlots.map((time) => (
<option key={t} value={t}>{t}</option> <option key={time} value={time}>
{time}
</option>
))} ))}
</select> </select>
</div> </div>
</div> </div>
</div> </div>
{/* Note */}
<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">
Note aggiuntive Note aggiuntive
</label> </label>
<textarea <textarea
@@ -304,19 +470,20 @@ export default function BookingSection() {
placeholder="Descrivi brevemente il motivo della visita..." placeholder="Descrivi brevemente il motivo della visita..."
value={form.notes} value={form.notes}
onChange={(e) => setForm({ ...form, notes: e.target.value })} 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> </div>
<Button <Button
type="submit" 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> </Button>
<p className="text-xs text-gray-400 text-center"> <p className="text-center text-xs text-gray-500">
* Campi obbligatori. Ti contatteremo entro 24 ore per confermare. * Campi obbligatori. La richiesta sara valutata e confermata dallo staff.
</p> </p>
</form> </form>
)} )}

View File

@@ -4,17 +4,15 @@
* Sfondo blu petrolio scuro, testo bianco/grigio chiaro * Sfondo blu petrolio scuro, testo bianco/grigio chiaro
*/ */
import { MapPin, Phone, Mail, Facebook, Clock, ArrowRight } from "lucide-react"; import { MapPin, Phone, Mail, Facebook, Clock, ArrowRight } from "lucide-react";
import { toast } from "sonner";
const quickLinks = [ const quickLinks = [
{ label: "Chi Siamo", href: "#chi-siamo" }, { label: "Chi Siamo", href: "/#chi-siamo" },
{ label: "Radiologia", href: "#servizi" }, { label: "Radiologia", href: "/#servizi" },
{ label: "Chirurgia", href: "#servizi" }, { label: "Chirurgia", href: "/#servizi" },
{ label: "Laboratorio", href: "#servizi" }, { label: "Laboratorio", href: "/#servizi" },
{ label: "Il Team", href: "#team" }, { label: "Il Team", href: "/#team" },
{ label: "News & Blog", href: "#news" }, { label: "Prenota Visita", href: "/#prenotazione" },
{ label: "Prenota Visita", href: "#prenotazione" }, { label: "Area Personale", href: "/#registrazione" },
{ label: "Area Personale", href: "#registrazione" },
]; ];
export default function Footer() { export default function Footer() {
@@ -23,20 +21,9 @@ export default function Footer() {
{/* Main footer */} {/* Main footer */}
<div className="container py-16"> <div className="container py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{/* Brand */} {/* Brand
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<div className="flex items-center gap-3 mb-4"> <div className="mb-4">
<div className="w-10 h-10 rounded-full bg-[#1B4F72] flex items-center justify-center">
<svg viewBox="0 0 40 40" fill="none" className="w-6 h-6">
<path d="M20 8c-1.5 0-2.5 1.2-2.5 2.5S18.5 13 20 13s2.5-1.2 2.5-2.5S21.5 8 20 8z" fill="#4ECDC4"/>
<path d="M13 11c-1.2 0-2 1-2 2.2S11.8 15.5 13 15.5s2-1 2-2.2S14.2 11 13 11z" fill="#4ECDC4"/>
<path d="M27 11c-1.2 0-2 1-2 2.2S25.8 15.5 27 15.5s2-1 2-2.2S28.2 11 27 11z" fill="#4ECDC4"/>
<path d="M10 17c-1.2 0-2 1-2 2.2S8.8 21.5 10 21.5s2-1 2-2.2S11.2 17 10 17z" fill="#4ECDC4"/>
<path d="M30 17c-1.2 0-2 1-2 2.2S28.8 21.5 30 21.5s2-1 2-2.2S31.2 17 30 17z" fill="#4ECDC4"/>
<path d="M20 17c-5 0-9 3.5-9 7.5 0 2.5 1.5 4.5 3.5 5.5.5.3 1 .5 1.5.5h8c.5 0 1-.2 1.5-.5 2-1 3.5-3 3.5-5.5 0-4-4-7.5-9-7.5z" fill="white"/>
</svg>
</div>
<div>
<div <div
className="font-bold text-white leading-tight" className="font-bold text-white leading-tight"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem" }} style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem" }}
@@ -47,7 +34,6 @@ export default function Footer() {
Formiginese Formiginese
</div> </div>
</div> </div>
</div>
<p className="text-white/60 text-sm leading-relaxed mb-4"> <p className="text-white/60 text-sm leading-relaxed mb-4">
Cura specialistica per cani e gatti a Formigine. Cura specialistica per cani e gatti a Formigine.
Un team di professionisti al servizio della salute dei tuoi animali. Un team di professionisti al servizio della salute dei tuoi animali.
@@ -61,7 +47,7 @@ export default function Footer() {
<Facebook size={16} /> <Facebook size={16} />
Seguici su Facebook Seguici su Facebook
</a> </a>
</div> </div>*/}
{/* Link rapidi */} {/* Link rapidi */}
<div> <div>
@@ -133,9 +119,11 @@ export default function Footer() {
</h4> </h4>
<div className="space-y-2"> <div className="space-y-2">
{[ {[
{ days: "Lunedì — Venerdì", hours: "09:00 — 12:30\n14:30 — 19:00" }, { days: "Visite: Lunedì — Venerdì", hours: "09:00 — 19:30" },
{ days: "Sabato", hours: "09:00 — 12:30" }, { days: "Visite: Sabato", hours: "09:00 — 17:00" },
{ days: "Domenica", hours: "Solo urgenze" }, { days: "Urgenze: Lunedì — Venerdì", hours: "08:00 — 22:30" },
{ days: "Urgenze: Sabato e Festivi", hours: "09:00 — 20:00" },
].map((slot) => ( ].map((slot) => (
<div key={slot.days} className="flex items-start gap-2"> <div key={slot.days} className="flex items-start gap-2">
<Clock size={13} className="text-[#4ECDC4] flex-shrink-0 mt-0.5" /> <Clock size={13} className="text-[#4ECDC4] flex-shrink-0 mt-0.5" />
@@ -148,12 +136,7 @@ export default function Footer() {
</div> </div>
{/* Badge reperibilità */} {/* Badge reperibilità */}
<div className="mt-4 bg-[#4ECDC4]/15 border border-[#4ECDC4]/30 rounded-lg p-3">
<p className="text-[#4ECDC4] text-xs font-semibold uppercase tracking-wide">
Reperibilità 24h
</p>
<p className="text-white/60 text-xs mt-0.5">7 giorni su 7</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -170,24 +153,15 @@ export default function Footer() {
</p> </p>
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <a href="/privacy-policy" className="hover:text-white/70 transition-colors">
onClick={() => toast.info("Privacy Policy in arrivo")}
className="hover:text-white/70 transition-colors"
>
Privacy Policy Privacy Policy
</button> </a>
<button <a href="/cookie-policy" className="hover:text-white/70 transition-colors">
onClick={() => toast.info("Cookie Policy in arrivo")}
className="hover:text-white/70 transition-colors"
>
Cookie Policy Cookie Policy
</button> </a>
<button <a href="/note-legali" className="hover:text-white/70 transition-colors">
onClick={() => toast.info("Note legali in arrivo")}
className="hover:text-white/70 transition-colors"
>
Note Legali Note Legali
</button> </a>
</div> </div>
</div> </div>
<p className="text-white/25 text-xs mt-3 text-center md:text-left"> <p className="text-white/25 text-xs mt-3 text-center md:text-left">

View File

@@ -1,13 +1,13 @@
/* /*
* DESIGN: "Clinical Warmth" * DESIGN: "Clinical Warmth"
* Hero a schermo intero con immagine animale + overlay gradiente + testo sovrapposto * Hero a schermo intero con immagine animale + overlay gradiente + testo sovrapposto
* Immagine: hero_dog_cat.jpg (golden retriever + gatto in prato soleggiato) * Testo elegante su overlay scuro, card orari raffinata, CTA verdi acqua
* Testo bianco su overlay scuro, CTA verde acqua
*/ */
import { useState, useEffect } from "react"; import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { ChevronDown, Calendar, Phone } from "lucide-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Calendar, ChevronDown, Clock, Phone } from "lucide-react";
import { Button } from "@/components/ui/button";
const heroImages = [ const heroImages = [
{ {
@@ -18,6 +18,10 @@ const heroImages = [
url: "/images/hero_dog_cat.jpg", url: "/images/hero_dog_cat.jpg",
alt: "Golden retriever e gatto tabby insieme in un prato soleggiato", alt: "Golden retriever e gatto tabby insieme in un prato soleggiato",
}, },
{
url: "/images/cavia.png",
alt: "Ritratto di una cavia",
},
{ {
url: "/images/clinica_ingresso1.png", url: "/images/clinica_ingresso1.png",
alt: "Clinica Veterinaria Formiginese - Ingresso e sala d'attesa", alt: "Clinica Veterinaria Formiginese - Ingresso e sala d'attesa",
@@ -34,26 +38,36 @@ const heroImages = [
url: "/images/hero_cat.jpg", url: "/images/hero_cat.jpg",
alt: "Ritratto maestoso di un Maine Coon", alt: "Ritratto maestoso di un Maine Coon",
}, },
{
url: "/images/coniglio.png",
alt: "Ritratto di un coniglio",
},
{ {
url: "/images/clinica_ingresso3.webp", url: "/images/clinica_ingresso3.webp",
alt: "Clinica Veterinaria Formiginese - Ingresso principale", alt: "Clinica Veterinaria Formiginese - Ingresso principale",
}, },
]; ];
const openingHours = [
{ days: "Visite: Lunedi - Venerdi", hours: "09:00 - 19:30" },
{ days: "Visite: Sabato", hours: "09:00 - 17:00" },
{ days: "Urgenze: Lunedi - Venerdi", hours: "08:00 - 22:30" },
{ days: "Urgenze: Sabato e Festivi", hours: "09:00 - 20:00" },
];
export default function HeroSection() { export default function HeroSection() {
const [currentImage, setCurrentImage] = useState(0); const [currentImage, setCurrentImage] = useState(0);
useEffect(() => { useEffect(() => {
// 3 secondi di visualizzazione + 2 secondi di dissolvenza = 5 secondi totali
const interval = setInterval(() => { const interval = setInterval(() => {
setCurrentImage((prev) => (prev + 1) % heroImages.length); setCurrentImage((prev) => (prev + 1) % heroImages.length);
}, 5000); }, 5000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
return ( return (
<section className="relative h-[92vh] min-h-[600px] max-h-[900px] overflow-hidden"> <section className="relative h-[92vh] min-h-[600px] max-h-[900px] overflow-hidden">
{/* Background images con crossfade */}
{heroImages.map((img, index) => ( {heroImages.map((img, index) => (
<div <div
key={img.url} key={img.url}
@@ -63,78 +77,95 @@ export default function HeroSection() {
<img <img
src={img.url} src={img.url}
alt={img.alt} alt={img.alt}
className="w-full h-full object-cover" className="h-full w-full object-cover"
style={{ transform: "scale(1.05)" }} style={{ transform: "scale(1.05)" }}
/> />
</div> </div>
))} ))}
{/* Overlay gradiente scuro */}
<div className="absolute inset-0 bg-gradient-to-r from-[#0d2b3e]/85 via-[#1B4F72]/60 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-r from-[#0d2b3e]/85 via-[#1B4F72]/60 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-[#0d2b3e]/50 via-transparent to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-[#0d2b3e]/50 via-transparent to-transparent" />
{/* Contenuto hero */} <div className="relative z-10 flex h-full items-center">
<div className="relative z-10 h-full flex items-center">
<div className="container"> <div className="container">
<div className="max-w-2xl"> <div className="max-w-3xl">
{/* Badge */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="inline-flex items-center gap-2 bg-[#4ECDC4]/20 border border-[#4ECDC4]/40 text-[#4ECDC4] text-sm font-semibold px-4 py-1.5 rounded-full mb-6 backdrop-blur-sm"
>
<span className="w-2 h-2 rounded-full bg-[#4ECDC4] animate-pulse" />
Reperibilità 24h · 7 giorni su 7
</motion.div>
{/* Titolo principale */}
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }} transition={{ duration: 0.8, delay: 0.3 }}
className="text-white leading-tight mb-4" className="mb-10 max-w-2xl text-white leading-[0.92]"
style={{ style={{
fontFamily: "'Cormorant Garamond', serif", fontFamily: "'Cormorant Garamond', serif",
fontSize: "clamp(2.5rem, 6vw, 4.5rem)", fontSize: "clamp(3.15rem, 6.4vw, 5.5rem)",
fontWeight: 600, fontWeight: 500,
letterSpacing: "0.03em",
textShadow: "0 14px 34px rgba(0,0,0,0.24)",
}} }}
> >
La salute del tuo animale,{" "} <span className="block text-white/92">Clinica Veterinaria</span>
<span className="italic text-[#4ECDC4]">la nostra missione</span> <span className="block italic text-white">Formiginese</span>
</motion.h1> </motion.h1>
{/* Sottotitolo */} <motion.div
<motion.p initial={{ opacity: 0, y: 22 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.5 }} transition={{ duration: 0.7, delay: 0.55 }}
className="text-white/85 text-lg mb-8 leading-relaxed max-w-xl" className="mb-14 max-w-[39rem]"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Clinica veterinaria specialistica a Formigine. Un team di 6 professionisti <div className="rounded-[26px] border border-white/20 bg-white/10 px-6 py-6 shadow-[0_18px_45px_rgba(0,0,0,0.18)] backdrop-blur-md sm:px-7 sm:py-7">
dedicati alla cura di cani e gatti, con tecnologie diagnostiche avanzate. <h4
</motion.p> className="mb-5 text-white/95 uppercase tracking-[0.28em]"
style={{
fontFamily: "'Nunito Sans', sans-serif",
fontSize: "0.8rem",
fontWeight: 700,
}}
>
Orari di apertura
</h4>
<div className="space-y-4">
{openingHours.map((slot) => (
<div key={slot.days} className="flex items-start gap-3.5">
<div className="mt-0.5 rounded-full bg-[#4ECDC4]/20 p-2">
<Clock size={14} className="text-[#7ce3dc]" />
</div>
<div>
<p className="text-sm font-semibold text-white/92 sm:text-[0.98rem]">
{slot.days}
</p>
<p className="mt-0.5 text-sm text-white/72 sm:text-[0.96rem]">
{slot.hours}
</p>
</div>
</div>
))}
</div>
</div>
</motion.div>
{/* CTA buttons */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.7 }} transition={{ duration: 0.7, delay: 0.75 }}
className="flex flex-col sm:flex-row gap-4" className="flex flex-col gap-4 pt-1 sm:flex-row"
> >
<Button <Button
size="lg" size="lg"
className="bg-[#4ECDC4] hover:bg-[#3ab5ad] text-white font-bold text-base px-8 py-6 shadow-lg shadow-[#4ECDC4]/30 transition-all duration-300 hover:shadow-xl hover:shadow-[#4ECDC4]/40 hover:-translate-y-0.5" className="bg-[#4ECDC4] px-8 py-6 text-base font-bold text-white shadow-lg shadow-[#4ECDC4]/30 transition-all duration-300 hover:-translate-y-0.5 hover:bg-[#3ab5ad] hover:shadow-xl hover:shadow-[#4ECDC4]/40"
onClick={() => document.getElementById("prenotazione")?.scrollIntoView({ behavior: "smooth" })} onClick={() =>
document.getElementById("prenotazione")?.scrollIntoView({ behavior: "smooth" })
}
> >
<Calendar size={18} className="mr-2" /> <Calendar size={18} className="mr-2" />
Prenota una Visita Prenota una Visita
</Button> </Button>
<Button <Button
size="lg" size="lg"
variant="outline" variant="outline"
className="border-2 border-white text-white hover:bg-white hover:text-[#1B4F72] font-semibold text-base px-8 py-6 transition-all duration-300 bg-transparent backdrop-blur-sm" className="border-2 border-white bg-transparent px-8 py-6 text-base font-semibold text-white backdrop-blur-sm transition-all duration-300 hover:bg-white hover:text-[#1B4F72]"
asChild asChild
> >
<a href="tel:0598396263"> <a href="tel:0598396263">
@@ -143,54 +174,24 @@ export default function HeroSection() {
</a> </a>
</Button> </Button>
</motion.div> </motion.div>
{/* Stats */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 1.0 }}
className="flex gap-8 mt-12 pt-8 border-t border-white/20"
>
{[
{ value: "6", label: "Specialisti" },
{ value: "15+", label: "Anni di esperienza" },
{ value: "24h", label: "Reperibilità" },
].map((stat) => (
<div key={stat.label} className="text-center">
<div
className="text-[#4ECDC4] font-bold"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "2rem" }}
>
{stat.value}
</div>
<div className="text-white/70 text-xs uppercase tracking-widest mt-0.5">
{stat.label}
</div>
</div>
))}
</motion.div>
</div> </div>
</div> </div>
</div> </div>
{/* Indicatori slideshow */} <div className="absolute bottom-8 left-1/2 z-10 flex -translate-x-1/2 gap-2">
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-10">
{heroImages.map((_, index) => ( {heroImages.map((_, index) => (
<button <button
key={index} key={index}
onClick={() => setCurrentImage(index)} onClick={() => setCurrentImage(index)}
className={`transition-all duration-300 rounded-full ${ className={`rounded-full transition-all duration-300 ${
index === currentImage index === currentImage ? "h-2 w-8 bg-[#4ECDC4]" : "h-2 w-2 bg-white/50 hover:bg-white/80"
? "w-8 h-2 bg-[#4ECDC4]"
: "w-2 h-2 bg-white/50 hover:bg-white/80"
}`} }`}
/> />
))} ))}
</div> </div>
{/* Scroll indicator */} <div className="absolute bottom-8 right-8 z-10 hidden flex-col items-center gap-2 text-white/60 md:flex">
<div className="absolute bottom-8 right-8 z-10 hidden md:flex flex-col items-center gap-2 text-white/60"> <span className="mb-2 rotate-90 text-xs uppercase tracking-widest">Scroll</span>
<span className="text-xs uppercase tracking-widest rotate-90 mb-2">Scroll</span>
<ChevronDown size={16} className="animate-bounce" /> <ChevronDown size={16} className="animate-bounce" />
</div> </div>
</section> </section>

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

@@ -5,13 +5,49 @@
*/ */
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Menu, X, Phone, MapPin } from "lucide-react"; import { ChevronDown, Menu, X, Phone, MapPin } from "lucide-react";
import { toast } from "sonner";
const navLinks = [ type NavLink = {
label: string;
href: string;
children?: Array<{
label: string;
href: string;
}>;
};
const navLinks: NavLink[] = [
{ label: "Chi Siamo", href: "#chi-siamo" }, { label: "Chi Siamo", href: "#chi-siamo" },
{ label: "Servizi", href: "#servizi" }, {
label: "Servizi",
href: "#servizi",
children: [
{ label: "Visite cliniche e medicina preventiva", href: "#servizi" },
{ label: "Ecografia", href: "#servizi" },
{ label: "Radiologia", href: "#servizi" },
{ label: "Laboratorio", href: "#servizi" },
{ label: "Ematologia", href: "#servizi" },
{ label: "Biochimica", href: "#servizi" },
{ label: "Urine", href: "#servizi" },
{ label: "Citologia", href: "#servizi" },
{ label: "Coagulazione", href: "#servizi" },
{ label: "Endoscopia", href: "#servizi" },
{ label: "Laparoscopia", href: "#servizi" },
],
},
{
label: "Visite Specialistiche",
href: "#servizi",
children: [
{ label: "Oncologia", href: "#servizi" },
{ label: "Dermatologia", href: "#servizi" },
{ label: "Oculistica", href: "#servizi" },
{ label: "Nutrizione", href: "#servizi" },
{ label: "Ortopedia", href: "#servizi" },
],
},
{ label: "Il Team", href: "#team" }, { label: "Il Team", href: "#team" },
{ label: "News", href: "#news" },
{ label: "Contatti", href: "#contatti" }, { label: "Contatti", href: "#contatti" },
]; ];
@@ -19,6 +55,12 @@ export default function Navbar() {
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = 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(() => { useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 80); const handleScroll = () => setScrolled(window.scrollY > 80);
window.addEventListener("scroll", handleScroll); window.addEventListener("scroll", handleScroll);
@@ -40,9 +82,7 @@ export default function Navbar() {
059 839.62.63 &nbsp;|&nbsp; 320 532.24.39 (urgenze 24h) 059 839.62.63 &nbsp;|&nbsp; 320 532.24.39 (urgenze 24h)
</span> </span>
</div> </div>
<span className="text-[#4ECDC4] font-semibold tracking-wide text-xs uppercase">
Reperibilità 24h · 7 giorni su 7
</span>
</div> </div>
</div> </div>
@@ -56,45 +96,55 @@ export default function Navbar() {
> >
<div className="container flex items-center justify-between h-16 md:h-20"> <div className="container flex items-center justify-between h-16 md:h-20">
{/* Logo */} {/* Logo */}
<a href="#" className="flex items-center gap-3 group"> <a href="#" className="flex items-center gap-2 md:gap-3 group shrink-0">
<div className="w-10 h-10 rounded-full bg-[#1B4F72] flex items-center justify-center flex-shrink-0"> <img
<svg viewBox="0 0 40 40" fill="none" className="w-6 h-6"> src="/images/logo_high.png"
<path d="M20 8c-1.5 0-2.5 1.2-2.5 2.5S18.5 13 20 13s2.5-1.2 2.5-2.5S21.5 8 20 8z" fill="#4ECDC4"/> alt="Simbolo Clinica Veterinaria Formiginese"
<path d="M13 11c-1.2 0-2 1-2 2.2S11.8 15.5 13 15.5s2-1 2-2.2S14.2 11 13 11z" fill="#4ECDC4"/> className="h-10 md:h-14 w-auto object-contain"
<path d="M27 11c-1.2 0-2 1-2 2.2S25.8 15.5 27 15.5s2-1 2-2.2S28.2 11 27 11z" fill="#4ECDC4"/> />
<path d="M10 17c-1.2 0-2 1-2 2.2S8.8 21.5 10 21.5s2-1 2-2.2S11.2 17 10 17z" fill="#4ECDC4"/> <img
<path d="M30 17c-1.2 0-2 1-2 2.2S28.8 21.5 30 21.5s2-1 2-2.2S31.2 17 30 17z" fill="#4ECDC4"/> src="/images/logo_low.png"
<path d="M20 17c-5 0-9 3.5-9 7.5 0 2.5 1.5 4.5 3.5 5.5.5.3 1 .5 1.5.5h8c.5 0 1-.2 1.5-.5 2-1 3.5-3 3.5-5.5 0-4-4-7.5-9-7.5z" fill="white"/> alt="Clinica Veterinaria Formiginese"
</svg> className="h-8 md:h-11 w-auto object-contain"
</div> />
<div>
<div
className="font-bold text-[#1B4F72] leading-tight"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.1rem" }}
>
Clinica Veterinaria
</div>
<div
className="text-[#4ECDC4] font-semibold leading-tight tracking-widest uppercase text-xs"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Formiginese
</div>
</div>
</a> </a>
{/* Desktop menu */} {/* Desktop menu */}
<div className="hidden md:flex items-center gap-8"> <div className="hidden md:flex items-center gap-6">
{navLinks.map((link) => ( {navLinks.map((link) => (
<div key={link.label} className="relative group py-7">
<a <a
key={link.href}
href={link.href} href={link.href}
className="text-[#1B4F72] font-medium text-sm hover:text-[#4ECDC4] transition-colors duration-200 relative group" className="text-[#1B4F72] font-medium text-sm hover:text-[#4ECDC4] transition-colors duration-200 relative inline-flex items-center gap-1.5"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{link.label} {link.label}
{link.children && (
<ChevronDown
size={14}
className="transition-transform duration-200 group-hover:rotate-180"
/>
)}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-[#4ECDC4] transition-all duration-300 group-hover:w-full" /> <span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-[#4ECDC4] transition-all duration-300 group-hover:w-full" />
</a> </a>
{link.children && (
<div className="absolute left-0 top-full w-72 rounded-2xl border border-gray-100 bg-white/95 p-3 shadow-xl backdrop-blur-md opacity-0 invisible translate-y-2 group-hover:opacity-100 group-hover:visible group-hover:translate-y-0 transition-all duration-200">
<div className="grid gap-1">
{link.children.map((child) => (
<a
key={child.label}
href={child.href}
className="rounded-xl px-3 py-2 text-sm text-[#1B4F72]/80 hover:bg-[#F5F0E8] hover:text-[#1B4F72] transition-colors"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{child.label}
</a>
))}
</div>
</div>
)}
</div>
))} ))}
</div> </div>
@@ -104,7 +154,7 @@ export default function Navbar() {
variant="outline" variant="outline"
size="sm" size="sm"
className="border-[#1B4F72] text-[#1B4F72] hover:bg-[#1B4F72] hover:text-white transition-all duration-200" 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 Accedi
</Button> </Button>
@@ -131,20 +181,36 @@ export default function Navbar() {
{mobileOpen && ( {mobileOpen && (
<div className="md:hidden bg-white border-t border-gray-100 px-4 py-4 flex flex-col gap-4 shadow-lg"> <div className="md:hidden bg-white border-t border-gray-100 px-4 py-4 flex flex-col gap-4 shadow-lg">
{navLinks.map((link) => ( {navLinks.map((link) => (
<div key={link.label} className="border-b border-gray-50 pb-2">
<a <a
key={link.href}
href={link.href} href={link.href}
className="text-[#1B4F72] font-medium py-2 border-b border-gray-50 hover:text-[#4ECDC4] transition-colors" className="text-[#1B4F72] font-medium py-2 hover:text-[#4ECDC4] transition-colors flex items-center justify-between"
onClick={() => setMobileOpen(false)} onClick={() => setMobileOpen(false)}
> >
{link.label} {link.label}
{link.children && <ChevronDown size={15} className="text-[#4ECDC4]" />}
</a> </a>
{link.children && (
<div className="grid gap-1 pl-3 pt-1">
{link.children.map((child) => (
<a
key={child.label}
href={child.href}
className="rounded-lg px-3 py-1.5 text-sm text-[#1B4F72]/70 hover:bg-[#F5F0E8] hover:text-[#1B4F72] transition-colors"
onClick={() => setMobileOpen(false)}
>
{child.label}
</a>
))}
</div>
)}
</div>
))} ))}
<div className="flex flex-col gap-2 pt-2"> <div className="flex flex-col gap-2 pt-2">
<Button <Button
variant="outline" variant="outline"
className="border-[#1B4F72] text-[#1B4F72] w-full" className="border-[#1B4F72] text-[#1B4F72] w-full"
onClick={() => { setMobileOpen(false); document.getElementById("registrazione")?.scrollIntoView({ behavior: "smooth" }); }} onClick={() => { setMobileOpen(false); showActivationToast(); }}
> >
Accedi / Registrati Accedi / Registrati
</Button> </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é.", "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", date: "15 Marzo 2026",
readTime: "4 min", readTime: "4 min",
image: "https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=600&q=80", image: "/images/news_gatto.jpg",
}, },
{ {
id: 2, 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.", "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", date: "8 Marzo 2026",
readTime: "6 min", readTime: "6 min",
image: "https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=600&q=80", image: "/images/news_consulenza.jpg",
}, },
{ {
id: 3, 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.", "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", date: "1 Marzo 2026",
readTime: "5 min", readTime: "5 min",
image: "https://images.unsplash.com/photo-1548767797-d8c844163c4c?w=600&q=80", image: "/images/news_cucciolo.jpg",
}, },
]; ];

View File

@@ -1,74 +1,219 @@
/* /*
* DESIGN: "Clinical Warmth" * DESIGN: "Clinical Warmth"
* Sezione servizi con 3 card principali (radiologia, chirurgia, laboratorio) * Sezione servizi con due livelli:
* Layout: immagine in alto, bordo superiore colorato, hover lift * - Servizi Clinici
* Sfondo sabbia calda per contrasto con la sezione hero * - Visite Specialistiche
*/ */
import { motion } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef } from "react"; import { useRef } from "react";
import { ArrowRight, Scan, Scissors, FlaskConical } from "lucide-react"; import {
import { toast } from "sonner"; Activity,
Apple,
Bone,
Eye,
FlaskConical,
HeartPulse,
Scan,
Sparkles,
Stethoscope,
Video,
} from "lucide-react";
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.",
image: "/images/services_visite_cliniche.jpg",
icon: Stethoscope,
color: "#1B4F72",
features: ["Check-up periodici", "Vaccinazioni", "Profilassi antiparassitaria", "Controlli senior"],
},
{
id: "ecografia",
title: "Ecografia",
subtitle: "Diagnostica non invasiva",
description:
"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"],
},
{ {
id: "radiologia", id: "radiologia",
title: "Radiologia", title: "Radiologia",
subtitle: "Diagnostica per immagini", subtitle: "Diagnostica per immagini",
description: description:
"Tecnologia digitale di ultima generazione per radiografie, ecografie e diagnostica avanzata. Refertazione rapida e precisa per supportare le decisioni terapeutiche con la massima accuratezza diagnostica.", "Radiografie digitali e diagnostica per immagini per apparato scheletrico, torace, addome e approfondimenti d'urgenza.",
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/radiology_39785f88.jpg", image: "/images/services_radiologia.jpg",
icon: Scan, icon: Scan,
color: "#1B4F72", color: "#2E86AB",
features: ["Radiografia digitale", "Ecografia", "Ecocardiografia", "Refertazione immediata"], features: ["Radiografia digitale", "Studi toracici", "Valutazioni ortopediche", "Diagnostica d'urgenza"],
},
{
id: "chirurgia",
title: "Chirurgia",
subtitle: "Sala operatoria specialistica",
description:
"Sala operatoria completamente attrezzata per interventi di chirurgia generale, ortopedica e d'urgenza. Il nostro team chirurgico garantisce le massime condizioni di sicurezza e sterilità.",
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/surgery_e92904ed.jpg",
icon: Scissors,
color: "#4ECDC4",
features: ["Chirurgia generale", "Chirurgia ortopedica", "Chirurgia d'urgenza", "Anestesia monitorata"],
}, },
{ {
id: "laboratorio", id: "laboratorio",
title: "Laboratorio", title: "Laboratorio",
subtitle: "Analisi e diagnostica", subtitle: "Analisi interne",
description: description:
"Laboratorio analisi interno per esami ematologici, biochimici, urinari e citologici. Risultati in tempi rapidi per diagnosi tempestive e trattamenti mirati.", "Laboratorio interno per esami rapidi e mirati, utile per diagnosi tempestive, monitoraggi terapeutici e controlli pre-operatori.",
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/laboratory_5a1c4119.jpg", image: "/images/services_laboratorio.jpg",
icon: FlaskConical, icon: FlaskConical,
color: "#1B4F72",
features: ["Ematologia", "Biochimica", "Urine", "Citologia", "Coagulazione"],
},
{
id: "endoscopia",
title: "Endoscopia",
subtitle: "Esplorazione mini-invasiva",
description:
"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"],
},
{
id: "laparoscopia",
title: "Laparoscopia",
subtitle: "Chirurgia mini-invasiva",
description:
"Procedure mini-invasive con incisioni ridotte, recuperi piu confortevoli e maggiore delicatezza nei passaggi operatori.",
image: "/images/services_laparoscopia.jpg",
icon: Activity,
color: "#2E86AB", color: "#2E86AB",
features: ["Ematologia completa", "Biochimica sierica", "Esame urine", "Citologia"], features: ["Procedure mini-invasive", "Biopsie laparoscopiche", "Riduzione del dolore", "Recupero post-operatorio"],
}, },
]; ];
function ServiceCard({ service, index }: { service: typeof services[0]; index: number }) { const cardiologyService: ServiceItem = {
id: "cardiologia",
title: "Cardiologia",
subtitle: "Diagnostica cardiaca",
description:
"Approfondimenti dedicati alla funzionalita cardiaca con esami mirati e monitoraggi utili nei percorsi diagnostici piu delicati.",
image: "/images/services_cardiologia.jpg",
icon: HeartPulse,
color: "#8C5A7A",
features: ["Ecocardio", "ECG", "Holter", "Test malattie ereditarie"],
tone: "specialist",
};
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 ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" }); const isInView = useInView(ref, { once: true, margin: "-50px" });
const specialist = service.tone === "specialist";
return ( return (
<motion.div <motion.div
ref={ref} ref={ref}
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: index * 0.15 }} transition={{ duration: 0.6, delay: index * 0.1 }}
className="group bg-white rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-400 hover:-translate-y-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"
}`}
> >
{/* Bordo superiore colorato */} <div className="h-1.5" style={{ backgroundColor: service.color }} />
<div className="h-1" style={{ backgroundColor: service.color }} />
{/* Immagine */}
<div className="relative h-52 overflow-hidden"> <div className="relative h-52 overflow-hidden">
<img <img
src={service.image} src={service.image}
alt={service.title} 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 ?? ""}`}
/> />
{/* Icon overlay */}
<div <div
className="absolute top-4 right-4 w-12 h-12 rounded-full flex items-center justify-center shadow-lg" className="absolute top-4 right-4 w-12 h-12 rounded-full flex items-center justify-center shadow-lg"
style={{ backgroundColor: service.color }} style={{ backgroundColor: service.color }}
@@ -77,40 +222,27 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
</div> </div>
</div> </div>
{/* Contenuto */}
<div className="p-6"> <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} {service.subtitle}
</div> </div>
<h3 <h3
className="text-[#1B4F72] mb-3" className="mb-3 text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.75rem", fontWeight: 600 }} style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.75rem", fontWeight: 600 }}
> >
{service.title} {service.title}
</h3> </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>
{/* Features */} <ul className="mb-5 space-y-1.5">
<ul className="space-y-1.5 mb-5">
{service.features.map((feat) => ( {service.features.map((feat) => (
<li key={feat} className="flex items-center gap-2 text-sm text-gray-600"> <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} {feat}
</li> </li>
))} ))}
</ul> </ul>
{/* Link */}
<button
className="flex items-center gap-1.5 text-sm font-semibold transition-all duration-200 group/btn"
style={{ color: service.color }}
onClick={() => {
toast("Sezione in arrivo", { description: `La pagina dedicata a ${service.title} sarà disponibile a breve.` });
}}
>
Scopri di più
<ArrowRight size={15} className="transition-transform duration-200 group-hover/btn:translate-x-1" />
</button>
</div> </div>
</motion.div> </motion.div>
); );
@@ -118,12 +250,13 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
export default function ServicesSection() { export default function ServicesSection() {
const ref = useRef(null); const ref = useRef(null);
const specialistRef = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-80px" }); const isInView = useInView(ref, { once: true, margin: "-80px" });
const specialistInView = useInView(specialistRef, { once: true, margin: "-80px" });
return ( return (
<section id="servizi" className="py-20 md:py-28" style={{ backgroundColor: "#F5F0E8" }}> <section id="servizi" className="py-20 md:py-28" style={{ backgroundColor: "#F5F0E8" }}>
<div className="container"> <div className="container">
{/* Header sezione */}
<motion.div <motion.div
ref={ref} ref={ref}
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
@@ -131,48 +264,63 @@ export default function ServicesSection() {
transition={{ duration: 0.7 }} transition={{ duration: 0.7 }}
className="mb-14" 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">
Specializzazioni
</span>
</div>
<h2 <h2
className="text-[#1B4F72] mb-4" className="mb-4 text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 600 }} style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 600 }}
> >
Servizi Specialistici Servizi
</h2> </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">
Tre aree di eccellenza clinica per garantire diagnosi precise e trattamenti efficaci
ai tuoi animali domestici, con tecnologie all'avanguardia e professionisti esperti.
</p> </p>
</motion.div> </motion.div>
{/* Grid servizi */} <div className="grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> {clinicalServices.map((service, index) => (
{services.map((service, index) => (
<ServiceCard key={service.id} service={service} index={index} /> <ServiceCard key={service.id} service={service} index={index} />
))} ))}
</div> </div>
{/* Link a tutti i servizi */} <div className="mt-8 xl:hidden flex justify-center">
<ServiceCard
service={cardiologyService}
index={clinicalServices.length}
className="w-full md:w-[calc(50%-1rem)]"
/>
</div>
<div className="mt-8 hidden xl:grid xl:grid-cols-3 xl:gap-8">
<div />
<ServiceCard service={cardiologyService} index={clinicalServices.length} className="w-full" />
<div />
</div>
<motion.div <motion.div
initial={{ opacity: 0 }} ref={specialistRef}
animate={isInView ? { opacity: 1 } : {}} initial={{ opacity: 0, y: 30 }}
transition={{ duration: 0.7, delay: 0.6 }} animate={specialistInView ? { opacity: 1, y: 0 } : {}}
className="text-center mt-12" transition={{ duration: 0.7, delay: 0.2 }}
className="mt-20"
> >
<button <h3
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="mb-4 text-[#1B4F72]"
onClick={() => { style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(1.9rem, 3.7vw, 2.8rem)", fontWeight: 600 }}
toast("Tutti i servizi", { description: "La pagina completa dei servizi sarà disponibile a breve." });
}}
> >
Tutti i servizi Visite Specialistiche
<ArrowRight size={16} /> </h3>
</button>
</motion.div> </motion.div>
<div className="mt-8 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>
</div> </div>
</section> </section>
); );

View File

@@ -1,153 +1,244 @@
/* /*
* DESIGN: "Clinical Warmth" * DESIGN: "Clinical Warmth"
* Sezione team: griglia di 6 professionisti con card eleganti * Sezione team: due aree distinte per team medico e collaborazioni
* Sfondo sabbia calda, foto placeholder con iniziali, specializzazioni colorate * Stessa grammatica visiva delle card precedenti, con schede pronte
* ad accogliere progressivamente i profili completi.
*/ */
import { motion } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { useInView } from "framer-motion";
import { useRef } from "react"; 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", name: "Dott. Paolo Parmeggiani",
role: "Direttore Sanitario", role: "Direttore sanitario",
specialization: "Oncologia Veterinaria", specialization: "Oncologia veterinaria",
color: "#1B4F72", color: "#1B4F72",
initials: "PP", bio: [
bio: "Master in Oncologia Veterinaria. Direttore sanitario della clinica con oltre 20 anni di esperienza.", "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", name: "Dott.ssa Irene Paganelli",
role: "Medico Veterinario", role: "Medico veterinario",
specialization: "Chirurgia", specialization: "Ecografia ed esotici",
color: "#4ECDC4", color: "#4ECDC4",
initials: "ER", bio: [
bio: "Specializzata in chirurgia ortopedica e dei tessuti molli. Referente per gli interventi d'urgenza.", "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). Ha seguito diversi corsi di perfezionamento in ecografia addominale ed ecocardiografia del cane e del gatto.",
],
}, },
{ {
name: "Dott. Marco Bianchi", name: "Dott. Simone Tinti",
role: "Medico Veterinario", role: "Medico veterinario",
specialization: "Radiologia", specialization: "Profilo in aggiornamento",
color: "#2E86AB", color: "#2E86AB",
initials: "MB",
bio: "Esperto in diagnostica per immagini, ecografia e radiologia digitale veterinaria.",
}, },
{ {
name: "Dott.ssa Sara Ferrari", name: "Dott.ssa Michela Sghedoni",
role: "Medico Veterinario", role: "Medico veterinario",
specialization: "Medicina Interna", specialization: "Profilo in aggiornamento",
color: "#E8A838", color: "#B76E79",
initials: "SF",
bio: "Specializzata in medicina interna felina e canina, con focus su malattie metaboliche.",
}, },
{ {
name: "Dott. Luca Moretti", name: "Dott. Luca Pietri",
role: "Medico Veterinario", role: "Medico veterinario",
specialization: "Dermatologia", specialization: "Profilo in aggiornamento",
color: "#6B8F71", color: "#6B8F71",
initials: "LM",
bio: "Esperto in dermatologia e allergologia veterinaria. Referente per le patologie cutanee.",
}, },
{ {
name: "Dott.ssa Anna Conti", name: "Dott.ssa Sara Casali",
role: "Medico Veterinario", role: "Medico veterinario",
specialization: "Odontoiatria", specialization: "Profilo in aggiornamento",
color: "#9B5DE5", color: "#C58C63",
initials: "AC", },
bio: "Specializzata in odontoiatria veterinaria e chirurgia orale per cani e gatti.", {
name: "Dott. Riccardo Suffritti",
role: "Medico veterinario",
specialization: "Profilo in aggiornamento",
color: "#7A8FA8",
},
{
name: "Dott.ssa Elena Venturelli",
role: "Medico veterinario",
specialization: "Profilo in aggiornamento",
color: "#4C7A9F",
},
{
name: "Dott.ssa Cinzia Pellegrini",
role: "Medico veterinario",
specialization: "Profilo in aggiornamento",
color: "#8E6C88",
}, },
]; ];
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. Boris Del Pozzo",
role: "Collaborazione specialistica",
specialization: "Profilo in aggiornamento",
color: "#6E8FA7",
},
{
name: "Dott.ssa Silvia Palladini",
role: "Collaborazione specialistica",
specialization: "Profilo in aggiornamento",
color: "#B6876F",
},
];
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 ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-40px" }); const isInView = useInView(ref, { once: true, margin: "-40px" });
const hasBio = Boolean(member.bio?.length);
return ( return (
<motion.div <motion.div
ref={ref} ref={ref}
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: index * 0.1 }} transition={{ duration: 0.6, delay: index * 0.08 }}
className="group bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1" 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 }} /> <div className="h-1" style={{ backgroundColor: member.color }} />
{/* Avatar */} <div className="p-6 pb-5">
<div className="p-6 pb-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div <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" }} style={{ backgroundColor: member.color, fontFamily: "'Cormorant Garamond', serif" }}
> >
{member.initials} {getInitials(member.name)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<h3 <h3
className="text-[#1B4F72] font-semibold leading-tight" className="leading-tight text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.15rem" }} style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.15rem", fontWeight: 600 }}
> >
{member.name} {member.name}
</h3> </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 <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 }} style={{ backgroundColor: `${member.color}18`, color: member.color }}
> >
{member.specialization} {member.specialization}
</span> </span>
)}
</div> </div>
</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> </div>
</motion.div> </motion.div>
); );
} }
export default function TeamSection() { function TeamGroup({
eyebrow,
members,
}: {
eyebrow: string;
members: TeamMember[];
}) {
const ref = useRef(null); const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-80px" }); const isInView = useInView(ref, { once: true, margin: "-80px" });
return ( return (
<section id="team" className="py-20 md:py-28 bg-white"> <div className="mt-14 first:mt-0">
<div className="container">
{/* Header */}
<motion.div <motion.div
ref={ref} ref={ref}
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7 }} transition={{ duration: 0.7 }}
className="mb-14" className="mb-10"
> >
<div className="flex items-center gap-3 mb-4"> <div className="mb-3 flex items-center gap-3">
<div className="w-12 h-0.5 bg-[#4ECDC4]" /> <div className="h-0.5 w-10 bg-[#4ECDC4]" />
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest"> <span className="text-xs font-semibold uppercase tracking-[0.2em] text-[#4ECDC4]">
Il Nostro Team {eyebrow}
</span> </span>
</div> </div>
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
<h2
className="text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2rem, 4vw, 3rem)", 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>
</div>
</motion.div> </motion.div>
{/* Grid team */} <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> {members.map((member, index) => (
{team.map((member, index) => (
<TeamCard key={member.name} member={member} index={index} /> <TeamCard key={member.name} member={member} index={index} />
))} ))}
</div> </div>
</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"
members={medicalTeam}
/>
<TeamGroup
eyebrow="Collaborazioni"
members={collaborators}
/>
</div>
</section> </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

@@ -9,7 +9,6 @@ import HeroSection from "@/components/HeroSection";
import ServicesSection from "@/components/ServicesSection"; import ServicesSection from "@/components/ServicesSection";
import AboutSection from "@/components/AboutSection"; import AboutSection from "@/components/AboutSection";
import TeamSection from "@/components/TeamSection"; import TeamSection from "@/components/TeamSection";
import NewsSection from "@/components/NewsSection";
import BookingSection from "@/components/BookingSection"; import BookingSection from "@/components/BookingSection";
import AuthSection from "@/components/AuthSection"; import AuthSection from "@/components/AuthSection";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
@@ -23,7 +22,6 @@ export default function Home() {
<ServicesSection /> <ServicesSection />
<AboutSection /> <AboutSection />
<TeamSection /> <TeamSection />
<NewsSection />
<BookingSection /> <BookingSection />
<AuthSection /> <AuthSection />
</main> </main>

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

@@ -150,7 +150,15 @@ function vitePluginManusDebugCollector(): Plugin {
}; };
} }
const plugins = [react(), tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime(), vitePluginManusDebugCollector()]; const isProductionBuild = process.env.NODE_ENV === "production";
const plugins = [
react(),
tailwindcss(),
jsxLocPlugin(),
!isProductionBuild && vitePluginManusRuntime(),
!isProductionBuild && vitePluginManusDebugCollector(),
].filter(Boolean);
export default defineConfig({ export default defineConfig({
plugins, plugins,
@@ -168,7 +176,7 @@ export default defineConfig({
emptyOutDir: true, emptyOutDir: true,
}, },
server: { server: {
port: 3000, port: 3001,
strictPort: false, // Will find next available port if 3000 is busy strictPort: false, // Will find next available port if 3000 is busy
host: true, host: true,
allowedHosts: [ allowedHosts: [

309
coming-soon/index.html Normal file
View File

@@ -0,0 +1,309 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clinica Veterinaria Formiginese | Coming Soon</title>
<meta
name="description"
content="Il nuovo sito della Clinica Veterinaria Formiginese sarà presto online."
/>
<style>
:root {
--ink: #15384f;
--ink-soft: #315f78;
--aqua: #54c8bf;
--sand: #f4ede2;
--paper: #fffdf9;
--mist: rgba(255, 255, 255, 0.72);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", Arial, sans-serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(84, 200, 191, 0.22), transparent 38%),
radial-gradient(circle at bottom right, rgba(21, 56, 79, 0.18), transparent 32%),
linear-gradient(135deg, #f8f4ec 0%, #fefcf9 48%, #eef7f7 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.shell {
width: min(1120px, 100%);
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 28px;
align-items: center;
}
.panel {
background: var(--mist);
backdrop-filter: blur(16px);
border: 1px solid rgba(21, 56, 79, 0.08);
border-radius: 30px;
box-shadow: 0 24px 70px rgba(21, 56, 79, 0.12);
}
.copy {
padding: 48px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-radius: 999px;
background: rgba(84, 200, 191, 0.16);
color: var(--ink);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--aqua);
box-shadow: 0 0 0 5px rgba(84, 200, 191, 0.16);
}
h1 {
margin: 28px 0 16px;
font-size: clamp(2.6rem, 5vw, 4.8rem);
line-height: 0.95;
letter-spacing: -0.04em;
}
h1 span {
display: block;
color: var(--ink-soft);
font-style: italic;
font-weight: 500;
}
p {
margin: 0;
max-width: 46ch;
font-size: 18px;
line-height: 1.75;
color: rgba(21, 56, 79, 0.82);
}
.chips {
margin-top: 28px;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.chip {
padding: 12px 16px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(21, 56, 79, 0.1);
font-size: 14px;
font-weight: 600;
color: var(--ink-soft);
}
.note {
margin-top: 34px;
padding: 18px 20px;
border-radius: 20px;
background: rgba(21, 56, 79, 0.06);
color: var(--ink-soft);
font-size: 14px;
}
.visual {
padding: 22px;
}
.artboard {
overflow: hidden;
border-radius: 26px;
min-height: 560px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.3)),
linear-gradient(160deg, #dff5f1 0%, #f7f0e7 48%, #dcecf8 100%);
position: relative;
}
.artboard::before,
.artboard::after {
content: "";
position: absolute;
border-radius: 50%;
filter: blur(4px);
}
.artboard::before {
width: 210px;
height: 210px;
background: rgba(84, 200, 191, 0.2);
top: -60px;
right: -40px;
}
.artboard::after {
width: 260px;
height: 260px;
background: rgba(21, 56, 79, 0.12);
bottom: -80px;
left: -70px;
}
.stamp {
position: absolute;
top: 22px;
left: 22px;
z-index: 2;
padding: 10px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(21, 56, 79, 0.08);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
svg {
display: block;
width: 100%;
height: 100%;
}
@media (max-width: 900px) {
.shell {
grid-template-columns: 1fr;
}
.copy {
padding: 34px 26px;
}
.visual {
padding: 18px;
}
.artboard {
min-height: 420px;
}
p {
font-size: 16px;
}
}
</style>
</head>
<body>
<main class="shell">
<section class="panel copy">
<div class="eyebrow">Nuovo sito in preparazione</div>
<h1>
Clinica Veterinaria
<span>Formiginese</span>
</h1>
<p>
Stiamo preparando una nuova esperienza digitale pensata per presentare al meglio la
clinica, il team medico e i servizi dedicati alla salute dei vostri animali.
</p>
<div class="chips">
<div class="chip">Servizi specialistici</div>
<div class="chip">Team clinico</div>
<div class="chip">Prenotazioni online</div>
<div class="chip">Area clienti dedicata</div>
</div>
<div class="note">
Il sito completo sarà pubblicato a breve. Nel frattempo la clinica continua a essere
disponibile ai consueti recapiti telefonici.
</div>
</section>
<section class="panel visual">
<div class="artboard">
<div class="stamp">Coming Soon</div>
<svg viewBox="0 0 680 760" aria-hidden="true">
<defs>
<linearGradient id="card" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="100%" stop-color="#f4ede2" />
</linearGradient>
<linearGradient id="teal" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#68d7cf" />
<stop offset="100%" stop-color="#2d9f98" />
</linearGradient>
<linearGradient id="blue" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#264f68" />
<stop offset="100%" stop-color="#16384f" />
</linearGradient>
</defs>
<g opacity="0.9">
<circle cx="540" cy="122" r="68" fill="#ffffff" />
<circle cx="578" cy="146" r="12" fill="#54c8bf" opacity="0.5" />
<circle cx="500" cy="106" r="14" fill="#15384f" opacity="0.08" />
</g>
<g transform="translate(62 118)">
<rect width="556" height="506" rx="38" fill="url(#card)" />
<rect x="24" y="22" width="508" height="462" rx="28" fill="#ffffff" opacity="0.78" />
<rect x="56" y="58" width="214" height="264" rx="26" fill="url(#teal)" opacity="0.18" />
<rect x="304" y="58" width="196" height="108" rx="24" fill="#edf5f7" />
<rect x="304" y="184" width="196" height="138" rx="24" fill="#f8f3ec" />
<rect x="56" y="348" width="444" height="84" rx="22" fill="#15384f" opacity="0.06" />
<circle cx="164" cy="172" r="82" fill="url(#blue)" />
<ellipse cx="162" cy="207" rx="104" ry="88" fill="#1c445d" />
<ellipse cx="392" cy="108" rx="46" ry="30" fill="#dce9ee" />
<ellipse cx="392" cy="234" rx="70" ry="42" fill="#efe4d4" />
<path d="M155 104c-18-28-45-43-76-43 8 20 21 42 39 62" fill="#0f344b" />
<path d="M174 104c18-28 45-43 76-43-8 20-21 42-39 62" fill="#0f344b" />
<ellipse cx="139" cy="192" rx="18" ry="14" fill="#ffffff" />
<ellipse cx="185" cy="192" rx="18" ry="14" fill="#ffffff" />
<circle cx="139" cy="192" r="7" fill="#15384f" />
<circle cx="185" cy="192" r="7" fill="#15384f" />
<path d="M151 222c12 14 26 14 38 0" stroke="#54c8bf" stroke-width="8" stroke-linecap="round" fill="none" />
<path d="M162 212l-12 12h24z" fill="#54c8bf" />
<g transform="translate(344 74)">
<rect width="146" height="18" rx="9" fill="#15384f" opacity="0.12" />
<rect y="34" width="108" height="14" rx="7" fill="#54c8bf" opacity="0.32" />
<rect y="60" width="122" height="14" rx="7" fill="#15384f" opacity="0.08" />
</g>
<g transform="translate(332 212)">
<rect width="144" height="16" rx="8" fill="#15384f" opacity="0.12" />
<rect y="34" width="122" height="14" rx="7" fill="#54c8bf" opacity="0.26" />
<rect y="60" width="138" height="14" rx="7" fill="#15384f" opacity="0.08" />
<rect y="86" width="104" height="14" rx="7" fill="#15384f" opacity="0.08" />
</g>
<g transform="translate(82 370)">
<rect width="84" height="40" rx="20" fill="#54c8bf" />
<rect x="100" width="112" height="40" rx="20" fill="#ffffff" stroke="#15384f" opacity="0.28" />
<rect x="228" width="160" height="40" rx="20" fill="#15384f" opacity="0.08" />
</g>
</g>
</svg>
</div>
</section>
</main>
</body>
</html>

View File

@@ -0,0 +1,71 @@
@echo off
setlocal
set "DEPLOY_ROOT=C:\deploy\clinica_veterinaria_formiginese"
set "APP_DIR=%DEPLOY_ROOT%\clinica-app"
set "DEPLOY_BRANCH="
echo === Deploy demo Clinica Veterinaria Formiginese ===
echo.
if not exist "%DEPLOY_ROOT%\" (
echo ERRORE: cartella deploy non trovata: "%DEPLOY_ROOT%"
exit /b 1
)
if not exist "%APP_DIR%\" (
echo ERRORE: cartella app non trovata: "%APP_DIR%"
exit /b 1
)
echo [1/3] Aggiornamento repository di deploy...
pushd "%DEPLOY_ROOT%" || exit /b 1
for /f "delims=" %%i in ('git branch --show-current') do set "DEPLOY_BRANCH=%%i"
if "%DEPLOY_BRANCH%"=="" (
echo.
echo ERRORE: impossibile determinare il branch corrente nella copia di deploy.
popd
exit /b 1
)
echo Branch deploy attivo: %DEPLOY_BRANCH%
git fetch origin
if errorlevel 1 (
echo.
echo ERRORE: git fetch non riuscito.
popd
exit /b 1
)
git reset --hard origin/%DEPLOY_BRANCH%
if errorlevel 1 (
echo.
echo ERRORE: git reset del branch di deploy non riuscito.
popd
exit /b 1
)
popd
echo.
echo [2/3] Build frontend...
pushd "%APP_DIR%" || exit /b 1
call pnpm run build
if errorlevel 1 (
echo.
echo ERRORE: build non riuscita.
popd
exit /b 1
)
popd
echo.
echo [3/3] Deploy completato.
echo I file aggiornati sono serviti da Caddy da:
echo %APP_DIR%\dist\public
echo.
endlocal

21
istruzioni.txt Normal file
View File

@@ -0,0 +1,21 @@
Ho aggiunto due file nel repository locale:
publish_demo.bat
coming-soon/index.html
Come funziona:
publish_demo.bat true pubblica la demo completa
publish_demo.bat false mostra la pagina coming soon
lo script aggiorna il blocco del dominio nel C:\caddy\Caddyfile e ricarica Caddy
Prima di usarlo, fai una volta sola il normale flusso repo -> deploy, così la cartella coming-soon arriva anche in:
C:\deploy\clinica_veterinaria_formiginese
Quindi:
fai git add ., git commit, git push nel repo locale
lancia deploy_to_local_server.bat
poi puoi usare:
publish_demo.bat false
oppure
publish_demo.bat true

47
menu_sito.txt Normal file
View File

@@ -0,0 +1,47 @@
- servizi
- visite clincihe e medicina preventiva
- ecografia
- radiologia
- laboratorio
- ematologia
- biochimica
- urine
- citologia
- coagulazione
- endoscopia
- laparoscopia
- visite specialistiche
- oncologia
- dermatologia
- oculistica
- nutrizione
- ortopedia
- 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

76
publish_demo.bat Normal file
View File

@@ -0,0 +1,76 @@
@echo off
setlocal
if "%~1"=="" goto usage
set "MODE=%~1"
set "CADDYFILE=C:\caddy\Caddyfile"
set "DOMAIN=www.clinicaveterinariaformiginese.it"
set "LIVE_ROOT=C:\deploy\clinica_veterinaria_formiginese\clinica-app\dist\public"
set "COMING_SOON_ROOT=C:\deploy\clinica_veterinaria_formiginese\coming-soon"
if /I "%MODE%"=="true" goto publish_live
if /I "%MODE%"=="false" goto publish_coming_soon
goto usage
:publish_live
echo Modalita selezionata: DEMO PUBBLICATA
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$path = '%CADDYFILE%';" ^
"$content = Get-Content -Raw $path;" ^
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
"$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 (
echo ERRORE: aggiornamento del Caddyfile non riuscito.
exit /b 1
)
goto reload_caddy
:publish_coming_soon
echo Modalita selezionata: COMING SOON
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$path = '%CADDYFILE%';" ^
"$content = Get-Content -Raw $path;" ^
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
"$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 (
echo ERRORE: aggiornamento del Caddyfile non riuscito.
exit /b 1
)
:reload_caddy
echo.
echo Ricarico Caddy...
C:\caddy\caddy.exe validate --config "%CADDYFILE%" --adapter caddyfile
if errorlevel 1 (
echo ERRORE: Caddyfile non valido. Nessun reload eseguito.
exit /b 1
)
C:\caddy\caddy.exe reload --config "%CADDYFILE%" --adapter caddyfile
if errorlevel 1 (
echo ERRORE: reload di Caddy non riuscito.
exit /b 1
)
echo.
if /I "%MODE%"=="true" (
echo Demo online: https://%DOMAIN%
) else (
echo Coming soon online: https://%DOMAIN%
)
echo Operazione completata.
exit /b 0
:usage
echo Uso:
echo publish_demo.bat true
echo publish_demo.bat false
echo.
echo true = pubblica la demo completa
echo false = mostra la pagina coming soon
exit /b 1