modifiche002

This commit is contained in:
2026-05-26 14:38:05 +02:00
parent 83fb4b8c1e
commit bd5b845e43
10 changed files with 162 additions and 152 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -6,6 +6,7 @@ class BookingRequestCreate(BaseModel):
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)

View File

@@ -28,6 +28,7 @@ def _build_booking_html(payload: BookingRequestCreate) -> str:
<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>
@@ -51,6 +52,7 @@ def _build_booking_text(payload: BookingRequestCreate) -> str:
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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -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" />

View File

@@ -24,6 +24,18 @@ const services = [
"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 timeSlots = [
"09:00",
"09:30",
@@ -44,6 +56,7 @@ type BookingFormState = {
phone: string;
petName: string;
petType: string;
doctor: string;
service: string;
date: string;
time: string;
@@ -55,6 +68,7 @@ const initialForm: BookingFormState = {
phone: "",
petName: "",
petType: "cane",
doctor: "",
service: "",
date: "",
time: "",
@@ -72,7 +86,7 @@ export default function BookingSection() {
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;
}
@@ -90,6 +104,7 @@ export default function BookingSection() {
phone: form.phone,
pet_name: form.petName,
pet_type: form.petType,
doctor: form.doctor,
service: form.service,
date: form.date,
time: form.time,
@@ -326,6 +341,25 @@ export default function BookingSection() {
</div>
</div>
<div>
<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 *

View File

@@ -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, Clock } 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 = [
{
@@ -48,20 +48,26 @@ const heroImages = [
},
];
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}
@@ -71,104 +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 */}
{/* 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)",
}}
>
CLINICA VETERINARIA FORMIGINESE{" "}
<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]"
>
</motion.p>
<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"
>
<div>
<h4
className="text-white font-semibold mb-4 text-sm uppercase tracking-widest"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Orari
</h4>
<div className="space-y-2">
{[
{ 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" />
<div>
<p className="text-white/70 text-xs">{slot.days}</p>
<p className="text-white/50 text-xs whitespace-pre-line">{slot.hours}</p>
</div>
</div>
))}
</div>
<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
</h4>
{/* Badge reperibilità */}
</div>
</motion.div>
<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">
@@ -177,32 +174,24 @@ export default function HeroSection() {
</a>
</Button>
</motion.div>
{/* Stats */}
{/* Orari */}
</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>

View File

@@ -9,16 +9,15 @@ import { useRef } from "react";
import {
Activity,
Apple,
ArrowRight,
Bone,
Eye,
FlaskConical,
HeartPulse,
Scan,
Sparkles,
Stethoscope,
Video,
} from "lucide-react";
import { toast } from "sonner";
type ServiceItem = {
id: string;
@@ -103,6 +102,19 @@ const clinicalServices: ServiceItem[] = [
},
];
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",
@@ -208,11 +220,6 @@ function ServiceCard({
>
<service.icon size={22} className="text-white" />
</div>
{specialist && (
<div className="absolute left-4 top-4 rounded-full bg-white/90 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-[#7d5233] shadow-sm">
Alta specializzazione
</div>
)}
</div>
<div className="p-6">
@@ -236,16 +243,6 @@ function ServiceCard({
))}
</ul>
<button
className="group/btn flex items-center gap-1.5 text-sm font-semibold transition-all duration-200"
style={{ color: service.color }}
onClick={() => {
toast("Sezione in arrivo", { description: `La pagina dedicata a ${service.title} sara disponibile a breve.` });
}}
>
Approfondisci
<ArrowRight size={15} className="transition-transform duration-200 group-hover/btn:translate-x-1" />
</button>
</div>
</motion.div>
);
@@ -267,12 +264,6 @@ export default function ServicesSection() {
transition={{ duration: 0.7 }}
className="mb-14"
>
<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]">
Specializzazioni
</span>
</div>
<h2
className="mb-4 text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(2rem, 4vw, 3rem)", fontWeight: 600 }}
@@ -290,6 +281,20 @@ export default function ServicesSection() {
))}
</div>
<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
ref={specialistRef}
initial={{ opacity: 0, y: 30 }}
@@ -297,25 +302,15 @@ export default function ServicesSection() {
transition={{ duration: 0.7, delay: 0.2 }}
className="mt-20"
>
<div className="mb-4 flex items-center gap-3">
<div className="h-0.5 w-12 bg-[#A95F3A]" />
<span className="text-sm font-semibold uppercase tracking-widest text-[#A95F3A]">
Alta specializzazione
</span>
</div>
<h3
className="mb-4 text-[#1B4F72]"
style={{ fontFamily: "'Cormorant Garamond', serif", fontSize: "clamp(1.9rem, 3.7vw, 2.8rem)", fontWeight: 600 }}
>
Visite Specialistiche
</h3>
<p className="max-w-2xl text-base leading-relaxed text-gray-600">
Un secondo livello di consulenza clinica per casi che richiedono competenze dedicate,
approfondimenti mirati e percorsi di valutazione ad alto valore aggiunto.
</p>
</motion.div>
<div className="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2 xl:grid-cols-6">
<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}
@@ -326,22 +321,6 @@ export default function ServicesSection() {
))}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={specialistInView ? { opacity: 1 } : {}}
transition={{ duration: 0.7, delay: 0.6 }}
className="mt-12 text-center"
>
<button
className="inline-flex items-center gap-2 rounded-full border-2 border-[#1B4F72] px-8 py-3 font-semibold text-[#1B4F72] transition-all duration-300 hover:bg-[#1B4F72] hover:text-white"
onClick={() => {
toast("Tutti i servizi", { description: "La pagina completa dei servizi sara disponibile a breve." });
}}
>
Tutti i servizi
<ArrowRight size={16} />
</button>
</motion.div>
</div>
</section>
);

View File

@@ -33,7 +33,7 @@ const medicalTeam: TeamMember[] = [
color: "#4ECDC4",
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), e socia SCIVAC e SIVAE. Ha seguito diversi corsi di perfezionamento in ecografia addominale ed ecocardiografia del cane e del gatto.",
"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.",
],
},
{
@@ -60,6 +60,24 @@ const medicalTeam: TeamMember[] = [
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",
},
];
const collaborators: TeamMember[] = [
@@ -80,16 +98,16 @@ const collaborators: TeamMember[] = [
color: "#7EA55A",
},
{
name: "Dott.ssa Elena Venturelli",
name: "Dott. Boris Del Pozzo",
role: "Collaborazione specialistica",
specialization: "Profilo in aggiornamento",
color: "#4C7A9F",
color: "#6E8FA7",
},
{
name: "Dott.ssa Cinzia Pellegrini",
name: "Dott.ssa Silvia Palladini",
role: "Collaborazione specialistica",
specialization: "Profilo in aggiornamento",
color: "#8E6C88",
color: "#B6876F",
},
];

View File

@@ -176,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: [