Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35de87511d | |||
| d4a5f736b9 | |||
| 3545d988ce | |||
| f5d0b65dfa | |||
| 3d7b4a5fcc | |||
| bc44ee497f | |||
| 680974994b | |||
| b9a5d7f36f | |||
| c73efb7680 | |||
| 051b9c2102 | |||
| 8fb5ff41c3 |
16
.gitignore
vendored
Normal 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
|
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 |
5
backend/.env.example
Normal 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
|
||||||
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.
|
||||||
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",
|
||||||
|
}
|
||||||
31
backend/app/config.py
Normal 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()
|
||||||
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()
|
||||||
11
backend/app/main.py
Normal 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)
|
||||||
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.
|
||||||
1
backend/app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Business services package.
|
||||||
6
backend/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
sqlalchemy
|
||||||
|
pydantic
|
||||||
|
python-dotenv
|
||||||
|
aiosqlite
|
||||||
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
|
||||||
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 |
@@ -25,27 +25,15 @@ export default function Footer() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="mb-4">
|
||||||
<div className="w-10 h-10 rounded-full bg-[#1B4F72] flex items-center justify-center">
|
<div
|
||||||
<svg viewBox="0 0 40 40" fill="none" className="w-6 h-6">
|
className="font-bold text-white leading-tight"
|
||||||
<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"/>
|
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1rem" }}
|
||||||
<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"/>
|
Clinica Veterinaria
|
||||||
<path d="M10 17c-1.2 0-2 1-2 2.2S8.8 21.5 10 21.5s2-1 2-2.2S11.2 17 10 17z" fill="#4ECDC4"/>
|
|
||||||
<path d="M30 17c-1.2 0-2 1-2 2.2S28.8 21.5 30 21.5s2-1 2-2.2S31.2 17 30 17z" fill="#4ECDC4"/>
|
|
||||||
<path d="M20 17c-5 0-9 3.5-9 7.5 0 2.5 1.5 4.5 3.5 5.5.5.3 1 .5 1.5.5h8c.5 0 1-.2 1.5-.5 2-1 3.5-3 3.5-5.5 0-4-4-7.5-9-7.5z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="text-[#4ECDC4] text-xs font-semibold tracking-widest uppercase">
|
||||||
<div
|
Formiginese
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/60 text-sm leading-relaxed mb-4">
|
<p className="text-white/60 text-sm leading-relaxed mb-4">
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const heroImages = [
|
|||||||
url: "/images/hero_dog_cat.jpg",
|
url: "/images/hero_dog_cat.jpg",
|
||||||
alt: "Golden retriever e gatto tabby insieme in un prato soleggiato",
|
alt: "Golden retriever e gatto tabby insieme in un prato soleggiato",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
url: "/images/cavia.png",
|
||||||
|
alt: "Ritratto di una cavia",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
url: "/images/clinica_ingresso1.png",
|
url: "/images/clinica_ingresso1.png",
|
||||||
alt: "Clinica Veterinaria Formiginese - Ingresso e sala d'attesa",
|
alt: "Clinica Veterinaria Formiginese - Ingresso e sala d'attesa",
|
||||||
@@ -34,6 +38,10 @@ const heroImages = [
|
|||||||
url: "/images/hero_cat.jpg",
|
url: "/images/hero_cat.jpg",
|
||||||
alt: "Ritratto maestoso di un Maine Coon",
|
alt: "Ritratto maestoso di un Maine Coon",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
url: "/images/coniglio.png",
|
||||||
|
alt: "Ritratto di un coniglio",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
url: "/images/clinica_ingresso3.webp",
|
url: "/images/clinica_ingresso3.webp",
|
||||||
alt: "Clinica Veterinaria Formiginese - Ingresso principale",
|
alt: "Clinica Veterinaria Formiginese - Ingresso principale",
|
||||||
|
|||||||
@@ -5,11 +5,47 @@
|
|||||||
*/
|
*/
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Menu, X, Phone, MapPin } from "lucide-react";
|
import { ChevronDown, Menu, X, Phone, MapPin } from "lucide-react";
|
||||||
|
|
||||||
const navLinks = [
|
type NavLink = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
children?: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLinks: NavLink[] = [
|
||||||
{ label: "Chi Siamo", href: "#chi-siamo" },
|
{ label: "Chi Siamo", href: "#chi-siamo" },
|
||||||
{ label: "Servizi", href: "#servizi" },
|
{
|
||||||
|
label: "Servizi",
|
||||||
|
href: "#servizi",
|
||||||
|
children: [
|
||||||
|
{ label: "Visite cliniche e medicina preventiva", href: "#servizi" },
|
||||||
|
{ label: "Ecografia", href: "#servizi" },
|
||||||
|
{ label: "Radiologia", href: "#servizi" },
|
||||||
|
{ label: "Laboratorio", href: "#servizi" },
|
||||||
|
{ label: "Ematologia", href: "#servizi" },
|
||||||
|
{ label: "Biochimica", href: "#servizi" },
|
||||||
|
{ label: "Urine", href: "#servizi" },
|
||||||
|
{ label: "Citologia", href: "#servizi" },
|
||||||
|
{ label: "Coagulazione", href: "#servizi" },
|
||||||
|
{ label: "Endoscopia", href: "#servizi" },
|
||||||
|
{ label: "Laparoscopia", href: "#servizi" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Visite Specialistiche",
|
||||||
|
href: "#servizi",
|
||||||
|
children: [
|
||||||
|
{ label: "Oncologia", href: "#servizi" },
|
||||||
|
{ label: "Dermatologia", href: "#servizi" },
|
||||||
|
{ label: "Oculistica", href: "#servizi" },
|
||||||
|
{ label: "Nutrizione", href: "#servizi" },
|
||||||
|
{ label: "Ortopedia", href: "#servizi" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ label: "Il Team", href: "#team" },
|
{ label: "Il Team", href: "#team" },
|
||||||
{ label: "News", href: "#news" },
|
{ label: "News", href: "#news" },
|
||||||
{ label: "Contatti", href: "#contatti" },
|
{ 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">
|
<div className="container flex items-center justify-between h-16 md:h-20">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<a href="#" className="flex items-center gap-3 group">
|
<a href="#" className="flex items-center gap-2 md:gap-3 group shrink-0">
|
||||||
<div className="w-10 h-10 rounded-full bg-[#1B4F72] flex items-center justify-center flex-shrink-0">
|
<img
|
||||||
<svg viewBox="0 0 40 40" fill="none" className="w-6 h-6">
|
src="/images/logo_high.png"
|
||||||
<path d="M20 8c-1.5 0-2.5 1.2-2.5 2.5S18.5 13 20 13s2.5-1.2 2.5-2.5S21.5 8 20 8z" fill="#4ECDC4"/>
|
alt="Simbolo Clinica Veterinaria Formiginese"
|
||||||
<path d="M13 11c-1.2 0-2 1-2 2.2S11.8 15.5 13 15.5s2-1 2-2.2S14.2 11 13 11z" fill="#4ECDC4"/>
|
className="h-10 md:h-14 w-auto object-contain"
|
||||||
<path d="M27 11c-1.2 0-2 1-2 2.2S25.8 15.5 27 15.5s2-1 2-2.2S28.2 11 27 11z" fill="#4ECDC4"/>
|
/>
|
||||||
<path d="M10 17c-1.2 0-2 1-2 2.2S8.8 21.5 10 21.5s2-1 2-2.2S11.2 17 10 17z" fill="#4ECDC4"/>
|
<img
|
||||||
<path d="M30 17c-1.2 0-2 1-2 2.2S28.8 21.5 30 21.5s2-1 2-2.2S31.2 17 30 17z" fill="#4ECDC4"/>
|
src="/images/logo_low.png"
|
||||||
<path d="M20 17c-5 0-9 3.5-9 7.5 0 2.5 1.5 4.5 3.5 5.5.5.3 1 .5 1.5.5h8c.5 0 1-.2 1.5-.5 2-1 3.5-3 3.5-5.5 0-4-4-7.5-9-7.5z" fill="white"/>
|
alt="Clinica Veterinaria Formiginese"
|
||||||
</svg>
|
className="h-8 md:h-11 w-auto object-contain"
|
||||||
</div>
|
/>
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="font-bold text-[#1B4F72] leading-tight"
|
|
||||||
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "1.1rem" }}
|
|
||||||
>
|
|
||||||
Clinica Veterinaria
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-[#4ECDC4] font-semibold leading-tight tracking-widest uppercase text-xs"
|
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
|
||||||
>
|
|
||||||
Formiginese
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Desktop menu */}
|
{/* Desktop menu */}
|
||||||
<div className="hidden md:flex items-center gap-8">
|
<div className="hidden md:flex items-center gap-6">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<a
|
<div key={link.label} className="relative group py-7">
|
||||||
key={link.href}
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="text-[#1B4F72] font-medium text-sm hover:text-[#4ECDC4] transition-colors duration-200 relative group"
|
className="text-[#1B4F72] font-medium text-sm hover:text-[#4ECDC4] transition-colors duration-200 relative inline-flex items-center gap-1.5"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-[#4ECDC4] transition-all duration-300 group-hover:w-full" />
|
{link.children && (
|
||||||
</a>
|
<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>
|
</div>
|
||||||
|
|
||||||
@@ -131,14 +177,30 @@ export default function Navbar() {
|
|||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
<div className="md:hidden bg-white border-t border-gray-100 px-4 py-4 flex flex-col gap-4 shadow-lg">
|
<div className="md:hidden bg-white border-t border-gray-100 px-4 py-4 flex flex-col gap-4 shadow-lg">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<a
|
<div key={link.label} className="border-b border-gray-50 pb-2">
|
||||||
key={link.href}
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className="text-[#1B4F72] font-medium py-2 border-b border-gray-50 hover:text-[#4ECDC4] transition-colors"
|
className="text-[#1B4F72] font-medium py-2 hover:text-[#4ECDC4] transition-colors flex items-center justify-between"
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</a>
|
{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">
|
<div className="flex flex-col gap-2 pt-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,48 +1,79 @@
|
|||||||
/*
|
/*
|
||||||
* DESIGN: "Clinical Warmth"
|
* DESIGN: "Clinical Warmth"
|
||||||
* Sezione servizi con 3 card principali (radiologia, chirurgia, laboratorio)
|
* Sezione servizi con 6 card principali allineate al menu della clinica.
|
||||||
* Layout: immagine in alto, bordo superiore colorato, hover lift
|
* Layout: immagine in alto, bordo superiore colorato, hover lift.
|
||||||
* Sfondo sabbia calda per contrasto con la sezione hero
|
|
||||||
*/
|
*/
|
||||||
import { motion } from "framer-motion";
|
import { motion, useInView } from "framer-motion";
|
||||||
import { useInView } from "framer-motion";
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { ArrowRight, Scan, Scissors, FlaskConical } from "lucide-react";
|
import { Activity, ArrowRight, FlaskConical, Scan, Stethoscope, Video } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const services = [
|
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",
|
id: "radiologia",
|
||||||
title: "Radiologia",
|
title: "Radiologia",
|
||||||
subtitle: "Diagnostica per immagini",
|
subtitle: "Diagnostica per immagini",
|
||||||
description:
|
description:
|
||||||
"Tecnologia digitale di ultima generazione per radiografie, ecografie e diagnostica avanzata. Refertazione rapida e precisa per supportare le decisioni terapeutiche con la massima accuratezza diagnostica.",
|
"Radiografie digitali e diagnostica per immagini a supporto della visita clinica, utili per valutare apparato scheletrico, torace, addome e urgenze.",
|
||||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/radiology_39785f88.jpg",
|
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/radiology_39785f88.jpg",
|
||||||
icon: Scan,
|
icon: Scan,
|
||||||
color: "#1B4F72",
|
color: "#2E86AB",
|
||||||
features: ["Radiografia digitale", "Ecografia", "Ecocardiografia", "Refertazione immediata"],
|
features: ["Radiografia digitale", "Studi toracici", "Valutazioni ortopediche", "Diagnostica d'urgenza"],
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "chirurgia",
|
|
||||||
title: "Chirurgia",
|
|
||||||
subtitle: "Sala operatoria specialistica",
|
|
||||||
description:
|
|
||||||
"Sala operatoria completamente attrezzata per interventi di chirurgia generale, ortopedica e d'urgenza. Il nostro team chirurgico garantisce le massime condizioni di sicurezza e sterilità.",
|
|
||||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/surgery_e92904ed.jpg",
|
|
||||||
icon: Scissors,
|
|
||||||
color: "#4ECDC4",
|
|
||||||
features: ["Chirurgia generale", "Chirurgia ortopedica", "Chirurgia d'urgenza", "Anestesia monitorata"],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "laboratorio",
|
id: "laboratorio",
|
||||||
title: "Laboratorio",
|
title: "Laboratorio",
|
||||||
subtitle: "Analisi e diagnostica",
|
subtitle: "Analisi interne",
|
||||||
description:
|
description:
|
||||||
"Laboratorio analisi interno per esami ematologici, biochimici, urinari e citologici. Risultati in tempi rapidi per diagnosi tempestive e trattamenti mirati.",
|
"Laboratorio interno per esami rapidi e mirati, fondamentale per supportare diagnosi tempestive, monitoraggi terapeutici e controlli pre-operatori.",
|
||||||
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/laboratory_5a1c4119.jpg",
|
image: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/laboratory_5a1c4119.jpg",
|
||||||
icon: FlaskConical,
|
icon: FlaskConical,
|
||||||
|
color: "#1B4F72",
|
||||||
|
features: ["Ematologia", "Biochimica", "Urine", "Citologia", "Coagulazione"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "endoscopia",
|
||||||
|
title: "Endoscopia",
|
||||||
|
subtitle: "Esplorazione mini-invasiva",
|
||||||
|
description:
|
||||||
|
"Tecniche endoscopiche per esplorare apparato digerente e vie respiratorie, riducendo invasivita e tempi di recupero 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",
|
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}
|
ref={ref}
|
||||||
initial={{ opacity: 0, y: 40 }}
|
initial={{ opacity: 0, y: 40 }}
|
||||||
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
animate={isInView ? { opacity: 1, y: 0 } : {}}
|
||||||
transition={{ duration: 0.6, delay: index * 0.15 }}
|
transition={{ duration: 0.6, delay: index * 0.12 }}
|
||||||
className="group bg-white rounded-2xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-400 hover:-translate-y-1"
|
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 }} />
|
<div className="h-1" style={{ backgroundColor: service.color }} />
|
||||||
|
|
||||||
{/* Immagine */}
|
|
||||||
<div className="relative h-52 overflow-hidden">
|
<div className="relative h-52 overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={service.image}
|
src={service.image}
|
||||||
alt={service.title}
|
alt={service.title}
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
{/* Icon overlay */}
|
|
||||||
<div
|
<div
|
||||||
className="absolute top-4 right-4 w-12 h-12 rounded-full flex items-center justify-center shadow-lg"
|
className="absolute top-4 right-4 w-12 h-12 rounded-full flex items-center justify-center shadow-lg"
|
||||||
style={{ backgroundColor: service.color }}
|
style={{ backgroundColor: service.color }}
|
||||||
@@ -77,7 +105,6 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contenuto */}
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="text-xs uppercase tracking-widest font-semibold mb-1" style={{ color: service.color }}>
|
<div className="text-xs uppercase tracking-widest font-semibold mb-1" style={{ color: service.color }}>
|
||||||
{service.subtitle}
|
{service.subtitle}
|
||||||
@@ -90,7 +117,6 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 text-sm leading-relaxed mb-4">{service.description}</p>
|
<p className="text-gray-600 text-sm leading-relaxed mb-4">{service.description}</p>
|
||||||
|
|
||||||
{/* Features */}
|
|
||||||
<ul className="space-y-1.5 mb-5">
|
<ul className="space-y-1.5 mb-5">
|
||||||
{service.features.map((feat) => (
|
{service.features.map((feat) => (
|
||||||
<li key={feat} className="flex items-center gap-2 text-sm text-gray-600">
|
<li key={feat} className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
@@ -100,15 +126,14 @@ function ServiceCard({ service, index }: { service: typeof services[0]; index: n
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{/* Link */}
|
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 text-sm font-semibold transition-all duration-200 group/btn"
|
className="flex items-center gap-1.5 text-sm font-semibold transition-all duration-200 group/btn"
|
||||||
style={{ color: service.color }}
|
style={{ color: service.color }}
|
||||||
onClick={() => {
|
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" />
|
<ArrowRight size={15} className="transition-transform duration-200 group-hover/btn:translate-x-1" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +148,6 @@ export default function ServicesSection() {
|
|||||||
return (
|
return (
|
||||||
<section id="servizi" className="py-20 md:py-28" style={{ backgroundColor: "#F5F0E8" }}>
|
<section id="servizi" className="py-20 md:py-28" style={{ backgroundColor: "#F5F0E8" }}>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{/* Header sezione */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
@@ -144,19 +168,17 @@ export default function ServicesSection() {
|
|||||||
Servizi Specialistici
|
Servizi Specialistici
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 max-w-xl text-base leading-relaxed">
|
<p className="text-gray-600 max-w-xl text-base leading-relaxed">
|
||||||
Tre aree di eccellenza clinica per garantire diagnosi precise e trattamenti efficaci
|
Sei aree di eccellenza clinica per accompagnare prevenzione, diagnosi e trattamento
|
||||||
ai tuoi animali domestici, con tecnologie all'avanguardia e professionisti esperti.
|
dei tuoi animali domestici, con tecnologie avanzate e professionisti esperti.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Grid servizi */}
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
||||||
{services.map((service, index) => (
|
{services.map((service, index) => (
|
||||||
<ServiceCard key={service.id} service={service} index={index} />
|
<ServiceCard key={service.id} service={service} index={index} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Link a tutti i servizi */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={isInView ? { opacity: 1 } : {}}
|
animate={isInView ? { opacity: 1 } : {}}
|
||||||
@@ -166,7 +188,7 @@ export default function ServicesSection() {
|
|||||||
<button
|
<button
|
||||||
className="inline-flex items-center gap-2 text-[#1B4F72] font-semibold border-2 border-[#1B4F72] px-8 py-3 rounded-full hover:bg-[#1B4F72] hover:text-white transition-all duration-300"
|
className="inline-flex items-center gap-2 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={() => {
|
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
|
Tutti i servizi
|
||||||
|
|||||||
309
coming-soon/index.html
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Clinica Veterinaria Formiginese | Coming Soon</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Il nuovo sito della Clinica Veterinaria Formiginese sarà presto online."
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ink: #15384f;
|
||||||
|
--ink-soft: #315f78;
|
||||||
|
--aqua: #54c8bf;
|
||||||
|
--sand: #f4ede2;
|
||||||
|
--paper: #fffdf9;
|
||||||
|
--mist: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "Segoe UI", Arial, sans-serif;
|
||||||
|
color: var(--ink);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(84, 200, 191, 0.22), transparent 38%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(21, 56, 79, 0.18), transparent 32%),
|
||||||
|
linear-gradient(135deg, #f8f4ec 0%, #fefcf9 48%, #eef7f7 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: min(1120px, 100%);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.1fr 0.9fr;
|
||||||
|
gap: 28px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: var(--mist);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid rgba(21, 56, 79, 0.08);
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 24px 70px rgba(21, 56, 79, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
padding: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(84, 200, 191, 0.16);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow::before {
|
||||||
|
content: "";
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--aqua);
|
||||||
|
box-shadow: 0 0 0 5px rgba(84, 200, 191, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 28px 0 16px;
|
||||||
|
font-size: clamp(2.6rem, 5vw, 4.8rem);
|
||||||
|
line-height: 0.95;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 span {
|
||||||
|
display: block;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 46ch;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: rgba(21, 56, 79, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
margin-top: 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
border: 1px solid rgba(21, 56, 79, 0.1);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
margin-top: 34px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(21, 56, 79, 0.06);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artboard {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 26px;
|
||||||
|
min-height: 560px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.3)),
|
||||||
|
linear-gradient(160deg, #dff5f1 0%, #f7f0e7 48%, #dcecf8 100%);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artboard::before,
|
||||||
|
.artboard::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artboard::before {
|
||||||
|
width: 210px;
|
||||||
|
height: 210px;
|
||||||
|
background: rgba(84, 200, 191, 0.2);
|
||||||
|
top: -60px;
|
||||||
|
right: -40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artboard::after {
|
||||||
|
width: 260px;
|
||||||
|
height: 260px;
|
||||||
|
background: rgba(21, 56, 79, 0.12);
|
||||||
|
bottom: -80px;
|
||||||
|
left: -70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stamp {
|
||||||
|
position: absolute;
|
||||||
|
top: 22px;
|
||||||
|
left: 22px;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
border: 1px solid rgba(21, 56, 79, 0.08);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy {
|
||||||
|
padding: 34px 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artboard {
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shell">
|
||||||
|
<section class="panel copy">
|
||||||
|
<div class="eyebrow">Nuovo sito in preparazione</div>
|
||||||
|
<h1>
|
||||||
|
Clinica Veterinaria
|
||||||
|
<span>Formiginese</span>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Stiamo preparando una nuova esperienza digitale pensata per presentare al meglio la
|
||||||
|
clinica, il team medico e i servizi dedicati alla salute dei vostri animali.
|
||||||
|
</p>
|
||||||
|
<div class="chips">
|
||||||
|
<div class="chip">Servizi specialistici</div>
|
||||||
|
<div class="chip">Team clinico</div>
|
||||||
|
<div class="chip">Prenotazioni online</div>
|
||||||
|
<div class="chip">Area clienti dedicata</div>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
Il sito completo sarà pubblicato a breve. Nel frattempo la clinica continua a essere
|
||||||
|
disponibile ai consueti recapiti telefonici.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel visual">
|
||||||
|
<div class="artboard">
|
||||||
|
<div class="stamp">Coming Soon</div>
|
||||||
|
<svg viewBox="0 0 680 760" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="card" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" />
|
||||||
|
<stop offset="100%" stop-color="#f4ede2" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="teal" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#68d7cf" />
|
||||||
|
<stop offset="100%" stop-color="#2d9f98" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="blue" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#264f68" />
|
||||||
|
<stop offset="100%" stop-color="#16384f" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<g opacity="0.9">
|
||||||
|
<circle cx="540" cy="122" r="68" fill="#ffffff" />
|
||||||
|
<circle cx="578" cy="146" r="12" fill="#54c8bf" opacity="0.5" />
|
||||||
|
<circle cx="500" cy="106" r="14" fill="#15384f" opacity="0.08" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(62 118)">
|
||||||
|
<rect width="556" height="506" rx="38" fill="url(#card)" />
|
||||||
|
<rect x="24" y="22" width="508" height="462" rx="28" fill="#ffffff" opacity="0.78" />
|
||||||
|
|
||||||
|
<rect x="56" y="58" width="214" height="264" rx="26" fill="url(#teal)" opacity="0.18" />
|
||||||
|
<rect x="304" y="58" width="196" height="108" rx="24" fill="#edf5f7" />
|
||||||
|
<rect x="304" y="184" width="196" height="138" rx="24" fill="#f8f3ec" />
|
||||||
|
<rect x="56" y="348" width="444" height="84" rx="22" fill="#15384f" opacity="0.06" />
|
||||||
|
|
||||||
|
<circle cx="164" cy="172" r="82" fill="url(#blue)" />
|
||||||
|
<ellipse cx="162" cy="207" rx="104" ry="88" fill="#1c445d" />
|
||||||
|
<ellipse cx="392" cy="108" rx="46" ry="30" fill="#dce9ee" />
|
||||||
|
<ellipse cx="392" cy="234" rx="70" ry="42" fill="#efe4d4" />
|
||||||
|
|
||||||
|
<path d="M155 104c-18-28-45-43-76-43 8 20 21 42 39 62" fill="#0f344b" />
|
||||||
|
<path d="M174 104c18-28 45-43 76-43-8 20-21 42-39 62" fill="#0f344b" />
|
||||||
|
<ellipse cx="139" cy="192" rx="18" ry="14" fill="#ffffff" />
|
||||||
|
<ellipse cx="185" cy="192" rx="18" ry="14" fill="#ffffff" />
|
||||||
|
<circle cx="139" cy="192" r="7" fill="#15384f" />
|
||||||
|
<circle cx="185" cy="192" r="7" fill="#15384f" />
|
||||||
|
<path d="M151 222c12 14 26 14 38 0" stroke="#54c8bf" stroke-width="8" stroke-linecap="round" fill="none" />
|
||||||
|
<path d="M162 212l-12 12h24z" fill="#54c8bf" />
|
||||||
|
|
||||||
|
<g transform="translate(344 74)">
|
||||||
|
<rect width="146" height="18" rx="9" fill="#15384f" opacity="0.12" />
|
||||||
|
<rect y="34" width="108" height="14" rx="7" fill="#54c8bf" opacity="0.32" />
|
||||||
|
<rect y="60" width="122" height="14" rx="7" fill="#15384f" opacity="0.08" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(332 212)">
|
||||||
|
<rect width="144" height="16" rx="8" fill="#15384f" opacity="0.12" />
|
||||||
|
<rect y="34" width="122" height="14" rx="7" fill="#54c8bf" opacity="0.26" />
|
||||||
|
<rect y="60" width="138" height="14" rx="7" fill="#15384f" opacity="0.08" />
|
||||||
|
<rect y="86" width="104" height="14" rx="7" fill="#15384f" opacity="0.08" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(82 370)">
|
||||||
|
<rect width="84" height="40" rx="20" fill="#54c8bf" />
|
||||||
|
<rect x="100" width="112" height="40" rx="20" fill="#ffffff" stroke="#15384f" opacity="0.28" />
|
||||||
|
<rect x="228" width="160" height="40" rx="20" fill="#15384f" opacity="0.08" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
71
deploy_to_local_server.bat
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set "DEPLOY_ROOT=C:\deploy\clinica_veterinaria_formiginese"
|
||||||
|
set "APP_DIR=%DEPLOY_ROOT%\clinica-app"
|
||||||
|
set "DEPLOY_BRANCH="
|
||||||
|
|
||||||
|
echo === Deploy demo Clinica Veterinaria Formiginese ===
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if not exist "%DEPLOY_ROOT%\" (
|
||||||
|
echo ERRORE: cartella deploy non trovata: "%DEPLOY_ROOT%"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%APP_DIR%\" (
|
||||||
|
echo ERRORE: cartella app non trovata: "%APP_DIR%"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [1/3] Aggiornamento repository di deploy...
|
||||||
|
pushd "%DEPLOY_ROOT%" || exit /b 1
|
||||||
|
|
||||||
|
for /f "delims=" %%i in ('git branch --show-current') do set "DEPLOY_BRANCH=%%i"
|
||||||
|
|
||||||
|
if "%DEPLOY_BRANCH%"=="" (
|
||||||
|
echo.
|
||||||
|
echo ERRORE: impossibile determinare il branch corrente nella copia di deploy.
|
||||||
|
popd
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Branch deploy attivo: %DEPLOY_BRANCH%
|
||||||
|
|
||||||
|
git fetch origin
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo ERRORE: git fetch non riuscito.
|
||||||
|
popd
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
git reset --hard origin/%DEPLOY_BRANCH%
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo ERRORE: git reset del branch di deploy non riuscito.
|
||||||
|
popd
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
popd
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [2/3] Build frontend...
|
||||||
|
pushd "%APP_DIR%" || exit /b 1
|
||||||
|
call pnpm run build
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo ERRORE: build non riuscita.
|
||||||
|
popd
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
popd
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [3/3] Deploy completato.
|
||||||
|
echo I file aggiornati sono serviti da Caddy da:
|
||||||
|
echo %APP_DIR%\dist\public
|
||||||
|
echo.
|
||||||
|
|
||||||
|
endlocal
|
||||||
21
istruzioni.txt
Normal file
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
76
publish_demo.bat
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
if "%~1"=="" goto usage
|
||||||
|
|
||||||
|
set "MODE=%~1"
|
||||||
|
set "CADDYFILE=C:\caddy\Caddyfile"
|
||||||
|
set "DOMAIN=www.clinicaveterinariaformiginese.it"
|
||||||
|
set "LIVE_ROOT=C:\deploy\clinica_veterinaria_formiginese\clinica-app\dist\public"
|
||||||
|
set "COMING_SOON_ROOT=C:\deploy\clinica_veterinaria_formiginese\coming-soon"
|
||||||
|
|
||||||
|
if /I "%MODE%"=="true" goto publish_live
|
||||||
|
if /I "%MODE%"=="false" goto publish_coming_soon
|
||||||
|
goto usage
|
||||||
|
|
||||||
|
:publish_live
|
||||||
|
echo Modalita selezionata: DEMO PUBBLICATA
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||||
|
"$path = '%CADDYFILE%';" ^
|
||||||
|
"$content = Get-Content -Raw $path;" ^
|
||||||
|
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||||
|
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %LIVE_ROOT%' + [Environment]::NewLine + ' try_files {path} /index.html' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
||||||
|
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||||
|
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERRORE: aggiornamento del Caddyfile non riuscito.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
goto reload_caddy
|
||||||
|
|
||||||
|
:publish_coming_soon
|
||||||
|
echo Modalita selezionata: COMING SOON
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||||
|
"$path = '%CADDYFILE%';" ^
|
||||||
|
"$content = Get-Content -Raw $path;" ^
|
||||||
|
"$pattern = '(?ms)^%DOMAIN% \{[\s\S]*?^\}';" ^
|
||||||
|
"$replacement = '%DOMAIN% {' + [Environment]::NewLine + ' root * %COMING_SOON_ROOT%' + [Environment]::NewLine + ' file_server' + [Environment]::NewLine + '}';" ^
|
||||||
|
"$updated = [regex]::Replace($content, $pattern, $replacement);" ^
|
||||||
|
"Set-Content -Path $path -Value $updated -Encoding UTF8;"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERRORE: aggiornamento del Caddyfile non riuscito.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:reload_caddy
|
||||||
|
echo.
|
||||||
|
echo Ricarico Caddy...
|
||||||
|
C:\caddy\caddy.exe validate --config "%CADDYFILE%" --adapter caddyfile
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERRORE: Caddyfile non valido. Nessun reload eseguito.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
C:\caddy\caddy.exe reload --config "%CADDYFILE%" --adapter caddyfile
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERRORE: reload di Caddy non riuscito.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
if /I "%MODE%"=="true" (
|
||||||
|
echo Demo online: https://%DOMAIN%
|
||||||
|
) else (
|
||||||
|
echo Coming soon online: https://%DOMAIN%
|
||||||
|
)
|
||||||
|
echo Operazione completata.
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:usage
|
||||||
|
echo Uso:
|
||||||
|
echo publish_demo.bat true
|
||||||
|
echo publish_demo.bat false
|
||||||
|
echo.
|
||||||
|
echo true = pubblica la demo completa
|
||||||
|
echo false = mostra la pagina coming soon
|
||||||
|
exit /b 1
|
||||||