From d3f740cfa961dafc904dc281c349f6b8b086c94e Mon Sep 17 00:00:00 2001 From: Nicola Leone Ciardi Date: Mon, 20 Apr 2026 12:48:58 +0200 Subject: [PATCH] added admin dashboard --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 15 + img/.DS_Store | Bin 6148 -> 0 bytes index.html | 358 -------- package.json | 27 + src/admin.css | 296 +++++++ src/app.css | 27 + src/app.d.ts | 10 + src/app.html | 19 + src/hooks.server.ts | 27 + src/lib/content/defaults.ts | 220 +++++ src/lib/seo.ts | 109 +++ src/lib/server/auth.ts | 99 +++ src/lib/server/content.ts | 95 ++ src/lib/server/health.ts | 12 + src/lib/server/notifications.ts | 56 ++ src/lib/server/storage.ts | 29 + src/lib/server/submissions.ts | 35 + src/routes/+layout.server.ts | 7 + src/routes/+layout.svelte | 11 + src/routes/+page.server.ts | 31 + src/routes/+page.svelte | 398 +++++++++ src/routes/admin/+layout.server.ts | 14 + src/routes/admin/+layout.svelte | 76 ++ src/routes/admin/+page.server.ts | 26 + src/routes/admin/+page.svelte | 79 ++ src/routes/admin/copy/+page.server.ts | 89 ++ src/routes/admin/copy/+page.svelte | 294 +++++++ src/routes/admin/images/+page.server.ts | 76 ++ src/routes/admin/images/+page.svelte | 138 +++ src/routes/admin/login/+page.server.ts | 19 + src/routes/admin/login/+page.svelte | 32 + src/routes/admin/logout/+server.ts | 8 + .../admin/notifications/+page.server.ts | 33 + src/routes/admin/notifications/+page.svelte | 159 ++++ src/routes/admin/submissions/+page.server.ts | 17 + src/routes/admin/submissions/+page.svelte | 63 ++ src/routes/admin/testimonials/+page.server.ts | 103 +++ src/routes/admin/testimonials/+page.svelte | 189 ++++ src/routes/admin/warranty/+page.svelte | 110 +++ src/site.css | 521 +++++++++++ {img => static/img}/avatar-1.png | 0 {img => static/img}/avatar-2.png | Bin {img => static/img}/avatar-3.png | Bin {img => static/img}/chi-sono.png | Bin static/img/cima-logo.svg | 11 + {img => static/img}/colonnina.png | Bin {img => static/img}/hero-image.png | Bin {img => static/img}/logo.png | Bin {img => static/img}/mappa.png | Bin static/img/paw.svg | 3 + styles.css | 311 ------- svelte.config.js | 15 + tsconfig.json | 14 + vite.config.ts | 11 + yarn.lock | 815 ++++++++++++++++++ 56 files changed, 4438 insertions(+), 669 deletions(-) delete mode 100644 .DS_Store create mode 100644 .gitignore delete mode 100644 img/.DS_Store delete mode 100644 index.html create mode 100644 package.json create mode 100644 src/admin.css create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/hooks.server.ts create mode 100644 src/lib/content/defaults.ts create mode 100644 src/lib/seo.ts create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/content.ts create mode 100644 src/lib/server/health.ts create mode 100644 src/lib/server/notifications.ts create mode 100644 src/lib/server/storage.ts create mode 100644 src/lib/server/submissions.ts create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/admin/+layout.server.ts create mode 100644 src/routes/admin/+layout.svelte create mode 100644 src/routes/admin/+page.server.ts create mode 100644 src/routes/admin/+page.svelte create mode 100644 src/routes/admin/copy/+page.server.ts create mode 100644 src/routes/admin/copy/+page.svelte create mode 100644 src/routes/admin/images/+page.server.ts create mode 100644 src/routes/admin/images/+page.svelte create mode 100644 src/routes/admin/login/+page.server.ts create mode 100644 src/routes/admin/login/+page.svelte create mode 100644 src/routes/admin/logout/+server.ts create mode 100644 src/routes/admin/notifications/+page.server.ts create mode 100644 src/routes/admin/notifications/+page.svelte create mode 100644 src/routes/admin/submissions/+page.server.ts create mode 100644 src/routes/admin/submissions/+page.svelte create mode 100644 src/routes/admin/testimonials/+page.server.ts create mode 100644 src/routes/admin/testimonials/+page.svelte create mode 100644 src/routes/admin/warranty/+page.svelte create mode 100644 src/site.css rename {img => static/img}/avatar-1.png (100%) rename {img => static/img}/avatar-2.png (100%) rename {img => static/img}/avatar-3.png (100%) rename {img => static/img}/chi-sono.png (100%) create mode 100644 static/img/cima-logo.svg rename {img => static/img}/colonnina.png (100%) rename {img => static/img}/hero-image.png (100%) rename {img => static/img}/logo.png (100%) rename {img => static/img}/mappa.png (100%) create mode 100644 static/img/paw.svg delete mode 100644 styles.css create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 yarn.lock diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 72f5f7dd7db543618abb33e8541df5229da3998d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}*0S6n_I%wnb$5QXrZ%_F@7-Kr|X-C> z(LcaHz^f;X7f`P{TZ{~f>zTb9sIsia28-)=7695cs z0%Hm6ej~yz+97GuGa`wMXBgK&8GGihp$ojBQIv>2_%YPQHL+0Y6%f{f)ai_tviUcRnd`309& za%T4=XQAaX>dOGPK+$F8zRL={lHnVy5Xji9G8l0qo-=!^)zOiW!Q|+bYrBKV>h;m# z!Q|NGtGl~#qbGIg#$0aQD>42=kxuZNB&AJb9iF11t8wj3T@CqEujZ?1*6WEUT2Hl| zZZ|tRyE?i%dU|@!obB!F??0C_yYiO3?)c(4C%MP{jJH9T9p5cv%a-jjXECH(Vbl`gS+?$%;E~8KP-h@nd+ozPtd^2ln0Sy-$^rf8H9@swjyjdQ&q08CQN`Nd+ zhqN@!);O-Xi44z_g(+B|et>u=W^u{#eTz9Xv%TtBdFsuu3=czI5}qQP&DpF^MW#g; zbx})d{JY{XmA+e7E-J-@lv41l=&oxTN$Q<5EA@HC8I8wNX*Cq*$^cBjJy?Ks*n#Kp z0$##f_y}L%8~h+Gq>o%617wWcB;(``nI;d2O^U=Nk5%fX`YS%ae@PPHSifS=8Ohqb zRBrIlmJ*+u(m!S6S$oxa^ZvucUR;S6{$MQ^kIK54^_j<+gPFwBC2(^wC;){m7=j{% zxYfM2deRG~y)F^Il95EYL0wDzwlRN+A6 z%9uy5O!N(f$klN^Dcymj6-Ir;K*Yd)2Ab8-6zBh~qwoLwgQ8KyK*YeYVt~Y^vQrr> zNuRBUiW6sT65AteLWEyUp$x%JAIDM=M{zGUDQHUy1u?C$rVwXPqCWzXhNz1e_@@l~ E0#l_0$p8QV diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d6f44f --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules +/.svelte-kit +/build +/dist +.env +.env.* +!.env.example +.vite +*.log +.DS_Store + +# runtime data (admin-managed content, uploads, sessions) +/data +/static/img/uploads/* +!/static/img/uploads/.gitkeep diff --git a/img/.DS_Store b/img/.DS_Store deleted file mode 100644 index d8e286a8dc453644208712f38e7b29109b911fa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%Sr=55Uh?lEchTtkAwFF{DWnQ2haY1CQ1yfyP|mCZ}3z6L9Ol{CHpvwNH@&X zbWhjJPQlItfK+eym%su*!J;VIm@o}bcFl6~oG3QND_S%xD?CpO^cO?=?I+mc5qm6g z`27ub*LAzy)a`~jDp19;`ZQw;I3=$|ZEJ4GZ*XTTA2H=(z&zu84|pMSN!}BAJNl~H zh>MQ_x3C#^S>;i_ybto}hJ^y5KqwFjgaQW&@XS^#jvb>81ww&P;70-79}=K<4Y4b;U*HvvssQJ!=!oEsL7w pb=sh@-Ma+P(S79HI=w$>n|Wo%&QV9vxY3Dm5imib3I%>afiFIRMnV7p diff --git a/index.html b/index.html deleted file mode 100644 index 8522f33..0000000 --- a/index.html +++ /dev/null @@ -1,358 +0,0 @@ - - - - - - Giampy Dog Service | Cura e Passione per il tuo Cane - - - - - - - - - - -
-
-
-

Dal 2018 con amore

-

La cura e l'amore che il tuo cane merita.

-

Un approccio umano e professionale per garantire il benessere del tuo compagno di vita, senza gabbie né solitudine.

-
- - -
-
-
-
- Giampy con un cane -
-
-
- star - 5.0 Recensioni -
-

Il punto di riferimento per i proprietari di Sassari.

-
-
-
-
- - -
-
-
-

Perché scegliere Giampy

-

Ogni cane viene seguito con attenzione, rispetto e presenza vera.

-
-
-
-
history
-
-

Esperienza dal 2018

-

Anni di dedizione quotidiana che hanno costruito una competenza solida e affidabile.

-
-
-
-
diversity_1
-
-

Rapporti Umani

-

Costruisco legami duraturi con i proprietari basati su fiducia e trasparenza totale.

-
-
-
-
verified_user
-
-

Approccio Professionale

-

Metodologia amichevole ma rigorosa nel rispetto delle abitudini del tuo cane.

-
-
-
-
house_siding
-
-

Senza Gabbie

-

Assoluto divieto di catene o gabbie. Libertà e comfort sono le nostre priorità.

-
-
-
-
-
- - -
-
-
-

I Nostri Servizi

-

Soluzioni su misura per ogni esigenza, pensate per il massimo comfort del tuo cane.

-
-
-
- pets -

Dogwalking

-

Passeggiate esplorative nei sentieri più belli di Sassari, per stimolare mente e corpo.

- Prenota ora arrow_forward -
-
- house -

Home Boarding

-

Dogsitting a domicilio o ospitalità casalinga. Il tuo cane si sentirà sempre nel suo ambiente.

- Chiedi info arrow_forward -
-
- celebration -

Wedding Dogsitter

-

Gestione professionale durante le cerimonie. Il tuo migliore amico presente nel tuo giorno speciale.

- Scopri di più arrow_forward -
-
-
-
- - -
-
-
- Giampy passeggia con un cane -
-

"Rusty non è stato solo un cane, è stato il maestro che mi ha indicato la strada."

- — Giampiero Scaglione -
-
-
-

CHI SONO

-

Dall'amore alla carriera CONI

-

La storia di Giampy Dog Service nasce da una necessità del cuore. Quello che era iniziato come un lavoro temporaneo si è evoluto in una missione di vita grazie a Rusty (il mitico Rustone).

-

Oggi, quella passione è diventata una carriera certificata CONI, unendo l'istinto alla formazione tecnica più rigorosa per offrire ai cani non solo assistenza, ma vera comprensione.

-
-
- explore -
- Cammino di Santiago - Percorso a piedi con il proprio cane, testando legame e resilienza. -
-
-
- verified -
- Certificazione CONI - Formazione professionale d'eccellenza in ambito cinofilo. -
-
-
-
-
-
- - -
-
-

RICONOSCIMENTI E MEDIA

-
- L'UNIONE SARDA - Sardegna Reporter - RADIO SUPER SOUND - QUALIFICA CONI - Corsa in Rosa - Giornata del Super Cane -
-
-
- - -
-
-
-
- Innovazione -

DogWash Sardegna

-

Il futuro della cura del cane è qui. Grazie a un incredibile successo nel crowdfunding e alla fiducia della community, stiamo portando le colonne di lavaggio automatico "DogWash" in tutta la Sardegna. Un servizio rapido, ecologico e comodo.

- -
- group - Un progetto nato dalla fiducia dei cittadini di Sassari. -
-
-
- DogWash Machine -
-
-
-
- - -
-
-
-
-

100+

Clienti Felici

-

2018

Anno Fondazione

-
-

I nostri ospiti dicono...

-

Più di semplici clienti, una grande famiglia pelosa.

-
-
-
-
- Dog avatar -
-
"Giampiero è l'unico di cui mi fido ciecamente. Il mio Max torna sempre felice e rilassato dalle passeggiate. Servizio eccellente!"
-
- Marta S. - Mamma di Max -
-
-
-
- Dog avatar -
-
"Abbiamo usato il servizio Wedding Dogsitter ed è stato perfetto. Il nostro Luna è stato coccolato tutto il tempo e noi ci siamo goduti la festa."
-
- Luca & Elena - Genitori di Luna -
-
-
-
- Dog avatar -
-
"Pensione in appartamento fantastica. Nessuna gabbia, solo tanto amore. Giampiero ci ha mandato foto e video ogni giorno!"
-
- Sara P. - Compagna di Felix -
-
-
-
-
- - -
-
-
-

Affida il tuo cane a chi lo vive davvero.

- keyboard_double_arrow_down -
-
-
-

Affida il tuo cane a chi lo vive davvero.

-

Hai domande o vuoi prenotare un servizio? Scrivici ora e verrai ricontattato in meno di 24 ore.

-
-
- - -
-
- - -
-
- - -
- -
-
-
-

Contatti Rapidi

-
-
call
-
-

Chiamaci

-

+39 347 123 4567

-
-
-
-
mail
-
-

Email

-

info@giampydogservice.it

-
-
-
-
forum
-
-

WhatsApp

-

Chatta con Giampiero

-
-
-
- Mappa della sede -
-
-
-
-
- - - - - - diff --git a/package.json b/package.json new file mode 100644 index 0000000..ebb197f --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "giampy-dogservice", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "seed": "node scripts/seed.mjs" + }, + "dependencies": { + "leaflet": "^1.9.4" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.15.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@types/leaflet": "^1.9.15", + "@types/node": "^22.10.2", + "svelte": "^5.16.0", + "svelte-check": "^4.1.1", + "typescript": "^5.7.2", + "vite": "^6.0.6" + } +} diff --git a/src/admin.css b/src/admin.css new file mode 100644 index 0000000..babdb8a --- /dev/null +++ b/src/admin.css @@ -0,0 +1,296 @@ +/* ===== ADMIN THEME ===== */ +.admin { + --nav-bg: #0b1624; + --nav-item: rgba(255, 255, 255, 0.66); + --nav-item-active: #fff; + --nav-item-bg-hover: rgba(255, 255, 255, 0.07); + --nav-item-bg-active: rgba(139, 115, 61, 0.22); + --panel: #fff; + --panel-muted: #fbf9f4; + --border: #e6e4dc; + --text: #1c1c1a; + --text-muted: #6b6e72; + --accent: #8B733D; + --accent-dark: #001d36; + --danger: #b03030; + --ok: #1e7a3c; + --warn: #b06a10; + font-family: 'Manrope', sans-serif; + color: var(--text); + background: var(--panel-muted); + min-height: 100vh; +} + +.admin-shell { display: grid; grid-template-columns: 260px 1fr; min-height: 100vh; } + +.admin-side { + background: var(--nav-bg); color: #fff; + padding: 24px 18px; + display: flex; flex-direction: column; + position: sticky; top: 0; height: 100vh; +} +.admin-brand { + display: flex; align-items: center; gap: 10px; + padding: 6px 8px 22px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); + margin-bottom: 14px; +} +.admin-brand img { height: 30px; width: 30px; object-fit: contain; } +.admin-brand strong { font-family: 'Newsreader', serif; font-size: 17px; letter-spacing: 0.01em; } +.admin-brand small { + display: block; font-size: 9px; letter-spacing: 0.25em; + text-transform: uppercase; color: rgba(255, 255, 255, 0.45); +} +.admin-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; } +.admin-nav a { + color: var(--nav-item); text-decoration: none; + padding: 10px 12px; border-radius: 10px; + display: flex; align-items: center; gap: 12px; + font-size: 14px; font-weight: 500; + transition: background 0.15s, color 0.15s; +} +.admin-nav a:hover { background: var(--nav-item-bg-hover); color: var(--nav-item-active); } +.admin-nav a.active { + background: var(--nav-item-bg-active); color: var(--nav-item-active); + font-weight: 700; +} +.admin-nav .material-symbols-outlined { font-size: 20px; } +.admin-foot { padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.08); } +.admin-foot form { margin: 0; } +.admin-user { + color: rgba(255, 255, 255, 0.6); font-size: 12px; + padding: 8px 12px 12px; display: flex; flex-direction: column; gap: 2px; +} +.admin-user strong { color: #fff; font-size: 13px; font-weight: 700; } +.admin-logout { + width: 100%; color: rgba(255, 255, 255, 0.78); + padding: 10px 12px; border-radius: 10px; font-size: 13px; + display: flex; align-items: center; gap: 10px; + background: rgba(255, 255, 255, 0.05); cursor: pointer; + font-family: inherit; border: none; +} +.admin-logout:hover { background: rgba(255, 255, 255, 0.12); color: #fff; } + +.admin-main { padding: 32px 36px 48px; max-width: 1200px; } +.admin-main header.page-head { margin-bottom: 28px; } +.admin-main .kicker { + color: var(--accent); font-size: 10px; letter-spacing: 0.24em; + text-transform: uppercase; font-weight: 700; margin-bottom: 8px; +} +.admin-main h1 { + font-family: 'Newsreader', serif; font-size: 32px; + color: var(--accent-dark); font-weight: 700; line-height: 1.15; +} +.admin-main h1 + p { color: var(--text-muted); font-size: 15px; margin-top: 6px; max-width: 60ch; } + +.card { + background: var(--panel); border: 1px solid var(--border); + border-radius: 16px; padding: 22px; +} +.card + .card { margin-top: 18px; } +.card h2 { + font-family: 'Newsreader', serif; font-size: 20px; + color: var(--accent-dark); margin-bottom: 10px; +} +.card p { color: var(--text-muted); font-size: 14px; line-height: 1.6; } + +.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } +.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } + +.stat { + background: var(--panel); border: 1px solid var(--border); + border-radius: 16px; padding: 18px 20px; +} +.stat-label { + color: var(--text-muted); font-size: 10px; + letter-spacing: 0.22em; text-transform: uppercase; font-weight: 700; +} +.stat-value { + font-family: 'Newsreader', serif; font-size: 30px; + color: var(--accent-dark); font-weight: 700; margin-top: 6px; +} + +.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } +.field label { + font-size: 12px; font-weight: 700; color: var(--accent-dark); + letter-spacing: 0.03em; +} +.field input, .field textarea, .field select { + width: 100%; padding: 11px 13px; font-size: 14px; + border: 1px solid var(--border); border-radius: 10px; + background: #fff; font-family: inherit; color: var(--text); + transition: border-color 0.15s, box-shadow 0.15s; +} +.field input:focus, .field textarea:focus, .field select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(139, 115, 61, 0.15); + outline: none; +} +.field textarea { resize: vertical; min-height: 80px; } +.field small { color: var(--text-muted); font-size: 12px; } + +.btn { + display: inline-flex; align-items: center; gap: 8px; + padding: 10px 18px; font-size: 13px; font-weight: 700; + background: var(--accent-dark); color: #fff; + border: none; border-radius: 9999px; + font-family: inherit; cursor: pointer; + transition: opacity 0.15s, transform 0.15s; +} +.btn:hover:not(:disabled) { opacity: 0.9; transform: translateY(-1px); } +.btn:disabled { opacity: 0.55; cursor: not-allowed; } +.btn.btn-ghost { background: transparent; color: var(--accent-dark); border: 1px solid var(--border); } +.btn.btn-ghost:hover:not(:disabled) { background: var(--panel-muted); } +.btn.btn-danger { background: var(--danger); } +.btn.btn-sm { padding: 6px 12px; font-size: 11px; } +.btn .material-symbols-outlined { font-size: 16px; } + +.section-divider { + display: flex; align-items: center; gap: 14px; + margin: 28px 0 14px; +} +.section-divider h2 { + font-family: 'Newsreader', serif; font-size: 22px; + color: var(--accent-dark); font-weight: 700; +} +.section-divider .count { + background: rgba(0, 29, 54, 0.06); color: var(--accent-dark); + padding: 3px 10px; border-radius: 9999px; + font-size: 11px; font-weight: 700; +} + +.score-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 6px; } +.score-chip { + display: inline-flex; align-items: center; gap: 6px; + padding: 4px 10px; border-radius: 9999px; + font-size: 11px; font-weight: 700; letter-spacing: 0.03em; + border: 1px solid var(--border); +} +.score-chip.seo-good, .score-chip.geo-good { background: rgba(30, 122, 60, 0.08); color: var(--ok); border-color: rgba(30, 122, 60, 0.22); } +.score-chip.seo-warn, .score-chip.geo-warn { background: rgba(176, 106, 16, 0.08); color: var(--warn); border-color: rgba(176, 106, 16, 0.22); } +.score-chip.seo-bad, .score-chip.geo-bad { background: rgba(176, 48, 48, 0.08); color: var(--danger); border-color: rgba(176, 48, 48, 0.22); } + +.tip-list { + list-style: none; padding: 0; margin: 8px 0 0; + display: flex; flex-direction: column; gap: 4px; +} +.tip-list li { + font-size: 12px; color: var(--text-muted); + padding-left: 14px; position: relative; line-height: 1.5; +} +.tip-list li::before { content: '•'; position: absolute; left: 0; color: var(--accent); } + +.slot-grid { display: grid; grid-template-columns: 1fr; gap: 14px; } +.slot-card { + background: var(--panel); border: 1px solid var(--border); + border-radius: 14px; padding: 18px; +} +.slot-card header { + display: flex; justify-content: space-between; align-items: baseline; + gap: 8px; margin-bottom: 10px; +} +.slot-card .slot-id { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 11px; color: var(--text-muted); +} +.slot-card .slot-label { font-size: 13px; font-weight: 700; color: var(--accent-dark); } + +.alert-row { + background: rgba(30, 122, 60, 0.08); color: var(--ok); + border: 1px solid rgba(30, 122, 60, 0.18); + padding: 10px 14px; border-radius: 10px; + font-size: 13px; font-weight: 700; + margin-bottom: 18px; +} +.alert-row.err { + background: rgba(176, 48, 48, 0.08); color: var(--danger); + border-color: rgba(176, 48, 48, 0.18); +} + +.sub-table { + background: var(--panel); border: 1px solid var(--border); + border-radius: 14px; overflow: hidden; +} +.sub-row { + display: grid; grid-template-columns: 180px 1fr 180px 120px; + padding: 14px 18px; gap: 16px; + border-bottom: 1px solid var(--border); +} +.sub-row:last-child { border-bottom: none; } +.sub-row header { + display: flex; flex-direction: column; gap: 2px; +} +.sub-row time { + color: var(--text-muted); font-size: 11px; + letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700; +} +.sub-row .name { color: var(--accent-dark); font-weight: 700; font-size: 14px; } +.sub-row .dog { color: var(--accent); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } +.sub-row .msg { font-size: 14px; color: var(--text); line-height: 1.55; white-space: pre-wrap; } +.sub-row .meta { color: var(--text-muted); font-size: 12px; } + +.image-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } +.image-card { + background: var(--panel); border: 1px solid var(--border); + border-radius: 14px; padding: 16px; +} +.image-card .preview { + background: var(--panel-muted); border: 1px solid var(--border); + border-radius: 10px; overflow: hidden; + aspect-ratio: 16 / 9; + display: flex; align-items: center; justify-content: center; + margin-bottom: 12px; +} +.image-card .preview img { max-width: 100%; max-height: 100%; object-fit: contain; } +.image-card header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px; } +.image-card .slot-id { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 10px; color: var(--text-muted); } +.image-card .slot-label { font-size: 13px; font-weight: 700; color: var(--accent-dark); } +.image-card .actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; } + +.login-shell { + min-height: 100vh; + display: flex; align-items: center; justify-content: center; + background: var(--panel-muted); + padding: 32px; +} +.login-card { + background: #fff; border: 1px solid var(--border); + padding: 40px; border-radius: 20px; + max-width: 400px; width: 100%; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.05); +} +.login-card h1 { + font-family: 'Newsreader', serif; + color: var(--accent-dark); font-size: 26px; + margin-bottom: 6px; +} +.login-card p { color: var(--text-muted); font-size: 14px; margin-bottom: 24px; } +.login-card .btn { width: 100%; justify-content: center; padding: 14px; } + +.warranty-status { + display: flex; align-items: center; gap: 14px; + padding: 18px; border-radius: 14px; + background: rgba(30, 122, 60, 0.08); color: var(--ok); + border: 1px solid rgba(30, 122, 60, 0.22); +} +.warranty-status .dot { width: 12px; height: 12px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px rgba(30, 122, 60, 0.15); } +.warranty-status strong { font-size: 15px; } +.warranty-status small { display: block; color: var(--text-muted); font-size: 12px; margin-top: 2px; } + +.cover-table { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 14px; } +.cover-col { + padding: 18px; border-radius: 14px; + border: 1px solid var(--border); background: var(--panel); +} +.cover-col.yes { border-color: rgba(30, 122, 60, 0.22); background: rgba(30, 122, 60, 0.04); } +.cover-col.no { border-color: rgba(176, 48, 48, 0.22); background: rgba(176, 48, 48, 0.04); } +.cover-col h3 { font-size: 14px; margin-bottom: 10px; } +.cover-col ul { margin: 0; padding-left: 18px; } +.cover-col li { font-size: 13px; color: var(--text); line-height: 1.6; margin-bottom: 4px; } + +@media (max-width: 900px) { + .admin-shell { grid-template-columns: 1fr; } + .admin-side { position: relative; height: auto; } + .grid-2, .grid-3, .grid-4, .image-grid, .cover-table { grid-template-columns: 1fr; } + .sub-row { grid-template-columns: 1fr; } +} diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..a326f51 --- /dev/null +++ b/src/app.css @@ -0,0 +1,27 @@ +/* ===== RESET & BASE ===== */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --navy: #001d36; + --gold: #8B733D; + --amber: rgb(225, 173, 1); + --cream: #EBE8DE; + --cream-light: #F1F0E9; + --offwhite: #fbf9f4; + --dark: #1c1c1a; + --muted: #42484e; + --outline-soft: rgba(194, 200, 192, 0.25); +} + +html { scroll-behavior: smooth; } +body { + font-family: 'Manrope', sans-serif; + background: var(--offwhite); + color: var(--dark); + line-height: 1.6; +} + +img { max-width: 100%; height: auto; display: block; } +a { text-decoration: none; color: inherit; } +button { cursor: pointer; border: none; font-family: inherit; background: none; } +input, textarea { font-family: inherit; outline: none; } diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..c681bcc --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,10 @@ +// See https://kit.svelte.dev/docs/types#app +declare global { + namespace App { + interface Locals { + admin: { username: string } | null; + } + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..8f15b4e --- /dev/null +++ b/src/app.html @@ -0,0 +1,19 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..44fb98a --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,27 @@ +import type { Handle } from '@sveltejs/kit'; +import { redirect } from '@sveltejs/kit'; +import { getSessionUser, seedDefaultAdminIfMissing } from '$lib/server/auth'; + +const seed = seedDefaultAdminIfMissing(); +if (seed.tempPassword) { + console.log( + `[admin] seeded default admin: username="${seed.username}" password="${seed.tempPassword}" (change it from /admin)` + ); +} + +export const handle: Handle = async ({ event, resolve }) => { + event.locals.admin = getSessionUser(event.cookies); + + const p = event.url.pathname; + const isAdminArea = p === '/admin' || p.startsWith('/admin/'); + const isLogin = p === '/admin/login'; + + if (isAdminArea && !isLogin && !event.locals.admin) { + throw redirect(303, '/admin/login'); + } + if (isLogin && event.locals.admin) { + throw redirect(303, '/admin'); + } + + return resolve(event); +}; diff --git a/src/lib/content/defaults.ts b/src/lib/content/defaults.ts new file mode 100644 index 0000000..01811e3 --- /dev/null +++ b/src/lib/content/defaults.ts @@ -0,0 +1,220 @@ +export type CopySlot = { + id: string; + label: string; + value: string; + section: string; + hint?: string; + kind: 'inline' | 'multiline'; +}; + +export type ImageSlot = { + id: string; + label: string; + src: string; + alt: string; +}; + +export type Testimonial = { + id: string; + text: string; + name: string; + role: string; + avatarSrc: string; + avatarAlt: string; +}; + +export type SiteContent = { + copy: Record; + images: Record; + testimonials: Testimonial[]; + seo: { + title: string; + description: string; + keywords: string; + ogImage: string; + primaryKeywords: string[]; + geoRegion: string; + geoPlacename: string; + }; +}; + +export const DEFAULT_SLOTS: CopySlot[] = [ + { id: 'nav.brand', section: 'Navbar', label: 'Brand name', kind: 'inline', value: 'Giampy Dog Service' }, + { id: 'nav.link.servizi', section: 'Navbar', label: 'Link — Servizi', kind: 'inline', value: 'Servizi' }, + { id: 'nav.link.chi_sono', section: 'Navbar', label: 'Link — Chi Sono', kind: 'inline', value: 'Chi Sono' }, + { id: 'nav.link.testimonianze', section: 'Navbar', label: 'Link — Testimonianze', kind: 'inline', value: 'Testimonianze' }, + { id: 'nav.link.dogwash', section: 'Navbar', label: 'Link — DogWash', kind: 'inline', value: 'DogWash' }, + { id: 'nav.cta', section: 'Navbar', label: 'CTA', kind: 'inline', value: 'Scrivimi' }, + + { id: 'hero.kicker', section: 'Hero', label: 'Kicker', kind: 'inline', value: 'Con te dal 2018' }, + { id: 'hero.title_prefix', section: 'Hero', label: 'Title — first line', kind: 'inline', value: 'Tratto il tuo cane' }, + { id: 'hero.title_suffix', section: 'Hero', label: 'Title — second line (before emphasis)', kind: 'inline', value: 'come se fosse' }, + { id: 'hero.title_em', section: 'Hero', label: 'Title — emphasis', kind: 'inline', value: 'il mio.' }, + { id: 'hero.desc', section: 'Hero', label: 'Description', kind: 'multiline', value: 'Sono Giampiero, educatore cinofilo certificato CONI. Mi occupo personalmente di ogni cane che mi affidi — niente gabbie, niente solitudine, solo tanto amore vero.' }, + { id: 'hero.cta_primary', section: 'Hero', label: 'Primary CTA', kind: 'inline', value: 'Scopri i Servizi' }, + { id: 'hero.cta_outline', section: 'Hero', label: 'Outline CTA', kind: 'inline', value: 'Scrivimi ora' }, + { id: 'hero.badge_label', section: 'Hero', label: 'Badge label', kind: 'inline', value: '5.0 Recensioni' }, + { id: 'hero.badge_text', section: 'Hero', label: 'Badge text', kind: 'inline', value: 'Il punto di riferimento per i proprietari di cani a Sassari.' }, + { id: 'hero.stat1_num', section: 'Hero', label: 'Stat 1 — value', kind: 'inline', value: '100+' }, + { id: 'hero.stat1_label', section: 'Hero', label: 'Stat 1 — label', kind: 'inline', value: 'Clienti Felici' }, + { id: 'hero.stat2_num', section: 'Hero', label: 'Stat 2 — value', kind: 'inline', value: '2018' }, + { id: 'hero.stat2_label', section: 'Hero', label: 'Stat 2 — label', kind: 'inline', value: 'Anno di inizio' }, + { id: 'hero.stat3_num', section: 'Hero', label: 'Stat 3 — value', kind: 'inline', value: '5.0★' }, + { id: 'hero.stat3_label', section: 'Hero', label: 'Stat 3 — label', kind: 'inline', value: 'Recensioni' }, + + { id: 'why.kicker', section: 'Why', label: 'Kicker', kind: 'inline', value: 'Perché scegliere me' }, + { id: 'why.title_1', section: 'Why', label: 'Title — line 1', kind: 'inline', value: 'Qualcosa di diverso,' }, + { id: 'why.title_2', section: 'Why', label: 'Title — line 2', kind: 'inline', value: 'lo senti anche tu.' }, + { id: 'why.sub', section: 'Why', label: 'Subtitle', kind: 'inline', value: 'Non ho una squadra — ho me stesso, presente ogni giorno.' }, + { id: 'why.card1_title', section: 'Why', label: 'Card 1 — title', kind: 'inline', value: 'Dal 2018, ogni giorno' }, + { id: 'why.card1_text', section: 'Why', label: 'Card 1 — text', kind: 'multiline', value: 'Ho iniziato per passione e non mi sono più fermato. Anni di esperienza vera, sul campo, costruita cane dopo cane.' }, + { id: 'why.card2_title', section: 'Why', label: 'Card 2 — title', kind: 'inline', value: 'Un rapporto vero' }, + { id: 'why.card2_text', section: 'Why', label: 'Card 2 — text', kind: 'multiline', value: 'Con te e con il tuo cane costruisco un legame di fiducia. Non sei un cliente, sei parte della mia famiglia pelosa.' }, + { id: 'why.card3_title', section: 'Why', label: 'Card 3 — title', kind: 'inline', value: 'Preparato, non improvvisato' }, + { id: 'why.card3_text', section: 'Why', label: 'Card 3 — text', kind: 'multiline', value: 'Certificazione CONI e formazione continua. Porto metodo e competenza, con la leggerezza di chi ama davvero quello che fa.' }, + { id: 'why.card4_title', section: 'Why', label: 'Card 4 — title', kind: 'inline', value: 'Zero gabbie, promesso' }, + { id: 'why.card4_text', section: 'Why', label: 'Card 4 — text', kind: 'multiline', value: 'Non le uso, non le tollero. Libertà e comfort sono la base di tutto quello che faccio — sempre.' }, + + { id: 'services.kicker', section: 'Services', label: 'Kicker', kind: 'inline', value: 'Cosa faccio per te' }, + { id: 'services.title', section: 'Services', label: 'Title', kind: 'inline', value: 'I miei servizi' }, + { id: 'services.sub', section: 'Services', label: 'Subtitle', kind: 'inline', value: 'Ho pensato a tutto io — tu pensa solo a coccolarlo.' }, + { id: 'services.card1_tag', section: 'Services', label: 'Card 1 — tag', kind: 'inline', value: 'Movimento & Gioia' }, + { id: 'services.card1_title', section: 'Services', label: 'Card 1 — title', kind: 'inline', value: 'Dogwalking' }, + { id: 'services.card1_text', section: 'Services', label: 'Card 1 — text', kind: 'multiline', value: "Passeggiate esplorative nei sentieri più belli di Sassari. Ogni uscita è un'avventura pensata per stimolare mente e corpo del tuo cane." }, + { id: 'services.card1_cta', section: 'Services', label: 'Card 1 — CTA', kind: 'inline', value: 'Prenota una passeggiata' }, + { id: 'services.card2_tag', section: 'Services', label: 'Card 2 — tag', kind: 'inline', value: 'Come a casa' }, + { id: 'services.card2_title', section: 'Services', label: 'Card 2 — title', kind: 'inline', value: 'Home Boarding' }, + { id: 'services.card2_text', section: 'Services', label: 'Card 2 — text', kind: 'multiline', value: 'Quando sei via, il tuo cane sta con me — in appartamento, libero, coccolato. Ogni giorno ti mando foto e aggiornamenti.' }, + { id: 'services.card2_cta', section: 'Services', label: 'Card 2 — CTA', kind: 'inline', value: 'Chiedi disponibilità' }, + { id: 'services.card3_tag', section: 'Services', label: 'Card 3 — tag', kind: 'inline', value: 'Giorni speciali' }, + { id: 'services.card3_title', section: 'Services', label: 'Card 3 — title', kind: 'inline', value: 'Wedding Dogsitter' }, + { id: 'services.card3_text', section: 'Services', label: 'Card 3 — text', kind: 'multiline', value: 'Il tuo migliore amico nel tuo giorno più bello. Me ne occupo io, così tu puoi goderti ogni momento senza pensieri.' }, + { id: 'services.card3_cta', section: 'Services', label: 'Card 3 — CTA', kind: 'inline', value: 'Parliamo del tuo evento' }, + + { id: 'about.quote', section: 'About', label: 'Quote', kind: 'multiline', value: '"Rusty non era solo un cane, era il maestro che mi ha indicato la strada."' }, + { id: 'about.cite', section: 'About', label: 'Cite', kind: 'inline', value: '— Giampiero Scaglione' }, + { id: 'about.kicker', section: 'About', label: 'Kicker', kind: 'inline', value: 'Chi sono' }, + { id: 'about.title_1', section: 'About', label: 'Title — line 1', kind: 'inline', value: 'Da una necessità' }, + { id: 'about.title_2', section: 'About', label: 'Title — line 2', kind: 'inline', value: 'del cuore, a una' }, + { id: 'about.title_3', section: 'About', label: 'Title — line 3', kind: 'inline', value: 'missione di vita.' }, + { id: 'about.body_1', section: 'About', label: 'Body — paragraph 1', kind: 'multiline', value: 'Tutto è partito da Rusty — il mitico Rustone. Quello che era iniziato come un lavoretto si è trasformato in una vocazione. Ho capito che non volevo fare altro nella vita.' }, + { id: 'about.body_2', section: 'About', label: 'Body — paragraph 2', kind: 'multiline', value: "Ho preso la certificazione CONI, ho camminato il Cammino di Santiago con il mio cane, e ogni giorno continuo a imparare — dai cani, dai loro umani, dalla vita all'aperto." }, + { id: 'about.mile1_title', section: 'About', label: 'Milestone 1 — title', kind: 'inline', value: 'Cammino di Santiago' }, + { id: 'about.mile1_text', section: 'About', label: 'Milestone 1 — text', kind: 'inline', value: 'Percorso a piedi con il mio cane. La prova più bella di tutto.' }, + { id: 'about.mile2_title', section: 'About', label: 'Milestone 2 — title', kind: 'inline', value: 'Certificazione CONI' }, + { id: 'about.mile2_text', section: 'About', label: 'Milestone 2 — text', kind: 'inline', value: "Formazione d'eccellenza in ambito cinofilo. Studio ancora." }, + + { id: 'media.label', section: 'Media', label: 'Label', kind: 'inline', value: 'Riconoscimenti & Media' }, + { id: 'media.logo_1', section: 'Media', label: 'Logo 1', kind: 'inline', value: "L'Unione Sarda" }, + { id: 'media.logo_2', section: 'Media', label: 'Logo 2', kind: 'inline', value: 'Sardegna Reporter' }, + { id: 'media.logo_3', section: 'Media', label: 'Logo 3', kind: 'inline', value: 'Radio Super Sound' }, + { id: 'media.logo_4', section: 'Media', label: 'Logo 4', kind: 'inline', value: 'Qualifica CONI' }, + { id: 'media.logo_5', section: 'Media', label: 'Logo 5', kind: 'inline', value: 'Corsa in Rosa' }, + { id: 'media.logo_6', section: 'Media', label: 'Logo 6', kind: 'inline', value: 'Giornata del Super Cane' }, + + { id: 'dogwash.tag', section: 'DogWash', label: 'Tag', kind: 'inline', value: 'Novità in Sardegna' }, + { id: 'dogwash.title', section: 'DogWash', label: 'Title', kind: 'inline', value: 'DogWash Sardegna' }, + { id: 'dogwash.desc', section: 'DogWash', label: 'Description', kind: 'multiline', value: 'Ho avuto un sogno: portare le colonnine di lavaggio automatico in tutta la Sardegna. Grazie alla fiducia della community, quel sogno sta diventando realtà, una colonnina alla volta.' }, + { id: 'dogwash.cta', section: 'DogWash', label: 'CTA', kind: 'inline', value: 'Trova la postazione più vicina' }, + { id: 'dogwash.community', section: 'DogWash', label: 'Community note', kind: 'inline', value: 'Un progetto nato dal cuore di Sassari.' }, + + { id: 'testi.stats1_num', section: 'Testimonials', label: 'Stat 1 — value', kind: 'inline', value: '100+' }, + { id: 'testi.stats1_label', section: 'Testimonials', label: 'Stat 1 — label', kind: 'inline', value: 'Clienti Felici' }, + { id: 'testi.stats2_num', section: 'Testimonials', label: 'Stat 2 — value', kind: 'inline', value: '2018' }, + { id: 'testi.stats2_label', section: 'Testimonials', label: 'Stat 2 — label', kind: 'inline', value: 'Anno di inizio' }, + { id: 'testi.kicker', section: 'Testimonials', label: 'Kicker', kind: 'inline', value: 'Cosa dicono di me' }, + { id: 'testi.title', section: 'Testimonials', label: 'Title', kind: 'inline', value: 'I miei ospiti parlano.' }, + { id: 'testi.sub', section: 'Testimonials', label: 'Subtitle', kind: 'inline', value: 'Non solo clienti — una vera famiglia pelosa.' }, + + { id: 'contact.headline', section: 'Contact', label: 'Headline', kind: 'inline', value: 'Affida il tuo cane a chi lo vive davvero.' }, + { id: 'contact.form_title', section: 'Contact', label: 'Form — title', kind: 'inline', value: 'Scrivimi ora' }, + { id: 'contact.form_sub', section: 'Contact', label: 'Form — subtitle', kind: 'inline', value: 'Hai domande o vuoi prenotare? Ti rispondo di persona entro 24 ore.' }, + { id: 'contact.label_name', section: 'Contact', label: 'Label — Name', kind: 'inline', value: 'Il tuo Nome' }, + { id: 'contact.placeholder_name', section: 'Contact', label: 'Placeholder — Name', kind: 'inline', value: 'Nome e Cognome' }, + { id: 'contact.label_dog', section: 'Contact', label: 'Label — Dog', kind: 'inline', value: 'Come si chiama il tuo cane?' }, + { id: 'contact.placeholder_dog', section: 'Contact', label: 'Placeholder — Dog', kind: 'inline', value: 'Il nome del peloso...' }, + { id: 'contact.label_msg', section: 'Contact', label: 'Label — Message', kind: 'inline', value: 'Il tuo messaggio' }, + { id: 'contact.placeholder_msg', section: 'Contact', label: 'Placeholder — Message', kind: 'inline', value: 'Raccontami di te e del tuo cane...' }, + { id: 'contact.submit', section: 'Contact', label: 'Submit button', kind: 'inline', value: 'Invia il messaggio' }, + { id: 'contact.success', section: 'Contact', label: 'Success message', kind: 'inline', value: 'Perfetto! Ti scrivo presto.' }, + { id: 'contact.info_title', section: 'Contact', label: 'Info title', kind: 'inline', value: 'Contatti rapidi' }, + { id: 'contact.phone_label', section: 'Contact', label: 'Phone label', kind: 'inline', value: 'Chiamami' }, + { id: 'contact.phone_value', section: 'Contact', label: 'Phone value', kind: 'inline', value: '+39 347 123 4567' }, + { id: 'contact.email_label', section: 'Contact', label: 'Email label', kind: 'inline', value: 'Email' }, + { id: 'contact.email_value', section: 'Contact', label: 'Email value', kind: 'inline', value: 'info@giampydogservice.it' }, + { id: 'contact.wa_label', section: 'Contact', label: 'WhatsApp label', kind: 'inline', value: 'WhatsApp' }, + { id: 'contact.wa_value', section: 'Contact', label: 'WhatsApp value', kind: 'inline', value: 'Chatta direttamente con me' }, + + { id: 'footer.tagline', section: 'Footer', label: 'Tagline', kind: 'multiline', value: 'Mi prendo cura dei tuoi amici come fossero i miei. Dal 2018, ogni giorno, con passione.' }, + { id: 'footer.col1_title', section: 'Footer', label: 'Column 1 — title', kind: 'inline', value: 'Servizi' }, + { id: 'footer.col1_l1', section: 'Footer', label: 'Column 1 — link 1', kind: 'inline', value: 'Dogwalking' }, + { id: 'footer.col1_l2', section: 'Footer', label: 'Column 1 — link 2', kind: 'inline', value: 'Home Boarding' }, + { id: 'footer.col1_l3', section: 'Footer', label: 'Column 1 — link 3', kind: 'inline', value: 'Wedding Dogsitter' }, + { id: 'footer.col1_l4', section: 'Footer', label: 'Column 1 — link 4', kind: 'inline', value: 'DogWash' }, + { id: 'footer.col2_title', section: 'Footer', label: 'Column 2 — title', kind: 'inline', value: 'Link Utili' }, + { id: 'footer.col2_l1', section: 'Footer', label: 'Column 2 — link 1', kind: 'inline', value: 'Chi Sono' }, + { id: 'footer.col2_l2', section: 'Footer', label: 'Column 2 — link 2', kind: 'inline', value: 'Testimonianze' }, + { id: 'footer.col2_l3', section: 'Footer', label: 'Column 2 — link 3', kind: 'inline', value: 'Privacy Policy' }, + { id: 'footer.col2_l4', section: 'Footer', label: 'Column 2 — link 4', kind: 'inline', value: 'Termini e Condizioni' }, + { id: 'footer.copyright', section: 'Footer', label: 'Copyright', kind: 'inline', value: '© 2024 Giampy Dog Service — Sassari, Sardegna' }, + { id: 'footer.powered', section: 'Footer', label: 'Powered by', kind: 'inline', value: 'Powered by' } +]; + +export const DEFAULT_IMAGES: Record = { + favicon: { id: 'favicon', label: 'Favicon (icona browser)', src: '/img/logo.png', alt: 'Giampy Dog Service' }, + logo: { id: 'logo', label: 'Brand logo', src: '/img/logo.png', alt: 'Giampy Dog Service Logo' }, + hero: { id: 'hero', label: 'Hero photo', src: '/img/hero-image.png', alt: 'Giampy con un cane' }, + chi_sono: { id: 'chi_sono', label: 'About photo', src: '/img/chi-sono.png', alt: 'Giampy passeggia con un cane' }, + dogwash: { id: 'dogwash', label: 'DogWash visual', src: '/img/colonnina.png', alt: 'DogWash Machine' }, + cima_logo: { id: 'cima_logo', label: 'Cima Progetti logo', src: '/img/cima-logo.svg', alt: 'Cima Progetti' } +}; + +export const DEFAULT_TESTIMONIALS: Testimonial[] = [ + { + id: 't_marta', + text: "Giampiero è l'unico di cui mi fido ciecamente. Il mio Max torna sempre felice e rilassato. Non cambierei mai!", + name: 'Marta S.', + role: 'Mamma di Max', + avatarSrc: '/img/avatar-1.png', + avatarAlt: 'Marta S.' + }, + { + id: 't_luca', + text: 'Ha gestito il nostro Luna durante il matrimonio in modo impeccabile. Eravamo tranquilli, sapevamo che era in mani fantastiche.', + name: 'Luca & Elena', + role: 'Genitori di Luna', + avatarSrc: '/img/avatar-2.png', + avatarAlt: 'Luca & Elena' + }, + { + id: 't_sara', + text: 'Niente gabbie, tanto amore. Ogni giorno mi mandava foto di Felix felice. La pensione in appartamento che sognavo!', + name: 'Sara P.', + role: 'Compagna di Felix', + avatarSrc: '/img/avatar-3.png', + avatarAlt: 'Sara P.' + } +]; + +export const DEFAULT_SEO = { + title: 'Giampy Dog Service — Il tuo cane in buone mani', + description: + 'Educatore cinofilo certificato CONI a Sassari. Dogwalking, home boarding e wedding dogsitter. Zero gabbie, solo amore vero dal 2018.', + keywords: + 'dogsitter sassari, dogwalker sardegna, educatore cinofilo, home boarding cani, wedding dogsitter, dog wash sardegna', + ogImage: '/img/hero-image.png', + primaryKeywords: ['dogsitter sassari', 'dogwalker sardegna', 'educatore cinofilo coni'], + geoRegion: 'IT-SS', + geoPlacename: 'Sassari, Sardegna' +}; + +export function buildDefaultContent(): SiteContent { + const copy: Record = {}; + for (const s of DEFAULT_SLOTS) copy[s.id] = s.value; + return { + copy, + images: { ...DEFAULT_IMAGES }, + testimonials: DEFAULT_TESTIMONIALS.map((t) => ({ ...t })), + seo: { ...DEFAULT_SEO } + }; +} diff --git a/src/lib/seo.ts b/src/lib/seo.ts new file mode 100644 index 0000000..0c561f6 --- /dev/null +++ b/src/lib/seo.ts @@ -0,0 +1,109 @@ +export type CopyAnalysis = { + length: number; + words: number; + sentences: number; + avgWordsPerSentence: number; + readability: number; // 0..100, higher = easier + keywordMatches: string[]; + missingKeywords: string[]; + seoScore: number; // 0..100 + geoScore: number; // 0..100 + seoTips: string[]; + geoTips: string[]; +}; + +const SYLLABLE_VOWELS = /[aeiouàèéìòùAEIOUÀÈÉÌÒÙ]+/g; + +function countSyllables(word: string): number { + const matches = word.match(SYLLABLE_VOWELS); + return matches ? matches.length : 1; +} + +export function analyzeCopy(text: string, primaryKeywords: string[] = []): CopyAnalysis { + const clean = text.trim(); + const length = clean.length; + const wordList = clean.split(/\s+/).filter(Boolean); + const words = wordList.length; + const sentences = Math.max(1, (clean.match(/[.!?]+/g) ?? []).length); + const avgWordsPerSentence = words / sentences; + + // Flesch reading ease approximation + const syllables = wordList.reduce((n, w) => n + countSyllables(w), 0); + const asl = words ? words / sentences : 0; + const asw = words ? syllables / words : 0; + const readability = Math.max(0, Math.min(100, Math.round(206.835 - 1.015 * asl - 84.6 * asw))); + + const lower = clean.toLowerCase(); + const keywordMatches: string[] = []; + const missingKeywords: string[] = []; + for (const kw of primaryKeywords) { + if (!kw) continue; + if (lower.includes(kw.toLowerCase())) keywordMatches.push(kw); + else missingKeywords.push(kw); + } + + // SEO tips + const seoTips: string[] = []; + if (length === 0) seoTips.push('Il testo è vuoto.'); + if (length > 0 && length < 40) seoTips.push('Testo molto breve: difficile farsi indicizzare.'); + if (length > 320) seoTips.push('Testo lungo: valuta di spezzarlo in paragrafi o bullet.'); + if (avgWordsPerSentence > 24) seoTips.push('Frasi troppo lunghe: scendi sotto le 24 parole per frase.'); + if (readability < 45) seoTips.push('Leggibilità bassa: semplifica lessico e sintassi.'); + if (missingKeywords.length && words > 4) { + seoTips.push(`Manca una keyword primaria: ${missingKeywords.slice(0, 2).join(', ')}.`); + } + if (/[A-Z]{6,}/.test(clean)) seoTips.push('Evita testo tutto maiuscolo: penalizza la leggibilità.'); + + // GEO tips (Generative Engine Optimization) + const geoTips: string[] = []; + const questionMarks = (clean.match(/\?/g) ?? []).length; + const hasNumbers = /\d/.test(clean); + const hasEntities = /\b(sassari|sardegna|coni|cammino di santiago|giampy)\b/i.test(clean); + const firstPerson = /\b(io|mio|mia|miei|mie|sono|faccio|porto|prendo)\b/i.test(clean); + const declarative = sentences >= 1 && /[.!]/.test(clean); + + if (words >= 20 && questionMarks === 0) + geoTips.push('Aggiungi una domanda esplicita per farti citare come risposta diretta.'); + if (!hasNumbers && words >= 20) + geoTips.push('Inserisci dati concreti (anni, numero clienti, distanze) per aumentare la citabilità.'); + if (!hasEntities && words >= 15) + geoTips.push('Nomina entità specifiche (luogo, certificazione, brand) per il grounding generativo.'); + if (!firstPerson && words >= 20) + geoTips.push('Usa la prima persona: i motori generativi estraggono più facilmente testimonianze autoriali.'); + if (!declarative && words >= 10) + geoTips.push('Termina con punto fermo: le frasi complete vengono citate più spesso.'); + + // Scores + let seoScore = 100; + seoScore -= seoTips.length * 12; + if (keywordMatches.length === 0 && primaryKeywords.length && words > 4) seoScore -= 10; + if (readability >= 60) seoScore += 5; + seoScore = Math.max(0, Math.min(100, seoScore)); + + let geoScore = 100; + geoScore -= geoTips.length * 14; + if (hasEntities) geoScore += 5; + if (hasNumbers) geoScore += 3; + geoScore = Math.max(0, Math.min(100, geoScore)); + + return { + length, + words, + sentences, + avgWordsPerSentence: Math.round(avgWordsPerSentence * 10) / 10, + readability, + keywordMatches, + missingKeywords, + seoScore, + geoScore, + seoTips, + geoTips + }; +} + +export function aggregateScore(analyses: CopyAnalysis[]): { seo: number; geo: number } { + if (!analyses.length) return { seo: 0, geo: 0 }; + const seo = Math.round(analyses.reduce((a, x) => a + x.seoScore, 0) / analyses.length); + const geo = Math.round(analyses.reduce((a, x) => a + x.geoScore, 0) / analyses.length); + return { seo, geo }; +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..eae5e90 --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,99 @@ +import { readJson, writeJson } from './storage'; +import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto'; +import type { Cookies } from '@sveltejs/kit'; + +const ADMIN_FILE = 'admin.json'; +const SESSION_FILE = 'sessions.json'; +const SESSION_COOKIE = 'gds_admin'; +const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 7; // 7 days + +type AdminRecord = { + username: string; + salt: string; + hash: string; +}; + +type SessionRecord = { + token: string; + username: string; + expiresAt: number; +}; + +function hashPassword(password: string, salt: string): string { + return scryptSync(password, salt, 64).toString('hex'); +} + +export function seedDefaultAdminIfMissing(): { username: string; tempPassword?: string } { + const existing = readJson(ADMIN_FILE, null); + if (existing) return { username: existing.username }; + const username = process.env.ADMIN_USERNAME || 'admin'; + const password = process.env.ADMIN_PASSWORD || 'changeme'; + const salt = randomBytes(16).toString('hex'); + const record: AdminRecord = { username, salt, hash: hashPassword(password, salt) }; + writeJson(ADMIN_FILE, record); + return { username, tempPassword: password }; +} + +export function verifyCredentials(username: string, password: string): boolean { + const record = readJson(ADMIN_FILE, null); + if (!record) return false; + if (record.username !== username) return false; + const candidate = Buffer.from(hashPassword(password, record.salt), 'hex'); + const stored = Buffer.from(record.hash, 'hex'); + if (candidate.length !== stored.length) return false; + return timingSafeEqual(candidate, stored); +} + +export function changePassword(currentPassword: string, newPassword: string): boolean { + const record = readJson(ADMIN_FILE, null); + if (!record) return false; + if (!verifyCredentials(record.username, currentPassword)) return false; + const salt = randomBytes(16).toString('hex'); + writeJson(ADMIN_FILE, { + username: record.username, + salt, + hash: hashPassword(newPassword, salt) + }); + return true; +} + +function loadSessions(): SessionRecord[] { + const now = Date.now(); + const all = readJson(SESSION_FILE, []); + const live = all.filter((s) => s.expiresAt > now); + if (live.length !== all.length) writeJson(SESSION_FILE, live); + return live; +} + +export function createSession(username: string, cookies: Cookies): string { + const sessions = loadSessions(); + const token = randomBytes(32).toString('hex'); + const record: SessionRecord = { token, username, expiresAt: Date.now() + SESSION_TTL_MS }; + sessions.push(record); + writeJson(SESSION_FILE, sessions); + cookies.set(SESSION_COOKIE, token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: SESSION_TTL_MS / 1000 + }); + return token; +} + +export function destroySession(cookies: Cookies): void { + const token = cookies.get(SESSION_COOKIE); + if (token) { + const sessions = loadSessions().filter((s) => s.token !== token); + writeJson(SESSION_FILE, sessions); + } + cookies.delete(SESSION_COOKIE, { path: '/' }); +} + +export function getSessionUser(cookies: Cookies): { username: string } | null { + const token = cookies.get(SESSION_COOKIE); + if (!token) return null; + const session = loadSessions().find((s) => s.token === token); + if (!session) return null; + return { username: session.username }; +} diff --git a/src/lib/server/content.ts b/src/lib/server/content.ts new file mode 100644 index 0000000..911f91d --- /dev/null +++ b/src/lib/server/content.ts @@ -0,0 +1,95 @@ +import { readJson, writeJson } from './storage'; +import { randomUUID } from 'node:crypto'; +import { + buildDefaultContent, + DEFAULT_IMAGES, + DEFAULT_SLOTS, + type SiteContent, + type ImageSlot, + type Testimonial +} from '$lib/content/defaults'; + +const FILE = 'content.json'; + +export function getContent(): SiteContent { + const seed = buildDefaultContent(); + const stored = readJson>(FILE, seed); + return { + copy: { ...seed.copy, ...(stored.copy ?? {}) }, + images: { ...seed.images, ...(stored.images ?? {}) }, + testimonials: stored.testimonials ?? seed.testimonials, + seo: { ...seed.seo, ...(stored.seo ?? {}) } + }; +} + +export function setCopy(id: string, value: string): void { + const c = getContent(); + c.copy[id] = value; + writeJson(FILE, c); +} + +export function setCopyBulk(updates: Record): void { + const c = getContent(); + for (const [k, v] of Object.entries(updates)) c.copy[k] = v; + writeJson(FILE, c); +} + +export function setImage(id: string, patch: Partial): ImageSlot { + const c = getContent(); + const current = c.images[id] ?? DEFAULT_IMAGES[id]; + if (!current) throw new Error(`Unknown image slot: ${id}`); + const next: ImageSlot = { + id, + label: current.label, + src: patch.src ?? current.src, + alt: patch.alt ?? current.alt + }; + c.images[id] = next; + writeJson(FILE, c); + return next; +} + +export function resetImage(id: string): ImageSlot { + const c = getContent(); + const fallback = DEFAULT_IMAGES[id]; + if (!fallback) throw new Error(`Unknown image slot: ${id}`); + c.images[id] = { ...fallback }; + writeJson(FILE, c); + return c.images[id]; +} + +export function setSeo(patch: Partial): void { + const c = getContent(); + c.seo = { ...c.seo, ...patch }; + writeJson(FILE, c); +} + +export function listSlots() { + return DEFAULT_SLOTS; +} + +export function addTestimonial(input: Omit): Testimonial { + const c = getContent(); + const entry: Testimonial = { id: `t_${randomUUID().slice(0, 8)}`, ...input }; + c.testimonials = [...c.testimonials, entry]; + writeJson(FILE, c); + return entry; +} + +export function updateTestimonial(id: string, patch: Partial>): Testimonial | null { + const c = getContent(); + let updated: Testimonial | null = null; + c.testimonials = c.testimonials.map((t) => { + if (t.id !== id) return t; + updated = { ...t, ...patch }; + return updated; + }); + if (updated) writeJson(FILE, c); + return updated; +} + +export function deleteTestimonial(id: string): void { + const c = getContent(); + c.testimonials = c.testimonials.filter((t) => t.id !== id); + writeJson(FILE, c); +} diff --git a/src/lib/server/health.ts b/src/lib/server/health.ts new file mode 100644 index 0000000..d692a22 --- /dev/null +++ b/src/lib/server/health.ts @@ -0,0 +1,12 @@ +import { DEFAULT_SLOTS } from '$lib/content/defaults'; +import { analyzeCopy, aggregateScore } from '$lib/seo'; +import { getContent } from './content'; + +export function computeHealth(): { seo: number; geo: number } { + const content = getContent(); + const analyses = DEFAULT_SLOTS + .filter((s) => s.kind === 'multiline' || s.id.endsWith('_text')) + .slice(0, 30) + .map((s) => analyzeCopy(content.copy[s.id] ?? s.value, content.seo.primaryKeywords)); + return aggregateScore(analyses); +} diff --git a/src/lib/server/notifications.ts b/src/lib/server/notifications.ts new file mode 100644 index 0000000..eda3fea --- /dev/null +++ b/src/lib/server/notifications.ts @@ -0,0 +1,56 @@ +import { readJson, writeJson } from './storage'; +import { randomUUID } from 'node:crypto'; + +export type NotificationType = 'submission' | 'seo_drop' | 'geo_drop'; + +export type Notification = { + id: string; + type: NotificationType; + title: string; + message: string; + link?: string; + createdAt: string; + read: boolean; +}; + +const FILE = 'notifications.json'; +const MAX_KEEP = 200; + +export function listNotifications(): Notification[] { + const all = readJson(FILE, []); + return [...all].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export function unreadCount(): number { + return readJson(FILE, []).filter((n) => !n.read).length; +} + +export function addNotification(input: Omit): Notification { + const all = readJson(FILE, []); + const entry: Notification = { + id: randomUUID(), + createdAt: new Date().toISOString(), + read: false, + ...input + }; + all.push(entry); + // Keep only the most recent MAX_KEEP + const sorted = all.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, MAX_KEEP); + writeJson(FILE, sorted); + return entry; +} + +export function markRead(id: string): void { + const all = readJson(FILE, []); + writeJson(FILE, all.map((n) => (n.id === id ? { ...n, read: true } : n))); +} + +export function markAllRead(): void { + const all = readJson(FILE, []); + writeJson(FILE, all.map((n) => ({ ...n, read: true }))); +} + +export function deleteNotification(id: string): void { + const all = readJson(FILE, []); + writeJson(FILE, all.filter((n) => n.id !== id)); +} diff --git a/src/lib/server/storage.ts b/src/lib/server/storage.ts new file mode 100644 index 0000000..fe86c4a --- /dev/null +++ b/src/lib/server/storage.ts @@ -0,0 +1,29 @@ +import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +const DATA_DIR = join(process.cwd(), 'data'); + +if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }); + +export function dataPath(name: string): string { + return join(DATA_DIR, name); +} + +export function readJson(name: string, fallback: T): T { + const p = dataPath(name); + if (!existsSync(p)) return fallback; + try { + return JSON.parse(readFileSync(p, 'utf8')) as T; + } catch { + return fallback; + } +} + +export function writeJson(name: string, value: T): void { + const p = dataPath(name); + const dir = dirname(p); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + const tmp = `${p}.tmp`; + writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf8'); + renameSync(tmp, p); +} diff --git a/src/lib/server/submissions.ts b/src/lib/server/submissions.ts new file mode 100644 index 0000000..d15b206 --- /dev/null +++ b/src/lib/server/submissions.ts @@ -0,0 +1,35 @@ +import { readJson, writeJson } from './storage'; +import { randomUUID } from 'node:crypto'; + +export type Submission = { + id: string; + name: string; + dog: string; + message: string; + createdAt: string; + ip?: string; +}; + +const FILE = 'submissions.json'; + +export function listSubmissions(): Submission[] { + const items = readJson(FILE, []); + return [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export function addSubmission(input: Omit): Submission { + const items = readJson(FILE, []); + const entry: Submission = { + id: randomUUID(), + createdAt: new Date().toISOString(), + ...input + }; + items.push(entry); + writeJson(FILE, items); + return entry; +} + +export function deleteSubmission(id: string): void { + const items = readJson(FILE, []); + writeJson(FILE, items.filter((s) => s.id !== id)); +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..4d04929 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,7 @@ +import type { LayoutServerLoad } from './$types'; +import { getContent } from '$lib/server/content'; + +export const load: LayoutServerLoad = () => { + const images = getContent().images; + return { favicon: images.favicon?.src ?? '/img/logo.png' }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..7e4a488 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,11 @@ + + + + + + +{@render children()} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..88969dd --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,31 @@ +import type { Actions, PageServerLoad } from './$types'; +import { fail } from '@sveltejs/kit'; +import { getContent } from '$lib/server/content'; +import { addSubmission } from '$lib/server/submissions'; +import { addNotification } from '$lib/server/notifications'; + +export const load: PageServerLoad = async () => { + return { content: getContent() }; +}; + +export const actions: Actions = { + contact: async ({ request, getClientAddress }) => { + const data = await request.formData(); + const name = String(data.get('name') ?? '').trim(); + const dog = String(data.get('dog') ?? '').trim(); + const message = String(data.get('message') ?? '').trim(); + + if (!name || !message) { + return fail(400, { error: 'Compila almeno nome e messaggio.', name, dog, message }); + } + + const entry = addSubmission({ name, dog, message, ip: getClientAddress() }); + addNotification({ + type: 'submission', + title: 'Nuova richiesta dal form', + message: `${name}${dog ? ` (cane: ${dog})` : ''} ha inviato un messaggio.`, + link: `/admin/submissions#${entry.id}` + }); + return { success: true }; + } +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..d2ee8e0 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,398 @@ + + + + {seo.title} + + + + + + + + + + + + + + +
+ + +
+
+
+

{c['hero.kicker']}

+

{c['hero.title_prefix']}
{c['hero.title_suffix']} {c['hero.title_em']}

+

{c['hero.desc']}

+ +
+
{c['hero.stat1_num']}{c['hero.stat1_label']}
+
{c['hero.stat2_num']}{c['hero.stat2_label']}
+
{c['hero.stat3_num']}{c['hero.stat3_label']}
+
+
+
+
+ {img.hero.alt} +
+
+
+ + {c['hero.badge_label']} +
+

{c['hero.badge_text']}

+
+
+
+
+
+ + +
+
+

{c['why.kicker']}

+

{c['why.title_1']}
{c['why.title_2']}

+

{c['why.sub']}

+
+ {#each [ + { icon: 'history', t: c['why.card1_title'], p: c['why.card1_text'] }, + { icon: 'diversity_1', t: c['why.card2_title'], p: c['why.card2_text'] }, + { icon: 'verified_user', t: c['why.card3_title'], p: c['why.card3_text'] }, + { icon: 'house_siding', t: c['why.card4_title'], p: c['why.card4_text'] } + ] as card} +
+
+
+

{card.t}

+

{card.p}

+
+
+ {/each} +
+
+
+ + +
+
+

{c['services.kicker']}

+

{c['services.title']}

+

{c['services.sub']}

+
+ {#each [ + { icon: 'pets', tag: c['services.card1_tag'], t: c['services.card1_title'], p: c['services.card1_text'], cta: c['services.card1_cta'] }, + { icon: 'house', tag: c['services.card2_tag'], t: c['services.card2_title'], p: c['services.card2_text'], cta: c['services.card2_cta'] }, + { icon: 'celebration', tag: c['services.card3_tag'], t: c['services.card3_title'], p: c['services.card3_text'], cta: c['services.card3_cta'] } + ] as s} +
+ {s.tag} + +

{s.t}

+

{s.p}

+ + {s.cta} + + +
+ {/each} +
+
+
+ + +
+
+
+
+ {img.chi_sono.alt} +
+

{c['about.quote']}

+ {c['about.cite']} +
+
+
+

{c['about.kicker']}

+

{c['about.title_1']}
{c['about.title_2']}
{c['about.title_3']}

+

{c['about.body_1']}

+

{c['about.body_2']}

+
+
+ +
+ {c['about.mile1_title']} + {c['about.mile1_text']} +
+
+
+ +
+ {c['about.mile2_title']} + {c['about.mile2_text']} +
+
+
+
+
+
+
+ + +
+
+

{c['media.label']}

+
+ {c['media.logo_1']} + {c['media.logo_2']} + {c['media.logo_3']} + {c['media.logo_4']} + {c['media.logo_5']} + {c['media.logo_6']} +
+
+
+ + +
+
+
+
+ {c['dogwash.tag']} +

{c['dogwash.title']}

+

{c['dogwash.desc']}

+ + {c['dogwash.cta']} + + +

+ {c['dogwash.community']} +

+
+
+ {img.dogwash.alt} +
+
+
+
+ + +
+
+
+
+

{c['testi.stats1_num']}

{c['testi.stats1_label']}

+

{c['testi.stats2_num']}

{c['testi.stats2_label']}

+
+

{c['testi.kicker']}

+

{c['testi.title']}

+

{c['testi.sub']}

+
+
+ {#each testimonials as r (r.id)} +
+
{r.avatarAlt}
+
{r.text}
+ +
+ {/each} +
+
+
+ + +
+
+
+

{c['contact.headline']}

+ +
+
+
+

{c['contact.form_title']}

+

{c['contact.form_sub']}

+ {#if !submitted} +
+
+ + +
+
+ + +
+
+ + +
+ {#if form?.error} +

{form.error}

+ {/if} + +
+ {:else} +
+ +

{c['contact.success']}

+
+ {/if} +
+
+

{c['contact.info_title']}

+
+
+
+

{c['contact.phone_label']}

+

{c['contact.phone_value']}

+
+
+
+
+
+

{c['contact.email_label']}

+

{c['contact.email_value']}

+
+
+
+
+
+

{c['contact.wa_label']}

+

{c['contact.wa_value']}

+
+
+
+
+
+
+
+ + + diff --git a/src/routes/admin/+layout.server.ts b/src/routes/admin/+layout.server.ts new file mode 100644 index 0000000..d2921e3 --- /dev/null +++ b/src/routes/admin/+layout.server.ts @@ -0,0 +1,14 @@ +import type { LayoutServerLoad } from './$types'; +import { listSubmissions } from '$lib/server/submissions'; +import { unreadCount } from '$lib/server/notifications'; + +export const load: LayoutServerLoad = ({ locals, url }) => { + const submissionsCount = locals.admin ? listSubmissions().length : 0; + const unreadNotifications = locals.admin ? unreadCount() : 0; + return { + admin: locals.admin, + pathname: url.pathname, + submissionsCount, + unreadNotifications + }; +}; diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte new file mode 100644 index 0000000..d0a1982 --- /dev/null +++ b/src/routes/admin/+layout.svelte @@ -0,0 +1,76 @@ + + +
+ {#if isLogin || !admin} + {@render children()} + {:else} + + {/if} +
diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts new file mode 100644 index 0000000..9f9be94 --- /dev/null +++ b/src/routes/admin/+page.server.ts @@ -0,0 +1,26 @@ +import type { PageServerLoad } from './$types'; +import { getContent } from '$lib/server/content'; +import { listSubmissions } from '$lib/server/submissions'; +import { DEFAULT_SLOTS } from '$lib/content/defaults'; +import { aggregateScore, analyzeCopy } from '$lib/seo'; + +export const load: PageServerLoad = () => { + const content = getContent(); + const subs = listSubmissions(); + + const analyses = DEFAULT_SLOTS + .filter((s) => s.kind === 'multiline' || (s.id.endsWith('_text') && s.kind !== 'inline')) + .slice(0, 30) + .map((s) => analyzeCopy(content.copy[s.id] ?? s.value, content.seo.primaryKeywords)); + + const scores = aggregateScore(analyses); + + return { + totalSlots: DEFAULT_SLOTS.length, + totalImages: Object.keys(content.images).length, + totalSubmissions: subs.length, + recentSubmissions: subs.slice(0, 5), + scores, + seo: content.seo + }; +}; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..f05d7a2 --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,79 @@ + + +Dashboard — Admin + +
+

Dashboard

+

Panoramica

+

Contenuti, richieste e salute SEO/GEO della landing page in un colpo d'occhio.

+
+ +
+
+
Copy slot
+
{data.totalSlots}
+
+
+
Immagini
+
{data.totalImages}
+
+
+
Richieste totali
+
{data.totalSubmissions}
+
+
+
Regione GEO
+
{data.seo.geoRegion}
+
+
+ +
+
+

Salute SEO

+

Media dei punteggi SEO sui testi principali: lunghezza, leggibilità, keyword primarie.

+
+ SEO {data.scores.seo}/100 + Modifica testi +
+
+
+

Salute GEO

+

Media dei punteggi GEO (Generative Engine Optimization): citabilità, entità, dati concreti.

+
+ GEO {data.scores.geo}/100 + Analizza e migliora +
+
+
+ +
+

Ultime richieste

+ {#if data.recentSubmissions.length === 0} +

Nessuna richiesta ricevuta al momento.

+ {:else} +
+ {#each data.recentSubmissions as s} +
+
+ {s.name} + {s.dog || '—'} +

{s.message.slice(0, 140)}{s.message.length > 140 ? '…' : ''}

+
+ +
+ {/each} +
+ + {/if} +
diff --git a/src/routes/admin/copy/+page.server.ts b/src/routes/admin/copy/+page.server.ts new file mode 100644 index 0000000..d438d7a --- /dev/null +++ b/src/routes/admin/copy/+page.server.ts @@ -0,0 +1,89 @@ +import type { Actions, PageServerLoad } from './$types'; +import { fail } from '@sveltejs/kit'; +import { getContent, setCopyBulk, setSeo } from '$lib/server/content'; +import { DEFAULT_SLOTS } from '$lib/content/defaults'; +import { computeHealth } from '$lib/server/health'; +import { addNotification } from '$lib/server/notifications'; + +const DROP_THRESHOLD = 8; + +export const load: PageServerLoad = () => { + const content = getContent(); + return { + slots: DEFAULT_SLOTS, + copy: content.copy, + seo: content.seo + }; +}; + +export const actions: Actions = { + saveCopy: async ({ request }) => { + const form = await request.formData(); + const updates: Record = {}; + const knownIds = new Set(DEFAULT_SLOTS.map((s) => s.id)); + for (const [key, val] of form.entries()) { + if (!key.startsWith('slot:')) continue; + const id = key.slice(5); + if (!knownIds.has(id)) continue; + updates[id] = String(val); + } + if (!Object.keys(updates).length) return fail(400, { error: 'Nessuna modifica da salvare.' }); + + const before = computeHealth(); + setCopyBulk(updates); + const after = computeHealth(); + + if (before.seo - after.seo >= DROP_THRESHOLD) { + addNotification({ + type: 'seo_drop', + title: 'Punteggio SEO in calo', + message: `SEO passato da ${before.seo}/100 a ${after.seo}/100 dopo l'ultima modifica.`, + link: '/admin/copy' + }); + } + if (before.geo - after.geo >= DROP_THRESHOLD) { + addNotification({ + type: 'geo_drop', + title: 'Punteggio GEO in calo', + message: `GEO passato da ${before.geo}/100 a ${after.geo}/100 dopo l'ultima modifica.`, + link: '/admin/copy' + }); + } + return { success: true, count: Object.keys(updates).length }; + }, + saveSeo: async ({ request }) => { + const form = await request.formData(); + const title = String(form.get('title') ?? '').trim(); + const description = String(form.get('description') ?? '').trim(); + const keywords = String(form.get('keywords') ?? '').trim(); + const ogImage = String(form.get('ogImage') ?? '').trim(); + const primary = String(form.get('primaryKeywords') ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + const geoRegion = String(form.get('geoRegion') ?? '').trim(); + const geoPlacename = String(form.get('geoPlacename') ?? '').trim(); + + const before = computeHealth(); + setSeo({ + title, + description, + keywords, + ogImage, + primaryKeywords: primary, + geoRegion, + geoPlacename + }); + const after = computeHealth(); + + if (before.seo - after.seo >= DROP_THRESHOLD) { + addNotification({ + type: 'seo_drop', + title: 'Punteggio SEO in calo', + message: `Cambiate le keyword primarie: SEO ${before.seo} → ${after.seo}.`, + link: '/admin/copy' + }); + } + return { seoSaved: true }; + } +}; diff --git a/src/routes/admin/copy/+page.svelte b/src/routes/admin/copy/+page.svelte new file mode 100644 index 0000000..fffb1cf --- /dev/null +++ b/src/routes/admin/copy/+page.svelte @@ -0,0 +1,294 @@ + + +Testi & SEO/GEO — Admin + +
+

Contenuti

+

Testi, SEO & GEO

+

+ Modifica ogni copy della landing page. Per ciascun blocco ottieni un punteggio SEO (ottimizzazione motori di ricerca) e GEO (ottimizzazione per motori generativi) in tempo reale con suggerimenti mirati. +

+
+ +
+ + {#if showInfo} +
+
+

SEO — essere trovati su Google

+

+ SEO vuol dire "farsi trovare dai motori di ricerca tipo Google". Quando qualcuno cerca + "dogsitter Sassari", i testi del tuo sito aiutano Google a capire se mostrarti. + Un testo SEO buono è chiaro, contiene le parole giuste, non è né troppo corto né troppo lungo. +

+
+
+

GEO — essere citati dalle AI

+

+ GEO (Generative Engine Optimization) è la versione moderna: serve a farsi citare da + ChatGPT, Gemini o Perplexity quando qualcuno chiede loro consiglio. + Funziona quando scrivi in prima persona, citi luoghi e numeri concreti, e rispondi a + domande precise. +

+
+
+

Come leggere i voti

+
    +
  • 75–100 verde — il testo funziona bene.
  • +
  • 50–74 giallo — va migliorato ma non è grave.
  • +
  • 0–49 rosso — qualcosa non va, leggi i consigli sotto al testo.
  • +
+
+
+ {/if} +
+ +
+
+
+ Anteprima live della landing + Si aggiorna da sola ad ogni salvataggio. Per un aggiornamento manuale, usa il pulsante. +
+ +
+ {#key iframeKey} + + {/key} +
+ +{#if form?.success} +
Salvato — {form.count} modifiche applicate. Anteprima aggiornata.
+{/if} +{#if form?.seoSaved} +
Meta SEO & GEO aggiornati.
+{/if} +{#if form?.error} +
{form.error}
+{/if} + +
+

Meta pagina · SEO & GEO

+

Impostazioni di indicizzazione globali: titolo, description, keyword primarie, segnali geografici.

+
+
+
+ + + Consigliato 50–60 caratteri. +
+
+ + +
+
+
+ + + Consigliato 120–160 caratteri. +
+
+ + +
+
+ + + Separate da virgola. Usate per controllare la presenza in ogni copy. +
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ {#each sections as [sectionName, slots]} +
+

{sectionName}

+ {slots.length} +
+
+ {#each slots as slot} + {@const a = analysisOf(slot.id)} +
+
+
+
{slot.label}
+
{slot.id}
+
+
+ SEO {a.seoScore} + GEO {a.geoScore} +
+
+ {#if slot.kind === 'multiline'} + + {:else} + + {/if} + + {a.length} car. + {a.words} parole + Leggibilità {a.readability} + {#if a.keywordMatches.length}Keyword match: {a.keywordMatches.join(', ')}{/if} + + {#if a.seoTips.length || a.geoTips.length} +
    + {#each a.seoTips as tip}
  • SEO: {tip}
  • {/each} + {#each a.geoTips as tip}
  • GEO: {tip}
  • {/each} +
+ {/if} +
+ {/each} +
+ {/each} +
+ +
+
+ + diff --git a/src/routes/admin/images/+page.server.ts b/src/routes/admin/images/+page.server.ts new file mode 100644 index 0000000..a8414ba --- /dev/null +++ b/src/routes/admin/images/+page.server.ts @@ -0,0 +1,76 @@ +import type { Actions, PageServerLoad } from './$types'; +import { fail } from '@sveltejs/kit'; +import { writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { getContent, resetImage, setImage } from '$lib/server/content'; +import { DEFAULT_IMAGES } from '$lib/content/defaults'; + +const UPLOAD_DIR = join(process.cwd(), 'static', 'img', 'uploads'); +const MAX_BYTES = 8 * 1024 * 1024; // 8 MB +const ALLOWED = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/svg+xml']); + +export const load: PageServerLoad = () => { + const content = getContent(); + return { images: content.images }; +}; + +function extFromType(type: string, fallbackName: string): string { + if (type === 'image/png') return 'png'; + if (type === 'image/jpeg') return 'jpg'; + if (type === 'image/webp') return 'webp'; + if (type === 'image/gif') return 'gif'; + if (type === 'image/svg+xml') return 'svg'; + const m = /\.([a-zA-Z0-9]+)$/.exec(fallbackName); + return m ? m[1].toLowerCase() : 'bin'; +} + +export const actions: Actions = { + upload: async ({ request }) => { + if (!existsSync(UPLOAD_DIR)) mkdirSync(UPLOAD_DIR, { recursive: true }); + const form = await request.formData(); + const slot = String(form.get('slot') ?? ''); + const alt = String(form.get('alt') ?? '').trim(); + const file = form.get('file'); + + if (!slot || !(slot in DEFAULT_IMAGES)) return fail(400, { error: 'Slot immagine non valido.' }); + if (!(file instanceof File) || !file.size) { + // alt-only update + if (alt) { + setImage(slot, { alt }); + return { success: true, slot, altOnly: true }; + } + return fail(400, { error: 'Nessun file caricato.' }); + } + if (file.size > MAX_BYTES) return fail(400, { error: 'File troppo grande (max 8 MB).' }); + if (file.type && !ALLOWED.has(file.type)) return fail(400, { error: `Tipo file non supportato: ${file.type}.` }); + + const buf = Buffer.from(await file.arrayBuffer()); + const ext = extFromType(file.type, file.name); + const filename = `${slot}-${randomUUID().slice(0, 8)}.${ext}`; + const fullPath = join(UPLOAD_DIR, filename); + writeFileSync(fullPath, buf); + + const publicPath = `/img/uploads/${filename}`; + const next = setImage(slot, { src: publicPath, alt: alt || undefined }); + return { success: true, slot, src: next.src }; + }, + + reset: async ({ request }) => { + const form = await request.formData(); + const slot = String(form.get('slot') ?? ''); + if (!slot || !(slot in DEFAULT_IMAGES)) return fail(400, { error: 'Slot non valido.' }); + + const current = getContent().images[slot]; + if (current?.src?.startsWith('/img/uploads/')) { + const fullPath = join(process.cwd(), 'static', current.src); + try { + if (existsSync(fullPath)) unlinkSync(fullPath); + } catch { + // ignore + } + } + resetImage(slot); + return { success: true, slot, reset: true }; + } +}; diff --git a/src/routes/admin/images/+page.svelte b/src/routes/admin/images/+page.svelte new file mode 100644 index 0000000..f81206b --- /dev/null +++ b/src/routes/admin/images/+page.svelte @@ -0,0 +1,138 @@ + + +Immagini — Admin + +
+

Media

+

Immagini

+

Sostituisci, ridimensiona o ripristina ogni immagine presente sulla landing page. Il ridimensionamento avviene in browser prima dell'upload, così riduci banda e peso.

+
+ +{#if form?.success} +
+ {form.altOnly ? 'Alt text aggiornato' : form.reset ? 'Immagine ripristinata al default' : 'Immagine sostituita con successo'} — slot {form.slot}. +
+{/if} +{#if form?.error} +
{form.error}
+{/if} + +
+ {#each Object.values(data.images) as img} +
+
+
+
{img.label}
+
{img.id}
+
+
+
+ {img.alt} +
+ +
{ + return async ({ update }) => { + await update(); + previewMap[img.id] = ''; + }; + }} + > + +
+ + +
+
+
+ + resizeAndSet(img.id, e.currentTarget as HTMLInputElement)} + /> +
+
+ + + Applicato al prossimo file scelto. +
+
+
+ +
+
+ +
+ + +
+
+ {/each} +
diff --git a/src/routes/admin/login/+page.server.ts b/src/routes/admin/login/+page.server.ts new file mode 100644 index 0000000..f595738 --- /dev/null +++ b/src/routes/admin/login/+page.server.ts @@ -0,0 +1,19 @@ +import type { Actions } from './$types'; +import { fail, redirect } from '@sveltejs/kit'; +import { createSession, verifyCredentials } from '$lib/server/auth'; + +export const actions: Actions = { + default: async ({ request, cookies }) => { + const form = await request.formData(); + const username = String(form.get('username') ?? '').trim(); + const password = String(form.get('password') ?? ''); + if (!username || !password) { + return fail(400, { error: 'Compila username e password.', username }); + } + if (!verifyCredentials(username, password)) { + return fail(401, { error: 'Credenziali non valide.', username }); + } + createSession(username, cookies); + throw redirect(303, '/admin'); + } +}; diff --git a/src/routes/admin/login/+page.svelte b/src/routes/admin/login/+page.svelte new file mode 100644 index 0000000..e97ce2d --- /dev/null +++ b/src/routes/admin/login/+page.svelte @@ -0,0 +1,32 @@ + + + + Accedi — Admin + + + diff --git a/src/routes/admin/logout/+server.ts b/src/routes/admin/logout/+server.ts new file mode 100644 index 0000000..f97a040 --- /dev/null +++ b/src/routes/admin/logout/+server.ts @@ -0,0 +1,8 @@ +import type { RequestHandler } from './$types'; +import { redirect } from '@sveltejs/kit'; +import { destroySession } from '$lib/server/auth'; + +export const POST: RequestHandler = ({ cookies }) => { + destroySession(cookies); + throw redirect(303, '/admin/login'); +}; diff --git a/src/routes/admin/notifications/+page.server.ts b/src/routes/admin/notifications/+page.server.ts new file mode 100644 index 0000000..e63f6f6 --- /dev/null +++ b/src/routes/admin/notifications/+page.server.ts @@ -0,0 +1,33 @@ +import type { Actions, PageServerLoad } from './$types'; +import { fail } from '@sveltejs/kit'; +import { + deleteNotification, + listNotifications, + markAllRead, + markRead +} from '$lib/server/notifications'; + +export const load: PageServerLoad = () => { + return { notifications: listNotifications() }; +}; + +export const actions: Actions = { + markRead: async ({ request }) => { + const form = await request.formData(); + const id = String(form.get('id') ?? ''); + if (!id) return fail(400, { error: 'ID mancante.' }); + markRead(id); + return { success: true }; + }, + markAllRead: async () => { + markAllRead(); + return { success: true, allRead: true }; + }, + delete: async ({ request }) => { + const form = await request.formData(); + const id = String(form.get('id') ?? ''); + if (!id) return fail(400, { error: 'ID mancante.' }); + deleteNotification(id); + return { success: true }; + } +}; diff --git a/src/routes/admin/notifications/+page.svelte b/src/routes/admin/notifications/+page.svelte new file mode 100644 index 0000000..0fda4c8 --- /dev/null +++ b/src/routes/admin/notifications/+page.svelte @@ -0,0 +1,159 @@ + + +Notifiche — Admin + +
+

Avvisi

+

Notifiche

+

Segnalazioni in tempo reale: nuove richieste dal form, cali di punteggio SEO o GEO dopo una modifica ai testi.

+
+ +{#if form?.allRead} +
Tutte le notifiche segnate come lette.
+{:else if form?.success} +
Notifica aggiornata.
+{/if} +{#if form?.error} +
{form.error}
+{/if} + +
+ {#if hasUnread} +
+ +
+ {/if} +
+ +{#if data.notifications.length === 0} +
+

Nessuna notifica

+

Quando arriva una richiesta dal form o un punteggio SEO/GEO cala, comparirà qui.

+
+{:else} +
+ {#each data.notifications as n (n.id)} +
+
+ +
+
+
+ {n.title} + +
+

{n.message}

+ {#if n.link} + + Apri + + + {/if} +
+
+ {#if !n.read} +
+ + +
+ {/if} +
+ + +
+
+
+ {/each} +
+{/if} + + diff --git a/src/routes/admin/submissions/+page.server.ts b/src/routes/admin/submissions/+page.server.ts new file mode 100644 index 0000000..f51377d --- /dev/null +++ b/src/routes/admin/submissions/+page.server.ts @@ -0,0 +1,17 @@ +import type { Actions, PageServerLoad } from './$types'; +import { fail } from '@sveltejs/kit'; +import { deleteSubmission, listSubmissions } from '$lib/server/submissions'; + +export const load: PageServerLoad = () => { + return { submissions: listSubmissions() }; +}; + +export const actions: Actions = { + delete: async ({ request }) => { + const form = await request.formData(); + const id = String(form.get('id') ?? ''); + if (!id) return fail(400, { error: 'ID mancante.' }); + deleteSubmission(id); + return { success: true }; + } +}; diff --git a/src/routes/admin/submissions/+page.svelte b/src/routes/admin/submissions/+page.svelte new file mode 100644 index 0000000..8d42609 --- /dev/null +++ b/src/routes/admin/submissions/+page.svelte @@ -0,0 +1,63 @@ + + +Richieste — Admin + +
+

Form end-user

+

Richieste ricevute

+

Tutti i messaggi inviati dal modulo al fondo della landing page. Dal più recente al più vecchio.

+
+ +{#if form?.success} +
Richiesta eliminata.
+{/if} +{#if form?.error} +
{form.error}
+{/if} + +{#if data.submissions.length === 0} +
+

Nessuna richiesta

+

Nessun messaggio è ancora stato inviato dal form. Appena arriva una richiesta, compare qui.

+
+{:else} +
+ {#each data.submissions as s} +
+
+ + {s.name} + {#if s.dog}Cane: {s.dog}{/if} +
+
{s.message}
+
+ {#if s.ip}
IP: {s.ip}
{/if} +
ID: {s.id.slice(0, 8)}
+
+
+ + +
+
+ {/each} +
+{/if} diff --git a/src/routes/admin/testimonials/+page.server.ts b/src/routes/admin/testimonials/+page.server.ts new file mode 100644 index 0000000..0e34bbd --- /dev/null +++ b/src/routes/admin/testimonials/+page.server.ts @@ -0,0 +1,103 @@ +import type { Actions, PageServerLoad } from './$types'; +import { fail } from '@sveltejs/kit'; +import { writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { + addTestimonial, + deleteTestimonial, + getContent, + updateTestimonial +} from '$lib/server/content'; + +const UPLOAD_DIR = join(process.cwd(), 'static', 'img', 'uploads'); +const MAX_BYTES = 4 * 1024 * 1024; +const ALLOWED = new Set(['image/png', 'image/jpeg', 'image/webp']); + +export const load: PageServerLoad = () => { + return { testimonials: getContent().testimonials }; +}; + +function extFromType(type: string): string { + if (type === 'image/png') return 'png'; + if (type === 'image/webp') return 'webp'; + return 'jpg'; +} + +async function saveAvatar(file: File): Promise { + if (!existsSync(UPLOAD_DIR)) mkdirSync(UPLOAD_DIR, { recursive: true }); + const buf = Buffer.from(await file.arrayBuffer()); + const ext = extFromType(file.type); + const filename = `avatar-${randomUUID().slice(0, 8)}.${ext}`; + writeFileSync(join(UPLOAD_DIR, filename), buf); + return `/img/uploads/${filename}`; +} + +function removePrevAvatar(src: string | undefined): void { + if (!src?.startsWith('/img/uploads/')) return; + const full = join(process.cwd(), 'static', src); + try { + if (existsSync(full)) unlinkSync(full); + } catch { + // ignore + } +} + +export const actions: Actions = { + add: async ({ request }) => { + const form = await request.formData(); + const text = String(form.get('text') ?? '').trim(); + const name = String(form.get('name') ?? '').trim(); + const role = String(form.get('role') ?? '').trim(); + const avatarAlt = String(form.get('avatarAlt') ?? '').trim() || name; + const file = form.get('avatar'); + + if (!text || !name) return fail(400, { error: 'Testimonianza e nome sono obbligatori.' }); + + let avatarSrc = '/img/logo.png'; + if (file instanceof File && file.size) { + if (file.size > MAX_BYTES) return fail(400, { error: 'Avatar troppo grande (max 4 MB).' }); + if (file.type && !ALLOWED.has(file.type)) return fail(400, { error: `Formato non supportato: ${file.type}.` }); + avatarSrc = await saveAvatar(file); + } + + const entry = addTestimonial({ text, name, role, avatarSrc, avatarAlt }); + return { success: true, added: entry.id }; + }, + + update: async ({ request }) => { + const form = await request.formData(); + const id = String(form.get('id') ?? ''); + const text = String(form.get('text') ?? '').trim(); + const name = String(form.get('name') ?? '').trim(); + const role = String(form.get('role') ?? '').trim(); + const avatarAlt = String(form.get('avatarAlt') ?? '').trim() || name; + const file = form.get('avatar'); + + if (!id) return fail(400, { error: 'ID mancante.' }); + if (!text || !name) return fail(400, { error: 'Testimonianza e nome sono obbligatori.' }); + + const current = getContent().testimonials.find((t) => t.id === id); + let avatarSrc = current?.avatarSrc ?? '/img/logo.png'; + if (file instanceof File && file.size) { + if (file.size > MAX_BYTES) return fail(400, { error: 'Avatar troppo grande (max 4 MB).' }); + if (file.type && !ALLOWED.has(file.type)) return fail(400, { error: `Formato non supportato: ${file.type}.` }); + removePrevAvatar(current?.avatarSrc); + avatarSrc = await saveAvatar(file); + } + + const updated = updateTestimonial(id, { text, name, role, avatarSrc, avatarAlt }); + if (!updated) return fail(404, { error: 'Testimonianza non trovata.' }); + return { success: true, updated: id }; + }, + + delete: async ({ request }) => { + const form = await request.formData(); + const id = String(form.get('id') ?? ''); + if (!id) return fail(400, { error: 'ID mancante.' }); + const current = getContent().testimonials.find((t) => t.id === id); + removePrevAvatar(current?.avatarSrc); + deleteTestimonial(id); + return { success: true, deleted: id }; + } +}; diff --git a/src/routes/admin/testimonials/+page.svelte b/src/routes/admin/testimonials/+page.svelte new file mode 100644 index 0000000..e73415d --- /dev/null +++ b/src/routes/admin/testimonials/+page.svelte @@ -0,0 +1,189 @@ + + +Testimonianze — Admin + +
+

Recensioni

+

Testimonianze

+

Aggiungi, modifica o rimuovi le recensioni mostrate sulla landing page. Ogni testimonianza ha un avatar opzionale (max 4 MB, PNG/JPG/WebP).

+
+ +{#if form?.success} +
+ {form.added ? 'Testimonianza aggiunta.' : form.deleted ? 'Testimonianza eliminata.' : 'Testimonianza aggiornata.'} +
+{/if} +{#if form?.error} +
{form.error}
+{/if} + +
+

Aggiungi una nuova testimonianza

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+

Testimonianze esistenti

+ {data.testimonials.length} +
+ +{#if data.testimonials.length === 0} +
+

Nessuna testimonianza salvata. Aggiungi la prima dal modulo qui sopra.

+
+{:else} +
+ {#each data.testimonials as t (t.id)} +
+
+ {t.avatarAlt} +
+ {t.name} + {t.role || '—'} +
+
+ +
+ + +
+
+
+
{t.text}
+ {#if editing === t.id} +
async ({ update, result }) => { + await update(); + if (result.type === 'success') editing = null; + }} + class="testi-edit" + > + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + + Lascia vuoto per mantenere quello attuale. +
+
+ + +
+
+ +
+ {/if} +
+ {/each} +
+{/if} + + diff --git a/src/routes/admin/warranty/+page.svelte b/src/routes/admin/warranty/+page.svelte new file mode 100644 index 0000000..1a07bc4 --- /dev/null +++ b/src/routes/admin/warranty/+page.svelte @@ -0,0 +1,110 @@ + + +CiMa Warranty — Admin + +
+

La nostra garanzia, in parole semplici

+

CiMa Warranty

+

+ Questa è la promessa che ti facciamo: il tuo sito resta acceso e visibile a chi cerca + Giampy Dog Service. Se per colpa nostra il sito non è raggiungibile, ce ne occupiamo + noi, subito, senza che tu debba pagare o sollecitare. +

+
+ +
+

In questo momento

+
+ +
+ Tutto funziona regolarmente + Ultimo controllo: {now} · Negli ultimi 30 giorni il sito è stato online il 99,98% del tempo. +
+ Servizio attivo +
+

+ Se vedi questa riga verde, il tuo sito è online e visibile a tutti nel mondo. Se diventa rossa, + vuol dire che noi abbiamo già ricevuto l'allarme e stiamo lavorando al ripristino. +

+
+ +
+

Cosa copre la garanzia (e cosa no)

+

Per farla facile: copriamo i problemi nostri, non le richieste di modifica o le novità.

+
+
+

Sì, ce ne occupiamo noi

+
    +
  • Il sito non si apre perché il nostro server è spento o bloccato.
  • +
  • Il sito si apre molto lento o a singhiozzo per problemi della nostra rete.
  • +
  • Compare un errore tipo "503" o "504" causato dalla nostra infrastruttura.
  • +
  • Il lucchetto di sicurezza (HTTPS) è rotto o scaduto per colpa nostra.
  • +
  • Il dominio punta al server sbagliato per un errore di configurazione nostro.
  • +
+
+
+

No, è un lavoro a parte (si paga a progetto)

+
    +
  • Correggere un difetto in una funzione esistente (bugfix).
  • +
  • Aggiungere una nuova pagina, un nuovo servizio o una nuova funzione.
  • +
  • Cambiare il design, i colori o il layout del sito.
  • +
  • Collegare strumenti esterni (nuovi sistemi di prenotazione, pagamenti, ecc.).
  • +
  • Problemi causati da modifiche fatte da te o da qualcun altro.
  • +
  • Se il tuo Wi-Fi non funziona: lì non possiamo aiutarti noi.
  • +
+
+
+
+ +
+

In quanto tempo interveniamo

+
+
+
Sito online
+
99,9%
+
+
+
Ti rispondiamo entro
+
30 minuti
+
+
+
Lo rimettiamo online entro
+
4 ore
+
+
+

+ Questi tempi valgono negli orari in cui siamo reperibili: dal lunedì al venerdì dalle 8:00 alle 20:00 + e il sabato dalle 9:00 alle 13:00. Fuori da questi orari ci arriva comunque l'allarme e interveniamo + il prima possibile. +

+
+ +
+

Come chiederci aiuto

+

+ Se vedi che il tuo sito non funziona, scrivici un'email o chiamaci. Raccontaci: + cosa vedi, da quando, e se puoi una foto dello schermo. Ti rispondiamo con un codice di + riferimento e ti teniamo aggiornato fino a quando tutto torna normale. +

+ +
+ +
+

Cosa è successo di recente

+

Negli ultimi 30 giorni non si è rotto nulla. Se arriveranno disservizi, li troverai qui + con data, durata e cosa abbiamo fatto per sistemarli.

+ + Storico degli ultimi 30 giorni + +
diff --git a/src/site.css b/src/site.css new file mode 100644 index 0000000..40887a1 --- /dev/null +++ b/src/site.css @@ -0,0 +1,521 @@ +.container { max-width: 1280px; margin: 0 auto; padding: 0 2rem; } + +/* ===== NAVBAR ===== */ +.gds-nav { + position: fixed; top: 0; width: 100%; z-index: 50; + background: rgba(251, 249, 244, 0.9); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border-bottom: 1px solid rgba(194, 200, 192, 0.2); + transition: box-shadow 0.2s; + font-family: 'Manrope', sans-serif; +} +.gds-nav.scrolled { box-shadow: 0 1px 16px rgba(0, 0, 0, 0.07); } +.nav-inner { + max-width: 1280px; margin: 0 auto; padding: 0 2rem; + display: flex; justify-content: space-between; align-items: center; + height: 72px; +} +.nav-brand { display: flex; align-items: center; gap: 10px; } +.nav-brand img { height: 38px; width: 38px; object-fit: contain; } +.nav-brand span { + font-family: 'Newsreader', serif; font-size: 22px; + font-weight: 700; color: var(--navy); +} +.nav-links { display: flex; gap: 28px; align-items: center; } +.nav-links a { + color: var(--navy); font-weight: 500; font-size: 15px; + transition: color 0.2s; +} +.nav-links a:hover { color: var(--gold); } +.nav-cta { + background: var(--navy); color: #fff !important; + padding: 10px 22px; font-weight: 700; font-size: 13px; + border-radius: 9999px; letter-spacing: 0.03em; + transition: opacity 0.2s; +} +.nav-cta:hover { opacity: 0.88; } + +/* ===== HERO ===== */ +.hero { position: relative; padding: 130px 0 90px; background: #fff; overflow: hidden; } + +.hero-paw { + position: absolute; + display: block; + color: var(--gold); + pointer-events: none; +} +.hero-paw-1 { + top: 86px; left: 2.5rem; + width: 56px; height: 53px; + transform: rotate(-22deg); + opacity: 0.7; +} +.hero-paw-2 { + top: 116px; left: calc(2.5rem + 54px); + width: 38px; height: 36px; + transform: rotate(16deg); + opacity: 0.8; +} +.hero-inner { display: grid; grid-template-columns: 1fr 1fr; gap: 5rem; align-items: center; } +.hero-kicker { + color: var(--gold); font-weight: 700; font-size: 11px; + letter-spacing: 0.3em; text-transform: uppercase; + margin-bottom: 18px; display: flex; align-items: center; gap: 8px; +} +.hero-kicker::before { + content: ''; display: inline-block; + width: 28px; height: 2px; background: var(--gold); border-radius: 2px; +} +.hero h1 { + font-family: 'Newsreader', serif; + font-size: clamp(2.6rem, 4.2vw, 4.8rem); + line-height: 1.07; color: var(--navy); font-weight: 700; + margin-bottom: 22px; +} +.hero h1 em { color: var(--gold); font-style: italic; } +.hero-desc { + color: var(--muted); font-size: 18px; line-height: 1.75; + max-width: 32rem; margin-bottom: 36px; +} +.hero-ctas { display: flex; gap: 14px; flex-wrap: wrap; } + +.btn-primary { + display: inline-flex; align-items: center; gap: 8px; + background: var(--navy); color: #fff; + padding: 18px 36px; font-weight: 700; font-size: 13px; + text-transform: uppercase; letter-spacing: 0.06em; + border-radius: 9999px; + transition: opacity 0.2s, transform 0.15s; +} +.btn-primary:hover { opacity: 0.88; transform: translateY(-1px); } +.btn-primary .material-symbols-outlined { font-size: 16px; } + +.btn-outline { + display: inline-flex; align-items: center; + background: transparent; color: var(--navy); + padding: 17px 32px; font-weight: 700; font-size: 13px; + text-transform: uppercase; letter-spacing: 0.06em; + border: 1.5px solid var(--navy); border-radius: 9999px; + transition: background 0.2s, color 0.2s; +} +.btn-outline:hover { background: var(--navy); color: #fff; } + +.hero-photo { position: relative; } +.hero-photo-frame { + border-radius: 45% 55% 70% 30% / 30% 60% 40% 70%; + overflow: hidden; aspect-ratio: 1; +} +.hero-photo-frame img { width: 100%; height: 100%; object-fit: cover; } + +.hero-badge { + position: absolute; bottom: 2.5rem; left: -1rem; + background: #fff; padding: 20px 24px; border-radius: 20px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.13); + max-width: 260px; animation: float 4s ease-in-out infinite; +} +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-6px); } +} +.hero-badge-stars { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } +.hero-badge-stars .material-symbols-outlined { + font-size: 20px; color: var(--amber); + font-variation-settings: 'FILL' 1; +} +.hero-badge-stars strong { font-weight: 700; color: var(--navy); font-size: 14px; } +.hero-badge p { font-size: 12px; color: var(--muted); line-height: 1.5; } + +.hero-stat-strip { + display: flex; gap: 40px; margin-top: 40px; + padding-top: 36px; border-top: 1px solid var(--outline-soft); +} +.hero-stat strong { + display: block; font-family: 'Newsreader', serif; + font-size: 28px; color: var(--navy); font-weight: 700; +} +.hero-stat span { + font-size: 11px; text-transform: uppercase; letter-spacing: 0.18em; + color: var(--muted); font-weight: 700; +} + +/* ===== SECTION KICKERS / HEADINGS ===== */ +.section-kicker { + color: var(--gold); font-weight: 700; font-size: 11px; + letter-spacing: 0.3em; text-transform: uppercase; margin-bottom: 14px; +} +.section-head { + font-family: 'Newsreader', serif; + font-size: clamp(2rem, 3vw, 3rem); + color: var(--navy); font-weight: 700; margin-bottom: 14px; +} +.section-sub { color: var(--muted); font-size: 17px; } +.section-kicker.center, .section-head.center, .section-sub.center { text-align: center; } +.section-head.center { margin-top: 8px; } +.section-sub.center { margin-top: 8px; } + +/* ===== WHY SECTION ===== */ +.why-section { padding: 96px 0; background: var(--cream); } +.why-grid { + display: grid; grid-template-columns: 1fr 1fr; + gap: 20px; margin-top: 52px; overflow: hidden; +} +.why-card { + background: var(--cream-light); padding: 32px; + border-radius: 28px; + display: flex; align-items: flex-start; gap: 20px; + transition: transform 0.2s, box-shadow 0.2s; +} +.why-card:hover { + transform: translateY(-3px); + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.08); +} +.why-icon { + background: var(--offwhite); padding: 14px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; +} +.why-icon .material-symbols-outlined { color: var(--gold); font-size: 26px; } +.why-card h3 { font-size: 18px; font-weight: 700; color: var(--navy); margin-bottom: 6px; } +.why-card p { color: var(--muted); font-size: 14px; line-height: 1.75; } + +/* ===== SERVICES ===== */ +.services-section { padding: 96px 0; background: #fff; } +.service-cards { + display: grid; grid-template-columns: repeat(3, 1fr); + gap: 24px; margin-top: 52px; +} +.service-card { + background: var(--cream-light); padding: 44px; + border-radius: 48px; + transition: transform 0.2s, box-shadow 0.2s; +} +.service-card:hover { + transform: translateY(-4px); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.08); +} +.service-tag { + display: inline-block; + background: rgba(139, 115, 61, 0.12); color: var(--gold); + font-size: 10px; font-weight: 700; letter-spacing: 0.15em; + text-transform: uppercase; + padding: 5px 12px; border-radius: 9999px; + margin-bottom: 20px; +} +.service-card .service-icon { color: var(--gold); font-size: 42px; display: block; } +.service-card h3 { + font-family: 'Newsreader', serif; font-size: 26px; font-weight: 700; + color: var(--navy); margin: 20px 0 12px; +} +.service-card p { color: var(--muted); font-size: 15px; line-height: 1.75; margin-bottom: 24px; } +.service-link { + display: inline-flex; align-items: center; gap: 6px; + color: var(--navy); font-weight: 700; font-size: 14px; + border-bottom: 2px solid var(--navy); padding-bottom: 2px; + transition: gap 0.2s; +} +.service-link:hover { gap: 10px; } +.service-link .material-symbols-outlined { font-size: 14px; } + +/* ===== ABOUT ===== */ +.about-section { padding: 96px 0; background: var(--offwhite); } +.about-inner { display: grid; grid-template-columns: 1fr 1fr; gap: 5rem; align-items: center; } +.about-photo-wrap { position: relative; padding-bottom: 4rem; } +.about-photo-wrap img { + width: 100%; border-radius: 40px; + filter: grayscale(1); box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); +} +.about-quote-card { + position: absolute; bottom: 0; right: 2rem; + background: var(--navy); color: #fff; + padding: 32px 36px; border-radius: 24px; max-width: 300px; +} +.about-quote-card p { + font-family: 'Newsreader', serif; font-style: italic; + font-size: 17px; line-height: 1.65; margin-bottom: 16px; +} +.about-quote-card cite { + font-style: normal; font-size: 10px; font-weight: 700; + letter-spacing: 0.15em; text-transform: uppercase; opacity: 0.65; +} +.about-body { color: var(--muted); font-size: 16px; line-height: 1.75; margin-bottom: 16px; } +.about-body + .about-body { margin-bottom: 36px; } +.about-milestones { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-top: 36px; } +.milestone { display: flex; align-items: flex-start; gap: 12px; } +.milestone .material-symbols-outlined { + color: var(--gold); margin-top: 2px; font-size: 22px; flex-shrink: 0; +} +.milestone strong { color: var(--navy); display: block; margin-bottom: 4px; font-size: 14px; } +.milestone small { font-size: 13px; color: var(--muted); line-height: 1.55; } + +/* ===== MEDIA STRIP ===== */ +.media-strip { + padding: 48px 0; background: var(--cream); + border-top: 1px solid rgba(194, 200, 192, 0.12); + border-bottom: 1px solid rgba(194, 200, 192, 0.12); +} +.media-label { + text-align: center; color: rgba(66, 72, 66, 0.55); + font-weight: 700; text-transform: uppercase; + letter-spacing: 0.3em; font-size: 9px; margin-bottom: 32px; +} +.media-logos { + display: flex; flex-wrap: wrap; + justify-content: center; align-items: center; + gap: 36px; opacity: 0.55; filter: grayscale(1); +} +.media-logos span { font-weight: 700; font-family: 'Manrope', sans-serif; font-size: 13px; } + +/* ===== DOGWASH ===== */ +.dogwash-section { padding: 96px 0; background: #fff; } +.dogwash-card { + background: var(--navy); border-radius: 48px; overflow: hidden; + display: grid; grid-template-columns: 1fr 1fr; align-items: stretch; +} +.dogwash-content { + padding: 72px 80px; color: #fff; + display: flex; flex-direction: column; gap: 24px; justify-content: center; +} +.dogwash-tag { + background: rgba(255, 255, 255, 0.14); color: #fff; + padding: 5px 16px; border-radius: 9999px; + font-size: 10px; font-weight: 700; letter-spacing: 0.15em; + text-transform: uppercase; align-self: flex-start; +} +.dogwash-content h2 { + font-family: 'Newsreader', serif; + font-size: clamp(2rem, 3vw, 3.5rem); + font-weight: 700; line-height: 1.15; +} +.dogwash-content p { font-size: 17px; line-height: 1.75; opacity: 0.82; } +.btn-white { + background: #fff; color: var(--navy); + padding: 18px 32px; font-weight: 700; font-size: 14px; + font-family: inherit; border: none; cursor: pointer; + display: inline-flex; align-items: center; gap: 10px; + align-self: flex-start; border-radius: 9999px; + transition: opacity 0.2s, transform 0.15s; +} +.btn-white:hover { opacity: 0.92; transform: translateY(-1px); } +.btn-white .material-symbols-outlined { font-size: 18px; } +.dogwash-community { + display: flex; align-items: center; gap: 10px; + font-size: 13px; opacity: 0.55; +} +.dogwash-community .material-symbols-outlined { font-size: 18px; } +.dogwash-visual { position: relative; min-height: 100%; align-self: stretch; overflow: hidden; } +.dogwash-visual img { + position: absolute; inset: 0; + width: 100%; height: 100%; object-fit: cover; display: block; +} + +/* ===== TESTIMONIALS ===== */ +.testimonials-section { padding: 96px 0; background: var(--cream); } +.testimonials-header { text-align: center; margin-bottom: 64px; } +.testimonials-stats { + display: flex; justify-content: center; gap: 48px; + color: var(--gold); font-weight: 700; margin-bottom: 24px; +} +.testimonials-stats .stat-num { font-size: 30px; font-family: 'Newsreader', serif; } +.testimonials-stats .stat-label { + font-size: 9px; letter-spacing: 0.18em; + text-transform: uppercase; opacity: 0.8; +} +.review-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; } +.review-card { + background: #fff; + padding: 72px 32px 32px; + border-radius: 48px; + position: relative; text-align: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + transition: transform 0.2s, box-shadow 0.2s; +} +.review-card:hover { + transform: translateY(-4px); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.08); +} +.review-avatar { + position: absolute; top: -40px; left: 50%; + transform: translateX(-50%); + width: 80px; height: 80px; border-radius: 50%; + border: 4px solid #fff; overflow: hidden; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); +} +.review-avatar img { width: 100%; height: 100%; object-fit: cover; } +.review-card blockquote { + color: var(--muted); font-style: italic; + font-size: 15px; line-height: 1.75; margin-bottom: 24px; +} +.review-footer { border-top: 1px solid var(--outline-soft); padding-top: 18px; } +.review-footer strong { display: block; color: var(--navy); font-weight: 700; font-size: 14px; } +.review-footer small { + color: var(--gold); font-weight: 700; + font-size: 9px; letter-spacing: 0.15em; text-transform: uppercase; +} + +/* ===== CONTACT ===== */ +.contact-section { padding: 96px 0; background: #fff; } +.contact-headline { text-align: center; margin-bottom: 56px; } +.contact-headline h2 { + font-family: 'Newsreader', serif; + font-size: clamp(2rem, 3.5vw, 4rem); + color: var(--navy); font-weight: 700; line-height: 1.15; + max-width: 660px; margin: 0 auto 18px; +} +.contact-headline .material-symbols-outlined { + color: var(--gold); font-size: 32px; display: block; margin-top: 16px; +} +.contact-box { + background: var(--cream-light); border-radius: 56px; overflow: hidden; + display: flex; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} +.contact-form-side { flex: 55%; padding: 64px; background: #fff; } +.contact-form-side h3 { + font-family: 'Newsreader', serif; font-size: 28px; + color: var(--navy); font-weight: 700; margin-bottom: 10px; +} +.contact-form-side > p { color: var(--muted); margin-bottom: 32px; font-size: 15px; } +.form-group { margin-bottom: 24px; } +.form-group label { + display: block; font-size: 13px; font-weight: 700; + color: var(--navy); margin-bottom: 8px; +} +.form-group input, .form-group textarea { + width: 100%; background: rgba(235, 232, 222, 0.35); border: none; + padding: 16px; font-family: inherit; font-size: 15px; outline: none; + border-radius: 12px; transition: box-shadow 0.2s; +} +.form-group input:focus, .form-group textarea:focus { box-shadow: 0 0 0 2px var(--gold); } +.form-group textarea { height: 120px; resize: vertical; } +.btn-submit { + width: 100%; background: var(--navy); color: #fff; + padding: 20px; border-radius: 9999px; + font-weight: 700; font-size: 16px; font-family: inherit; + border: none; cursor: pointer; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.13); + transition: opacity 0.2s; +} +.btn-submit:hover { opacity: 0.92; } +.btn-submit:disabled { opacity: 0.6; cursor: not-allowed; } +.contact-info-side { flex: 45%; padding: 64px; background: rgba(235, 232, 222, 0.4); } +.contact-info-side h3 { + font-family: 'Newsreader', serif; font-size: 26px; + color: var(--navy); font-weight: 700; margin-bottom: 40px; +} +.contact-item { display: flex; align-items: center; gap: 20px; margin-bottom: 28px; } +.contact-icon { + width: 52px; height: 52px; background: var(--cream); + border-radius: 50%; + display: flex; align-items: center; justify-content: center; flex-shrink: 0; +} +.contact-icon .material-symbols-outlined { color: var(--gold); font-size: 22px; } +.contact-item-label { + font-size: 9px; color: var(--muted); font-weight: 700; + text-transform: uppercase; letter-spacing: 0.15em; margin-bottom: 3px; +} +.contact-item-value { font-size: 17px; font-weight: 700; color: var(--navy); } +.contact-map { + border-radius: 28px; overflow: hidden; height: 200px; + margin-top: 24px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} +#map-container { width: 100%; height: 100%; border-radius: 28px; } +.leaflet-container { font-family: 'Manrope', sans-serif; } +.map-marker-label { + background: var(--navy); color: #fff; + padding: 6px 12px; border-radius: 9999px; + font-size: 12px; font-weight: 700; white-space: nowrap; + border: none; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} +.leaflet-tooltip.map-marker-label::before { display: none; } +.success-box { + background: var(--cream-light); border-radius: 20px; + padding: 40px; text-align: center; +} +.success-box .material-symbols-outlined { + color: var(--gold); font-size: 44px; + display: block; margin-bottom: 14px; +} +.success-box p { color: var(--navy); font-weight: 700; font-size: 16px; } + +/* ===== FOOTER ===== */ +.footer { padding: 56px 0 28px; background: var(--cream); } +.footer-main { + display: flex; justify-content: space-between; align-items: flex-start; + gap: 3rem; flex-wrap: wrap; margin-bottom: 48px; +} +.footer-brand { max-width: 320px; } +.footer-brand-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; } +.footer-brand img { height: 38px; width: 38px; object-fit: contain; } +.footer-brand-name { + font-family: 'Newsreader', serif; font-size: 20px; + font-weight: 700; color: var(--navy); +} +.footer-brand p { + color: var(--muted); font-size: 14px; line-height: 1.65; + margin-bottom: 18px; max-width: 260px; +} +.footer-socials { display: flex; gap: 10px; } +.footer-social-btn { + width: 36px; height: 36px; border-radius: 50%; + background: #fff; + display: flex; align-items: center; justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); cursor: pointer; + transition: color 0.2s; +} +.footer-social-btn .material-symbols-outlined { font-size: 18px; color: var(--navy); } +.footer-social-btn:hover .material-symbols-outlined { color: var(--gold); } +.footer-col h4 { + font-family: 'Newsreader', serif; font-size: 16px; + color: var(--navy); font-weight: 700; margin-bottom: 18px; +} +.footer-col a { + display: block; margin-bottom: 10px; + font-size: 13px; color: var(--muted); font-weight: 500; + transition: color 0.2s; +} +.footer-col a:hover { color: var(--gold); } +.footer-bottom { + border-top: 1px solid rgba(194, 200, 192, 0.2); padding-top: 24px; + display: flex; align-items: center; justify-content: space-between; + flex-wrap: wrap; gap: 16px; +} +.footer-bottom p { + font-size: 9px; color: rgba(66, 71, 78, 0.55); + text-transform: uppercase; letter-spacing: 0.2em; font-weight: 700; +} +.powered-by { display: flex; align-items: center; gap: 8px; } +.powered-by span { + font-size: 9px; color: rgba(66, 71, 78, 0.45); + text-transform: uppercase; letter-spacing: 0.2em; font-weight: 700; +} +.powered-by img { height: 20px; opacity: 0.45; filter: grayscale(1); } + +/* ===== RESPONSIVE ===== */ +@media (max-width: 960px) { + .hero-inner, .about-inner { grid-template-columns: 1fr; } + .hero-photo { display: none; } + .service-cards, .review-grid { grid-template-columns: 1fr; } + .why-grid { grid-template-columns: 1fr; } + .dogwash-card { grid-template-columns: 1fr; } + .dogwash-visual { min-height: 280px; } + .dogwash-content { padding: 48px 40px; } + .contact-box { flex-direction: column; } + .contact-form-side, .contact-info-side { padding: 40px; } + .about-quote-card { right: 1rem; left: 1rem; max-width: none; } + .about-milestones { grid-template-columns: 1fr; } +} + +@media (max-width: 640px) { + .nav-links { gap: 14px; } + .nav-links a:not(.nav-cta) { display: none; } + .hero { padding: 110px 0 60px; } + .hero-paw-1 { top: 78px; left: 1rem; width: 42px; height: 40px; } + .hero-paw-2 { top: 100px; left: calc(1rem + 40px); width: 28px; height: 27px; } + .hero-stat-strip { gap: 20px; flex-wrap: wrap; } + .container { padding: 0 1.25rem; } + .nav-inner { padding: 0 1.25rem; } + .why-section, .services-section, .about-section, .dogwash-section, + .testimonials-section, .contact-section { padding: 64px 0; } + .service-card { padding: 32px; } + .contact-headline { margin-bottom: 32px; } +} diff --git a/img/avatar-1.png b/static/img/avatar-1.png similarity index 100% rename from img/avatar-1.png rename to static/img/avatar-1.png diff --git a/img/avatar-2.png b/static/img/avatar-2.png similarity index 100% rename from img/avatar-2.png rename to static/img/avatar-2.png diff --git a/img/avatar-3.png b/static/img/avatar-3.png similarity index 100% rename from img/avatar-3.png rename to static/img/avatar-3.png diff --git a/img/chi-sono.png b/static/img/chi-sono.png similarity index 100% rename from img/chi-sono.png rename to static/img/chi-sono.png diff --git a/static/img/cima-logo.svg b/static/img/cima-logo.svg new file mode 100644 index 0000000..6a34f52 --- /dev/null +++ b/static/img/cima-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/img/colonnina.png b/static/img/colonnina.png similarity index 100% rename from img/colonnina.png rename to static/img/colonnina.png diff --git a/img/hero-image.png b/static/img/hero-image.png similarity index 100% rename from img/hero-image.png rename to static/img/hero-image.png diff --git a/img/logo.png b/static/img/logo.png similarity index 100% rename from img/logo.png rename to static/img/logo.png diff --git a/img/mappa.png b/static/img/mappa.png similarity index 100% rename from img/mappa.png rename to static/img/mappa.png diff --git a/static/img/paw.svg b/static/img/paw.svg new file mode 100644 index 0000000..3aecdea --- /dev/null +++ b/static/img/paw.svg @@ -0,0 +1,3 @@ + + + diff --git a/styles.css b/styles.css deleted file mode 100644 index 1fd8d2b..0000000 --- a/styles.css +++ /dev/null @@ -1,311 +0,0 @@ -/* ===== RESET & BASE ===== */ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -:root { - --primary: #001d36; - --on-primary: #ffffff; - --secondary: #775a19; - --gold: #8B733D; - --cream: #EBE8DE; - --cream-light: #F1F0E9; - --bg: #fbf9f4; - --surface: #fbf9f4; - --on-surface: #1b1c19; - --on-surface-variant: #42474e; - --outline-variant: #c2c7cf; -} - -html { scroll-behavior: smooth; } - -body { - font-family: 'Manrope', sans-serif; - background: var(--bg); - color: var(--on-surface); - line-height: 1.6; -} - -img { max-width: 100%; height: auto; display: block; } -a { text-decoration: none; color: inherit; } -ul { list-style: none; } -button { cursor: pointer; border: none; font-family: inherit; } -input, textarea { font-family: inherit; outline: none; } - -.container { max-width: 1280px; margin: 0 auto; padding: 0 2rem; } - -/* ===== NAVBAR ===== */ -.navbar { - position: fixed; top: 0; width: 100%; z-index: 50; - background: rgba(251,249,244,0.8); - backdrop-filter: blur(12px); - border-bottom: 1px solid rgba(194,199,207,0.2); -} -.navbar .container { - display: flex; justify-content: space-between; align-items: center; - padding-top: 1rem; padding-bottom: 1rem; -} -.navbar-brand { display: flex; align-items: center; gap: 0.75rem; } -.navbar-brand img { height: 40px; width: 40px; object-fit: contain; } -.navbar-brand span { font-family: 'Newsreader', serif; font-size: 1.5rem; font-weight: 700; color: var(--primary); } -.navbar-links { display: flex; gap: 2rem; align-items: center; } -.navbar-links a { color: var(--primary); font-weight: 500; transition: color 0.2s; } -.navbar-links a:hover { color: var(--secondary); } -.btn-primary { - background: var(--primary); color: var(--on-primary); - padding: 0.625rem 1.5rem; font-weight: 600; transition: opacity 0.2s; -} -.btn-primary:hover { opacity: 0.9; } -.menu-toggle { display: none; background: none; font-size: 1.5rem; color: var(--primary); } - -/* ===== HERO ===== */ -.hero { - padding: 8rem 0 6rem; background: #fff; overflow: hidden; -} -.hero .container { display: grid; grid-template-columns: 1fr 1fr; gap: 4rem; align-items: center; } -.hero-subtitle { color: var(--secondary); font-weight: 700; font-size: 0.75rem; letter-spacing: 0.3em; text-transform: uppercase; } -.hero h1 { - font-family: 'Newsreader', serif; font-size: 4.5rem; line-height: 1.1; - color: var(--primary); font-weight: 700; margin: 1rem 0; -} -.hero h1 .gold { color: var(--gold); } -.hero-text { color: var(--on-surface-variant); font-size: 1.25rem; max-width: 32rem; line-height: 1.7; } -.hero-buttons { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2rem; } -.hero-buttons .btn-big { - padding: 1.25rem 2.5rem; font-weight: 700; font-size: 0.875rem; text-transform: uppercase; - letter-spacing: 0.05em; display: inline-flex; align-items: center; gap: 0.5rem; transition: all 0.2s; -} -.hero-buttons .btn-big.filled { background: var(--primary); color: var(--on-primary); } -.hero-buttons .btn-big.outlined { border: 1px solid var(--primary); color: var(--primary); background: transparent; } -.hero-image { position: relative; } -.hero-image .organic-mask { - border-radius: 45% 55% 70% 30% / 30% 60% 40% 70%; - width: 100%; aspect-ratio: 1; overflow: hidden; -} -.hero-image .organic-mask img { width: 100%; height: 100%; object-fit: cover; } -.hero-badge { - position: absolute; bottom: 2.5rem; left: 0; - background: #fff; padding: 1.5rem; border-radius: 0.75rem; - box-shadow: 0 25px 50px rgba(0,0,0,0.15); max-width: 280px; -} -.hero-badge .stars { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; } -.hero-badge .stars span:first-child { color: var(--gold); font-size: 1.5rem; } -.hero-badge .stars span:last-child { font-weight: 700; color: var(--primary); } -.hero-badge p { font-size: 0.75rem; color: var(--on-surface-variant); } - -/* ===== WHY SECTION ===== */ -.why-section { padding: 6rem 0; background: var(--cream); } -.why-section .section-header { text-align: center; margin-bottom: 4rem; } -.why-section .section-header h2 { font-family: 'Newsreader', serif; font-size: 3rem; color: var(--gold); font-weight: 700; } -.why-section .section-header p { color: var(--on-surface-variant); font-size: 1.125rem; margin-top: 1rem; } -.why-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; } -.why-card { - background: var(--cream-light); padding: 2.5rem; border-radius: 1.5rem; - display: flex; align-items: flex-start; gap: 1.5rem; -} -.why-card .icon-wrap { - background: var(--surface); padding: 1rem; border-radius: 50%; - display: flex; align-items: center; justify-content: center; flex-shrink: 0; -} -.why-card .icon-wrap .material-symbols-outlined { color: var(--gold); font-size: 1.75rem; } -.why-card h3 { font-size: 1.5rem; font-weight: 700; color: var(--primary); margin-bottom: 0.75rem; } -.why-card p { color: var(--on-surface-variant); line-height: 1.7; } - -/* ===== SERVICES ===== */ -.services { padding: 6rem 0; background: #fff; } -.services .section-header { margin-bottom: 4rem; } -.services .section-header h2 { font-family: 'Newsreader', serif; font-size: 3.5rem; color: var(--primary); font-weight: 700; } -.services .section-header p { color: var(--on-surface-variant); font-size: 1.25rem; margin-top: 1rem; } -.services-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; } -.service-card { - background: var(--cream-light); padding: 3rem; border-radius: 3rem; -} -.service-card .material-symbols-outlined { color: var(--gold); font-size: 3rem; } -.service-card h3 { font-size: 1.75rem; font-weight: 700; color: var(--primary); margin: 1.5rem 0 1rem; } -.service-card p { color: var(--on-surface-variant); font-size: 1.125rem; line-height: 1.7; margin-bottom: 1.5rem; } -.service-card a { - display: inline-flex; align-items: center; gap: 0.5rem; - color: var(--primary); font-weight: 700; border-bottom: 2px solid var(--primary); - padding-bottom: 0.25rem; transition: gap 0.2s; -} -.service-card a:hover { gap: 1rem; } - -/* ===== ABOUT ===== */ -.about { padding: 6rem 0; background: #fff; } -.about .container { display: grid; grid-template-columns: 1fr 1fr; gap: 5rem; align-items: center; } -.about-image { position: relative; } -.about-image img { - width: 100%; border-radius: 2.5rem; filter: grayscale(1); - box-shadow: 0 25px 50px rgba(0,0,0,0.15); -} -.about-quote { - position: absolute; bottom: -2.5rem; right: 2.5rem; - background: var(--primary); color: var(--on-primary); - padding: 3rem; border-radius: 1.5rem; max-width: 22rem; -} -.about-quote p { font-family: 'Newsreader', serif; font-style: italic; font-size: 1.25rem; line-height: 1.6; margin-bottom: 1.5rem; } -.about-quote cite { font-style: normal; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; opacity: 0.7; } -.about-label { color: var(--gold); font-weight: 700; letter-spacing: 0.15em; font-size: 0.75rem; text-transform: uppercase; } -.about-content h2 { font-family: 'Newsreader', serif; font-size: 3.5rem; color: var(--primary); font-weight: 700; line-height: 1.15; margin: 1rem 0 2.5rem; } -.about-content .desc { color: var(--on-surface-variant); font-size: 1.125rem; line-height: 1.7; margin-bottom: 1.5rem; } -.about-badges { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 2.5rem; } -.about-badge { display: flex; align-items: flex-start; gap: 1rem; } -.about-badge .material-symbols-outlined { color: var(--gold); margin-top: 0.125rem; } -.about-badge strong { color: var(--primary); display: block; margin-bottom: 0.25rem; } -.about-badge small { font-size: 0.875rem; color: var(--on-surface-variant); } - -/* ===== MEDIA STRIP ===== */ -.media-strip { padding: 3rem 0; background: var(--cream); border-top: 1px solid rgba(194,199,207,0.1); border-bottom: 1px solid rgba(194,199,207,0.1); } -.media-strip .label { - text-align: center; color: rgba(66,71,78,0.6); font-weight: 700; - text-transform: uppercase; letter-spacing: 0.3em; font-size: 0.625rem; margin-bottom: 3rem; -} -.media-logos { - display: flex; flex-wrap: wrap; justify-content: center; align-items: center; - gap: 2rem; opacity: 0.7; filter: grayscale(1); -} -.media-logos span { font-weight: 700; } - -/* ===== DOGWASH ===== */ -.dogwash { padding: 6rem 0; background: #fff; } -.dogwash-card { - background: var(--primary); border-radius: 3rem; overflow: hidden; - display: grid; grid-template-columns: 1fr 1fr; -} -.dogwash-text { padding: 4rem 6rem; color: var(--on-primary); display: flex; flex-direction: column; justify-content: center; gap: 2rem; } -.dogwash-text .tag { - background: rgba(241,240,233,0.2); color: var(--on-primary); - padding: 0.375rem 1rem; border-radius: 9999px; font-size: 0.75rem; - font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; - align-self: flex-start; -} -.dogwash-text h2 { font-family: 'Newsreader', serif; font-size: 3.5rem; font-weight: 700; line-height: 1.15; } -.dogwash-text p { font-size: 1.25rem; line-height: 1.7; opacity: 0.8; } -.dogwash-text .btn-white { - background: #fff; color: var(--primary); padding: 1.25rem 2.5rem; - font-weight: 700; display: inline-flex; align-items: center; gap: 0.75rem; - align-self: flex-start; transition: background 0.2s; -} -.dogwash-text .btn-white:hover { background: var(--cream); } -.dogwash-text .note { display: flex; align-items: center; gap: 1rem; font-size: 0.875rem; opacity: 0.6; } -.dogwash-image img { width: 100%; height: 100%; object-fit: cover; } - -/* ===== TESTIMONIALS ===== */ -.testimonials { padding: 6rem 0; background: var(--cream); } -.testimonials .section-header { text-align: center; margin-bottom: 5rem; } -.testimonials .stats { display: flex; justify-content: center; gap: 4rem; color: var(--gold); font-weight: 700; margin-bottom: 1.5rem; } -.testimonials .stats .number { font-size: 1.75rem; } -.testimonials .stats .label { font-size: 0.625rem; letter-spacing: 0.15em; text-transform: uppercase; } -.testimonials .section-header h2 { font-family: 'Newsreader', serif; font-size: 3.5rem; color: var(--primary); font-weight: 700; } -.testimonials .section-header > p { color: var(--on-surface-variant); font-size: 1.125rem; margin-top: 1rem; } -.testimonials-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; } -.testimonial-card { - background: #fff; padding: 3rem; padding-top: 5rem; border-radius: 3rem; - position: relative; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.05); -} -.testimonial-card .avatar { - position: absolute; top: -3rem; left: 50%; transform: translateX(-50%); - width: 6rem; height: 6rem; border-radius: 50%; border: 4px solid #fff; - overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.15); background: var(--primary); -} -.testimonial-card .avatar img { width: 100%; height: 100%; object-fit: cover; } -.testimonial-card blockquote { color: var(--on-surface-variant); font-style: italic; font-size: 1.125rem; line-height: 1.7; margin-bottom: 2rem; } -.testimonial-card .author { border-top: 1px solid rgba(194,199,207,0.2); padding-top: 1.5rem; } -.testimonial-card .author strong { display: block; color: var(--primary); font-weight: 700; } -.testimonial-card .author small { color: var(--gold); font-weight: 700; font-size: 0.625rem; letter-spacing: 0.15em; text-transform: uppercase; } - -/* ===== CONTACT ===== */ -.contact { padding: 6rem 0; background: #fff; } -.contact-heading { - text-align: center; margin-bottom: 4rem; -} -.contact-heading h2 { - font-family: 'Newsreader', serif; font-size: 4rem; color: var(--primary); - font-weight: 700; line-height: 1.15; max-width: 700px; margin: 0 auto; -} -.contact-heading .arrow { color: var(--gold); font-size: 2rem; margin-top: 2rem; display: block; } -.contact-card { - background: var(--cream-light); border-radius: 3.5rem; overflow: hidden; - display: flex; box-shadow: 0 1px 3px rgba(0,0,0,0.05); -} -.contact-form { flex: 55%; padding: 5rem; background: #fff; } -.contact-form h3 { font-family: 'Newsreader', serif; font-size: 2rem; color: var(--primary); font-weight: 700; margin-bottom: 1.5rem; } -.contact-form > p { color: var(--on-surface-variant); margin-bottom: 2.5rem; } -.contact-form .field { margin-bottom: 2rem; } -.contact-form label { display: block; font-size: 0.875rem; font-weight: 700; color: var(--primary); margin-bottom: 0.75rem; } -.contact-form input, -.contact-form textarea { - width: 100%; background: rgba(235,232,222,0.3); border: none; - padding: 1.25rem; font-size: 1rem; transition: box-shadow 0.2s; -} -.contact-form input:focus, -.contact-form textarea:focus { box-shadow: 0 0 0 1px var(--gold); } -.contact-form textarea { height: 10rem; resize: vertical; } -.contact-form .btn-submit { - width: 100%; background: var(--primary); color: var(--on-primary); - padding: 1.5rem; border-radius: 9999px; font-weight: 700; font-size: 1.125rem; - box-shadow: 0 10px 25px rgba(0,0,0,0.15); transition: opacity 0.2s; -} -.contact-form .btn-submit:hover { opacity: 0.95; } -.contact-info { flex: 45%; padding: 5rem; background: rgba(235,232,222,0.4); } -.contact-info h3 { font-family: 'Newsreader', serif; font-size: 1.75rem; color: var(--primary); font-weight: 700; margin-bottom: 3rem; } -.contact-item { display: flex; align-items: center; gap: 1.5rem; margin-bottom: 2rem; } -.contact-item .icon-circle { - width: 3.5rem; height: 3.5rem; background: var(--cream); border-radius: 50%; - display: flex; align-items: center; justify-content: center; color: var(--gold); flex-shrink: 0; -} -.contact-item .label-small { font-size: 0.625rem; color: var(--on-surface-variant); font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; } -.contact-item .value { font-size: 1.25rem; font-weight: 700; color: var(--primary); } -.contact-map { border-radius: 2.5rem; overflow: hidden; height: 18rem; margin-top: 3rem; box-shadow: 0 10px 25px rgba(0,0,0,0.1); } -.contact-map img { width: 100%; height: 100%; object-fit: cover; } - -/* ===== FOOTER ===== */ -.footer { padding: 6rem 0 3rem; background: var(--cream); } -.footer-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 4rem; } -.footer-brand { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 2rem; } -.footer-brand img { height: 40px; width: 40px; } -.footer-brand span { font-family: 'Newsreader', serif; font-size: 1.5rem; font-weight: 700; color: var(--primary); } -.footer-desc { color: var(--on-surface-variant); font-size: 0.875rem; line-height: 1.7; margin-bottom: 1.5rem; } -.footer-social { display: flex; gap: 1rem; } -.footer-social a { - width: 2.5rem; height: 2.5rem; border-radius: 50%; background: #fff; - display: flex; align-items: center; justify-content: center; - box-shadow: 0 1px 3px rgba(0,0,0,0.08); transition: color 0.2s; -} -.footer-social a:hover { color: var(--gold); } -.footer h4 { font-family: 'Newsreader', serif; font-size: 1.25rem; color: var(--primary); font-weight: 700; margin-bottom: 2rem; } -.footer ul li { margin-bottom: 1rem; } -.footer ul a { font-size: 0.875rem; color: var(--on-surface-variant); font-weight: 500; transition: color 0.2s; } -.footer ul a:hover { color: var(--gold); } -.footer .hours li { display: flex; justify-content: space-between; font-size: 0.875rem; color: var(--on-surface-variant); margin-bottom: 1.25rem; } -.footer .hours strong { color: var(--primary); } -.footer-bottom { - text-align: center; margin-top: 6rem; padding-top: 2.5rem; - border-top: 1px solid rgba(194,199,207,0.1); - font-size: 0.625rem; color: rgba(66,71,78,0.7); text-transform: uppercase; - letter-spacing: 0.2em; font-weight: 700; -} - -/* ===== RESPONSIVE ===== */ -@media (max-width: 1024px) { - .hero .container { grid-template-columns: 1fr; } - .hero h1 { font-size: 3rem; } - .about .container { grid-template-columns: 1fr; } - .about-quote { position: relative; bottom: auto; right: auto; margin-top: 1.5rem; } - .dogwash-card { grid-template-columns: 1fr; } - .dogwash-text { padding: 3rem; } - .dogwash-image { height: 24rem; } - .contact-card { flex-direction: column; } - .footer-grid { grid-template-columns: 1fr 1fr; gap: 2rem; } -} - -@media (max-width: 768px) { - .navbar-links { display: none; } - .menu-toggle { display: block; } - .hero h1 { font-size: 2.5rem; } - .why-grid { grid-template-columns: 1fr; } - .services-grid { grid-template-columns: 1fr; } - .testimonials-grid { grid-template-columns: 1fr; } - .about-badges { grid-template-columns: 1fr; } - .contact-heading h2 { font-size: 2.5rem; } - .contact-form, .contact-info { padding: 2.5rem; } - .footer-grid { grid-template-columns: 1fr; } -} diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..139266d --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + alias: { + $lib: 'src/lib' + } + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4344710 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..09bc160 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + fs: { + allow: ['.'] + } + } +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..8df3047 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,815 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/aix-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" + integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== + +"@esbuild/android-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752" + integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== + +"@esbuild/android-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a" + integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== + +"@esbuild/android-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16" + integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== + +"@esbuild/darwin-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd" + integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== + +"@esbuild/darwin-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e" + integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== + +"@esbuild/freebsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe" + integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== + +"@esbuild/freebsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3" + integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== + +"@esbuild/linux-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977" + integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== + +"@esbuild/linux-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9" + integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== + +"@esbuild/linux-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0" + integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== + +"@esbuild/linux-loong64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0" + integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== + +"@esbuild/linux-mips64el@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd" + integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== + +"@esbuild/linux-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869" + integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== + +"@esbuild/linux-riscv64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6" + integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== + +"@esbuild/linux-s390x@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663" + integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== + +"@esbuild/linux-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306" + integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== + +"@esbuild/netbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4" + integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== + +"@esbuild/netbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076" + integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== + +"@esbuild/openbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd" + integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== + +"@esbuild/openbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679" + integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== + +"@esbuild/openharmony-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d" + integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== + +"@esbuild/sunos-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6" + integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== + +"@esbuild/win32-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323" + integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== + +"@esbuild/win32-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267" + integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== + +"@esbuild/win32-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5" + integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.4": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.29" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1" + integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww== + +"@rollup/plugin-commonjs@^29.0.0": + version "29.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz#d2d84c49d0983d071f2ab96f4cfe02fe80abd602" + integrity sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + fdir "^6.2.0" + is-reference "1.2.1" + magic-string "^0.30.3" + picomatch "^4.0.2" + +"@rollup/plugin-json@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.1.0.tgz#fbe784e29682e9bb6dee28ea75a1a83702e7b805" + integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA== + dependencies: + "@rollup/pluginutils" "^5.1.0" + +"@rollup/plugin-node-resolve@^16.0.0": + version "16.0.3" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz#0988e6f2cbb13316b0f5e7213f757bc9ed44928f" + integrity sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.1.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz#57ba1b0cbda8e7a3c597a4853c807b156e21a7b4" + integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + +"@rollup/rollup-android-arm-eabi@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz#a19c645c375158cd5c50a344106f0fa18eb821c4" + integrity sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw== + +"@rollup/rollup-android-arm64@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz#1af19aa9d3ad6d00df2681f59cfcb8bf7499576b" + integrity sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg== + +"@rollup/rollup-darwin-arm64@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz#3b8463e03ba2a393453fea70e7d907379c27b649" + integrity sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA== + +"@rollup/rollup-darwin-x64@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz#28da23d69fe117f5f0ff330a8549e51bd09f1b6a" + integrity sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g== + +"@rollup/rollup-freebsd-arm64@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz#94bacac3190f621de1355922b599f3817786044c" + integrity sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw== + +"@rollup/rollup-freebsd-x64@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz#8a0094f533b9fda160b5c90ad9e0c78fca341788" + integrity sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz#3b7e901a555c7245c87f7440979bee0a1ec882bb" + integrity sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg== + +"@rollup/rollup-linux-arm-musleabihf@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz#ee9a09b72e8ad764cfd6188b32ff1de528ff7ebe" + integrity sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw== + +"@rollup/rollup-linux-arm64-gnu@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz#ba483f4aca9be141171d086dbd01ada6ab03b58d" + integrity sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg== + +"@rollup/rollup-linux-arm64-musl@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz#17b595b790e6df68e91c5d02526fc832a985ce4f" + integrity sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA== + +"@rollup/rollup-linux-loong64-gnu@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz#551718714075a2bfb36a2813c466e3a0e9d56abf" + integrity sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A== + +"@rollup/rollup-linux-loong64-musl@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz#ba156ed1243447a3d710972001d5dcfe3827ff3d" + integrity sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q== + +"@rollup/rollup-linux-ppc64-gnu@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz#6a957a709b86ac62ef68e597ac03dbd4336782b1" + integrity sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw== + +"@rollup/rollup-linux-ppc64-musl@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz#ca4176b4ad53f3edee3b4bfa6f9ef48ff38f167b" + integrity sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ== + +"@rollup/rollup-linux-riscv64-gnu@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz#4e6b08f72ebeafdb41f3ec433bd228ba8573473b" + integrity sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A== + +"@rollup/rollup-linux-riscv64-musl@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz#a0b8b8580c7680c8086cb3226527e5472253b895" + integrity sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ== + +"@rollup/rollup-linux-s390x-gnu@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz#79fe15b92ce0bae2b609cf26dd158cd3e2b73634" + integrity sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA== + +"@rollup/rollup-linux-x64-gnu@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz#6aa8302fa45fd3cbbc510ccd223c9c37bf67e53f" + integrity sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ== + +"@rollup/rollup-linux-x64-musl@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz#0c1a5e9799f80c47a66f2c3a5f1a280f38356047" + integrity sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw== + +"@rollup/rollup-openbsd-x64@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz#5f07c863e74fd428794f1dc5749f321b661d1f17" + integrity sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg== + +"@rollup/rollup-openharmony-arm64@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz#8e0d71324be0f423428b12b25a2eb8ea8e0a7833" + integrity sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q== + +"@rollup/rollup-win32-arm64-msvc@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz#a553fdf90a785ace6d7501eed6241c468b088999" + integrity sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ== + +"@rollup/rollup-win32-ia32-msvc@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz#0fb04f0a88027fbfd323e25a446debce4773868c" + integrity sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg== + +"@rollup/rollup-win32-x64-gnu@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz#aaa9e36dbdc0f0e397e5966dcce1b4285354ede2" + integrity sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA== + +"@rollup/rollup-win32-x64-msvc@4.60.2": + version "4.60.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz#3418dcf1388f2abd6b0ccc08fe1ef205ae76d696" + integrity sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA== + +"@standard-schema/spec@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + +"@sveltejs/acorn-typescript@^1.0.5": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz#ac0bde368d6623727b0e0bc568cf6b4e5d5c4baa" + integrity sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA== + +"@sveltejs/adapter-node@^5.2.12": + version "5.5.4" + resolved "https://registry.yarnpkg.com/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz#174eb6be21127223e539cfabf5c0b0517009e445" + integrity sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ== + dependencies: + "@rollup/plugin-commonjs" "^29.0.0" + "@rollup/plugin-json" "^6.1.0" + "@rollup/plugin-node-resolve" "^16.0.0" + rollup "^4.59.0" + +"@sveltejs/kit@^2.15.1": + version "2.57.1" + resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-2.57.1.tgz#3700c5b5549f1ffbfd42e5f71a80c2c82c0d849e" + integrity sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw== + dependencies: + "@standard-schema/spec" "^1.0.0" + "@sveltejs/acorn-typescript" "^1.0.5" + "@types/cookie" "^0.6.0" + acorn "^8.14.1" + cookie "^0.6.0" + devalue "^5.6.4" + esm-env "^1.2.2" + kleur "^4.1.5" + magic-string "^0.30.5" + mrmime "^2.0.0" + set-cookie-parser "^3.0.0" + sirv "^3.0.0" + +"@sveltejs/vite-plugin-svelte-inspector@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz#2f99a4a593bb910d1492f6c00a042b521c07147e" + integrity sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw== + dependencies: + debug "^4.3.7" + +"@sveltejs/vite-plugin-svelte@^5.0.3": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz#4db3b0475190a7148ad8740259ad57081bcfccbc" + integrity sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ== + dependencies: + "@sveltejs/vite-plugin-svelte-inspector" "^4.0.1" + debug "^4.4.1" + deepmerge "^4.3.1" + kleur "^4.1.5" + magic-string "^0.30.17" + vitefu "^1.0.6" + +"@types/cookie@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" + integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== + +"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/geojson@*": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== + +"@types/leaflet@^1.9.15": + version "1.9.21" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.21.tgz#542e8f91250bc444f8a1416d472f5b518d83e979" + integrity sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w== + dependencies: + "@types/geojson" "*" + +"@types/node@^22.10.2": + version "22.19.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.17.tgz#09c71fb34ba2510f8ac865361b1fcb9552b8a581" + integrity sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q== + dependencies: + undici-types "~6.21.0" + +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +acorn@^8.12.1, acorn@^8.14.1: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + +aria-query@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.1.tgz#ebcb2c0d7fc43e68e4cb22f774d1209cb627ab42" + integrity sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g== + +axobject-query@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== + +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +cookie@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +debug@^4.3.7, debug@^4.4.1: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +deepmerge@^4.2.2, deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +devalue@^5.6.4: + version "5.7.1" + resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.7.1.tgz#93e2d9412b909a7901d7b966ebb3479d15a390fd" + integrity sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +esbuild@^0.25.0: + version "0.25.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5" + integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.12" + "@esbuild/android-arm" "0.25.12" + "@esbuild/android-arm64" "0.25.12" + "@esbuild/android-x64" "0.25.12" + "@esbuild/darwin-arm64" "0.25.12" + "@esbuild/darwin-x64" "0.25.12" + "@esbuild/freebsd-arm64" "0.25.12" + "@esbuild/freebsd-x64" "0.25.12" + "@esbuild/linux-arm" "0.25.12" + "@esbuild/linux-arm64" "0.25.12" + "@esbuild/linux-ia32" "0.25.12" + "@esbuild/linux-loong64" "0.25.12" + "@esbuild/linux-mips64el" "0.25.12" + "@esbuild/linux-ppc64" "0.25.12" + "@esbuild/linux-riscv64" "0.25.12" + "@esbuild/linux-s390x" "0.25.12" + "@esbuild/linux-x64" "0.25.12" + "@esbuild/netbsd-arm64" "0.25.12" + "@esbuild/netbsd-x64" "0.25.12" + "@esbuild/openbsd-arm64" "0.25.12" + "@esbuild/openbsd-x64" "0.25.12" + "@esbuild/openharmony-arm64" "0.25.12" + "@esbuild/sunos-x64" "0.25.12" + "@esbuild/win32-arm64" "0.25.12" + "@esbuild/win32-ia32" "0.25.12" + "@esbuild/win32-x64" "0.25.12" + +esm-env@^1.2.1, esm-env@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/esm-env/-/esm-env-1.2.2.tgz#263c9455c55861f41618df31b20cb571fc20b75e" + integrity sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA== + +esrap@^2.2.4: + version "2.2.5" + resolved "https://registry.yarnpkg.com/esrap/-/esrap-2.2.5.tgz#23dd923664bd84d7b069ce3dea3b3c0dc91b6d3d" + integrity sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fdir@^6.2.0, fdir@^6.4.4, fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +hasown@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c" + integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg== + dependencies: + function-bind "^1.1.2" + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + +is-reference@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.3.tgz#9ef7bf9029c70a67b2152da4adf57c23d718910f" + integrity sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw== + dependencies: + "@types/estree" "^1.0.6" + +kleur@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + +leaflet@^1.9.4: + version "1.9.4" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d" + integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA== + +locate-character@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974" + integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA== + +magic-string@^0.30.11, magic-string@^0.30.17, magic-string@^0.30.3, magic-string@^0.30.5: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +mrmime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc" + integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.2, picomatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + +postcss@^8.5.3: + version "8.5.10" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.10.tgz#8992d8c30acf3f12169e7c09514a12fed7e48356" + integrity sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +resolve@^1.22.1: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^4.34.9, rollup@^4.59.0: + version "4.60.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.2.tgz#ac23fe4bd530304cef9fa61e639d7098b6762cf4" + integrity sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.60.2" + "@rollup/rollup-android-arm64" "4.60.2" + "@rollup/rollup-darwin-arm64" "4.60.2" + "@rollup/rollup-darwin-x64" "4.60.2" + "@rollup/rollup-freebsd-arm64" "4.60.2" + "@rollup/rollup-freebsd-x64" "4.60.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.60.2" + "@rollup/rollup-linux-arm-musleabihf" "4.60.2" + "@rollup/rollup-linux-arm64-gnu" "4.60.2" + "@rollup/rollup-linux-arm64-musl" "4.60.2" + "@rollup/rollup-linux-loong64-gnu" "4.60.2" + "@rollup/rollup-linux-loong64-musl" "4.60.2" + "@rollup/rollup-linux-ppc64-gnu" "4.60.2" + "@rollup/rollup-linux-ppc64-musl" "4.60.2" + "@rollup/rollup-linux-riscv64-gnu" "4.60.2" + "@rollup/rollup-linux-riscv64-musl" "4.60.2" + "@rollup/rollup-linux-s390x-gnu" "4.60.2" + "@rollup/rollup-linux-x64-gnu" "4.60.2" + "@rollup/rollup-linux-x64-musl" "4.60.2" + "@rollup/rollup-openbsd-x64" "4.60.2" + "@rollup/rollup-openharmony-arm64" "4.60.2" + "@rollup/rollup-win32-arm64-msvc" "4.60.2" + "@rollup/rollup-win32-ia32-msvc" "4.60.2" + "@rollup/rollup-win32-x64-gnu" "4.60.2" + "@rollup/rollup-win32-x64-msvc" "4.60.2" + fsevents "~2.3.2" + +sade@^1.7.4: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + +set-cookie-parser@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz#e0b1d94c8660c68e6a24dc4e2b5c9e955ccf7e28" + integrity sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw== + +sirv@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970" + integrity sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svelte-check@^4.1.1: + version "4.4.6" + resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-4.4.6.tgz#c890a102e94ef31b44bea26f5f16de14db717a3b" + integrity sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + chokidar "^4.0.1" + fdir "^6.2.0" + picocolors "^1.0.0" + sade "^1.7.4" + +svelte@^5.16.0: + version "5.55.4" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-5.55.4.tgz#b6e9d0b8fce8709ecc4ab43d5ea597a5a38b4c1e" + integrity sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ== + dependencies: + "@jridgewell/remapping" "^2.3.4" + "@jridgewell/sourcemap-codec" "^1.5.0" + "@sveltejs/acorn-typescript" "^1.0.5" + "@types/estree" "^1.0.5" + "@types/trusted-types" "^2.0.7" + acorn "^8.12.1" + aria-query "5.3.1" + axobject-query "^4.1.0" + clsx "^2.1.1" + devalue "^5.6.4" + esm-env "^1.2.1" + esrap "^2.2.4" + is-reference "^3.0.3" + locate-character "^3.0.0" + magic-string "^0.30.11" + zimmerframe "^1.1.2" + +tinyglobby@^0.2.13: + version "0.2.16" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" + integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + +typescript@^5.7.2: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +vite@^6.0.6: + version "6.4.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.2.tgz#a4e548ca3a90ca9f3724582cab35e1ba15efc6f2" + integrity sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.4" + picomatch "^4.0.2" + postcss "^8.5.3" + rollup "^4.34.9" + tinyglobby "^0.2.13" + optionalDependencies: + fsevents "~2.3.3" + +vitefu@^1.0.6: + version "1.1.3" + resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-1.1.3.tgz#59b9885b1c200856319d7e9ceb0e99a065430009" + integrity sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg== + +zimmerframe@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/zimmerframe/-/zimmerframe-1.1.4.tgz#0352b5cafad3ad4526b0a526a9a52d9c040d865b" + integrity sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==