Compare commits
15 Commits
main
...
backend_in
| Author | SHA1 | Date | |
|---|---|---|---|
| d9b617e706 | |||
| 580a659b5a | |||
| bd5b845e43 | |||
| 83fb4b8c1e | |||
| 57367b75c8 | |||
| f8e6daa084 | |||
| 35de87511d | |||
| d4a5f736b9 | |||
| 3545d988ce | |||
| f5d0b65dfa | |||
| 3d7b4a5fcc | |||
| bc44ee497f | |||
| 680974994b | |||
| b9a5d7f36f | |||
| c73efb7680 |
17
.gitignore
vendored
Normal 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
|
After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.3 MiB |
BIN
assets/coniglio.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
assets/logo.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
assets/logo_high.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/logo_low.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/news_consulenza.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/news_cucciolo.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
assets/news_gatto.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/ortovet.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/services_cardiologia.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
assets/services_dermatologia.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/services_ecografia.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
assets/services_endoscopia.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/services_endoscopia.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/services_laboratorio.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/services_laparoscopia.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
assets/services_oculistica.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
assets/services_radiologia.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/services_visite_cliniche.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
13
backend/.env.example
Normal 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]
|
||||||
22
backend/activate_backend_env.bat
Normal 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
@@ -0,0 +1 @@
|
|||||||
|
# Backend package for the Clinica Veterinaria Formiginese project.
|
||||||
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# API routers package.
|
||||||
27
backend/app/api/booking.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
|
||||||
|
from app.schemas.booking import BookingRequestCreate, BookingRequestResponse
|
||||||
|
from app.services.mailer import MailConfigurationError, send_booking_request_email
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api", tags=["booking"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/booking-request", response_model=BookingRequestResponse)
|
||||||
|
def create_booking_request(payload: BookingRequestCreate) -> BookingRequestResponse:
|
||||||
|
try:
|
||||||
|
send_booking_request_email(payload)
|
||||||
|
except MailConfigurationError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="Il servizio email non è ancora configurato.",
|
||||||
|
) from exc
|
||||||
|
except Exception as exc: # pragma: no cover - mail delivery path
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Si è verificato un problema durante l'invio della richiesta.",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
return BookingRequestResponse(
|
||||||
|
message="La richiesta è stata inviata correttamente e sarà presa in carico al più presto dal team.",
|
||||||
|
)
|
||||||
29
backend/app/api/health.py
Normal 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
@@ -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()
|
||||||
1
backend/app/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Database package.
|
||||||
5
backend/app/db/base.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
24
backend/app/db/init_db.py
Normal 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
@@ -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
@@ -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)
|
||||||
3
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from app.models.test_user import TestUser
|
||||||
|
|
||||||
|
__all__ = ["TestUser"]
|
||||||
12
backend/app/models/test_user.py
Normal 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)
|
||||||
1
backend/app/repositories/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Data access layer package.
|
||||||
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Pydantic schemas package.
|
||||||
18
backend/app/schemas/booking.py
Normal 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
|
||||||
1
backend/app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Business services package.
|
||||||
80
backend/app/services/mailer.py
Normal 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
@@ -0,0 +1,7 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
sqlalchemy
|
||||||
|
pydantic
|
||||||
|
python-dotenv
|
||||||
|
aiosqlite
|
||||||
|
pydantic[email]
|
||||||
17
backend/run_backend.bat
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
BIN
clinica-app/client/public/images/cavia.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.3 MiB |
BIN
clinica-app/client/public/images/coniglio.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
clinica-app/client/public/images/logo.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
clinica-app/client/public/images/logo_high.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
clinica-app/client/public/images/logo_low.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
clinica-app/client/public/images/news_consulenza.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
clinica-app/client/public/images/news_cucciolo.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
clinica-app/client/public/images/news_gatto.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
clinica-app/client/public/images/ortovet.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
clinica-app/client/public/images/services_cardiologia.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
clinica-app/client/public/images/services_dermatologia.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
clinica-app/client/public/images/services_ecografia.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
clinica-app/client/public/images/services_endoscopia.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
clinica-app/client/public/images/services_endoscopia.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
clinica-app/client/public/images/services_laboratorio.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
clinica-app/client/public/images/services_laparoscopia.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
clinica-app/client/public/images/services_oculistica.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
clinica-app/client/public/images/services_radiologia.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
clinica-app/client/public/images/services_visite_cliniche.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
@@ -1,6 +1,9 @@
|
|||||||
import { Toaster } from "@/components/ui/sonner";
|
import { 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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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'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'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>
|
||||||
|
|||||||
@@ -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'invio del modulo non costituisce conferma automatica dell'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'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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
83
clinica-app/client/src/components/LegalPageLayout.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import Footer from "@/components/Footer";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
type LegalPageLayoutProps = {
|
||||||
|
eyebrow: string;
|
||||||
|
title: string;
|
||||||
|
intro: string;
|
||||||
|
updatedAt: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LegalPageLayout({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
intro,
|
||||||
|
updatedAt,
|
||||||
|
children,
|
||||||
|
}: LegalPageLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#F8F4EC] text-[#18364A]">
|
||||||
|
<header className="border-b border-[#d8cec0] bg-white/95 backdrop-blur">
|
||||||
|
<div className="container flex items-center justify-between py-4">
|
||||||
|
<a href="/" className="inline-flex items-center gap-2 text-sm font-semibold text-[#1B4F72] hover:text-[#4ECDC4] transition-colors">
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
Torna alla home
|
||||||
|
</a>
|
||||||
|
<div className="text-right">
|
||||||
|
<div
|
||||||
|
className="leading-tight text-[#1B4F72]"
|
||||||
|
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem", fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Clinica Veterinaria
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[#4ECDC4]">
|
||||||
|
Formiginese
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section className="border-b border-[#e4d8c9] bg-white">
|
||||||
|
<div className="container py-14 md:py-18">
|
||||||
|
<div className="mb-4 flex items-center gap-3">
|
||||||
|
<div className="h-0.5 w-12 bg-[#4ECDC4]" />
|
||||||
|
<span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
|
||||||
|
{eyebrow}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)] lg:items-end">
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
className="text-[#1B4F72]"
|
||||||
|
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2.2rem, 5vw, 4rem)", fontWeight: 600, lineHeight: 1.06 }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 max-w-3xl text-base leading-relaxed text-gray-600">
|
||||||
|
{intro}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-[#e6ddcf] bg-[#F8F4EC] px-5 py-4 text-sm text-gray-600 shadow-sm">
|
||||||
|
<p className="font-semibold text-[#1B4F72]">Ultimo aggiornamento</p>
|
||||||
|
<p className="mt-1">{updatedAt}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="container py-12 md:py-16">
|
||||||
|
<div className="rounded-[28px] border border-[#e9dece] bg-white px-6 py-8 shadow-sm md:px-10 md:py-10">
|
||||||
|
<div className="legal-content prose prose-slate max-w-none prose-headings:text-[#1B4F72] prose-headings:font-semibold prose-p:text-gray-600 prose-li:text-gray-600 prose-strong:text-[#1B4F72]">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 | 320 532.24.39 (urgenze 24h)
|
059 839.62.63 | 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>
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
78
clinica-app/client/src/pages/CookiePolicyPage.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import LegalPageLayout from "@/components/LegalPageLayout";
|
||||||
|
|
||||||
|
export default function CookiePolicyPage() {
|
||||||
|
return (
|
||||||
|
<LegalPageLayout
|
||||||
|
eyebrow="Informativa cookie"
|
||||||
|
title="Cookie Policy"
|
||||||
|
intro="Questa pagina descrive l'uso di cookie e di altri strumenti tecnici da parte del sito della Clinica Veterinaria Formiginese."
|
||||||
|
updatedAt="23 maggio 2026"
|
||||||
|
>
|
||||||
|
<h2>1. Cosa sono i cookie</h2>
|
||||||
|
<p>
|
||||||
|
I cookie sono piccoli file di testo che i siti visitati possono salvare sul dispositivo
|
||||||
|
dell'utente per consentire il funzionamento del sito, migliorare la navigazione o memorizzare
|
||||||
|
preferenze e impostazioni.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>2. Tipologie di strumenti attualmente utilizzati</h2>
|
||||||
|
<p>Allo stato attuale il sito utilizza esclusivamente strumenti tecnici o assimilabili ai tecnici, necessari a:</p>
|
||||||
|
<ul>
|
||||||
|
<li>erogare correttamente le pagine e i contenuti richiesti;</li>
|
||||||
|
<li>gestire eventuali elementi tecnici indispensabili al funzionamento dell'interfaccia;</li>
|
||||||
|
<li>garantire sicurezza, stabilità e corretta erogazione del servizio.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>3. Cookie tecnici</h2>
|
||||||
|
<p>
|
||||||
|
I cookie tecnici sono quelli strettamente necessari alla navigazione o alla fornitura di un
|
||||||
|
servizio esplicitamente richiesto dall'utente. Per tali strumenti, secondo la normativa
|
||||||
|
applicabile, non è richiesto il consenso preventivo dell'utente, fermo restando l'obbligo
|
||||||
|
di fornire un'informativa adeguata.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>4. Cookie di profilazione e analytics</h2>
|
||||||
|
<p>
|
||||||
|
Alla data di aggiornamento di questa pagina il sito non utilizza cookie di profilazione,
|
||||||
|
strumenti di marketing né sistemi di analytics attivi configurati per il monitoraggio del
|
||||||
|
comportamento degli utenti a fini non tecnici.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Qualora in futuro venissero introdotti strumenti di tracciamento non tecnici, la presente
|
||||||
|
policy verrà aggiornata e, ove necessario, sarà implementato un meccanismo di raccolta del
|
||||||
|
consenso conforme alla normativa applicabile.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>5. Risorse di terze parti</h2>
|
||||||
|
<p>
|
||||||
|
Il sito può richiamare risorse esterne di supporto, come librerie o font distribuiti da terzi.
|
||||||
|
In tali casi il browser dell'utente può effettuare richieste ai rispettivi server per
|
||||||
|
scaricare le risorse necessarie alla visualizzazione della pagina.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>6. Futuri servizi con autenticazione</h2>
|
||||||
|
<p>
|
||||||
|
In occasione della futura attivazione dell'area riservata potranno essere utilizzati cookie
|
||||||
|
o identificatori tecnici di sessione strettamente necessari all'autenticazione e alla gestione
|
||||||
|
sicura dell'accesso. Anche tali strumenti, se limitati alla funzione tecnica richiesta
|
||||||
|
dall'utente, rientrano normalmente tra quelli esenti da consenso preventivo.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>7. Come gestire i cookie</h2>
|
||||||
|
<p>
|
||||||
|
L'utente può controllare e cancellare i cookie attraverso le impostazioni del proprio browser.
|
||||||
|
La disattivazione dei cookie tecnici potrebbe tuttavia compromettere il corretto funzionamento
|
||||||
|
di alcune funzionalità del sito.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>8. Contatti</h2>
|
||||||
|
<p>
|
||||||
|
Per qualsiasi chiarimento relativo all'uso dei cookie e degli altri strumenti tecnici è possibile
|
||||||
|
contattare la Clinica Veterinaria Formiginese all'indirizzo{" "}
|
||||||
|
<a href="mailto:clinicaveterinariaformiginese@gmail.com">
|
||||||
|
clinicaveterinariaformiginese@gmail.com
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
|
</LegalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
78
clinica-app/client/src/pages/LegalNotesPage.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import LegalPageLayout from "@/components/LegalPageLayout";
|
||||||
|
|
||||||
|
export default function LegalNotesPage() {
|
||||||
|
return (
|
||||||
|
<LegalPageLayout
|
||||||
|
eyebrow="Note legali"
|
||||||
|
title="Note Legali"
|
||||||
|
intro="Le presenti note disciplinano l'accesso e l'utilizzo del sito della Clinica Veterinaria Formiginese e forniscono informazioni generali sull'uso dei contenuti pubblicati."
|
||||||
|
updatedAt="23 maggio 2026"
|
||||||
|
>
|
||||||
|
<h2>1. Informazioni generali</h2>
|
||||||
|
<p>
|
||||||
|
Il presente sito è dedicato alla presentazione della Clinica Veterinaria Formiginese,
|
||||||
|
dei suoi servizi e delle modalità di contatto con la struttura. I contenuti pubblicati
|
||||||
|
hanno finalità prevalentemente informative e organizzative.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>2. Utilizzo del sito</h2>
|
||||||
|
<p>
|
||||||
|
L'utente si impegna a utilizzare il sito in modo conforme alla legge, al buon costume e
|
||||||
|
alle finalità per cui esso è messo a disposizione, evitando qualsiasi uso improprio,
|
||||||
|
fraudolento o lesivo dei diritti del titolare o di terzi.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>3. Contenuti informativi</h2>
|
||||||
|
<p>
|
||||||
|
Le informazioni presenti sul sito, incluse quelle relative ad attività cliniche, visite,
|
||||||
|
servizi e aggiornamenti divulgativi, non sostituiscono in alcun modo una valutazione
|
||||||
|
veterinaria diretta e personalizzata. Per esigenze cliniche specifiche è sempre necessario
|
||||||
|
rivolgersi direttamente alla struttura o al professionista competente.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>4. Prenotazioni e richieste online</h2>
|
||||||
|
<p>
|
||||||
|
Eventuali richieste di registrazione o di prenotazione visita inoltrate attraverso il sito
|
||||||
|
hanno natura informativa o pre-organizzativa e non costituiscono, salvo diversa conferma da
|
||||||
|
parte della clinica, appuntamento automaticamente confermato né accettazione vincolante della
|
||||||
|
prestazione richiesta.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>5. Proprietà intellettuale</h2>
|
||||||
|
<p>
|
||||||
|
Salvo diversa indicazione, testi, immagini, elementi grafici, marchi, struttura del sito e
|
||||||
|
altri contenuti presenti nel sito sono protetti dalle norme applicabili in materia di proprietà
|
||||||
|
intellettuale e non possono essere copiati, riprodotti, diffusi o riutilizzati senza preventiva
|
||||||
|
autorizzazione del titolare o dei rispettivi aventi diritto.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>6. Link a siti esterni</h2>
|
||||||
|
<p>
|
||||||
|
Il sito può contenere collegamenti a risorse esterne, gestite da soggetti terzi. La presenza di
|
||||||
|
tali link non implica approvazione o controllo costante sui relativi contenuti. La clinica non
|
||||||
|
risponde dei contenuti, delle policy o del funzionamento dei siti esterni.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>7. Limitazione di responsabilità</h2>
|
||||||
|
<p>
|
||||||
|
Pur ponendo la massima attenzione nell'aggiornamento dei contenuti, la clinica non garantisce
|
||||||
|
che tutte le informazioni presenti sul sito siano sempre complete, prive di errori o aggiornate
|
||||||
|
in tempo reale. Nei limiti consentiti dalla legge, il titolare non potrà essere ritenuto
|
||||||
|
responsabile per danni derivanti dall'utilizzo del sito o dall'affidamento riposto nelle
|
||||||
|
informazioni ivi contenute.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>8. Modifiche</h2>
|
||||||
|
<p>
|
||||||
|
Il titolare si riserva il diritto di aggiornare, modificare o rimuovere in qualsiasi momento
|
||||||
|
contenuti, servizi, condizioni d'uso e presenti note legali, senza obbligo di preavviso.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>9. Legge applicabile</h2>
|
||||||
|
<p>
|
||||||
|
L'utilizzo del sito è regolato dalla legge italiana. Restano salve le tutele eventualmente
|
||||||
|
previste dalla normativa inderogabile applicabile agli utenti.
|
||||||
|
</p>
|
||||||
|
</LegalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
clinica-app/client/src/pages/PrivacyPolicyPage.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import LegalPageLayout from "@/components/LegalPageLayout";
|
||||||
|
|
||||||
|
export default function PrivacyPolicyPage() {
|
||||||
|
return (
|
||||||
|
<LegalPageLayout
|
||||||
|
eyebrow="Informativa privacy"
|
||||||
|
title="Privacy Policy"
|
||||||
|
intro="Questa informativa descrive come vengono trattati i dati personali degli utenti che consultano il sito della Clinica Veterinaria Formiginese, contattano la struttura o utilizzano i servizi online via via attivati."
|
||||||
|
updatedAt="23 maggio 2026"
|
||||||
|
>
|
||||||
|
<h2>1. Titolare del trattamento</h2>
|
||||||
|
<p>
|
||||||
|
Il titolare del trattamento dei dati personali trattati attraverso questo sito è la
|
||||||
|
Clinica Veterinaria Formiginese, con sede operativa in Via Quattro Passi, 16 -
|
||||||
|
41043 Formigine (MO), contattabile all'indirizzo email{" "}
|
||||||
|
<a href="mailto:clinicaveterinariaformiginese@gmail.com">
|
||||||
|
clinicaveterinariaformiginese@gmail.com
|
||||||
|
</a>{" "}
|
||||||
|
e al numero <a href="tel:0598396263">059 839.62.63</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Eventuali ulteriori dati identificativi del titolare e i riferimenti amministrativi
|
||||||
|
potranno essere integrati in questa pagina in occasione dei successivi aggiornamenti.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>2. Tipologie di dati trattati</h2>
|
||||||
|
<p>Il sito può trattare le seguenti categorie di dati:</p>
|
||||||
|
<ul>
|
||||||
|
<li>dati di navigazione e dati tecnici necessari al funzionamento del sito;</li>
|
||||||
|
<li>dati comunicati spontaneamente tramite email, telefono o moduli di contatto;</li>
|
||||||
|
<li>dati inseriti nei servizi online di registrazione e richiesta di prenotazione;</li>
|
||||||
|
<li>dati necessari alla gestione delle richieste informative e organizzative.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>3. Finalità del trattamento</h2>
|
||||||
|
<p>I dati possono essere trattati per:</p>
|
||||||
|
<ul>
|
||||||
|
<li>consentire la navigazione e la sicurezza del sito;</li>
|
||||||
|
<li>rispondere a richieste di informazioni inviate dagli utenti;</li>
|
||||||
|
<li>gestire richieste di registrazione all'area riservata;</li>
|
||||||
|
<li>gestire richieste di prenotazione visita non vincolanti e i relativi contatti di follow-up;</li>
|
||||||
|
<li>adempiere a obblighi di legge, amministrativi o di tutela dei diritti del titolare.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>4. Base giuridica</h2>
|
||||||
|
<p>
|
||||||
|
Il trattamento è effettuato, a seconda dei casi, sulla base dell'art. 6, par. 1, lett. b)
|
||||||
|
del Regolamento (UE) 2016/679 per l'esecuzione di misure precontrattuali richieste
|
||||||
|
dall'interessato, dell'art. 6, par. 1, lett. c) per obblighi di legge, e dell'art. 6,
|
||||||
|
par. 1, lett. f) per il legittimo interesse del titolare alla sicurezza del sito e alla
|
||||||
|
gestione organizzativa delle richieste.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>5. Modalità di trattamento</h2>
|
||||||
|
<p>
|
||||||
|
I dati sono trattati con strumenti elettronici e organizzativi idonei a garantirne
|
||||||
|
riservatezza, integrità, disponibilità e aggiornamento, nel rispetto dei principi di
|
||||||
|
liceità, correttezza, trasparenza e minimizzazione.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>6. Conservazione dei dati</h2>
|
||||||
|
<p>
|
||||||
|
I dati sono conservati per il tempo strettamente necessario alle finalità per cui sono
|
||||||
|
raccolti e, successivamente, nei limiti previsti dalle norme applicabili o comunque per
|
||||||
|
il tempo necessario alla tutela dei diritti del titolare.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Le richieste di contatto o prenotazione possono essere conservate per il tempo necessario
|
||||||
|
alla loro gestione e per l'eventuale definizione dei successivi rapporti organizzativi o
|
||||||
|
amministrativi con l'utente.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>7. Destinatari dei dati</h2>
|
||||||
|
<p>
|
||||||
|
I dati possono essere trattati da personale autorizzato della clinica e, ove necessario,
|
||||||
|
da fornitori di servizi tecnici e informatici che operano quali responsabili o soggetti
|
||||||
|
autorizzati al trattamento, nei limiti delle rispettive competenze.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>8. Trasferimenti verso Paesi terzi</h2>
|
||||||
|
<p>
|
||||||
|
Alcune risorse tecniche di terze parti eventualmente richiamate dal sito, come servizi di
|
||||||
|
distribuzione di font o contenuti esterni, possono comportare il trattamento di dati tecnici
|
||||||
|
da parte dei relativi fornitori. Tali trattamenti avvengono secondo le condizioni e le
|
||||||
|
garanzie previste dai rispettivi operatori.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>9. Diritti degli interessati</h2>
|
||||||
|
<p>
|
||||||
|
Gli interessati possono esercitare i diritti previsti dagli articoli 15 e seguenti del
|
||||||
|
Regolamento (UE) 2016/679, tra cui accesso, rettifica, cancellazione, limitazione del
|
||||||
|
trattamento, opposizione e, ove applicabile, portabilità dei dati, contattando il titolare
|
||||||
|
ai recapiti sopra indicati.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Resta ferma la possibilità di proporre reclamo al Garante per la protezione dei dati
|
||||||
|
personali, qualora si ritenga che il trattamento avvenga in violazione della normativa
|
||||||
|
applicabile.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>10. Natura del conferimento</h2>
|
||||||
|
<p>
|
||||||
|
Il conferimento dei dati eventualmente richiesti nei moduli del sito è facoltativo, ma il
|
||||||
|
mancato conferimento può impedire alla clinica di dare seguito alla richiesta dell'utente.
|
||||||
|
</p>
|
||||||
|
</LegalPageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: [
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ setlocal
|
|||||||
|
|
||||||
set "DEPLOY_ROOT=C:\deploy\clinica_veterinaria_formiginese"
|
set "DEPLOY_ROOT=C:\deploy\clinica_veterinaria_formiginese"
|
||||||
set "APP_DIR=%DEPLOY_ROOT%\clinica-app"
|
set "APP_DIR=%DEPLOY_ROOT%\clinica-app"
|
||||||
|
set "DEPLOY_BRANCH="
|
||||||
|
|
||||||
echo === Deploy demo Clinica Veterinaria Formiginese ===
|
echo === Deploy demo Clinica Veterinaria Formiginese ===
|
||||||
echo.
|
echo.
|
||||||
@@ -19,13 +20,34 @@ if not exist "%APP_DIR%\" (
|
|||||||
|
|
||||||
echo [1/3] Aggiornamento repository di deploy...
|
echo [1/3] Aggiornamento repository di deploy...
|
||||||
pushd "%DEPLOY_ROOT%" || exit /b 1
|
pushd "%DEPLOY_ROOT%" || exit /b 1
|
||||||
git pull
|
|
||||||
if errorlevel 1 (
|
for /f "delims=" %%i in ('git branch --show-current') do set "DEPLOY_BRANCH=%%i"
|
||||||
|
|
||||||
|
if "%DEPLOY_BRANCH%"=="" (
|
||||||
echo.
|
echo.
|
||||||
echo ERRORE: git pull non riuscito.
|
echo ERRORE: impossibile determinare il branch corrente nella copia di deploy.
|
||||||
popd
|
popd
|
||||||
exit /b 1
|
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
|
popd
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
|
|||||||
21
istruzioni.txt
Normal 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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
|||||||
"$path = '%CADDYFILE%';" ^
|
"$path = '%CADDYFILE%';" ^
|
||||||
"$content = Get-Content -Raw $path;" ^
|
"$content = Get-Content -Raw $path;" ^
|
||||||
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %LIVE_ROOT%' + [Environment]::NewLine + ' try_files {path} /index.html' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' route {' + [Environment]::NewLine + ' handle /api/* {' + [Environment]::NewLine + ' reverse_proxy 127.0.0.1:8000' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' handle {' + [Environment]::NewLine + ' root * %LIVE_ROOT%' + [Environment]::NewLine + ' try_files {path} /index.html' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' }' + [Environment]::NewLine + '}';" ^
|
||||||
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||||
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -34,7 +34,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
|||||||
"$path = '%CADDYFILE%';" ^
|
"$path = '%CADDYFILE%';" ^
|
||||||
"$content = Get-Content -Raw $path;" ^
|
"$content = Get-Content -Raw $path;" ^
|
||||||
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %COMING_SOON_ROOT%' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' route {' + [Environment]::NewLine + ' handle /api/* {' + [Environment]::NewLine + ' reverse_proxy 127.0.0.1:8000' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' handle {' + [Environment]::NewLine + ' root * %COMING_SOON_ROOT%' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' }' + [Environment]::NewLine + '}';" ^
|
||||||
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||||
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
|
|||||||