commit 0ca0306a348ff32fee9c012b2c28081d23a3898a Author: allebonvi Date: Sat Apr 4 10:14:07 2026 +0200 Initial commit: demo sito clinica veterinaria diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5678bc --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# 🏥 Clinica Veterinaria Formiginese - Deploy Package + +Questo pacchetto contiene il codice sorgente completo, le immagini e le istruzioni per deployare il sito della Clinica Veterinaria Formiginese su un server privato con Node.js. + +## 📋 Contenuti del Pacchetto + +- **clinica-app/** - Progetto React completo con sorgenti +- **assets/** - Tutte le 7 immagini (animali + clinica) +- **SETUP_ISTRUZIONI.md** - Guida dettagliata di installazione +- **README.md** - Questo file + +## 🚀 Quick Start + +```bash +# 1. Estrai il pacchetto +unzip clinica-veterinaria-deploy.zip +cd clinica-deploy + +# 2. Copia le immagini +cp -r assets/* clinica-app/client/public/images/ +mkdir -p clinica-app/client/public/images + +# 3. Installa e avvia +cd clinica-app +npm install +npm run build +npm start +``` + +Il sito sarà disponibile a `http://localhost:3000` + +## 📖 Documentazione Completa + +Leggi il file **SETUP_ISTRUZIONI.md** per: +- Requisiti di sistema +- Installazione passo-passo +- Configurazione avanzata (Nginx, PM2, ecc.) +- Personalizzazione (colori, testi, immagini) +- Troubleshooting + +## 🎯 Caratteristiche + +✅ Homepage completa con carousel hero (7 immagini) +✅ Sezioni: Chi Siamo, Servizi, Team, Blog, Prenotazioni, Registrazione +✅ Design "Clinical Warmth" con blu petrolio e verde acqua +✅ Responsive design (mobile, tablet, desktop) +✅ Animazioni fluide e transizioni eleganti +✅ Performance ottimizzate + +## 🛠️ Stack Tecnologico + +- **Frontend**: React 19 + TypeScript + Tailwind CSS 4 +- **Backend**: Node.js + Express +- **Build Tool**: Vite +- **UI Components**: shadcn/ui +- **Animazioni**: Framer Motion + +## 📞 Supporto + +Per problemi durante l'installazione, consulta la sezione "Troubleshooting" in SETUP_ISTRUZIONI.md + +--- + +**Versione**: 1.0.0 +**Data**: 31 Marzo 2026 +**Licenza**: Privato diff --git a/SETUP_ISTRUZIONI.md b/SETUP_ISTRUZIONI.md new file mode 100644 index 0000000..0ab7218 --- /dev/null +++ b/SETUP_ISTRUZIONI.md @@ -0,0 +1,295 @@ +# 🏥 Clinica Veterinaria Formiginese - Guida di Installazione + +## Requisiti di Sistema + +- **Node.js**: versione 18.x o superiore +- **npm** o **pnpm**: gestore pacchetti +- **Linux/Unix**: per il server di produzione +- **Porta disponibile**: 3000 (sviluppo) o 5000 (produzione) + +--- + +## 📦 Struttura del Pacchetto + +``` +clinica-deploy/ +├── clinica-app/ # Sorgenti React completi +│ ├── client/ # Frontend React +│ ├── server/ # Backend Express +│ ├── package.json # Dipendenze +│ └── ... +├── assets/ # Tutte le immagini (7 file) +│ ├── hero_dog_cat.jpg +│ ├── hero_dog.jpg +│ ├── hero_cat.jpg +│ ├── clinica_sede1.png +│ ├── clinica_ingresso1.png +│ ├── clinica_ingresso2.png +│ └── clinica_ingresso3.webp +└── SETUP_ISTRUZIONI.md # Questo file +``` + +--- + +## 🚀 Installazione Passo-Passo + +### 1. Estrai il pacchetto ZIP + +```bash +unzip clinica-veterinaria-deploy.zip +cd clinica-deploy +``` + +### 2. Copia le immagini nel progetto + +```bash +cp -r assets/* clinica-app/client/public/images/ +mkdir -p clinica-app/client/public/images +``` + +### 3. Installa le dipendenze + +```bash +cd clinica-app +npm install +# oppure se usi pnpm: +pnpm install +``` + +### 4. Aggiorna i percorsi delle immagini + +Apri il file `clinica-app/client/src/components/HeroSection.tsx` e sostituisci gli URL CDN con percorsi locali: + +**Prima (URL CDN):** +```javascript +const heroImages = [ + { + url: "https://d2xsxph8kpxj0f.cloudfront.net/310519663483796440/DLUbzKbSnCG3dLeob4sLeC/ChatGPTImage28mar2026,18_49_09_7d867bcd.png", + alt: "Clinica Veterinaria Formiginese - Sede esterna", + }, + // ... +]; +``` + +**Dopo (Percorsi locali):** +```javascript +const heroImages = [ + { + url: "/images/clinica_sede1.png", + alt: "Clinica Veterinaria Formiginese - Sede esterna", + }, + { + url: "/images/hero_dog_cat.jpg", + alt: "Golden retriever e gatto tabby insieme in un prato soleggiato", + }, + { + url: "/images/clinica_ingresso1.png", + alt: "Clinica Veterinaria Formiginese - Ingresso e sala d'attesa", + }, + { + url: "/images/hero_dog.jpg", + alt: "Ritratto maestoso di un Labrador", + }, + { + url: "/images/clinica_ingresso2.png", + alt: "Clinica Veterinaria Formiginese - Ingresso con logo", + }, + { + url: "/images/hero_cat.jpg", + alt: "Ritratto maestoso di un Maine Coon", + }, + { + url: "/images/clinica_ingresso3.webp", + alt: "Clinica Veterinaria Formiginese - Ingresso principale", + }, +]; +``` + +### 5. Build per produzione + +```bash +npm run build +``` + +Questo creerà una cartella `dist/` con il sito compilato e ottimizzato. + +### 6. Avvia il server di produzione + +```bash +npm run start +``` + +Il sito sarà disponibile a: **http://localhost:3000** + +--- + +## 🔧 Configurazione Avanzata + +### Cambiare la porta + +Modifica il file `server/index.ts`: + +```typescript +const port = process.env.PORT || 5000; // Cambia 5000 con la porta desiderata +``` + +### Configurare Nginx come reverse proxy + +Se usi Nginx, crea un file di configurazione: + +```nginx +server { + listen 80; + server_name tuodominio.it; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` + +### Usare PM2 per gestire il processo + +```bash +npm install -g pm2 +pm2 start npm --name "clinica" -- start +pm2 save +pm2 startup +``` + +--- + +## 📝 Modifiche Personalizzate + +### Cambiare il numero di telefono + +File: `client/src/components/HeroSection.tsx` + +```typescript + // Cambia questo numero +``` + +### Cambiare i testi + +Tutti i testi sono nei componenti React in `client/src/components/`: + +- `HeroSection.tsx` - Titolo, sottotitolo, CTA +- `AboutSection.tsx` - Descrizione della clinica +- `ServicesSection.tsx` - Descrizione servizi +- `TeamSection.tsx` - Nomi e specializzazioni +- `NewsSection.tsx` - Articoli del blog +- `BookingSection.tsx` - Form prenotazioni +- `AuthSection.tsx` - Registrazione/Login +- `Footer.tsx` - Informazioni di contatto + +### Cambiare i colori + +File: `client/src/index.css` + +Cerca le variabili OKLCH e modifica i valori: + +```css +--primary: oklch(0.623 0.214 259.815); /* Blu petrolio */ +--accent: oklch(0.967 0.001 286.375); /* Verde acqua */ +``` + +--- + +## 🐛 Troubleshooting + +### Errore: "npm: command not found" + +Installa Node.js: +```bash +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs +``` + +### Errore: "Port 3000 already in use" + +Cambia la porta: +```bash +PORT=5000 npm start +``` + +### Immagini non si caricano + +Verifica che le immagini siano in `client/public/images/` e che i percorsi in `HeroSection.tsx` siano corretti. + +--- + +## 📊 Struttura dei Componenti + +``` +client/src/ +├── components/ +│ ├── Navbar.tsx # Barra di navigazione +│ ├── HeroSection.tsx # Carousel hero con immagini +│ ├── ServicesSection.tsx # Radiologia, Chirurgia, Laboratorio +│ ├── AboutSection.tsx # Chi Siamo +│ ├── TeamSection.tsx # I 6 professionisti +│ ├── NewsSection.tsx # Blog/News +│ ├── BookingSection.tsx # Prenotazione visite +│ ├── AuthSection.tsx # Registrazione/Login +│ ├── Footer.tsx # Footer +│ └── ui/ # Componenti shadcn/ui +├── pages/ +│ ├── Home.tsx # Pagina principale +│ └── NotFound.tsx # Pagina 404 +├── App.tsx # Router e layout +├── index.css # Stili globali +└── main.tsx # Entry point React +``` + +--- + +## 🎨 Design System + +**Colori:** +- Blu petrolio (primary): `#1B4F72` +- Verde acqua (accent): `#4ECDC4` +- Sabbia (secondary): `#F5E6D3` +- Bianco (background): `#FFFFFF` + +**Font:** +- Display: Cormorant Garamond (serif) +- Body: Nunito Sans (sans-serif) + +**Spacing:** +- Base unit: 4px (Tailwind default) +- Container max-width: 1280px + +--- + +## 📞 Supporto + +Per domande o problemi: +1. Controlla i log: `npm run dev` per vedere gli errori in tempo reale +2. Verifica che Node.js sia aggiornato: `node --version` +3. Pulisci la cache: `rm -rf node_modules && npm install` + +--- + +## ✅ Checklist Pre-Produzione + +- [ ] Tutte le immagini sono copiate in `client/public/images/` +- [ ] I percorsi delle immagini in `HeroSection.tsx` sono aggiornati +- [ ] Il numero di telefono è corretto +- [ ] I testi sono personalizzati +- [ ] La build è stata creata: `npm run build` +- [ ] Il server parte senza errori: `npm start` +- [ ] Le immagini si caricano correttamente +- [ ] Il carousel funziona (3 secondi per immagine) +- [ ] I pulsanti CTA funzionano +- [ ] Il sito è responsive su mobile + +--- + +**Versione:** 1.0.0 +**Data:** 31 Marzo 2026 +**Tecnologie:** React 19, Node.js, Express, Tailwind CSS 4 diff --git a/assets/clinica_ingresso1.png b/assets/clinica_ingresso1.png new file mode 100644 index 0000000..8c266fa Binary files /dev/null and b/assets/clinica_ingresso1.png differ diff --git a/assets/clinica_ingresso2.png b/assets/clinica_ingresso2.png new file mode 100644 index 0000000..59584e2 Binary files /dev/null and b/assets/clinica_ingresso2.png differ diff --git a/assets/clinica_ingresso3.webp b/assets/clinica_ingresso3.webp new file mode 100644 index 0000000..6d83cc3 Binary files /dev/null and b/assets/clinica_ingresso3.webp differ diff --git a/assets/clinica_sede1.png b/assets/clinica_sede1.png new file mode 100644 index 0000000..dbe12af Binary files /dev/null and b/assets/clinica_sede1.png differ diff --git a/assets/hero_cat.jpg b/assets/hero_cat.jpg new file mode 100644 index 0000000..a2b6ba9 Binary files /dev/null and b/assets/hero_cat.jpg differ diff --git a/assets/hero_dog.jpg b/assets/hero_dog.jpg new file mode 100644 index 0000000..d91282d Binary files /dev/null and b/assets/hero_dog.jpg differ diff --git a/assets/hero_dog_cat.jpg b/assets/hero_dog_cat.jpg new file mode 100644 index 0000000..4176943 Binary files /dev/null and b/assets/hero_dog_cat.jpg differ diff --git a/clinica-app/.env.example b/clinica-app/.env.example new file mode 100644 index 0000000..3084cca --- /dev/null +++ b/clinica-app/.env.example @@ -0,0 +1,19 @@ +# Variabili di ambiente - Copia questo file a .env.local e personalizza i valori + +# Ambiente +NODE_ENV=production + +# Server +PORT=3000 +HOST=0.0.0.0 + +# Frontend +VITE_APP_TITLE=Clinica Veterinaria Formiginese +VITE_APP_LOGO=/logo.svg + +# Analytics (opzionale) +VITE_ANALYTICS_ENDPOINT= +VITE_ANALYTICS_WEBSITE_ID= + +# API (se necessario in futuro) +VITE_API_URL=http://localhost:3000/api diff --git a/clinica-app/.gitignore b/clinica-app/.gitignore new file mode 100644 index 0000000..c2c15de --- /dev/null +++ b/clinica-app/.gitignore @@ -0,0 +1,112 @@ +# Dependencies +**/node_modules +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Webdev artifacts (checkpoint zips, migrations, etc.) +.webdev/ + +# Manus version file (auto-generated, not part of source) +client/public/__manus__/version.json diff --git a/clinica-app/.gitkeep b/clinica-app/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/clinica-app/.prettierignore b/clinica-app/.prettierignore new file mode 100644 index 0000000..27a587d --- /dev/null +++ b/clinica-app/.prettierignore @@ -0,0 +1,5 @@ +dist +node_modules +.git +*.min.js +*.min.css diff --git a/clinica-app/.prettierrc b/clinica-app/.prettierrc new file mode 100644 index 0000000..67c0bc8 --- /dev/null +++ b/clinica-app/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "proseWrap": "preserve" +} diff --git a/clinica-app/client/index.html b/clinica-app/client/index.html new file mode 100644 index 0000000..cebd12e --- /dev/null +++ b/clinica-app/client/index.html @@ -0,0 +1,20 @@ + + + + + + + Clinica Veterinaria Formiginese + + + + + +
+ + + + diff --git a/clinica-app/client/public/.gitkeep b/clinica-app/client/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/clinica-app/client/public/__manus__/debug-collector.js b/clinica-app/client/public/__manus__/debug-collector.js new file mode 100644 index 0000000..0504555 --- /dev/null +++ b/clinica-app/client/public/__manus__/debug-collector.js @@ -0,0 +1,821 @@ +/** + * Manus Debug Collector (agent-friendly) + * + * Captures: + * 1) Console logs + * 2) Network requests (fetch + XHR) + * 3) User interactions (semantic uiEvents: click/type/submit/nav/scroll/etc.) + * + * Data is periodically sent to /__manus__/logs + * Note: uiEvents are mirrored to sessionEvents for sessionReplay.log + */ +(function () { + "use strict"; + + // Prevent double initialization + if (window.__MANUS_DEBUG_COLLECTOR__) return; + + // ========================================================================== + // Configuration + // ========================================================================== + const CONFIG = { + reportEndpoint: "/__manus__/logs", + bufferSize: { + console: 500, + network: 200, + // semantic, agent-friendly UI events + ui: 500, + }, + reportInterval: 2000, + sensitiveFields: [ + "password", + "token", + "secret", + "key", + "authorization", + "cookie", + "session", + ], + maxBodyLength: 10240, + // UI event logging privacy policy: + // - inputs matching sensitiveFields or type=password are masked by default + // - non-sensitive inputs log up to 200 chars + uiInputMaxLen: 200, + uiTextMaxLen: 80, + // Scroll throttling: minimum ms between scroll events + scrollThrottleMs: 500, + }; + + // ========================================================================== + // Storage + // ========================================================================== + const store = { + consoleLogs: [], + networkRequests: [], + uiEvents: [], + lastReportTime: Date.now(), + lastScrollTime: 0, + }; + + // ========================================================================== + // Utility Functions + // ========================================================================== + + function sanitizeValue(value, depth) { + if (depth === void 0) depth = 0; + if (depth > 5) return "[Max Depth]"; + if (value === null) return null; + if (value === undefined) return undefined; + + if (typeof value === "string") { + return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value; + } + + if (typeof value !== "object") return value; + + if (Array.isArray(value)) { + return value.slice(0, 100).map(function (v) { + return sanitizeValue(v, depth + 1); + }); + } + + var sanitized = {}; + for (var k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + var isSensitive = CONFIG.sensitiveFields.some(function (f) { + return k.toLowerCase().indexOf(f) !== -1; + }); + if (isSensitive) { + sanitized[k] = "[REDACTED]"; + } else { + sanitized[k] = sanitizeValue(value[k], depth + 1); + } + } + } + return sanitized; + } + + function formatArg(arg) { + try { + if (arg instanceof Error) { + return { type: "Error", message: arg.message, stack: arg.stack }; + } + if (typeof arg === "object") return sanitizeValue(arg); + return String(arg); + } catch (e) { + return "[Unserializable]"; + } + } + + function formatArgs(args) { + var result = []; + for (var i = 0; i < args.length; i++) result.push(formatArg(args[i])); + return result; + } + + function pruneBuffer(buffer, maxSize) { + if (buffer.length > maxSize) buffer.splice(0, buffer.length - maxSize); + } + + function tryParseJson(str) { + if (typeof str !== "string") return str; + try { + return JSON.parse(str); + } catch (e) { + return str; + } + } + + // ========================================================================== + // Semantic UI Event Logging (agent-friendly) + // ========================================================================== + + function shouldIgnoreTarget(target) { + try { + if (!target || !(target instanceof Element)) return false; + return !!target.closest(".manus-no-record"); + } catch (e) { + return false; + } + } + + function compactText(s, maxLen) { + try { + var t = (s || "").trim().replace(/\s+/g, " "); + if (!t) return ""; + return t.length > maxLen ? t.slice(0, maxLen) + "…" : t; + } catch (e) { + return ""; + } + } + + function elText(el) { + try { + var t = el.innerText || el.textContent || ""; + return compactText(t, CONFIG.uiTextMaxLen); + } catch (e) { + return ""; + } + } + + function describeElement(el) { + if (!el || !(el instanceof Element)) return null; + + var getAttr = function (name) { + return el.getAttribute(name); + }; + + var tag = el.tagName ? el.tagName.toLowerCase() : null; + var id = el.id || null; + var name = getAttr("name") || null; + var role = getAttr("role") || null; + var ariaLabel = getAttr("aria-label") || null; + + var dataLoc = getAttr("data-loc") || null; + var testId = + getAttr("data-testid") || + getAttr("data-test-id") || + getAttr("data-test") || + null; + + var type = tag === "input" ? (getAttr("type") || "text") : null; + var href = tag === "a" ? getAttr("href") || null : null; + + // a small, stable hint for agents (avoid building full CSS paths) + var selectorHint = null; + if (testId) selectorHint = '[data-testid="' + testId + '"]'; + else if (dataLoc) selectorHint = '[data-loc="' + dataLoc + '"]'; + else if (id) selectorHint = "#" + id; + else selectorHint = tag || "unknown"; + + return { + tag: tag, + id: id, + name: name, + type: type, + role: role, + ariaLabel: ariaLabel, + testId: testId, + dataLoc: dataLoc, + href: href, + text: elText(el), + selectorHint: selectorHint, + }; + } + + function isSensitiveField(el) { + if (!el || !(el instanceof Element)) return false; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea") return false; + + var type = (el.getAttribute("type") || "").toLowerCase(); + if (type === "password") return true; + + var name = (el.getAttribute("name") || "").toLowerCase(); + var id = (el.id || "").toLowerCase(); + + return CONFIG.sensitiveFields.some(function (f) { + return name.indexOf(f) !== -1 || id.indexOf(f) !== -1; + }); + } + + function getInputValueSafe(el) { + if (!el || !(el instanceof Element)) return null; + var tag = el.tagName ? el.tagName.toLowerCase() : ""; + if (tag !== "input" && tag !== "textarea" && tag !== "select") return null; + + var v = ""; + try { + v = el.value != null ? String(el.value) : ""; + } catch (e) { + v = ""; + } + + if (isSensitiveField(el)) return { masked: true, length: v.length }; + + if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…"; + return v; + } + + function logUiEvent(kind, payload) { + var entry = { + timestamp: Date.now(), + kind: kind, + url: location.href, + viewport: { width: window.innerWidth, height: window.innerHeight }, + payload: sanitizeValue(payload), + }; + store.uiEvents.push(entry); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + } + + function installUiEventListeners() { + // Clicks + document.addEventListener( + "click", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("click", { + target: describeElement(t), + x: e.clientX, + y: e.clientY, + }); + }, + true + ); + + // Typing "commit" events + document.addEventListener( + "change", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("change", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + document.addEventListener( + "focusin", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusin", { target: describeElement(t) }); + }, + true + ); + + document.addEventListener( + "focusout", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("focusout", { + target: describeElement(t), + value: getInputValueSafe(t), + }); + }, + true + ); + + // Enter/Escape are useful for form flows & modals + document.addEventListener( + "keydown", + function (e) { + if (e.key !== "Enter" && e.key !== "Escape") return; + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("keydown", { key: e.key, target: describeElement(t) }); + }, + true + ); + + // Form submissions + document.addEventListener( + "submit", + function (e) { + var t = e.target; + if (shouldIgnoreTarget(t)) return; + logUiEvent("submit", { target: describeElement(t) }); + }, + true + ); + + // Throttled scroll events + window.addEventListener( + "scroll", + function () { + var now = Date.now(); + if (now - store.lastScrollTime < CONFIG.scrollThrottleMs) return; + store.lastScrollTime = now; + + logUiEvent("scroll", { + scrollX: window.scrollX, + scrollY: window.scrollY, + documentHeight: document.documentElement.scrollHeight, + viewportHeight: window.innerHeight, + }); + }, + { passive: true } + ); + + // Navigation tracking for SPAs + function nav(reason) { + logUiEvent("navigate", { reason: reason }); + } + + var origPush = history.pushState; + history.pushState = function () { + origPush.apply(this, arguments); + nav("pushState"); + }; + + var origReplace = history.replaceState; + history.replaceState = function () { + origReplace.apply(this, arguments); + nav("replaceState"); + }; + + window.addEventListener("popstate", function () { + nav("popstate"); + }); + window.addEventListener("hashchange", function () { + nav("hashchange"); + }); + } + + // ========================================================================== + // Console Interception + // ========================================================================== + + var originalConsole = { + log: console.log.bind(console), + debug: console.debug.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), + }; + + ["log", "debug", "info", "warn", "error"].forEach(function (method) { + console[method] = function () { + var args = Array.prototype.slice.call(arguments); + + var entry = { + timestamp: Date.now(), + level: method.toUpperCase(), + args: formatArgs(args), + stack: method === "error" ? new Error().stack : null, + }; + + store.consoleLogs.push(entry); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + originalConsole[method].apply(console, args); + }; + }); + + window.addEventListener("error", function (event) { + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UncaughtError", + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + stack: event.error ? event.error.stack : null, + }, + ], + stack: event.error ? event.error.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + // Mark an error moment in UI event stream for agents + logUiEvent("error", { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }); + }); + + window.addEventListener("unhandledrejection", function (event) { + var reason = event.reason; + store.consoleLogs.push({ + timestamp: Date.now(), + level: "ERROR", + args: [ + { + type: "UnhandledRejection", + reason: reason && reason.message ? reason.message : String(reason), + stack: reason && reason.stack ? reason.stack : null, + }, + ], + stack: reason && reason.stack ? reason.stack : null, + }); + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + + logUiEvent("unhandledrejection", { + reason: reason && reason.message ? reason.message : String(reason), + }); + }); + + // ========================================================================== + // Fetch Interception + // ========================================================================== + + var originalFetch = window.fetch.bind(window); + + window.fetch = function (input, init) { + init = init || {}; + var startTime = Date.now(); + // Handle string, Request object, or URL object + var url = typeof input === "string" + ? input + : (input && (input.url || input.href || String(input))) || ""; + var method = init.method || (input && input.method) || "GET"; + + // Don't intercept internal requests + if (url.indexOf("/__manus__/") === 0) { + return originalFetch(input, init); + } + + // Safely parse headers (avoid breaking if headers format is invalid) + var requestHeaders = {}; + try { + if (init.headers) { + requestHeaders = Object.fromEntries(new Headers(init.headers).entries()); + } + } catch (e) { + requestHeaders = { _parseError: true }; + } + + var entry = { + timestamp: startTime, + type: "fetch", + method: method.toUpperCase(), + url: url, + request: { + headers: requestHeaders, + body: init.body ? sanitizeValue(tryParseJson(init.body)) : null, + }, + response: null, + duration: null, + error: null, + }; + + return originalFetch(input, init) + .then(function (response) { + entry.duration = Date.now() - startTime; + + var contentType = (response.headers.get("content-type") || "").toLowerCase(); + var contentLength = response.headers.get("content-length"); + + entry.response = { + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: null, + }; + + // Semantic network hint for agents on failures (sync, no need to wait for body) + if (response.status >= 400) { + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + status: response.status, + statusText: response.statusText, + }); + } + + // Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + if (isStreaming) { + entry.response.body = "[Streaming response - not captured]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for large responses to avoid memory issues + if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) { + entry.response.body = "[Response too large: " + contentLength + " bytes]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + if (isBinary) { + entry.response.body = "[Binary content: " + contentType + "]"; + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + return response; + } + + // For text responses, clone and read body in background + var clonedResponse = response.clone(); + + // Async: read body in background, don't block the response + clonedResponse + .text() + .then(function (text) { + if (text.length <= CONFIG.maxBodyLength) { + entry.response.body = sanitizeValue(tryParseJson(text)); + } else { + entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } + }) + .catch(function () { + entry.response.body = "[Unable to read body]"; + }) + .finally(function () { + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + }); + + // Return response immediately, don't wait for body reading + return response; + }) + .catch(function (error) { + entry.duration = Date.now() - startTime; + entry.error = { message: error.message, stack: error.stack }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "fetch", + method: entry.method, + url: entry.url, + message: error.message, + }); + + throw error; + }); + }; + + // ========================================================================== + // XHR Interception + // ========================================================================== + + var originalXHROpen = XMLHttpRequest.prototype.open; + var originalXHRSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function (method, url) { + this._manusData = { + method: (method || "GET").toUpperCase(), + url: url, + startTime: null, + }; + return originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function (body) { + var xhr = this; + + if ( + xhr._manusData && + xhr._manusData.url && + xhr._manusData.url.indexOf("/__manus__/") !== 0 + ) { + xhr._manusData.startTime = Date.now(); + xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null; + + xhr.addEventListener("load", function () { + var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase(); + var responseBody = null; + + // Skip body capture for streaming responses + var isStreaming = contentType.indexOf("text/event-stream") !== -1 || + contentType.indexOf("application/stream") !== -1 || + contentType.indexOf("application/x-ndjson") !== -1; + + // Skip body capture for binary content types + var isBinary = contentType.indexOf("image/") !== -1 || + contentType.indexOf("video/") !== -1 || + contentType.indexOf("audio/") !== -1 || + contentType.indexOf("application/octet-stream") !== -1 || + contentType.indexOf("application/pdf") !== -1 || + contentType.indexOf("application/zip") !== -1; + + if (isStreaming) { + responseBody = "[Streaming response - not captured]"; + } else if (isBinary) { + responseBody = "[Binary content: " + contentType + "]"; + } else { + // Safe to read responseText for text responses + try { + var text = xhr.responseText || ""; + if (text.length > CONFIG.maxBodyLength) { + responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]"; + } else { + responseBody = sanitizeValue(tryParseJson(text)); + } + } catch (e) { + // responseText may throw for non-text responses + responseBody = "[Unable to read response: " + e.message + "]"; + } + } + + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: { + status: xhr.status, + statusText: xhr.statusText, + body: responseBody, + }, + duration: Date.now() - xhr._manusData.startTime, + error: null, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + if (entry.response && entry.response.status >= 400) { + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + status: entry.response.status, + statusText: entry.response.statusText, + }); + } + }); + + xhr.addEventListener("error", function () { + var entry = { + timestamp: xhr._manusData.startTime, + type: "xhr", + method: xhr._manusData.method, + url: xhr._manusData.url, + request: { body: xhr._manusData.requestBody }, + response: null, + duration: Date.now() - xhr._manusData.startTime, + error: { message: "Network error" }, + }; + + store.networkRequests.push(entry); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + + logUiEvent("network_error", { + kind: "xhr", + method: entry.method, + url: entry.url, + message: "Network error", + }); + }); + } + + return originalXHRSend.apply(this, arguments); + }; + + // ========================================================================== + // Data Reporting + // ========================================================================== + + function reportLogs() { + var consoleLogs = store.consoleLogs.splice(0); + var networkRequests = store.networkRequests.splice(0); + var uiEvents = store.uiEvents.splice(0); + + // Skip if no new data + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return Promise.resolve(); + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + // agent-friendly semantic events + uiEvents: uiEvents, + }; + + return originalFetch(CONFIG.reportEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }).catch(function () { + // Put data back on failure (but respect limits) + store.consoleLogs = consoleLogs.concat(store.consoleLogs); + store.networkRequests = networkRequests.concat(store.networkRequests); + store.uiEvents = uiEvents.concat(store.uiEvents); + + pruneBuffer(store.consoleLogs, CONFIG.bufferSize.console); + pruneBuffer(store.networkRequests, CONFIG.bufferSize.network); + pruneBuffer(store.uiEvents, CONFIG.bufferSize.ui); + }); + } + + // Periodic reporting + setInterval(reportLogs, CONFIG.reportInterval); + + // Report on page unload + window.addEventListener("beforeunload", function () { + var consoleLogs = store.consoleLogs; + var networkRequests = store.networkRequests; + var uiEvents = store.uiEvents; + + if ( + consoleLogs.length === 0 && + networkRequests.length === 0 && + uiEvents.length === 0 + ) { + return; + } + + var payload = { + timestamp: Date.now(), + consoleLogs: consoleLogs, + networkRequests: networkRequests, + // Mirror uiEvents to sessionEvents for sessionReplay.log + sessionEvents: uiEvents, + uiEvents: uiEvents, + }; + + if (navigator.sendBeacon) { + var payloadStr = JSON.stringify(payload); + // sendBeacon has ~64KB limit, truncate if too large + var MAX_BEACON_SIZE = 60000; // Leave some margin + if (payloadStr.length > MAX_BEACON_SIZE) { + // Prioritize: keep recent events, drop older logs + var truncatedPayload = { + timestamp: Date.now(), + consoleLogs: consoleLogs.slice(-50), + networkRequests: networkRequests.slice(-20), + sessionEvents: uiEvents.slice(-100), + uiEvents: uiEvents.slice(-100), + _truncated: true, + }; + payloadStr = JSON.stringify(truncatedPayload); + } + navigator.sendBeacon(CONFIG.reportEndpoint, payloadStr); + } + }); + + // ========================================================================== + // Initialization + // ========================================================================== + + // Install semantic UI listeners ASAP + try { + installUiEventListeners(); + } catch (e) { + console.warn("[Manus] Failed to install UI listeners:", e); + } + + // Mark as initialized + window.__MANUS_DEBUG_COLLECTOR__ = { + version: "2.0-no-rrweb", + store: store, + forceReport: reportLogs, + }; + + console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)"); +})(); diff --git a/clinica-app/client/public/images/clinica_ingresso1.png b/clinica-app/client/public/images/clinica_ingresso1.png new file mode 100644 index 0000000..8c266fa Binary files /dev/null and b/clinica-app/client/public/images/clinica_ingresso1.png differ diff --git a/clinica-app/client/public/images/clinica_ingresso2.png b/clinica-app/client/public/images/clinica_ingresso2.png new file mode 100644 index 0000000..59584e2 Binary files /dev/null and b/clinica-app/client/public/images/clinica_ingresso2.png differ diff --git a/clinica-app/client/public/images/clinica_ingresso3.webp b/clinica-app/client/public/images/clinica_ingresso3.webp new file mode 100644 index 0000000..6d83cc3 Binary files /dev/null and b/clinica-app/client/public/images/clinica_ingresso3.webp differ diff --git a/clinica-app/client/public/images/clinica_sede1.png b/clinica-app/client/public/images/clinica_sede1.png new file mode 100644 index 0000000..dbe12af Binary files /dev/null and b/clinica-app/client/public/images/clinica_sede1.png differ diff --git a/clinica-app/client/public/images/hero_cat.jpg b/clinica-app/client/public/images/hero_cat.jpg new file mode 100644 index 0000000..a2b6ba9 Binary files /dev/null and b/clinica-app/client/public/images/hero_cat.jpg differ diff --git a/clinica-app/client/public/images/hero_dog.jpg b/clinica-app/client/public/images/hero_dog.jpg new file mode 100644 index 0000000..d91282d Binary files /dev/null and b/clinica-app/client/public/images/hero_dog.jpg differ diff --git a/clinica-app/client/public/images/hero_dog_cat.jpg b/clinica-app/client/public/images/hero_dog_cat.jpg new file mode 100644 index 0000000..4176943 Binary files /dev/null and b/clinica-app/client/public/images/hero_dog_cat.jpg differ diff --git a/clinica-app/client/src/App.tsx b/clinica-app/client/src/App.tsx new file mode 100644 index 0000000..c698e71 --- /dev/null +++ b/clinica-app/client/src/App.tsx @@ -0,0 +1,32 @@ +import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import NotFound from "@/pages/NotFound"; +import { Route, Switch } from "wouter"; +import ErrorBoundary from "./components/ErrorBoundary"; +import { ThemeProvider } from "./contexts/ThemeContext"; +import Home from "./pages/Home"; + +function Router() { + return ( + + + + + + ); +} + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/clinica-app/client/src/components/AboutSection.tsx b/clinica-app/client/src/components/AboutSection.tsx new file mode 100644 index 0000000..c6b0815 --- /dev/null +++ b/clinica-app/client/src/components/AboutSection.tsx @@ -0,0 +1,129 @@ +/* + * DESIGN: "Clinical Warmth" + * Sezione istituzionale: layout asimmetrico testo sinistra + immagine destra + * Sfondo bianco, testo blu petrolio, accenti verde acqua + */ +import { motion } from "framer-motion"; +import { useInView } from "framer-motion"; +import { useRef } from "react"; +import { CheckCircle2, Award, Heart } from "lucide-react"; + +const values = [ + { icon: Heart, text: "Cura e attenzione per ogni paziente" }, + { icon: Award, text: "Dir. San. Dott. Paolo Parmeggiani — Master in Oncologia Veterinaria" }, + { icon: CheckCircle2, text: "Autorizzazione sanitaria n. 43560 del 30/12/2024" }, +]; + +export default function AboutSection() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: "-80px" }); + + return ( +
+
+
+ {/* Colonna testo */} + +
+
+ + Chi Siamo + +
+ +

+ Una clinica veterinaria{" "} + al servizio della vita +

+ +

+ La Clinica Veterinaria Formiginese è un punto di riferimento per la cura degli animali + domestici nel territorio di Formigine e della provincia di Modena. Fondata con la + missione di offrire cure veterinarie di eccellenza, la nostra struttura riunisce + sei professionisti specializzati, ciascuno con competenze specifiche. +

+

+ Crediamo che ogni animale meriti la migliore assistenza medica possibile. Per questo + investiamo continuamente in formazione, tecnologia e in un ambiente clinico che metta + a proprio agio sia i pazienti che i loro proprietari. +

+ + {/* Valori */} +
+ {values.map((item) => ( +
+
+ +
+

{item.text}

+
+ ))} +
+ + {/* Indirizzo */} +
+ + + {/* Colonna immagine */} + + {/* Immagine principale */} +
+ Cane e gatto insieme — la nostra missione + {/* Overlay leggero */} +
+
+ + {/* Badge flottante */} +
+
+ 15+ +
+
+ Anni di esperienza nella cura degli animali +
+
+ + {/* Decorazione */} +
+
+ +
+
+
+ ); +} diff --git a/clinica-app/client/src/components/AuthSection.tsx b/clinica-app/client/src/components/AuthSection.tsx new file mode 100644 index 0000000..3009677 --- /dev/null +++ b/clinica-app/client/src/components/AuthSection.tsx @@ -0,0 +1,279 @@ +/* + * DESIGN: "Clinical Warmth" + * Sezione registrazione/login: tabs con form eleganti + * Sfondo bianco, accenti blu petrolio e verde acqua + */ +import { useState } from "react"; +import { motion } from "framer-motion"; +import { useInView } from "framer-motion"; +import { useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { User, Mail, Lock, Eye, EyeOff, CheckCircle2, PawPrint } from "lucide-react"; +import { toast } from "sonner"; + +export default function AuthSection() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: "-80px" }); + const [tab, setTab] = useState<"login" | "register">("register"); + const [showPassword, setShowPassword] = useState(false); + const [registered, setRegistered] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (tab === "register") { + setRegistered(true); + toast.success("Registrazione completata!", { + description: "Benvenuto nella Clinica Veterinaria Formiginese.", + }); + } else { + toast.success("Accesso effettuato!", { + description: "Bentornato nella tua area personale.", + }); + } + }; + + const benefits = [ + "Storico visite e referti digitali", + "Promemoria vaccinazioni automatici", + "Prenotazioni online prioritarie", + "Comunicazioni dirette con il veterinario", + "Gestione di più animali domestici", + ]; + + return ( +
+
+
+ {/* Colonna sinistra: benefici */} + +
+
+ + Area Personale + +
+ +

+ Registrati e gestisci{" "} + la salute del tuo animale +

+ +

+ Crea il tuo profilo personale per accedere a tutti i servizi digitali della clinica. + Tieni traccia della storia clinica del tuo animale, ricevi promemoria e prenota + le visite in pochi click. +

+ + {/* Benefits list */} +
+ {benefits.map((benefit) => ( +
+
+ +
+ {benefit} +
+ ))} +
+ + {/* Decorazione */} +
+
+ +
+
+

Già più di 500 famiglie

+

+ si affidano alla nostra clinica per la cura dei loro animali +

+
+
+ + + {/* Colonna destra: form */} + + {registered ? ( +
+
+ +
+

+ Benvenuto! +

+

+ La tua registrazione è avvenuta con successo. Ora puoi accedere a tutti i servizi + della tua area personale. +

+ +
+ ) : ( +
+ {/* Tabs */} +
+ {(["register", "login"] as const).map((t) => ( + + ))} +
+ +
+ {tab === "register" && ( +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ )} + +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ + {tab === "register" && ( +
+ + +
+ )} + + + + {tab === "login" && ( +

+ +

+ )} +
+
+ )} +
+
+
+
+ ); +} diff --git a/clinica-app/client/src/components/BookingSection.tsx b/clinica-app/client/src/components/BookingSection.tsx new file mode 100644 index 0000000..847cbdd --- /dev/null +++ b/clinica-app/client/src/components/BookingSection.tsx @@ -0,0 +1,328 @@ +/* + * DESIGN: "Clinical Warmth" + * Sezione prenotazione: form elegante su sfondo blu petrolio + * Layout: testo a sinistra + form a destra + */ +import { useState } from "react"; +import { motion } from "framer-motion"; +import { useInView } from "framer-motion"; +import { useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { Calendar, Clock, User, Phone, PawPrint, CheckCircle2 } from "lucide-react"; +import { toast } from "sonner"; + +const services = [ + "Visita generale", + "Radiologia / Ecografia", + "Chirurgia (consulenza)", + "Laboratorio analisi", + "Vaccinazione", + "Dermatologia", + "Odontoiatria", + "Oncologia", +]; + +const timeSlots = [ + "09:00", "09:30", "10:00", "10:30", "11:00", "11:30", + "14:30", "15:00", "15:30", "16:00", "16:30", "17:00", +]; + +export default function BookingSection() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: "-80px" }); + const [submitted, setSubmitted] = useState(false); + const [form, setForm] = useState({ + name: "", + phone: "", + petName: "", + petType: "cane", + service: "", + date: "", + time: "", + notes: "", + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!form.name || !form.phone || !form.service || !form.date) { + toast.error("Compila tutti i campi obbligatori"); + return; + } + setSubmitted(true); + toast.success("Richiesta inviata!", { + description: "Ti contatteremo entro 24 ore per confermare l'appuntamento.", + }); + }; + + return ( +
+ {/* Decorazioni di sfondo */} +
+
+ +
+
+ {/* Colonna sinistra: testo */} + +
+
+ + Prenotazioni + +
+ +

+ Prenota la tua visita{" "} + online +

+ +

+ Compila il modulo per richiedere un appuntamento. Ti contatteremo entro 24 ore + per confermare la data e l'orario. Per urgenze, chiama direttamente il numero + dedicato disponibile 24 ore su 24. +

+ + {/* Info box urgenze */} +
+

+ Urgenze 24h +

+ + 320 532.24.39 + +

Disponibile 7 giorni su 7

+
+ + {/* Orari */} +
+

+ Orari di apertura +

+ {[ + { days: "Lunedì — Venerdì", hours: "09:00 — 12:30 · 14:30 — 19:00" }, + { days: "Sabato", hours: "09:00 — 12:30" }, + { days: "Domenica", hours: "Solo urgenze" }, + ].map((slot) => ( +
+ {slot.days} + {slot.hours} +
+ ))} +
+ + + {/* Colonna destra: form */} + + {submitted ? ( +
+
+ +
+

+ Richiesta inviata! +

+

+ Abbiamo ricevuto la tua richiesta di appuntamento. Ti contatteremo entro 24 ore + per confermare data e orario. +

+ +
+ ) : ( +
+

+ Richiedi un appuntamento +

+ + {/* Nome e telefono */} +
+
+ +
+ + setForm({ ...form, name: e.target.value })} + className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all" + /> +
+
+
+ +
+ + setForm({ ...form, phone: e.target.value })} + className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all" + /> +
+
+
+ + {/* Animale */} +
+
+ +
+ + setForm({ ...form, petName: e.target.value })} + className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all" + /> +
+
+
+ + +
+
+ + {/* Servizio */} +
+ + +
+ + {/* Data e ora */} +
+
+ +
+ + setForm({ ...form, date: e.target.value })} + className="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#4ECDC4] focus:border-transparent transition-all" + /> +
+
+
+ +
+ + +
+
+
+ + {/* Note */} +
+ +