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>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script
|
||||
defer
|
||||
src="%VITE_ANALYTICS_ENDPOINT%/umami"
|
||||
data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
clinica-app/client/public/images/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 { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import CookiePolicyPage from "@/pages/CookiePolicyPage";
|
||||
import LegalNotesPage from "@/pages/LegalNotesPage";
|
||||
import NotFound from "@/pages/NotFound";
|
||||
import PrivacyPolicyPage from "@/pages/PrivacyPolicyPage";
|
||||
import { Route, Switch } from "wouter";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
@@ -10,6 +13,9 @@ function Router() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={"/"} component={Home} />
|
||||
<Route path={"/privacy-policy"} component={PrivacyPolicyPage} />
|
||||
<Route path={"/cookie-policy"} component={CookiePolicyPage} />
|
||||
<Route path={"/note-legali"} component={LegalNotesPage} />
|
||||
<Route path={"/404"} component={NotFound} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function AboutSection() {
|
||||
{/* Immagine principale */}
|
||||
<div className="relative rounded-2xl overflow-hidden shadow-2xl">
|
||||
<img
|
||||
src="https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/hero_dog_cat_30af7cf4.jpg"
|
||||
src="/images/hero_dog_cat.jpg"
|
||||
alt="Cane e gatto insieme — la nostra missione"
|
||||
className="w-full h-[480px] object-cover"
|
||||
/>
|
||||
@@ -105,19 +105,6 @@ export default function AboutSection() {
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#1B4F72]/30 to-transparent" />
|
||||
</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 */}
|
||||
<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" />
|
||||
|
||||
@@ -1,65 +1,76 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione registrazione/login: tabs con form eleganti
|
||||
* Sfondo bianco, accenti blu petrolio e verde acqua
|
||||
* Sezione registrazione/login visibile ma non ancora attiva.
|
||||
* Gli elementi restano leggibili, ma ogni interazione mostra
|
||||
* il messaggio "Servizio in corso di attivazione".
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useInView } from "framer-motion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { User, Mail, Lock, Eye, EyeOff, CheckCircle2, PawPrint } from "lucide-react";
|
||||
import { User, Mail, Lock, Eye, CheckCircle2, PawPrint, Clock3 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function AuthSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
const [tab, setTab] = useState<"login" | "register">("register");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [registered, setRegistered] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (tab === "register") {
|
||||
setRegistered(true);
|
||||
toast.success("Registrazione completata!", {
|
||||
description: "Benvenuto nella Clinica Veterinaria Formiginese.",
|
||||
});
|
||||
} else {
|
||||
toast.success("Accesso effettuato!", {
|
||||
description: "Bentornato nella tua area personale.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const benefits = [
|
||||
"Storico visite e referti digitali",
|
||||
"Promemoria vaccinazioni automatici",
|
||||
"Prenotazioni online prioritarie",
|
||||
"Comunicazioni dirette con il veterinario",
|
||||
"Gestione di più animali domestici",
|
||||
"Gestione di piu animali domestici",
|
||||
];
|
||||
|
||||
function showActivationToast() {
|
||||
toast.info("Servizio in corso di attivazione", {
|
||||
description: "L'area riservata sara disponibile a breve.",
|
||||
});
|
||||
}
|
||||
|
||||
function DisabledInput({
|
||||
icon,
|
||||
type,
|
||||
placeholder,
|
||||
}: {
|
||||
icon: React.ComponentType<{ size?: number; className?: string }>;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const Icon = icon;
|
||||
|
||||
return (
|
||||
<section id="registrazione" className="py-20 md:py-28 bg-white">
|
||||
<div className="relative">
|
||||
<Icon size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
disabled
|
||||
className="w-full cursor-not-allowed rounded-lg border border-gray-200 bg-gray-50/90 py-2.5 pl-9 pr-3 text-sm text-gray-400 opacity-90"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuthSection() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
|
||||
return (
|
||||
<section id="registrazione" className="bg-white py-20 md:py-28">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
{/* Colonna sinistra: benefici */}
|
||||
<div className="grid grid-cols-1 items-center gap-16 lg:grid-cols-2">
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-0.5 w-12 bg-[#4ECDC4]" />
|
||||
<span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
|
||||
Area Personale
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
className="text-[#1B4F72] mb-6"
|
||||
className="mb-6 text-[#1B4F72]"
|
||||
style={{
|
||||
fontFamily: "'Cormorant Garamond', serif",
|
||||
fontSize: "clamp(2rem, 4vw, 3rem)",
|
||||
@@ -67,210 +78,148 @@ export default function AuthSection() {
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Registrati e gestisci{" "}
|
||||
Accedi e gestisci{" "}
|
||||
<span className="italic">la salute del tuo animale</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600 leading-relaxed mb-8 text-base">
|
||||
Crea il tuo profilo personale per accedere a tutti i servizi digitali della clinica.
|
||||
Tieni traccia della storia clinica del tuo animale, ricevi promemoria e prenota
|
||||
le visite in pochi click.
|
||||
<p className="mb-8 text-base leading-relaxed text-gray-600">
|
||||
L'area riservata e la registrazione online sono in fase di attivazione.
|
||||
Qui i clienti potranno gestire i propri dati, consultare lo storico e accedere
|
||||
ai servizi digitali della clinica in uno spazio personale dedicato.
|
||||
</p>
|
||||
|
||||
{/* Benefits list */}
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="mb-8 space-y-3">
|
||||
{benefits.map((benefit) => (
|
||||
<div key={benefit} className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-[#4ECDC4]/15 flex items-center justify-center flex-shrink-0">
|
||||
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-[#4ECDC4]/15">
|
||||
<CheckCircle2 size={14} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<span className="text-gray-700 text-sm">{benefit}</span>
|
||||
<span className="text-sm text-gray-700">{benefit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Decorazione */}
|
||||
<div className="bg-[#F5F0E8] rounded-2xl p-6 flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-[#1B4F72] flex items-center justify-center flex-shrink-0">
|
||||
<div className="flex items-center gap-4 rounded-2xl bg-[#F5F0E8] p-6">
|
||||
<div className="flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-full bg-[#1B4F72]">
|
||||
<PawPrint size={24} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[#1B4F72] font-semibold text-sm">Già più di 500 famiglie</p>
|
||||
<p className="text-gray-600 text-xs mt-0.5">
|
||||
si affidano alla nostra clinica per la cura dei loro animali
|
||||
<p className="text-sm font-semibold text-[#1B4F72]">Servizio in attivazione</p>
|
||||
<p className="mt-0.5 text-xs text-gray-600">
|
||||
La clinica sta completando la configurazione dell'area riservata per offrirti
|
||||
un accesso semplice e sicuro.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Colonna destra: form */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
{registered ? (
|
||||
<div className="bg-[#F5F0E8] rounded-2xl p-8 text-center shadow-sm">
|
||||
<div className="w-16 h-16 rounded-full bg-[#4ECDC4]/15 flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle2 size={32} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-2"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.8rem" }}
|
||||
>
|
||||
Benvenuto!
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-6">
|
||||
La tua registrazione è avvenuta con successo. Ora puoi accedere a tutti i servizi
|
||||
della tua area personale.
|
||||
</p>
|
||||
<Button
|
||||
className="bg-[#1B4F72] hover:bg-[#163d5a] text-white"
|
||||
onClick={() => setRegistered(false)}
|
||||
>
|
||||
Vai all'area personale
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
|
||||
{/* Tabs */}
|
||||
<div className="relative overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-lg">
|
||||
<div className="flex border-b border-gray-100">
|
||||
{(["register", "login"] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
className={`flex-1 py-4 text-sm font-semibold transition-all duration-200 ${
|
||||
tab === t
|
||||
? "text-[#1B4F72] border-b-2 border-[#4ECDC4] bg-[#F5F0E8]/50"
|
||||
: "text-gray-400 hover:text-gray-600"
|
||||
}`}
|
||||
type="button"
|
||||
disabled
|
||||
className="flex-1 cursor-not-allowed border-b-2 border-[#4ECDC4] bg-[#F5F0E8]/50 py-4 text-sm font-semibold text-[#1B4F72]"
|
||||
>
|
||||
{t === "register" ? "Registrati" : "Accedi"}
|
||||
Registrati
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="flex-1 cursor-not-allowed py-4 text-sm font-semibold text-gray-400"
|
||||
>
|
||||
Accedi
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-4">
|
||||
{tab === "register" && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="mb-5 flex items-center gap-3 rounded-xl border border-[#E4D7C6] bg-[#FFF9F1] px-4 py-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-[#A95F3A]/12">
|
||||
<Clock3 size={18} className="text-[#A95F3A]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<p className="text-sm font-semibold text-[#1B4F72]">Servizio in corso di attivazione</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
Registrazione e accesso saranno disponibili a breve.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 opacity-75">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Nome
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Mario"
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<DisabledInput icon={User} type="text" placeholder="Mario" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Cognome
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Rossi"
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
/>
|
||||
<DisabledInput icon={User} type="text" placeholder="Rossi" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="mario.rossi@email.it"
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<DisabledInput icon={Mail} type="email" placeholder="mario.rossi@email.it" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-9 pr-10 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
disabled
|
||||
className="w-full cursor-not-allowed rounded-lg border border-gray-200 bg-gray-50/90 py-2.5 pl-9 pr-10 text-sm text-gray-400 opacity-90"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? <EyeOff size={15} /> : <Eye size={15} />}
|
||||
</button>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<Eye size={15} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tab === "register" && (
|
||||
<div className="flex items-start gap-2 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
required
|
||||
id="privacy"
|
||||
className="mt-0.5 accent-[#4ECDC4]"
|
||||
disabled
|
||||
className="mt-0.5 cursor-not-allowed accent-[#4ECDC4]"
|
||||
/>
|
||||
<label htmlFor="privacy" className="text-xs text-gray-500 leading-relaxed">
|
||||
Accetto la{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#4ECDC4] hover:underline"
|
||||
onClick={() => toast.info("Privacy policy in arrivo")}
|
||||
>
|
||||
Privacy Policy
|
||||
</button>{" "}
|
||||
e i{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#4ECDC4] hover:underline"
|
||||
onClick={() => toast.info("Termini di servizio in arrivo")}
|
||||
>
|
||||
Termini di Servizio
|
||||
</button>
|
||||
</label>
|
||||
<p className="text-xs leading-relaxed text-gray-500">
|
||||
Accetto la Privacy Policy e i Termini di Servizio.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-[#1B4F72] hover:bg-[#163d5a] text-white font-bold py-3 text-base transition-all duration-300"
|
||||
type="button"
|
||||
disabled
|
||||
className="w-full cursor-not-allowed bg-[#1B4F72] py-3 text-base font-bold text-white opacity-70"
|
||||
>
|
||||
{tab === "register" ? "Crea il tuo account" : "Accedi all'area personale"}
|
||||
Crea il tuo account
|
||||
</Button>
|
||||
|
||||
{tab === "login" && (
|
||||
<p className="text-center text-xs text-gray-400">
|
||||
<p className="text-center text-xs text-gray-400">Password dimenticata?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#4ECDC4] hover:underline"
|
||||
onClick={() => toast.info("Recupero password in arrivo")}
|
||||
>
|
||||
Password dimenticata?
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
aria-label="Servizio in corso di attivazione"
|
||||
onClick={showActivationToast}
|
||||
className="absolute inset-0 z-10"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,68 +1,166 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione prenotazione: form elegante su sfondo blu petrolio
|
||||
* Layout: testo a sinistra + form a destra
|
||||
* Sezione prenotazione: form to mail con conferma non vincolante.
|
||||
*/
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar, Clock, User, Phone, PawPrint, CheckCircle2 } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import {
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
PawPrint,
|
||||
Phone,
|
||||
ShieldCheck,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const services = [
|
||||
"Visita generale",
|
||||
"Radiologia / Ecografia",
|
||||
"Chirurgia (consulenza)",
|
||||
"Laboratorio analisi",
|
||||
"Visita clinica generale",
|
||||
"Ecografia",
|
||||
"Radiologia",
|
||||
"Laboratorio",
|
||||
"Vaccinazione",
|
||||
"Dermatologia",
|
||||
"Odontoiatria",
|
||||
"Oncologia",
|
||||
"Dermatologia",
|
||||
"Oculistica",
|
||||
"Nutrizione",
|
||||
"Ortopedia",
|
||||
"Endoscopia",
|
||||
"Laparoscopia",
|
||||
];
|
||||
|
||||
const 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 = [
|
||||
"09:00", "09:30", "10:00", "10:30", "11:00", "11:30",
|
||||
"14:30", "15:00", "15:30", "16:00", "16:30", "17:00",
|
||||
"09:00",
|
||||
"09:30",
|
||||
"10:00",
|
||||
"10:30",
|
||||
"11:00",
|
||||
"11:30",
|
||||
"14:30",
|
||||
"15:00",
|
||||
"15:30",
|
||||
"16:00",
|
||||
"16:30",
|
||||
"17:00",
|
||||
];
|
||||
|
||||
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() {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
name: "",
|
||||
phone: "",
|
||||
petName: "",
|
||||
petType: "cane",
|
||||
service: "",
|
||||
date: "",
|
||||
time: "",
|
||||
notes: "",
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submittedName, setSubmittedName] = useState("");
|
||||
const [form, setForm] = useState<BookingFormState>(initialForm);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
setSubmitted(true);
|
||||
toast.success("Richiesta inviata!", {
|
||||
description: "Ti contatteremo entro 24 ore per confermare l'appuntamento.",
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
|
||||
const response = await fetch("/api/booking-request", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: form.name,
|
||||
phone: form.phone,
|
||||
pet_name: form.petName,
|
||||
pet_type: form.petType,
|
||||
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 (
|
||||
<section id="prenotazione" className="py-20 md:py-28 bg-[#1B4F72] relative overflow-hidden">
|
||||
{/* Decorazioni di sfondo */}
|
||||
<div className="absolute top-0 right-0 w-96 h-96 rounded-full bg-white/5 -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-64 h-64 rounded-full bg-[#4ECDC4]/10 translate-y-1/2 -translate-x-1/2" />
|
||||
<section id="prenotazione" className="relative overflow-hidden bg-[#1B4F72] py-20 md:py-28">
|
||||
<div className="absolute right-0 top-0 h-96 w-96 translate-x-1/2 -translate-y-1/2 rounded-full bg-white/5" />
|
||||
<div className="absolute bottom-0 left-0 h-64 w-64 -translate-x-1/2 translate-y-1/2 rounded-full bg-[#4ECDC4]/10" />
|
||||
|
||||
<div className="container relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
|
||||
{/* Colonna sinistra: testo */}
|
||||
<div className="grid grid-cols-1 items-start gap-16 lg:grid-cols-2">
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
@@ -70,15 +168,15 @@ export default function BookingSection() {
|
||||
transition={{ duration: 0.8 }}
|
||||
className="text-white"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-0.5 w-12 bg-[#4ECDC4]" />
|
||||
<span className="text-sm font-semibold uppercase tracking-widest text-[#4ECDC4]">
|
||||
Prenotazioni
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
className="text-white mb-6"
|
||||
className="mb-6 text-white"
|
||||
style={{
|
||||
fontFamily: "'Cormorant Garamond', serif",
|
||||
fontSize: "clamp(2rem, 4vw, 3rem)",
|
||||
@@ -86,93 +184,139 @@ export default function BookingSection() {
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Prenota la tua visita{" "}
|
||||
<span className="italic text-[#4ECDC4]">online</span>
|
||||
Richiedi una visita <span className="italic text-[#4ECDC4]">online</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-white/80 leading-relaxed mb-8 text-base">
|
||||
Compila il modulo per richiedere un appuntamento. Ti contatteremo entro 24 ore
|
||||
per confermare la data e l'orario. Per urgenze, chiama direttamente il numero
|
||||
dedicato disponibile 24 ore su 24.
|
||||
<p className="mb-8 text-base leading-relaxed text-white/80">
|
||||
Compila il modulo per inviare una richiesta di appuntamento. La richiesta
|
||||
non e vincolante e dovra essere confermata dallo staff della clinica, che ti
|
||||
ricontattera il prima possibile.
|
||||
</p>
|
||||
|
||||
{/* Info box urgenze */}
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-5 border border-white/20 mb-8">
|
||||
<p className="text-[#4ECDC4] font-semibold text-sm mb-2 uppercase tracking-wide">
|
||||
<div className="mb-8 rounded-xl border border-white/20 bg-white/10 p-5 backdrop-blur-sm">
|
||||
<p className="mb-2 text-sm font-semibold uppercase tracking-wide text-[#4ECDC4]">
|
||||
Urgenze 24h
|
||||
</p>
|
||||
<a
|
||||
href="tel:3205322439"
|
||||
className="text-white text-2xl font-bold hover:text-[#4ECDC4] transition-colors"
|
||||
className="text-2xl font-bold text-white transition-colors hover:text-[#4ECDC4]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif" }}
|
||||
>
|
||||
320 532.24.39
|
||||
</a>
|
||||
<p className="text-white/60 text-xs mt-1">Disponibile 7 giorni su 7</p>
|
||||
<p className="mt-1 text-xs text-white/60">Disponibile 7 giorni su 7</p>
|
||||
</div>
|
||||
|
||||
{/* Orari */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/70 text-sm font-semibold uppercase tracking-wide mb-3">
|
||||
Orari di apertura
|
||||
<div className="mb-8 rounded-2xl border border-[#4ECDC4]/30 bg-[#14384F] p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-[#4ECDC4]/15">
|
||||
<ShieldCheck size={18} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Richiesta non vincolante</p>
|
||||
<p className="mt-1 text-sm leading-relaxed text-white/75">
|
||||
L'invio del modulo non costituisce conferma automatica dell'appuntamento.
|
||||
Il team verifichera disponibilita, tipologia di visita e urgenza del caso prima
|
||||
di confermare data e orario.
|
||||
</p>
|
||||
{[
|
||||
{ days: "Lunedì — Venerdì", hours: "09:00 — 12:30 · 14:30 — 19:00" },
|
||||
{ days: "Sabato", hours: "09:00 — 12:30" },
|
||||
{ days: "Domenica", hours: "Solo urgenze" },
|
||||
].map((slot) => (
|
||||
<div key={slot.days} className="flex justify-between items-center text-sm">
|
||||
<span className="text-white/70">{slot.days}</span>
|
||||
<span className="text-white font-medium">{slot.hours}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-[39rem]">
|
||||
<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">
|
||||
<h4
|
||||
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>
|
||||
</motion.div>
|
||||
|
||||
{/* Colonna destra: form */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
{submitted ? (
|
||||
<div className="bg-white rounded-2xl p-8 text-center shadow-2xl">
|
||||
<div className="w-16 h-16 rounded-full bg-[#4ECDC4]/15 flex items-center justify-center mx-auto mb-4">
|
||||
<div className="rounded-2xl bg-white p-8 text-center shadow-2xl">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-[#4ECDC4]/15">
|
||||
<CheckCircle2 size={32} className="text-[#4ECDC4]" />
|
||||
</div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-2"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.8rem" }}
|
||||
className="mb-2 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.9rem" }}
|
||||
>
|
||||
Richiesta inviata!
|
||||
Grazie {submittedName || "per la tua richiesta"}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm mb-6">
|
||||
Abbiamo ricevuto la tua richiesta di appuntamento. Ti contatteremo entro 24 ore
|
||||
per confermare data e orario.
|
||||
<p className="mb-3 text-sm leading-relaxed text-gray-600">
|
||||
La tua richiesta di prenotazione e stata inviata correttamente.
|
||||
</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
|
||||
className="bg-[#1B4F72] hover:bg-[#163d5a] text-white"
|
||||
className="bg-[#1B4F72] text-white hover:bg-[#163d5a]"
|
||||
onClick={() => setSubmitted(false)}
|
||||
>
|
||||
Nuova prenotazione
|
||||
Invia una nuova richiesta
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-white rounded-2xl p-6 md:p-8 shadow-2xl space-y-4"
|
||||
className="space-y-4 rounded-2xl bg-white p-6 shadow-2xl md:p-8"
|
||||
>
|
||||
<div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-2"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.5rem", fontWeight: 600 }}
|
||||
className="mb-2 text-[#1B4F72]"
|
||||
style={{
|
||||
fontFamily: "'Cormorant Garamond', serif",
|
||||
fontSize: "1.5rem",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Richiedi un appuntamento
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
Compila i campi richiesti e inviaci una proposta di data: sara lo staff a
|
||||
confermare la visita.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Nome e telefono */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Nome e Cognome *
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -183,12 +327,13 @@ export default function BookingSection() {
|
||||
placeholder="Mario Rossi"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Telefono *
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -199,17 +344,16 @@ export default function BookingSection() {
|
||||
placeholder="333 123 4567"
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animale */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
Nome dell'animale
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Nome dell'animale
|
||||
</label>
|
||||
<div className="relative">
|
||||
<PawPrint size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
@@ -218,18 +362,19 @@ export default function BookingSection() {
|
||||
placeholder="Fido"
|
||||
value={form.petName}
|
||||
onChange={(e) => setForm({ ...form, petName: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Tipo di animale
|
||||
</label>
|
||||
<select
|
||||
value={form.petType}
|
||||
onChange={(e) => setForm({ ...form, petType: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all bg-white"
|
||||
className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
>
|
||||
<option value="cane">Cane</option>
|
||||
<option value="gatto">Gatto</option>
|
||||
@@ -238,28 +383,47 @@ export default function BookingSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Servizio */}
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
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 *
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
value={form.service}
|
||||
onChange={(e) => setForm({ ...form, service: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all bg-white"
|
||||
className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
>
|
||||
<option value="">Seleziona un servizio</option>
|
||||
{services.map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
{services.map((service) => (
|
||||
<option key={service} value={service}>
|
||||
{service}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Data e ora */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Data preferita *
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -270,12 +434,13 @@ export default function BookingSection() {
|
||||
value={form.date}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
onChange={(e) => setForm({ ...form, date: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all"
|
||||
className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Orario preferito
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -283,20 +448,21 @@ export default function BookingSection() {
|
||||
<select
|
||||
value={form.time}
|
||||
onChange={(e) => setForm({ ...form, time: e.target.value })}
|
||||
className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all bg-white"
|
||||
className="w-full rounded-lg border border-gray-200 bg-white py-2.5 pl-9 pr-3 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
>
|
||||
<option value="">Qualsiasi orario</option>
|
||||
{timeSlots.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
{timeSlots.map((time) => (
|
||||
<option key={time} value={time}>
|
||||
{time}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-wide block mb-1.5">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wide text-gray-600">
|
||||
Note aggiuntive
|
||||
</label>
|
||||
<textarea
|
||||
@@ -304,19 +470,20 @@ export default function BookingSection() {
|
||||
placeholder="Descrivi brevemente il motivo della visita..."
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm({ ...form, notes: e.target.value })}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all resize-none"
|
||||
className="w-full resize-none rounded-lg border border-gray-200 px-3 py-2.5 text-sm transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-[#4ECDC4]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-[#4ECDC4] hover:bg-[#3ab5ad] text-white font-bold py-3 text-base shadow-lg shadow-[#4ECDC4]/30 transition-all duration-300 hover:shadow-xl"
|
||||
disabled={submitting}
|
||||
className="w-full bg-[#4ECDC4] py-3 text-base font-bold text-white shadow-lg shadow-[#4ECDC4]/30 transition-all duration-300 hover:bg-[#3ab5ad] hover:shadow-xl disabled:cursor-wait disabled:opacity-80"
|
||||
>
|
||||
Invia Richiesta di Appuntamento
|
||||
{submitting ? "Invio in corso..." : "Invia richiesta di prenotazione"}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
* Campi obbligatori. Ti contatteremo entro 24 ore per confermare.
|
||||
<p className="text-center text-xs text-gray-500">
|
||||
* Campi obbligatori. La richiesta sara valutata e confermata dallo staff.
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -4,17 +4,15 @@
|
||||
* Sfondo blu petrolio scuro, testo bianco/grigio chiaro
|
||||
*/
|
||||
import { MapPin, Phone, Mail, Facebook, Clock, ArrowRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const quickLinks = [
|
||||
{ label: "Chi Siamo", href: "#chi-siamo" },
|
||||
{ label: "Radiologia", href: "#servizi" },
|
||||
{ label: "Chirurgia", href: "#servizi" },
|
||||
{ label: "Laboratorio", href: "#servizi" },
|
||||
{ label: "Il Team", href: "#team" },
|
||||
{ label: "News & Blog", href: "#news" },
|
||||
{ label: "Prenota Visita", href: "#prenotazione" },
|
||||
{ label: "Area Personale", href: "#registrazione" },
|
||||
{ label: "Chi Siamo", href: "/#chi-siamo" },
|
||||
{ label: "Radiologia", href: "/#servizi" },
|
||||
{ label: "Chirurgia", href: "/#servizi" },
|
||||
{ label: "Laboratorio", href: "/#servizi" },
|
||||
{ label: "Il Team", href: "/#team" },
|
||||
{ label: "Prenota Visita", href: "/#prenotazione" },
|
||||
{ label: "Area Personale", href: "/#registrazione" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
@@ -23,20 +21,9 @@ export default function Footer() {
|
||||
{/* Main footer */}
|
||||
<div className="container py-16">
|
||||
<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="flex items-center gap-3 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 className="mb-4">
|
||||
<div
|
||||
className="font-bold text-white leading-tight"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem" }}
|
||||
@@ -47,7 +34,6 @@ export default function Footer() {
|
||||
Formiginese
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm leading-relaxed mb-4">
|
||||
Cura specialistica per cani e gatti a Formigine.
|
||||
Un team di professionisti al servizio della salute dei tuoi animali.
|
||||
@@ -61,7 +47,7 @@ export default function Footer() {
|
||||
<Facebook size={16} />
|
||||
Seguici su Facebook
|
||||
</a>
|
||||
</div>
|
||||
</div>*/}
|
||||
|
||||
{/* Link rapidi */}
|
||||
<div>
|
||||
@@ -133,9 +119,11 @@ export default function Footer() {
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ days: "Lunedì — Venerdì", hours: "09:00 — 12:30\n14:30 — 19:00" },
|
||||
{ days: "Sabato", hours: "09:00 — 12:30" },
|
||||
{ days: "Domenica", hours: "Solo urgenze" },
|
||||
{ days: "Visite: Lunedì — Venerdì", hours: "09:00 — 19:30" },
|
||||
{ days: "Visite: Sabato", hours: "09:00 — 17:00" },
|
||||
{ days: "Urgenze: Lunedì — Venerdì", hours: "08:00 — 22:30" },
|
||||
{ days: "Urgenze: Sabato e Festivi", hours: "09:00 — 20:00" },
|
||||
|
||||
].map((slot) => (
|
||||
<div key={slot.days} className="flex items-start gap-2">
|
||||
<Clock size={13} className="text-[#4ECDC4] flex-shrink-0 mt-0.5" />
|
||||
@@ -148,12 +136,7 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
@@ -170,24 +153,15 @@ export default function Footer() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => toast.info("Privacy Policy in arrivo")}
|
||||
className="hover:text-white/70 transition-colors"
|
||||
>
|
||||
<a href="/privacy-policy" className="hover:text-white/70 transition-colors">
|
||||
Privacy Policy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toast.info("Cookie Policy in arrivo")}
|
||||
className="hover:text-white/70 transition-colors"
|
||||
>
|
||||
</a>
|
||||
<a href="/cookie-policy" className="hover:text-white/70 transition-colors">
|
||||
Cookie Policy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toast.info("Note legali in arrivo")}
|
||||
className="hover:text-white/70 transition-colors"
|
||||
>
|
||||
</a>
|
||||
<a href="/note-legali" className="hover:text-white/70 transition-colors">
|
||||
Note Legali
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-white/25 text-xs mt-3 text-center md:text-left">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Hero a schermo intero con immagine animale + overlay gradiente + testo sovrapposto
|
||||
* Immagine: hero_dog_cat.jpg (golden retriever + gatto in prato soleggiato)
|
||||
* Testo bianco su overlay scuro, CTA verde acqua
|
||||
* Testo elegante su overlay scuro, card orari raffinata, CTA verdi acqua
|
||||
*/
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown, Calendar, Phone } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Calendar, ChevronDown, Clock, Phone } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const heroImages = [
|
||||
{
|
||||
@@ -18,6 +18,10 @@ const heroImages = [
|
||||
url: "/images/hero_dog_cat.jpg",
|
||||
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",
|
||||
alt: "Clinica Veterinaria Formiginese - Ingresso e sala d'attesa",
|
||||
@@ -34,26 +38,36 @@ const heroImages = [
|
||||
url: "/images/hero_cat.jpg",
|
||||
alt: "Ritratto maestoso di un Maine Coon",
|
||||
},
|
||||
{
|
||||
url: "/images/coniglio.png",
|
||||
alt: "Ritratto di un coniglio",
|
||||
},
|
||||
{
|
||||
url: "/images/clinica_ingresso3.webp",
|
||||
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() {
|
||||
const [currentImage, setCurrentImage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// 3 secondi di visualizzazione + 2 secondi di dissolvenza = 5 secondi totali
|
||||
const interval = setInterval(() => {
|
||||
setCurrentImage((prev) => (prev + 1) % heroImages.length);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative h-[92vh] min-h-[600px] max-h-[900px] overflow-hidden">
|
||||
{/* Background images con crossfade */}
|
||||
{heroImages.map((img, index) => (
|
||||
<div
|
||||
key={img.url}
|
||||
@@ -63,78 +77,95 @@ export default function HeroSection() {
|
||||
<img
|
||||
src={img.url}
|
||||
alt={img.alt}
|
||||
className="w-full h-full object-cover"
|
||||
className="h-full w-full object-cover"
|
||||
style={{ transform: "scale(1.05)" }}
|
||||
/>
|
||||
</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-t from-[#0d2b3e]/50 via-transparent to-transparent" />
|
||||
|
||||
{/* Contenuto hero */}
|
||||
<div className="relative z-10 h-full flex items-center">
|
||||
<div className="relative z-10 flex h-full items-center">
|
||||
<div className="container">
|
||||
<div className="max-w-2xl">
|
||||
{/* 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 */}
|
||||
<div className="max-w-3xl">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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={{
|
||||
fontFamily: "'Cormorant Garamond', serif",
|
||||
fontSize: "clamp(2.5rem, 6vw, 4.5rem)",
|
||||
fontWeight: 600,
|
||||
fontSize: "clamp(3.15rem, 6.4vw, 5.5rem)",
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.03em",
|
||||
textShadow: "0 14px 34px rgba(0,0,0,0.24)",
|
||||
}}
|
||||
>
|
||||
La salute del tuo animale,{" "}
|
||||
<span className="italic text-[#4ECDC4]">la nostra missione</span>
|
||||
<span className="block text-white/92">Clinica Veterinaria</span>
|
||||
<span className="block italic text-white">Formiginese</span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Sottotitolo */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 22 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.5 }}
|
||||
className="text-white/85 text-lg mb-8 leading-relaxed max-w-xl"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
transition={{ duration: 0.7, delay: 0.55 }}
|
||||
className="mb-14 max-w-[39rem]"
|
||||
>
|
||||
Clinica veterinaria specialistica a Formigine. Un team di 6 professionisti
|
||||
dedicati alla cura di cani e gatti, con tecnologie diagnostiche avanzate.
|
||||
</motion.p>
|
||||
<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">
|
||||
<h4
|
||||
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
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.7 }}
|
||||
className="flex flex-col sm:flex-row gap-4"
|
||||
transition={{ duration: 0.7, delay: 0.75 }}
|
||||
className="flex flex-col gap-4 pt-1 sm:flex-row"
|
||||
>
|
||||
<Button
|
||||
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"
|
||||
onClick={() => document.getElementById("prenotazione")?.scrollIntoView({ behavior: "smooth" })}
|
||||
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" })
|
||||
}
|
||||
>
|
||||
<Calendar size={18} className="mr-2" />
|
||||
Prenota una Visita
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
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
|
||||
>
|
||||
<a href="tel:0598396263">
|
||||
@@ -143,54 +174,24 @@ export default function HeroSection() {
|
||||
</a>
|
||||
</Button>
|
||||
</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>
|
||||
|
||||
{/* Indicatori slideshow */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-10">
|
||||
<div className="absolute bottom-8 left-1/2 z-10 flex -translate-x-1/2 gap-2">
|
||||
{heroImages.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentImage(index)}
|
||||
className={`transition-all duration-300 rounded-full ${
|
||||
index === currentImage
|
||||
? "w-8 h-2 bg-[#4ECDC4]"
|
||||
: "w-2 h-2 bg-white/50 hover:bg-white/80"
|
||||
className={`rounded-full transition-all duration-300 ${
|
||||
index === currentImage ? "h-2 w-8 bg-[#4ECDC4]" : "h-2 w-2 bg-white/50 hover:bg-white/80"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-8 right-8 z-10 hidden md:flex flex-col items-center gap-2 text-white/60">
|
||||
<span className="text-xs uppercase tracking-widest rotate-90 mb-2">Scroll</span>
|
||||
<div className="absolute bottom-8 right-8 z-10 hidden flex-col items-center gap-2 text-white/60 md:flex">
|
||||
<span className="mb-2 rotate-90 text-xs uppercase tracking-widest">Scroll</span>
|
||||
<ChevronDown size={16} className="animate-bounce" />
|
||||
</div>
|
||||
</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 { 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: "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: "News", href: "#news" },
|
||||
{ label: "Contatti", href: "#contatti" },
|
||||
];
|
||||
|
||||
@@ -19,6 +55,12 @@ export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const showActivationToast = () => {
|
||||
toast.info("Servizio in corso di attivazione", {
|
||||
description: "L'area riservata sara disponibile a breve.",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 80);
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
@@ -40,9 +82,7 @@ export default function Navbar() {
|
||||
059 839.62.63 | 320 532.24.39 (urgenze 24h)
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[#4ECDC4] font-semibold tracking-wide text-xs uppercase">
|
||||
Reperibilità 24h · 7 giorni su 7
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,45 +96,55 @@ export default function Navbar() {
|
||||
>
|
||||
<div className="container flex items-center justify-between h-16 md:h-20">
|
||||
{/* Logo */}
|
||||
<a href="#" className="flex items-center gap-3 group">
|
||||
<div className="w-10 h-10 rounded-full bg-[#1B4F72] flex items-center justify-center flex-shrink-0">
|
||||
<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
|
||||
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 href="#" className="flex items-center gap-2 md:gap-3 group shrink-0">
|
||||
<img
|
||||
src="/images/logo_high.png"
|
||||
alt="Simbolo Clinica Veterinaria Formiginese"
|
||||
className="h-10 md:h-14 w-auto object-contain"
|
||||
/>
|
||||
<img
|
||||
src="/images/logo_low.png"
|
||||
alt="Clinica Veterinaria Formiginese"
|
||||
className="h-8 md:h-11 w-auto object-contain"
|
||||
/>
|
||||
</a>
|
||||
|
||||
{/* Desktop menu */}
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
{navLinks.map((link) => (
|
||||
<div key={link.label} className="relative group py-7">
|
||||
<a
|
||||
key={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" }}
|
||||
>
|
||||
{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" />
|
||||
</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>
|
||||
|
||||
@@ -104,7 +154,7 @@ export default function Navbar() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-[#1B4F72] text-[#1B4F72] hover:bg-[#1B4F72] hover:text-white transition-all duration-200"
|
||||
onClick={() => document.getElementById("registrazione")?.scrollIntoView({ behavior: "smooth" })}
|
||||
onClick={showActivationToast}
|
||||
>
|
||||
Accedi
|
||||
</Button>
|
||||
@@ -131,20 +181,36 @@ export default function Navbar() {
|
||||
{mobileOpen && (
|
||||
<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) => (
|
||||
<div key={link.label} className="border-b border-gray-50 pb-2">
|
||||
<a
|
||||
key={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)}
|
||||
>
|
||||
{link.label}
|
||||
{link.children && <ChevronDown size={15} className="text-[#4ECDC4]" />}
|
||||
</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">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-[#1B4F72] text-[#1B4F72] w-full"
|
||||
onClick={() => { setMobileOpen(false); document.getElementById("registrazione")?.scrollIntoView({ behavior: "smooth" }); }}
|
||||
onClick={() => { setMobileOpen(false); showActivationToast(); }}
|
||||
>
|
||||
Accedi / Registrati
|
||||
</Button>
|
||||
|
||||
@@ -19,7 +19,7 @@ const news = [
|
||||
"Con l'arrivo della primavera è il momento ideale per aggiornare il piano vaccinale del tuo cane o gatto. Scopri quali vaccini sono essenziali e perché.",
|
||||
date: "15 Marzo 2026",
|
||||
readTime: "4 min",
|
||||
image: "https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=600&q=80",
|
||||
image: "/images/news_gatto.jpg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -30,7 +30,7 @@ const news = [
|
||||
"La dieta è fondamentale per la salute del tuo felino. I nostri veterinari spiegano come scegliere il cibo giusto e le quantità raccomandate per ogni fase della vita.",
|
||||
date: "8 Marzo 2026",
|
||||
readTime: "6 min",
|
||||
image: "https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=600&q=80",
|
||||
image: "/images/news_consulenza.jpg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -41,7 +41,7 @@ const news = [
|
||||
"La sterilizzazione è una delle procedure più comuni nella medicina veterinaria. Ecco perché è importante, quando eseguirla e cosa aspettarsi nel post-operatorio.",
|
||||
date: "1 Marzo 2026",
|
||||
readTime: "5 min",
|
||||
image: "https://images.unsplash.com/photo-1548767797-d8c844163c4c?w=600&q=80",
|
||||
image: "/images/news_cucciolo.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,74 +1,219 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione servizi con 3 card principali (radiologia, chirurgia, laboratorio)
|
||||
* Layout: immagine in alto, bordo superiore colorato, hover lift
|
||||
* Sfondo sabbia calda per contrasto con la sezione hero
|
||||
* Sezione servizi con due livelli:
|
||||
* - Servizi Clinici
|
||||
* - Visite Specialistiche
|
||||
*/
|
||||
import { motion } from "framer-motion";
|
||||
import { useInView } from "framer-motion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { ArrowRight, Scan, Scissors, FlaskConical } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
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",
|
||||
title: "Radiologia",
|
||||
subtitle: "Diagnostica per immagini",
|
||||
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.",
|
||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/radiology_39785f88.jpg",
|
||||
"Radiografie digitali e diagnostica per immagini per apparato scheletrico, torace, addome e approfondimenti d'urgenza.",
|
||||
image: "/images/services_radiologia.jpg",
|
||||
icon: Scan,
|
||||
color: "#1B4F72",
|
||||
features: ["Radiografia digitale", "Ecografia", "Ecocardiografia", "Refertazione immediata"],
|
||||
},
|
||||
{
|
||||
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"],
|
||||
color: "#2E86AB",
|
||||
features: ["Radiografia digitale", "Studi toracici", "Valutazioni ortopediche", "Diagnostica d'urgenza"],
|
||||
},
|
||||
{
|
||||
id: "laboratorio",
|
||||
title: "Laboratorio",
|
||||
subtitle: "Analisi e diagnostica",
|
||||
subtitle: "Analisi interne",
|
||||
description:
|
||||
"Laboratorio analisi interno per esami ematologici, biochimici, urinari e citologici. Risultati in tempi rapidi per diagnosi tempestive e trattamenti mirati.",
|
||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/laboratory_5a1c4119.jpg",
|
||||
"Laboratorio interno per esami rapidi e mirati, utile per diagnosi tempestive, monitoraggi terapeutici e controlli pre-operatori.",
|
||||
image: "/images/services_laboratorio.jpg",
|
||||
icon: FlaskConical,
|
||||
color: "#1B4F72",
|
||||
features: ["Ematologia", "Biochimica", "Urine", "Citologia", "Coagulazione"],
|
||||
},
|
||||
{
|
||||
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",
|
||||
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 isInView = useInView(ref, { once: true, margin: "-50px" });
|
||||
const specialist = service.tone === "specialist";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: index * 0.15 }}
|
||||
className="group bg-white rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-400 hover:-translate-y-1"
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
className={`group overflow-hidden rounded-2xl transition-all duration-400 hover:-translate-y-1 ${className} ${
|
||||
specialist
|
||||
? "border border-[#D7C1A8] bg-gradient-to-b from-[#fffaf2] to-white shadow-[0_18px_45px_rgba(169,95,58,0.12)] hover:shadow-[0_22px_50px_rgba(169,95,58,0.18)]"
|
||||
: "bg-white shadow-md hover:shadow-xl"
|
||||
}`}
|
||||
>
|
||||
{/* Bordo superiore colorato */}
|
||||
<div className="h-1" style={{ backgroundColor: service.color }} />
|
||||
<div className="h-1.5" style={{ backgroundColor: service.color }} />
|
||||
|
||||
{/* Immagine */}
|
||||
<div className="relative h-52 overflow-hidden">
|
||||
<img
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
className={`h-full w-full object-cover transition-transform duration-700 ${service.imageHoverClassName ?? "group-hover:scale-105"} ${service.imageClassName ?? ""}`}
|
||||
/>
|
||||
{/* Icon overlay */}
|
||||
<div
|
||||
className="absolute top-4 right-4 w-12 h-12 rounded-full flex items-center justify-center shadow-lg"
|
||||
style={{ backgroundColor: service.color }}
|
||||
@@ -77,40 +222,27 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenuto */}
|
||||
<div className="p-6">
|
||||
<div className="text-xs uppercase tracking-widest font-semibold mb-1" style={{ color: service.color }}>
|
||||
<div className="mb-1 text-xs font-semibold uppercase tracking-widest" style={{ color: service.color }}>
|
||||
{service.subtitle}
|
||||
</div>
|
||||
<h3
|
||||
className="text-[#1B4F72] mb-3"
|
||||
className="mb-3 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.75rem", fontWeight: 600 }}
|
||||
>
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm leading-relaxed mb-4">{service.description}</p>
|
||||
<p className="mb-4 text-sm leading-relaxed text-gray-600">{service.description}</p>
|
||||
|
||||
{/* Features */}
|
||||
<ul className="space-y-1.5 mb-5">
|
||||
<ul className="mb-5 space-y-1.5">
|
||||
{service.features.map((feat) => (
|
||||
<li key={feat} className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ backgroundColor: service.color }} />
|
||||
<span className="h-1.5 w-1.5 flex-shrink-0 rounded-full" style={{ backgroundColor: service.color }} />
|
||||
{feat}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* 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>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -118,12 +250,13 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
|
||||
|
||||
export default function ServicesSection() {
|
||||
const ref = useRef(null);
|
||||
const specialistRef = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
const specialistInView = useInView(specialistRef, { once: true, margin: "-80px" });
|
||||
|
||||
return (
|
||||
<section id="servizi" className="py-20 md:py-28" style={{ backgroundColor: "#F5F0E8" }}>
|
||||
<div className="container">
|
||||
{/* Header sezione */}
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
@@ -131,48 +264,63 @@ export default function ServicesSection() {
|
||||
transition={{ duration: 0.7 }}
|
||||
className="mb-14"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
Specializzazioni
|
||||
</span>
|
||||
</div>
|
||||
<h2
|
||||
className="text-[#1B4F72] mb-4"
|
||||
className="mb-4 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 600 }}
|
||||
>
|
||||
Servizi Specialistici
|
||||
Servizi
|
||||
</h2>
|
||||
<p className="text-gray-600 max-w-xl text-base leading-relaxed">
|
||||
Tre aree di eccellenza clinica per garantire diagnosi precise e trattamenti efficaci
|
||||
ai tuoi animali domestici, con tecnologie all'avanguardia e professionisti esperti.
|
||||
<p className="max-w-xl text-base leading-relaxed text-gray-600">
|
||||
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Grid servizi */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{services.map((service, index) => (
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-3">
|
||||
{clinicalServices.map((service, index) => (
|
||||
<ServiceCard key={service.id} service={service} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isInView ? { opacity: 1 } : {}}
|
||||
transition={{ duration: 0.7, delay: 0.6 }}
|
||||
className="text-center mt-12"
|
||||
ref={specialistRef}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={specialistInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.7, delay: 0.2 }}
|
||||
className="mt-20"
|
||||
>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 text-[#1B4F72] font-semibold border-2 border-[#1B4F72] px-8 py-3 rounded-full hover:bg-[#1B4F72] hover:text-white transition-all duration-300"
|
||||
onClick={() => {
|
||||
toast("Tutti i servizi", { description: "La pagina completa dei servizi sarà disponibile a breve." });
|
||||
}}
|
||||
<h3
|
||||
className="mb-4 text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(1.9rem, 3.7vw, 2.8rem)", fontWeight: 600 }}
|
||||
>
|
||||
Tutti i servizi
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
Visite Specialistiche
|
||||
</h3>
|
||||
</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>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,153 +1,244 @@
|
||||
/*
|
||||
* DESIGN: "Clinical Warmth"
|
||||
* Sezione team: griglia di 6 professionisti con card eleganti
|
||||
* Sfondo sabbia calda, foto placeholder con iniziali, specializzazioni colorate
|
||||
* Sezione team: due aree distinte per team medico e collaborazioni
|
||||
* Stessa grammatica visiva delle card precedenti, con schede pronte
|
||||
* ad accogliere progressivamente i profili completi.
|
||||
*/
|
||||
import { motion } from "framer-motion";
|
||||
import { useInView } from "framer-motion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
|
||||
const team = [
|
||||
type TeamMember = {
|
||||
name: string;
|
||||
role: string;
|
||||
specialization?: string;
|
||||
color: string;
|
||||
bio?: string[];
|
||||
};
|
||||
|
||||
const medicalTeam: TeamMember[] = [
|
||||
{
|
||||
name: "Dott. Paolo Parmeggiani",
|
||||
role: "Direttore Sanitario",
|
||||
specialization: "Oncologia Veterinaria",
|
||||
role: "Direttore sanitario",
|
||||
specialization: "Oncologia veterinaria",
|
||||
color: "#1B4F72",
|
||||
initials: "PP",
|
||||
bio: "Master in Oncologia Veterinaria. Direttore sanitario della clinica con oltre 20 anni di esperienza.",
|
||||
bio: [
|
||||
"Laureato a pieni voti presso l'Universita degli Studi di Milano, ha conseguito un Master di II livello in Oncologia Veterinaria presso l'Universita di Pisa e ha svolto un tirocinio di chirurgia e medicina interna presso la facolta di Sao Joao da Boa Vista, in Brasile.",
|
||||
"Iscritto all'Ordine dei Medici Veterinari di Modena (n. 715), e socio SCIVAC e SIONCOV.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Elena Rossi",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Chirurgia",
|
||||
name: "Dott.ssa Irene Paganelli",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Ecografia ed esotici",
|
||||
color: "#4ECDC4",
|
||||
initials: "ER",
|
||||
bio: "Specializzata in chirurgia ortopedica e dei tessuti molli. Referente per gli interventi d'urgenza.",
|
||||
bio: [
|
||||
"Laureata a pieni voti con lode presso l'Universita di Bologna, ha svolto un tirocinio in medicina interna e chirurgia presso l'Ospedale Veterinario dell'Universita di Copenhagen.",
|
||||
"Iscritta all'Ordine dei Medici Veterinari di Modena (n. 749). Ha seguito diversi corsi di perfezionamento in ecografia addominale ed ecocardiografia del cane e del gatto.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Dott. Marco Bianchi",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Radiologia",
|
||||
name: "Dott. Simone Tinti",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#2E86AB",
|
||||
initials: "MB",
|
||||
bio: "Esperto in diagnostica per immagini, ecografia e radiologia digitale veterinaria.",
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Sara Ferrari",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Medicina Interna",
|
||||
color: "#E8A838",
|
||||
initials: "SF",
|
||||
bio: "Specializzata in medicina interna felina e canina, con focus su malattie metaboliche.",
|
||||
name: "Dott.ssa Michela Sghedoni",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#B76E79",
|
||||
},
|
||||
{
|
||||
name: "Dott. Luca Moretti",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Dermatologia",
|
||||
name: "Dott. Luca Pietri",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#6B8F71",
|
||||
initials: "LM",
|
||||
bio: "Esperto in dermatologia e allergologia veterinaria. Referente per le patologie cutanee.",
|
||||
},
|
||||
{
|
||||
name: "Dott.ssa Anna Conti",
|
||||
role: "Medico Veterinario",
|
||||
specialization: "Odontoiatria",
|
||||
color: "#9B5DE5",
|
||||
initials: "AC",
|
||||
bio: "Specializzata in odontoiatria veterinaria e chirurgia orale per cani e gatti.",
|
||||
name: "Dott.ssa Sara Casali",
|
||||
role: "Medico veterinario",
|
||||
specialization: "Profilo in aggiornamento",
|
||||
color: "#C58C63",
|
||||
},
|
||||
{
|
||||
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 isInView = useInView(ref, { once: true, margin: "-40px" });
|
||||
const hasBio = Boolean(member.bio?.length);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
className="group bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1"
|
||||
transition={{ duration: 0.6, delay: index * 0.08 }}
|
||||
className="group h-full overflow-hidden rounded-2xl bg-white shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
{/* Bordo superiore */}
|
||||
<div className="h-1" style={{ backgroundColor: member.color }} />
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="p-6 pb-4">
|
||||
<div className="p-6 pb-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-xl flex-shrink-0 shadow-md"
|
||||
className="flex h-16 w-16 flex-shrink-0 items-center justify-center rounded-full text-xl font-bold text-white shadow-md"
|
||||
style={{ backgroundColor: member.color, fontFamily: "'Cormorant Garamond', serif" }}
|
||||
>
|
||||
{member.initials}
|
||||
{getInitials(member.name)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3
|
||||
className="text-[#1B4F72] font-semibold leading-tight"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.15rem" }}
|
||||
className="leading-tight text-[#1B4F72]"
|
||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.15rem", fontWeight: 600 }}
|
||||
>
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className="text-gray-500 text-xs mt-0.5">{member.role}</p>
|
||||
<p className="mt-0.5 text-xs text-gray-500">{member.role}</p>
|
||||
{member.specialization && (
|
||||
<span
|
||||
className="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full mt-2"
|
||||
className="mt-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold"
|
||||
style={{ backgroundColor: `${member.color}18`, color: member.color }}
|
||||
>
|
||||
{member.specialization}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm leading-relaxed mt-4">{member.bio}</p>
|
||||
{hasBio ? (
|
||||
<div className="mt-4 space-y-3">
|
||||
{member.bio?.map((paragraph) => (
|
||||
<p key={paragraph} className="text-sm leading-relaxed text-gray-600">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 rounded-xl border border-dashed border-gray-200 bg-[#F9F6F1] px-4 py-5">
|
||||
<p className="text-sm italic leading-relaxed text-gray-400">
|
||||
Profilo in aggiornamento.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamSection() {
|
||||
function TeamGroup({
|
||||
eyebrow,
|
||||
members,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
members: TeamMember[];
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-80px" });
|
||||
|
||||
return (
|
||||
<section id="team" className="py-20 md:py-28 bg-white">
|
||||
<div className="container">
|
||||
{/* Header */}
|
||||
<div className="mt-14 first:mt-0">
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.7 }}
|
||||
className="mb-14"
|
||||
className="mb-10"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-0.5 bg-[#4ECDC4]" />
|
||||
<span className="text-[#4ECDC4] text-sm font-semibold uppercase tracking-widest">
|
||||
Il Nostro Team
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<div className="h-0.5 w-10 bg-[#4ECDC4]" />
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.2em] text-[#4ECDC4]">
|
||||
{eyebrow}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<h2
|
||||
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>
|
||||
|
||||
{/* Grid team */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{team.map((member, index) => (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{members.map((member, index) => (
|
||||
<TeamCard key={member.name} member={member} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamSection() {
|
||||
return (
|
||||
<section id="team" className="bg-white py-20 md:py-28">
|
||||
<div className="container">
|
||||
<TeamGroup
|
||||
eyebrow="Team medico"
|
||||
members={medicalTeam}
|
||||
/>
|
||||
|
||||
<TeamGroup
|
||||
eyebrow="Collaborazioni"
|
||||
members={collaborators}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
78
clinica-app/client/src/pages/CookiePolicyPage.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import LegalPageLayout from "@/components/LegalPageLayout";
|
||||
|
||||
export default function CookiePolicyPage() {
|
||||
return (
|
||||
<LegalPageLayout
|
||||
eyebrow="Informativa cookie"
|
||||
title="Cookie Policy"
|
||||
intro="Questa pagina descrive l'uso di cookie e di altri strumenti tecnici da parte del sito della Clinica Veterinaria Formiginese."
|
||||
updatedAt="23 maggio 2026"
|
||||
>
|
||||
<h2>1. Cosa sono i cookie</h2>
|
||||
<p>
|
||||
I cookie sono piccoli file di testo che i siti visitati possono salvare sul dispositivo
|
||||
dell'utente per consentire il funzionamento del sito, migliorare la navigazione o memorizzare
|
||||
preferenze e impostazioni.
|
||||
</p>
|
||||
|
||||
<h2>2. Tipologie di strumenti attualmente utilizzati</h2>
|
||||
<p>Allo stato attuale il sito utilizza esclusivamente strumenti tecnici o assimilabili ai tecnici, necessari a:</p>
|
||||
<ul>
|
||||
<li>erogare correttamente le pagine e i contenuti richiesti;</li>
|
||||
<li>gestire eventuali elementi tecnici indispensabili al funzionamento dell'interfaccia;</li>
|
||||
<li>garantire sicurezza, stabilità e corretta erogazione del servizio.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Cookie tecnici</h2>
|
||||
<p>
|
||||
I cookie tecnici sono quelli strettamente necessari alla navigazione o alla fornitura di un
|
||||
servizio esplicitamente richiesto dall'utente. Per tali strumenti, secondo la normativa
|
||||
applicabile, non è richiesto il consenso preventivo dell'utente, fermo restando l'obbligo
|
||||
di fornire un'informativa adeguata.
|
||||
</p>
|
||||
|
||||
<h2>4. Cookie di profilazione e analytics</h2>
|
||||
<p>
|
||||
Alla data di aggiornamento di questa pagina il sito non utilizza cookie di profilazione,
|
||||
strumenti di marketing né sistemi di analytics attivi configurati per il monitoraggio del
|
||||
comportamento degli utenti a fini non tecnici.
|
||||
</p>
|
||||
<p>
|
||||
Qualora in futuro venissero introdotti strumenti di tracciamento non tecnici, la presente
|
||||
policy verrà aggiornata e, ove necessario, sarà implementato un meccanismo di raccolta del
|
||||
consenso conforme alla normativa applicabile.
|
||||
</p>
|
||||
|
||||
<h2>5. Risorse di terze parti</h2>
|
||||
<p>
|
||||
Il sito può richiamare risorse esterne di supporto, come librerie o font distribuiti da terzi.
|
||||
In tali casi il browser dell'utente può effettuare richieste ai rispettivi server per
|
||||
scaricare le risorse necessarie alla visualizzazione della pagina.
|
||||
</p>
|
||||
|
||||
<h2>6. Futuri servizi con autenticazione</h2>
|
||||
<p>
|
||||
In occasione della futura attivazione dell'area riservata potranno essere utilizzati cookie
|
||||
o identificatori tecnici di sessione strettamente necessari all'autenticazione e alla gestione
|
||||
sicura dell'accesso. Anche tali strumenti, se limitati alla funzione tecnica richiesta
|
||||
dall'utente, rientrano normalmente tra quelli esenti da consenso preventivo.
|
||||
</p>
|
||||
|
||||
<h2>7. Come gestire i cookie</h2>
|
||||
<p>
|
||||
L'utente può controllare e cancellare i cookie attraverso le impostazioni del proprio browser.
|
||||
La disattivazione dei cookie tecnici potrebbe tuttavia compromettere il corretto funzionamento
|
||||
di alcune funzionalità del sito.
|
||||
</p>
|
||||
|
||||
<h2>8. Contatti</h2>
|
||||
<p>
|
||||
Per qualsiasi chiarimento relativo all'uso dei cookie e degli altri strumenti tecnici è possibile
|
||||
contattare la Clinica Veterinaria Formiginese all'indirizzo{" "}
|
||||
<a href="mailto:clinicaveterinariaformiginese@gmail.com">
|
||||
clinicaveterinariaformiginese@gmail.com
|
||||
</a>.
|
||||
</p>
|
||||
</LegalPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import HeroSection from "@/components/HeroSection";
|
||||
import ServicesSection from "@/components/ServicesSection";
|
||||
import AboutSection from "@/components/AboutSection";
|
||||
import TeamSection from "@/components/TeamSection";
|
||||
import NewsSection from "@/components/NewsSection";
|
||||
import BookingSection from "@/components/BookingSection";
|
||||
import AuthSection from "@/components/AuthSection";
|
||||
import Footer from "@/components/Footer";
|
||||
@@ -23,7 +22,6 @@ export default function Home() {
|
||||
<ServicesSection />
|
||||
<AboutSection />
|
||||
<TeamSection />
|
||||
<NewsSection />
|
||||
<BookingSection />
|
||||
<AuthSection />
|
||||
</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({
|
||||
plugins,
|
||||
@@ -168,7 +176,7 @@ export default defineConfig({
|
||||
emptyOutDir: true,
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
port: 3001,
|
||||
strictPort: false, // Will find next available port if 3000 is busy
|
||||
host: true,
|
||||
allowedHosts: [
|
||||
|
||||
@@ -3,6 +3,7 @@ setlocal
|
||||
|
||||
set "DEPLOY_ROOT=C:\deploy\clinica_veterinaria_formiginese"
|
||||
set "APP_DIR=%DEPLOY_ROOT%\clinica-app"
|
||||
set "DEPLOY_BRANCH="
|
||||
|
||||
echo === Deploy demo Clinica Veterinaria Formiginese ===
|
||||
echo.
|
||||
@@ -19,13 +20,34 @@ if not exist "%APP_DIR%\" (
|
||||
|
||||
echo [1/3] Aggiornamento repository di deploy...
|
||||
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 ERRORE: git pull non riuscito.
|
||||
echo ERRORE: impossibile determinare il branch corrente nella copia di deploy.
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Branch deploy attivo: %DEPLOY_BRANCH%
|
||||
|
||||
git fetch origin
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ERRORE: git fetch non riuscito.
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
git reset --hard origin/%DEPLOY_BRANCH%
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ERRORE: git reset del branch di deploy non riuscito.
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
popd
|
||||
|
||||
echo.
|
||||
|
||||
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%';" ^
|
||||
"$content = Get-Content -Raw $path;" ^
|
||||
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %LIVE_ROOT%' + [Environment]::NewLine + ' try_files {path} /index.html' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' route {' + [Environment]::NewLine + ' handle /api/* {' + [Environment]::NewLine + ' reverse_proxy 127.0.0.1:8000' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' handle {' + [Environment]::NewLine + ' root * %LIVE_ROOT%' + [Environment]::NewLine + ' try_files {path} /index.html' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' }' + [Environment]::NewLine + '}';" ^
|
||||
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||
if errorlevel 1 (
|
||||
@@ -34,7 +34,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$path = '%CADDYFILE%';" ^
|
||||
"$content = Get-Content -Raw $path;" ^
|
||||
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %COMING_SOON_ROOT%' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
||||
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' route {' + [Environment]::NewLine + ' handle /api/* {' + [Environment]::NewLine + ' reverse_proxy 127.0.0.1:8000' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' handle {' + [Environment]::NewLine + ' root * %COMING_SOON_ROOT%' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + ' }' + [Environment]::NewLine + ' }' + [Environment]::NewLine + '}';" ^
|
||||
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||
if errorlevel 1 (
|
||||
|
||||