9 Commits

38 changed files with 466 additions and 108 deletions

16
.gitignore vendored Normal file
View File

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

BIN
assets/cavia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
assets/coniglio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
assets/logo_high.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
assets/logo_low.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

5
backend/.env.example Normal file
View File

@@ -0,0 +1,5 @@
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

View File

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

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

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

View File

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

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

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

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

@@ -0,0 +1,31 @@
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 = PROJECT_ROOT / ".env"
load_dotenv(ENV_FILE)
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()}")
@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
settings = Settings()

View File

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

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

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

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

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

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

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

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

@@ -0,0 +1,11 @@
from fastapi import FastAPI
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)

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
# Business services package.

6
backend/requirements.txt Normal file
View File

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

17
backend/run_backend.bat Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -25,27 +25,15 @@ export default function Footer() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{/* 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 className="mb-4">
<div
className="font-bold text-white leading-tight"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem" }}
>
Clinica Veterinaria
</div>
<div>
<div
className="font-bold text-white leading-tight"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem" }}
>
Clinica Veterinaria
</div>
<div className="text-[#4ECDC4] text-xs font-semibold tracking-widest uppercase">
Formiginese
</div>
<div className="text-[#4ECDC4] text-xs font-semibold tracking-widest uppercase">
Formiginese
</div>
</div>
<p className="text-white/60 text-sm leading-relaxed mb-4">

View File

@@ -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,6 +38,10 @@ 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",

View File

@@ -5,11 +5,47 @@
*/
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";
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" },
@@ -56,45 +92,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) => (
<a
key={link.href}
href={link.href}
className="text-[#1B4F72] font-medium text-sm hover:text-[#4ECDC4] transition-colors duration-200 relative group"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{link.label}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-[#4ECDC4] transition-all duration-300 group-hover:w-full" />
</a>
<div key={link.label} className="relative group py-7">
<a
href={link.href}
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>
@@ -131,14 +177,30 @@ 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) => (
<a
key={link.href}
href={link.href}
className="text-[#1B4F72] font-medium py-2 border-b border-gray-50 hover:text-[#4ECDC4] transition-colors"
onClick={() => setMobileOpen(false)}
>
{link.label}
</a>
<div key={link.label} className="border-b border-gray-50 pb-2">
<a
href={link.href}
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

View File

@@ -1,48 +1,79 @@
/*
* 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 6 card principali allineate al menu della clinica.
* Layout: immagine in alto, bordo superiore colorato, hover lift.
*/
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 { Activity, ArrowRight, FlaskConical, Scan, Stethoscope, Video } from "lucide-react";
import { toast } from "sonner";
const services = [
{
id: "visite-cliniche",
title: "Visite Cliniche",
subtitle: "Medicina preventiva",
description:
"Percorsi di prevenzione e visite cliniche complete per monitorare lo stato di salute di cani e gatti in ogni fase della vita. Un approccio attento, continuativo e personalizzato.",
image: "https://images.unsplash.com/photo-1628009368231-7bb7cfcb0def?w=900&q=85",
icon: Stethoscope,
color: "#1B4F72",
features: ["Check-up periodici", "Vaccinazioni", "Profilassi antiparassitaria", "Piani prevenzione senior"],
},
{
id: "ecografia",
title: "Ecografia",
subtitle: "Diagnostica non invasiva",
description:
"Indagini ecografiche per approfondire in modo rapido e non invasivo organi addominali, apparato urinario, apparato riproduttivo e principali sospetti clinici.",
image: "https://images.unsplash.com/photo-1579684385127-1ef15d508118?w=900&q=85",
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.",
"Radiografie digitali e diagnostica per immagini a supporto della visita clinica, utili per valutare apparato scheletrico, torace, addome e urgenze.",
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/radiology_39785f88.jpg",
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.",
"Laboratorio interno per esami rapidi e mirati, fondamentale per supportare diagnosi tempestive, monitoraggi terapeutici e controlli pre-operatori.",
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/laboratory_5a1c4119.jpg",
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 quando il quadro clinico lo consente.",
image: "https://images.unsplash.com/photo-1530026405186-ed1f139313f8?w=900&q=85",
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 che permettono interventi piu delicati, incisioni ridotte e recuperi piu confortevoli rispetto alla chirurgia tradizionale.",
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/surgery_e92904ed.jpg",
icon: Activity,
color: "#2E86AB",
features: ["Ematologia completa", "Biochimica sierica", "Esame urine", "Citologia"],
features: ["Procedure mini-invasive", "Biopsie laparoscopiche", "Riduzione del dolore", "Recupero post-operatorio"],
},
];
@@ -55,20 +86,17 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: index * 0.15 }}
transition={{ duration: 0.6, delay: index * 0.12 }}
className="group bg-white rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-400 hover:-translate-y-1"
>
{/* Bordo superiore colorato */}
<div className="h-1" 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"
/>
{/* 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,7 +105,6 @@ 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 }}>
{service.subtitle}
@@ -90,7 +117,6 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
</h3>
<p className="text-gray-600 text-sm leading-relaxed mb-4">{service.description}</p>
{/* Features */}
<ul className="space-y-1.5 mb-5">
{service.features.map((feat) => (
<li key={feat} className="flex items-center gap-2 text-sm text-gray-600">
@@ -100,15 +126,14 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
))}
</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.` });
toast("Sezione in arrivo", { description: `La pagina dedicata a ${service.title} sara disponibile a breve.` });
}}
>
Scopri di più
Scopri di piu
<ArrowRight size={15} className="transition-transform duration-200 group-hover/btn:translate-x-1" />
</button>
</div>
@@ -123,7 +148,6 @@ export default function ServicesSection() {
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 }}
@@ -144,19 +168,17 @@ export default function ServicesSection() {
Servizi Specialistici
</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.
Sei aree di eccellenza clinica per accompagnare prevenzione, diagnosi e trattamento
dei tuoi animali domestici, con tecnologie avanzate e professionisti esperti.
</p>
</motion.div>
{/* Grid servizi */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8">
{services.map((service, index) => (
<ServiceCard key={service.id} service={service} index={index} />
))}
</div>
{/* Link a tutti i servizi */}
<motion.div
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
@@ -166,7 +188,7 @@ export default function ServicesSection() {
<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." });
toast("Tutti i servizi", { description: "La pagina completa dei servizi sara disponibile a breve." });
}}
>
Tutti i servizi

View File

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

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

23
menu_sito.txt Normal file
View File

@@ -0,0 +1,23 @@
- servizi
- visite clincihe e medicina preventiva
- ecografia
- radiologia
- laboratorio
- ematologia
- biochimica
- urine
- citologia
- coagulazione
- endoscopia
- laparoscopia
- viite specialistiche
- oncologia
- dermatologia
- oculistica
- nutrizione
- ortopedia
- team medico