diff --git a/scripts/testi-17-agosto.mjs b/scripts/testi-17-agosto.mjs new file mode 100644 index 0000000..e491ee1 --- /dev/null +++ b/scripts/testi-17-agosto.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Porta nel database i testi decisi dal cliente il 17/08/2026 (MODIFICHE_PER_SITO.pdf). + * + * Perché serve uno script e non basta il deploy: i testi mostrati dal sito vivono in + * `content_blocks`, e `resolveContent` legge **prima** il database e solo dopo il seed nel + * codice. Un tag già presente in tabella continua quindi a mostrare il vecchio valore anche + * dopo che il seed è stato aggiornato — in produzione sono 19 righe. + * + * Perché una tantum e non una migrazione all'avvio: una riscrittura a ogni restart + * rimetterebbe le mani su testi che il cliente può cambiare dal pannello. Qui si passa una + * volta, si guarda cosa è cambiato, e poi quei testi tornano suoi. + * + * Uso: node scripts/testi-17-agosto.mjs [percorso.db] [--dry-run] + */ +import Database from 'better-sqlite3'; + +/** + * "Small Group" diventa "Personalized Training Group", **con gli articoli giusti**. + * + * Una sostituzione meccanica produrrebbe "Lo Personalized Training Group" e "gli + * personalized training group": le due frasi che portano un articolo si correggono per + * intero, prima. Il resto è la sostituzione del nome, che resta capitalizzato come nome + * proprio anche dove prima era minuscolo ("in formula small group" → "in formula + * Personalized Training Group"). + * + * Idempotente: al secondo giro non c'è più niente da sostituire. + */ +export function rinominaSmallGroup(value) { + return value + .replace(/\bLo Small Group\b/g, 'Il Personalized Training Group') + .replace(/\bgli small groups?\b/gi, 'i Personalized Training Group') + .replace(/\bsmall groups?\b/gi, 'Personalized Training Group'); +} + +/** + * I testi riscritti per intero, tag per tag: il cliente ne ha dettato il contenuto nuovo, + * quindi non c'è una parola da sostituire ma una frase da rimpiazzare. + * + * Sono solo i tag che in produzione esistono già con il valore vecchio. Quelli nuovi (la + * sezione «Check-up Avanzato», gli eventi, la pagina di scelta) non stanno in tabella e li + * serve il seed dal codice, senza bisogno di scriverli qui. + */ +export const TESTI = { + 'service.profilazione.title': 'Check-up', + 'service.profilazione.subtitle': 'Base e avanzato', + 'service.profilazione.excerpt': + 'Il Check-up Base fotografa la tua condizione di partenza e rappresenta il punto di inizio del metodo InsanityLab. Attraverso la profilazione e la valutazione fisica definiamo lo stato di salute iniziale e le priorità del percorso.\n\nIl Check-up Avanzato va oltre: con macchinari di ultima generazione misuriamo composizione corporea, parametri vitali, spirometria, HRV — un’analisi completa e scientifica che guida le nostre scelte.', + 'footer.services.4.label': 'Check-up', + 'services.meta.description': + 'I servizi InsanityLab: Personal Training, Class, Coaching, Check-up, Osteopatia e Nutrizione.', +}; + +/** I cambiamenti da fare, con il prima e il dopo. Non tocca niente. */ +export function calcola(db) { + const righe = db.prepare('SELECT tag, value FROM content_blocks').all(); + return righe + .map((r) => ({ + tag: r.tag, + prima: r.value, + dopo: r.tag in TESTI ? TESTI[r.tag] : rinominaSmallGroup(r.value), + })) + .filter((c) => c.prima !== c.dopo); +} + +export function applica(db, { dryRun = false } = {}) { + const cambi = calcola(db); + if (!dryRun && cambi.length) { + const upd = db.prepare( + "UPDATE content_blocks SET value = ?, updated_at = datetime('now'), updated_by = 'testi-17-agosto' WHERE tag = ?", + ); + db.transaction(() => { for (const c of cambi) upd.run(c.dopo, c.tag); })(); + } + return cambi; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const args = process.argv.slice(2); + const dryRun = args.includes('--dry-run'); + const path = args.find((a) => !a.startsWith('--')) ?? process.env.DB_PATH ?? 'data/insanitylab.db'; + const db = new Database(path); + const cambi = applica(db, { dryRun }); + for (const c of cambi) console.log(`${c.tag}\n - ${c.prima}\n + ${c.dopo}\n`); + console.log(`${cambi.length} contenuti ${dryRun ? 'da aggiornare (dry-run, niente scritto)' : 'aggiornati'} in ${path}`); + db.close(); +} diff --git a/src/components/home/TrainingCards.astro b/src/components/home/TrainingCards.astro index 8a710e5..91ee789 100644 --- a/src/components/home/TrainingCards.astro +++ b/src/components/home/TrainingCards.astro @@ -4,6 +4,10 @@ import { trainings } from '../../data/trainings'; import T from '../content/T.astro'; import TImg from '../content/TImg.astro'; import { t } from '../../lib/content'; +// `items` permette di mostrare solo una parte dei training (le viste di /training): +// senza, la fascia di card resterebbe l'elenco intero sopra ai blocchi filtrati. +interface Props { items?: readonly (typeof trainings)[number][] } +const { items = trainings } = Astro.props; const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/training-*.{jpg,png}', { eager: true }); const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default; const tones = ['taupe', 'tan', 'dark', 'charcoal']; @@ -11,7 +15,7 @@ const tones = ['taupe', 'tan', 'dark', 'charcoal']; const isSwapRow = (i: number) => Math.floor(i / 2) % 2 === 1; ---
- {trainings.map((tr, i) => { + {items.map((tr, i) => { const img = imgOf(tr.image); const card = (
diff --git a/src/data/calendari.ts b/src/data/calendari.ts index 502d295..a42103f 100644 --- a/src/data/calendari.ts +++ b/src/data/calendari.ts @@ -146,8 +146,22 @@ export function avvisiAttivi(now: Date, list: AvvisoCalendario[] = avvisi): Avvi .sort((x, y) => Date.parse(x.to) - Date.parse(y.to)); } -/** Servizi dalla cui pagina si apre il calendario della sala. */ -export const SERVIZI_CON_CALENDARIO = ['performance-class', 'holistic-class']; +/** + * Le schede servizio da cui si apre il calendario della sala. + * + * Sono le due class che si fanno davvero: la Performance Class è uscita di scena il + * 17/08/2026 (il cliente non la eroga più) e con lei la sua scheda. + */ +export const PAGINE_CON_CALENDARIO = ['holistic-class', 'reformer-class']; + +/** + * Le card della griglia /services che portano il bottone del calendario. + * + * È un elenco a parte, e non un doppione del precedente: nella griglia la card si chiama + * «Class» ma ha slug `performance-class`, e da lì si va alla vista Training con Holistic e + * Reformer. Il calendario è lo stesso, il punto da cui si apre no. + */ +export const CARD_CON_CALENDARIO = ['performance-class']; export interface CalendarioMostrato { cal: Calendario; diff --git a/src/data/content-seed.ts b/src/data/content-seed.ts index 286899e..b3f8d1f 100644 --- a/src/data/content-seed.ts +++ b/src/data/content-seed.ts @@ -222,7 +222,7 @@ export const contentSeed: SeedEntry[] = [ T('about.storylab.title', 'Storylab'), T('about.storylab.body', 'Scopri la nostra storia: come nasce InsanityLab, perché abbiamo deciso di creare qualcosa di diverso e chi sono i visionari che hanno trasformato questa idea in un nuovo modo di vivere benessere, movimento e salute.'), T('about.timeline.label', 'INSANITYLAB 2021-2024', []), - T('about.timeline.open-text', "Il percorso di crescita di InsanityLab si è sviluppato attraverso tappe precise e coerenti con la visione iniziale. Nato nel 2021 con un piccolo team e grandi ambizioni, il progetto ha conosciuto una forte evoluzione nel 2024, anno in cui il servizio si è ampliato attraverso nuovi format come gli small group e una sempre maggiore specializzazione nella profilazione fisica del cliente, grazie a sistemi avanzati di valutazione e monitoraggio. Parallelamente, anche l'area dedicata alla riabilitazione e all'esercizio preventivo si è consolidata, ponendo le basi per un importante salto di qualità."), + T('about.timeline.open-text', "Il percorso di crescita di InsanityLab si è sviluppato attraverso tappe precise e coerenti con la visione iniziale. Nato nel 2021 con un piccolo team e grandi ambizioni, il progetto ha conosciuto una forte evoluzione nel 2024, anno in cui il servizio si è ampliato attraverso nuovi format come i Personalized Training Group e una sempre maggiore specializzazione nella profilazione fisica del cliente, grazie a sistemi avanzati di valutazione e monitoraggio. Parallelamente, anche l'area dedicata alla riabilitazione e all'esercizio preventivo si è consolidata, ponendo le basi per un importante salto di qualità."), T('about.timeline.stage1.label', 'INSANITYLAB 2025', []), T('about.timeline.stage1.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'), T('about.timeline.stage2.label', 'INSANITYLAB 2026', []), @@ -231,7 +231,7 @@ export const contentSeed: SeedEntry[] = [ T('about.timeline.stage3.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'), // --- Services: lista (/services) --- T('services.meta.title', 'Servizi', []), - T('services.meta.description', 'I servizi InsanityLab: Personal Training, Performance Class, Coaching, Profilazione, Osteopatia e Nutrizione.', []), + T('services.meta.description', 'I servizi InsanityLab: Personal Training, Class, Coaching, Check-up, Osteopatia e Nutrizione.', []), T('services.hero.title', 'Servizi', []), T('services.intro.body', "Questi sono i servizi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata."), T('services.card.more-label', 'Per saperne di più →', []), @@ -247,6 +247,13 @@ export const contentSeed: SeedEntry[] = [ T(`service.${p.slug}.faq.${i + 1}.q`, item.q), T(`service.${p.slug}.faq.${i + 1}.a`, item.a), ]), + ...(p.extra + ? [ + T(`service.${p.slug}.extra.title`, p.extra.title), + T(`service.${p.slug}.extra.body`, p.extra.body), + ...p.extra.items.map((it, i) => T(`service.${p.slug}.extra.item.${i + 1}`, it)), + ] + : []), I(`service.${p.slug}.image`), ]), @@ -277,8 +284,9 @@ export const contentSeed: SeedEntry[] = [ // --- Training: meta, hero, intro, trial --- T('training.meta.title', 'Training', []), - T('training.meta.description', 'One to One, Small Group, Rehab, Holistic Class, Reformer Class e Coaching: trova il training InsanityLab più adatto a te.', []), + T('training.meta.description', 'One to One, Personalized Training Group, Rehab, Holistic Class, Reformer Class e Coaching: trova il training InsanityLab più adatto a te.', []), T('training.hero.title', 'training', []), + T('training.tutti.cta-label', 'Vedi tutti i training', []), T('training.intro.subtitle', 'Porta i tuoi allenamenti al livello successivo: programmi completi e percorsi su misura. Trova il training più adatto alle tue esigenze.'), // --- Contact --- @@ -293,6 +301,28 @@ export const contentSeed: SeedEntry[] = [ T('contact.info.email-label', 'E-mail'), T('contact.invite', 'Per informazioni o per richiedere una consulenza iniziale, compila il form. Un team specializzato ti accoglierà e ti ricontatterà al più presto, oppure lasciaci il tuo contatto.'), + // --- Promo ed eventi: scelta (/promo-eventi) --- + T('promo-eventi.meta.title', 'Promo ed eventi', []), + T('promo-eventi.meta.description', 'Le promozioni in corso e gli eventi di InsanityLab, a partire dall’inaugurazione del nuovo spazio.', []), + T('promo-eventi.hero.title', 'Promo ed eventi', []), + T('promo-eventi.intro', 'Scegli cosa vuoi vedere: le promozioni attive in questo momento oppure gli eventi in programma al centro.'), + T('promo-eventi.promo.label', 'Promo'), + T('promo-eventi.promo.desc', 'I pacchetti e le offerte del periodo, con prezzi e condizioni.'), + T('promo-eventi.promo.nessuna', 'Nessuna promozione in corso', []), + T('promo-eventi.promo.cta', 'Vedi le promo', []), + T('promo-eventi.eventi.label', 'Eventi'), + T('promo-eventi.eventi.desc', 'Le date da segnare: aperture, presentazioni e appuntamenti in sede.'), + T('promo-eventi.eventi.cta', 'Vedi gli eventi', []), + + // --- Eventi (/promo-eventi/eventi) --- + T('eventi.meta.title', 'Eventi', []), + T('eventi.meta.description', 'Gli eventi InsanityLab: inaugurazione del nuovo spazio il 19 settembre 2026, con aperitivo e dj set.', []), + T('eventi.hero.title', 'Eventi', []), + T('eventi.inaugurazione.title', 'Inaugurazione — 19 settembre 2026'), + T('eventi.inaugurazione.body', 'Apriamo le porte del nuovo spazio con un aperitivo e dj set. Sarà l’occasione per conoscere lo staff, visitare la struttura e prenotare una prova gratuita dei nostri servizi. Il programma completo della giornata sarà pubblicato a breve.'), + T('eventi.cta.label', 'Scrivici su WhatsApp', []), + T('eventi.torna.label', '← Torna a promo ed eventi', []), + // --- 404 --- T('notfound.meta.title', 'Pagina non trovata', []), T('notfound.eyebrow', 'Errore 404', []), @@ -327,10 +357,10 @@ export const contentSeed: SeedEntry[] = [ T('promo.card.1.feature.2', 'Check-up con profilazione fisica, il tuo punto di partenza'), T('promo.card.1.feature.3', 'PT dedicato, presente a ogni sessione'), T('promo.card.1.feature.4', 'In-sanity plan, il tuo programma personalizzato'), - // Card 2 — Small Group (in evidenza) + // Card 2 — Personalized Training Group (in evidenza) T('promo.card.2.badge', 'Più scelto', []), T('promo.card.2.eyebrow', 'Personal training', []), - T('promo.card.2.title', 'Small Group'), + T('promo.card.2.title', 'Personalized Training Group'), T('promo.card.2.price', '125 €', []), T('promo.card.2.price-old', '220 €', []), T('promo.card.2.unit', '5 incontri', []), @@ -373,12 +403,12 @@ export const contentSeed: SeedEntry[] = [ // presenti nei DB non verrebbero riscritti dall'INSERT OR IGNORE del seed). T('promo.faq.0.q', 'Come faccio ad acquistare i pacchetti?'), H('promo.faq.0.a', 'Clicca sull\'icona del pacchetto che desideri acquistare e compila tutti i campi richiesti per completare la registrazione.

Una volta terminata l\'iscrizione, riceverai un\'email con le istruzioni per scaricare la nostra app Bsport. Dopo aver effettuato l\'accesso, entra nella sezione Club: qui troverai tutti i pacchetti disponibili. Seleziona quello che preferisci e completa l\'acquisto direttamente dall\'app effettuando il pagamento.

Se hai bisogno di aiuto scrivi su WhatsApp ({{whatsapp}}) e sarai presto ricontattato!'), - T('promo.faq.1.q', 'Come funziona il pacchetto a incontri per One to One e Small Group?'), + T('promo.faq.1.q', 'Come funziona il pacchetto a incontri per One to One e Personalized Training Group?'), H('promo.faq.1.a', 'Per acquistare il pacchetto online segui le istruzioni riportate nella risposta della prima domanda. Una volta avvenuto con successo l\'acquisto, potrai contattarci scrivendoci su WhatsApp ({{whatsapp}}) per prenotare lo screening iniziale ovvero il colloquio conoscitivo per la definizione dei tuoi obiettivi. Verrai ricontattato subito tramite messaggio o chiamata! Gli incontri sono validi 2 mesi dalla data del primo incontro e comunque non oltre il 1° dicembre 2026.'), T('promo.faq.2.q', 'Come funziona il percorso Reformer o Holistic Class?'), H('promo.faq.2.a', 'Per acquistare il pacchetto online segui le istruzioni riportate nella risposta della prima domanda. Una volta scaricata l\'app Bsport e acquistato il pacchetto potrai procedere a selezionare autonomamente la class che preferisci nel giorno e nell\'orario che desideri. Gli orari delle class saranno pubblicati entro Settembre. In caso di problemi contattaci su WhatsApp ({{whatsapp}})! Gli incontri sono validi 2 mesi dalla data del primo incontro e comunque non oltre il 1° dicembre 2026.'), T('promo.faq.3.q', 'Posso usare gli incontri su servizi diversi?'), - H('promo.faq.3.a', 'Ogni pacchetto è dedicato a un singolo servizio (One to One, Small Group, Reformer o Holistic Class). Se vuoi accedere a più servizi puoi acquistare pacchetti separati per ciascuno.'), + H('promo.faq.3.a', 'Ogni pacchetto è dedicato a un singolo servizio (One to One, Personalized Training Group, Reformer o Holistic Class). Se vuoi accedere a più servizi puoi acquistare pacchetti separati per ciascuno.'), T('promo.faq.4.q', 'Posso regalare un pacchetto a qualcun altro?'), H('promo.faq.4.a', 'Sì. Puoi acquistare il pacchetto a tuo nome — per la fatturazione — e farlo utilizzare a un\'altra persona: perfetto se vuoi fare un regalo a un amico o un familiare. Ti basterà comunicarci chi userà gli incontri quando ci scriverai su WhatsApp ({{whatsapp}}) per organizzare le sessioni.'), T('promo.faq.5.q', 'Il pagamento è sicuro?'), diff --git a/src/data/services.ts b/src/data/services.ts index 5b68b8d..5338ef7 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -13,12 +13,24 @@ export interface Service { /** false = pagina dettaglio esiste ma la voce non compare nella griglia /services * (usata come approfondimento dai riquadri Training: Rehab, Holistic, Reformer). */ grid?: boolean; + /** Dove porta la card (foto e "Per saperne di più"). Assente = la scheda del servizio. + * Serve per le card che instradano a una vista di /training invece che a una scheda. */ + href?: string; + /** false = nessuna pagina dettaglio: /services/ risponde 404. + * La voce resta in griglia con il proprio `href`. */ + page?: boolean; + /** Sezione in coda alla scheda, oltre alla descrizione principale. + * Tag: service..extra.title | .body | .item.N */ + extra?: { title: string; body: string; items: string[] }; } export const services: Service[] = [ { slug: "one-to-one", title: "One to One", + // La card di /services porta alla vista Training con One to One, Personalized Training + // Group e Rehab: è come il cliente vuole instradare chi cerca l'allenamento seguito. + href: "/training/one-to-one", heroTitle: "Personal Training", subtitle: "Allenamento con un trainer dedicato", excerpt: "L’allenamento One to One di InsanityLab è un percorso su misura, seguito da un professionista dedicato, per raggiungere in modo sicuro ed efficace obiettivi di forma fisica, performance e benessere.", @@ -33,13 +45,13 @@ export const services: Service[] = [ }, { slug: "small-group", - title: "Small Group", + title: "Personalized Training Group", subtitle: "Mini gruppi (max 4 persone) con trainer dedicato", - excerpt: "Lo Small Group di InsanityLab è un allenamento in mini gruppi da massimo 4 persone con un trainer dedicato, che unisce attenzione personalizzata, motivazione e condivisione.", - longDescription: "Allenati in mini gruppi da massimo 4 persone con un trainer dedicato. Un’esperienza small group che unisce attenzione personalizzata, motivazione e condivisione: programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme, senza rinunciare alla cura del singolo.", + excerpt: "Il Personalized Training Group di InsanityLab è un allenamento in mini gruppi da massimo 4 persone con un trainer dedicato, che unisce attenzione personalizzata, motivazione e condivisione.", + longDescription: "Allenati in mini gruppi da massimo 4 persone con un trainer dedicato. Un’esperienza Personalized Training Group che unisce attenzione personalizzata, motivazione e condivisione: programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme, senza rinunciare alla cura del singolo.", features: [], faq: [ - { q: "Quante persone ci sono in un Small Group?", a: "Massimo 4 persone per gruppo, così il trainer può seguire ciascuno con attenzione." }, + { q: "Quante persone ci sono in un Personalized Training Group?", a: "Massimo 4 persone per gruppo, così il trainer può seguire ciascuno con attenzione." }, { q: "Con quale frequenza ci si allena?", a: "La frequenza viene definita insieme al trainer in base a obiettivi, disponibilità e livello di partenza." }, { q: "È adatto a chi parte da zero?", a: "Sì: il percorso è costruito sul livello dei partecipanti, con progressioni graduali e supervisione costante." }, ], @@ -47,8 +59,14 @@ export const services: Service[] = [ grid: false, }, { + // La Performance Class non si fa più (cliente, 17/08/2026): la scheda che la spiegava + // sparisce (`page: false` → /services/performance-class risponde 404) mentre la card + // "Class" resta in griglia e porta alla vista Training con Holistic e Reformer. + // Il pricing è caduto con la scheda: era l'unico servizio ad averlo. slug: "performance-class", title: "Class", + href: "/training/class", + page: false, subtitle: "Gruppo max 8 persone", excerpt: "La Performance Class è un allenamento in piccoli gruppi, basato su una programmazione progressiva che combina forza e condizionamento per ottenere risultati concreti e costanti nel tempo.", longDescription: "La Performance Class è un allenamento in piccoli gruppi con programmazione progressiva e scientifica, pensato per migliorare forza, composizione corporea e performance attraverso sessioni strutturate che combinano forza, ipertrofia e condizionamento metabolico. Non si improvvisa: ogni sessione ha un posto preciso all’interno di un piano più ampio.", @@ -61,7 +79,7 @@ export const services: Service[] = [ { q: "Che tipo di risultati o miglioramenti si ottengono?", a: "Miglioramento di forza, composizione corporea e performance, con risultati concreti e costanti nel tempo." }, { q: "Qual è l’intensità del lavoro?", a: "Alternata: fasi di bassa, media e alta intensità all’interno della stessa sessione, adattate al livello dei partecipanti." }, ], - hasPricing: true, + hasPricing: false, }, { slug: "coaching", @@ -81,9 +99,9 @@ export const services: Service[] = [ // NB: sostituisce "Pilates Flow" (il Pilates confluisce nella Holistic Class in Training). // Copy BOZZA basata sul metodo ISL (profilazione fisica) — da confermare col cliente. slug: "profilazione", - title: "Profilazione", - subtitle: "Check-up e valutazione fisica", - excerpt: "La profilazione fisica è il punto di partenza del metodo InsanityLab: una valutazione completa che definisce lo stato di salute iniziale e le priorità del percorso.", + title: "Check-up", + subtitle: "Base e avanzato", + excerpt: "Il Check-up Base fotografa la tua condizione di partenza e rappresenta il punto di inizio del metodo InsanityLab. Attraverso la profilazione e la valutazione fisica definiamo lo stato di salute iniziale e le priorità del percorso.\n\nIl Check-up Avanzato va oltre: con macchinari di ultima generazione misuriamo composizione corporea, parametri vitali, spirometria, HRV — un’analisi completa e scientifica che guida le nostre scelte.", longDescription: "Attraverso check-up, plicometria e analisi antropometriche misuriamo composizione corporea, postura e stato di forma. I dati raccolti guidano la costruzione di un programma personalizzato e permettono di monitorare i progressi nel tempo.", features: ["Check-up fisico iniziale", "Plicometria e analisi antropometriche", "Definizione delle priorità del percorso", "Monitoraggio periodico dei progressi"], faq: [ @@ -91,6 +109,19 @@ export const services: Service[] = [ { q: "Che cosa comprende?", a: "Check-up fisico, plicometria e analisi antropometriche; alcune misurazioni avanzate sono disponibili su richiesta." }, ], hasPricing: false, + // Il testo della scheda descrive il Check-up Base; l'Avanzato è la parte aggiunta il + // 17/08/2026 su richiesta del cliente, con la Wellness Tower e i sensori HRV. + extra: { + title: "Check-up Avanzato", + body: "Con la Wellness Tower, tecnologia di ultima generazione, andiamo oltre: bioimpedenza, scansione posturale 3D e spirometria restituiscono un quadro completo e oggettivo del tuo corpo. A completare il quadro, misuriamo anche l’HRV attraverso sensori dedicati — un dato chiave per capire come il tuo corpo recupera e si adatta al carico di lavoro. Ogni dato raccolto guida scelte più precise nella costruzione del programma e nel monitoraggio dei progressi.", + items: [ + "Composizione corporea di precisione (massa grassa, magra, metabolismo basale)", + "Scansione posturale 3D e analisi di simmetria", + "Spirometria e parametri cardiovascolari", + "Analisi HRV tramite sensori dedicati", + "Report immediato, confrontabile nel tempo", + ], + }, }, { // NB: sostituisce "Rehab" nella pagina Servizi (il Rehab si sposta in Training). diff --git a/src/data/site.ts b/src/data/site.ts index d8e415c..073088a 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -21,17 +21,17 @@ export const site = { ], footerTraining: [ { label: "One to one", href: "/training#one-to-one" }, - { label: "Small group", href: "/training#small-groups" }, + { label: "Personalized Training Group", href: "/training#small-groups" }, { label: "Rehab", href: "/training#rehab" }, { label: "Holistic class", href: "/training#holistic-class" }, { label: "Reformer class", href: "/training#reformer-class" }, { label: "Coaching", href: "/training#coaching" }, ], footerServices: [ - { label: "Personal Training", href: "/services/personal-training" }, - { label: "Class", href: "/services/performance-class" }, + { label: "Personal Training", href: "/training/one-to-one" }, + { label: "Class", href: "/training/class" }, { label: "Coaching", href: "/services/coaching" }, - { label: "Profilazione", href: "/services/profilazione" }, + { label: "Check-up", href: "/services/profilazione" }, { label: "Osteopatia", href: "/services/osteopatia" }, { label: "Nutrizione", href: "/services/nutrition" }, ], diff --git a/src/data/trainings.ts b/src/data/trainings.ts index d3ab22c..1ab0ba6 100644 --- a/src/data/trainings.ts +++ b/src/data/trainings.ts @@ -1,8 +1,35 @@ export const trainings = [ { id: "one-to-one", title: "One to One", caption: "Allenamento con un trainer dedicato", image: "training-one-to-one", description: "Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati. Supporto costante, attenzione totale e allenamenti personalizzati per dare il massimo.", cta: { label: "Scopri", href: "/services/one-to-one" } }, - { id: "small-groups", title: "Small Group", caption: "Mini gruppi (max 4 persone) con trainer dedicato", image: "training-small-groups", description: "Allenati in mini gruppi da massimo 4 persone con un trainer dedicato. Un’esperienza small group che unisce attenzione personalizzata, motivazione e condivisione. Programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme.", cta: { label: "Scopri", href: "/services/small-group" } }, + { id: "small-groups", title: "Personalized Training Group", caption: "Mini gruppi (max 4 persone) con trainer dedicato", image: "training-small-groups", description: "Allenati in mini gruppi da massimo 4 persone con un trainer dedicato. Un’esperienza Personalized Training Group che unisce attenzione personalizzata, motivazione e condivisione. Programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme.", cta: { label: "Scopri", href: "/services/small-group" } }, { id: "rehab", title: "Rehab", caption: "Recupero funzionale attraverso il movimento guidato", image: "training-rehab", description: "Il Rehab InsanityLab accompagna il corpo nel recupero della sua funzionalità attraverso il movimento guidato: il ponte tra la fine del percorso medico-riabilitativo e il ritorno a una vita attiva piena, con la stessa cura scientifica ma in un contesto di allenamento.", cta: { label: "Scopri", href: "/services/rehab" } }, { id: "holistic-class", title: "Holistic Class", caption: "Classi fino a 8 persone con trainer certificato", image: "training-holistic-class", description: "Mobilità articolare, respirazione, equilibrio. Da sola o integrata nei tuoi allenamenti, adatta a tutti i livelli. Sessioni da 40 minuti che uniscono Pilates Flow, Mobility e Yoga.", cta: { label: "Scopri", href: "/services/holistic-class" } }, { id: "reformer-class", title: "Reformer Class", caption: "Classi fino a 5 persone con macchinari reformer", image: "training-reformer-class", description: "Core, postura e mobilità su macchina reformer. Lavoro a basso impatto sulle articolazioni, adatto a tutti i livelli. Lezioni da 40 minuti in classi fino a 5 persone.", cta: { label: "Scopri", href: "/services/reformer-class" } }, { id: "coaching", title: "Coaching", caption: "Programma su misura e monitoraggio, anche online", image: "training-coaching", description: "Con il coaching hai un programma di allenamento su misura e un monitoraggio costante, ovunque ti alleni: in palestra, a casa o in viaggio. Il percorso sviluppa autonomia e continuità, adattandosi al tuo stile di vita (anche online, formula Fit Remote).", cta: { label: "Scopri", href: "/services/coaching" } }, ] as const; + +/** + * Le viste di /training: la pagina mostra tutti i training, oppure il solo sottoinsieme + * chiesto dal cliente (documento MODIFICHE_PER_SITO.pdf del 17/08/2026). + * + * Da /services la card «One to One» porta a One to One / Personalized Training Group / Rehab, + * la card «Class» a Holistic Class / Reformer Class: chi clicca cerca quella famiglia di + * servizi, e vedersi comparire l'elenco intero costringe a cercare di nuovo. + */ +export const VISTE_TRAINING: Record = { + 'one-to-one': { titolo: 'Allenamento seguito', blocchi: ['one-to-one', 'small-groups', 'rehab'] }, + class: { titolo: 'Class', blocchi: ['holistic-class', 'reformer-class'] }, +}; + +/** + * I training da mostrare per una vista. `undefined` = pagina intera. + * + * Una vista sconosciuta restituisce `null`, e la pagina risponde 404: mostrare l'elenco + * completo al posto di un filtro sbagliato farebbe passare per buono un indirizzo rotto — + * il link continuerebbe a "funzionare" mentre porta altrove. + */ +export function trainingDaMostrare(vista?: string): typeof trainings[number][] | null { + if (!vista) return [...trainings]; + const v = VISTE_TRAINING[vista]; + if (!v) return null; + return v.blocchi.map((id) => trainings.find((t) => t.id === id)!).filter(Boolean); +} diff --git a/src/pages/promo-eventi/eventi.astro b/src/pages/promo-eventi/eventi.astro new file mode 100644 index 0000000..9907a4f --- /dev/null +++ b/src/pages/promo-eventi/eventi.astro @@ -0,0 +1,36 @@ +--- +import Base from '../../layouts/Base.astro'; +import PageHero from '../../components/PageHero.astro'; +import T from '../../components/content/T.astro'; +import { t } from '../../lib/content'; +export const prerender = false; + +// Gli eventi del centro. Oggi ce n'è uno solo — l'inaugurazione del 19 settembre — e il +// programma della giornata non c'è ancora: il testo lo dichiara invece di lasciare una +// pagina vuota, ed è editabile dal pannello, così il cliente lo completa senza un deploy. +const wa = t('global.whatsapp'); +const waLink = `https://wa.me/39${wa.replace(/\D/g, '')}`; +--- + + + +
+ +
+ + + diff --git a/src/pages/promo-eventi/index.astro b/src/pages/promo-eventi/index.astro new file mode 100644 index 0000000..194213e --- /dev/null +++ b/src/pages/promo-eventi/index.astro @@ -0,0 +1,56 @@ +--- +import Base from '../../layouts/Base.astro'; +import PageHero from '../../components/PageHero.astro'; +import T from '../../components/content/T.astro'; +import { t } from '../../lib/content'; +import { edizioneAttiva } from '../../data/promo-edizioni'; +export const prerender = false; + +// Chi entra da «Promo ed eventi» sceglie: le promozioni in corso oppure gli eventi del +// centro (chiesto dal cliente il 17/08/2026). Erano la stessa pagina, e chi cercava +// l'inaugurazione finiva in mezzo ai prezzi dei pacchetti. +// +// Il bottone delle promo dice se ce n'è davvero una in corso: mandare su una pagina che +// risponde «nessuna promozione» senza averlo detto prima è un giro a vuoto. +const ed = edizioneAttiva(new Date()); +// La pagina cambia contenuto col passare del tempo (l'edizione promo scade da sola): una +// copia in cache la congelerebbe a com'era. +Astro.response.headers.set('Cache-Control', 'no-store'); +--- + + + +
+ +
+ + + diff --git a/src/pages/promo-eventi.astro b/src/pages/promo-eventi/promo.astro similarity index 98% rename from src/pages/promo-eventi.astro rename to src/pages/promo-eventi/promo.astro index 7f678d3..bf2ccc5 100644 --- a/src/pages/promo-eventi.astro +++ b/src/pages/promo-eventi/promo.astro @@ -1,10 +1,10 @@ --- -import Base from '../layouts/Base.astro'; -import T from '../components/content/T.astro'; -import { t, resolveContent } from '../lib/content'; -import { edizioneAttiva, tagEdizione } from '../data/promo-edizioni'; -import { getSessionUser, SESSION_COOKIE } from '../lib/auth'; -import { getDb } from '../lib/db'; +import Base from '../../layouts/Base.astro'; +import T from '../../components/content/T.astro'; +import { t, resolveContent } from '../../lib/content'; +import { edizioneAttiva, tagEdizione } from '../../data/promo-edizioni'; +import { getSessionUser, SESSION_COOKIE } from '../../lib/auth'; +import { getDb } from '../../lib/db'; // Landing promo/lancio integrata nel sito (header e footer del sito, non standalone). // I bottoni "Acquista" portano al gestionale bsport: l'utente crea l'account e sceglie lì // dentro il prodotto, poi paga via Stripe. Il link è uno solo per tutte le card, ed è voluto: diff --git a/src/pages/services/[slug].astro b/src/pages/services/[slug].astro index 231c648..e57bf97 100644 --- a/src/pages/services/[slug].astro +++ b/src/pages/services/[slug].astro @@ -8,18 +8,20 @@ import { serviceIcons } from '../../data/service-icons'; import T from '../../components/content/T.astro'; import TImg from '../../components/content/TImg.astro'; import CalendarioModal from '../../components/CalendarioModal.astro'; -import { calendariDaMostrare, SERVIZI_CON_CALENDARIO } from '../../data/calendari'; +import { calendariDaMostrare, PAGINE_CON_CALENDARIO } from '../../data/calendari'; import { t } from '../../lib/content'; export const prerender = false; const service = getService(Astro.params.slug!); -if (!service) return new Response(null, { status: 404 }); +// `page: false` = servizio senza scheda (oggi la Performance Class, che non si eroga più): +// la voce esiste ancora nei dati per la card in griglia, ma la pagina non c'è. +if (!service || service.page === false) return new Response(null, { status: 404 }); const slug = service.slug; // Il calendario della sala si apre dalle pagine delle class. La voce compare solo se c'è // davvero una griglia da mostrare (in vigore o prossima): un link che apre un popup vuoto è // peggio di nessun link. -const conCalendario = SERVIZI_CON_CALENDARIO.includes(slug) && calendariDaMostrare(new Date()).length > 0; -const others = services.filter((p) => p.slug !== service.slug && p.grid !== false); +const conCalendario = PAGINE_CON_CALENDARIO.includes(slug) && calendariDaMostrare(new Date()).length > 0; +const others = services.filter((p) => p.slug !== service.slug && p.grid !== false && p.page !== false); const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/program-*.jpg', { eager: true }); const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default; const heroImg = imgOf(service.slug); @@ -44,6 +46,15 @@ const tones = ['tan', 'taupe', 'dark']; {heroImg && ( )} + {service.extra && ( +
+ + +
    + {service.extra.items.map((_, i) => )} +
+
+ )}
{service.faq.map((_, i) => (
@@ -119,6 +130,8 @@ const tones = ['tan', 'taupe', 'dark']; .prog { display: grid; grid-template-columns: 2.2fr 1fr; gap: 60px; } .prog :global(.prog__desc) { white-space: pre-line; } .prog__features { padding-left: 20px; display: grid; gap: 8px; margin-bottom: 30px; } + .prog__extra { margin-bottom: 10px; } + .prog__extra h2 { letter-spacing: .1em; margin-bottom: .5em; } .prog__features li::marker { color: var(--c-accent); } .prog__img { width: 100%; height: auto; margin-bottom: 30px; } .prog__faq details { border-top: 1px solid #e4ded6; padding-block: 14px; } diff --git a/src/pages/services/index.astro b/src/pages/services/index.astro index ff7b0cc..edbf294 100644 --- a/src/pages/services/index.astro +++ b/src/pages/services/index.astro @@ -5,6 +5,8 @@ import type { ImageMetadata } from 'astro'; import { services } from '../../data/services'; import T from '../../components/content/T.astro'; import TImg from '../../components/content/TImg.astro'; +import CalendarioModal from '../../components/CalendarioModal.astro'; +import { calendariDaMostrare, CARD_CON_CALENDARIO } from '../../data/calendari'; import { t } from '../../lib/content'; const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/program-*.jpg', { eager: true }); const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default; @@ -13,7 +15,13 @@ const gridServices = services.filter((p) => p.grid !== false); // costruzione: erano due espressioni diverse e sono divergute — su One to One e Performance // Class il testo mandava a /training mentre la foto apriva la scheda del servizio, così la // destinazione dipendeva da dove capitava il dito. -const hrefOf = (slug: string) => `/services/${slug}`; +// Alcune card non portano alla propria scheda ma a una vista di /training (One to One e +// Class, deciso dal cliente il 17/08/2026): la destinazione la dichiara il servizio. +const hrefOf = (p: (typeof services)[number]) => p.href ?? `/services/${p.slug}`; +// Il calendario delle class si apre anche da qui, dalla card Class: chi guarda i servizi +// vuole sapere quando si allena prima ancora di aprire la scheda. Compare solo se c'è +// davvero una griglia da mostrare, come nelle pagine delle class. +const conCalendario = calendariDaMostrare(new Date()).length > 0; export const prerender = false; --- @@ -27,7 +35,7 @@ export const prerender = false; {gridServices.map((p, i) => (
))}
+ {conCalendario && } diff --git a/src/pages/training.astro b/src/pages/training/[...vista].astro similarity index 52% rename from src/pages/training.astro rename to src/pages/training/[...vista].astro index f9e8d0a..3a52fc8 100644 --- a/src/pages/training.astro +++ b/src/pages/training/[...vista].astro @@ -1,28 +1,37 @@ --- -import Base from '../layouts/Base.astro'; -import PageHero from '../components/PageHero.astro'; -import TrainingCards from '../components/home/TrainingCards.astro'; +import Base from '../../layouts/Base.astro'; +import PageHero from '../../components/PageHero.astro'; +import TrainingCards from '../../components/home/TrainingCards.astro'; import { Image } from 'astro:assets'; -import { trainings } from '../data/trainings'; -import T from '../components/content/T.astro'; -import { t } from '../lib/content'; -const images = import.meta.glob<{ default: ImageMetadata }>('../assets/img/training-*.{jpg,png}', { eager: true }); +import type { ImageMetadata } from 'astro'; +import { VISTE_TRAINING, trainingDaMostrare } from '../../data/trainings'; +import T from '../../components/content/T.astro'; +import { t } from '../../lib/content'; +const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/training-*.{jpg,png}', { eager: true }); const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1]?.default; export const prerender = false; + +// `/training` mostra tutto; `/training/one-to-one` e `/training/class` il solo sottoinsieme +// da cui si arriva cliccando la card corrispondente in /services. Un indirizzo inventato è +// 404 e non l'elenco intero: un filtro che non c'è deve vedersi, non passare per buono. +const vista = Astro.params.vista; +const daMostrare = trainingDaMostrare(vista); +if (!daMostrare) return new Response(null, { status: 404 }); +const filtrata = Boolean(vista); ---
- + {filtrata ?

{VISTE_TRAINING[vista!].titolo}

: }
- +
- {trainings.map((tr, i) => ( + {daMostrare.map((tr, i) => (
{imgOf(tr.image) && {t(`training.${tr.id}.title`)}}
@@ -32,6 +41,11 @@ export const prerender = false;
))} + {/* Da una vista filtrata si esce: senza questo link chi è entrato da una card di + /services non ha modo di vedere gli altri training se non dal menu. */} + {filtrata && ( +

{t('training.tutti.cta-label')}

+ )}
@@ -40,5 +54,6 @@ export const prerender = false; .tblock { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; padding-block: 40px; scroll-margin-top: calc(var(--header-h) + 20px); } .tblock--reverse img { order: 2; } .tblock h2 { letter-spacing: .1em; } + .tblock__tutti { text-align: center; margin-top: 20px; } @media (max-width: 767px) { .tblock { grid-template-columns: 1fr; gap: 24px; } .tblock--reverse img { order: 0; } } diff --git a/tests/modifiche-agosto.test.ts b/tests/modifiche-agosto.test.ts new file mode 100644 index 0000000..16f1a79 --- /dev/null +++ b/tests/modifiche-agosto.test.ts @@ -0,0 +1,189 @@ +import { describe, it, expect } from 'vitest'; +import { createDb } from '../src/lib/db'; +import { services, getService } from '../src/data/services'; +import { trainings, trainingDaMostrare, VISTE_TRAINING } from '../src/data/trainings'; +import { contentSeed } from '../src/data/content-seed'; +import { site } from '../src/data/site'; +import { PAGINE_CON_CALENDARIO, CARD_CON_CALENDARIO } from '../src/data/calendari'; +import { rinominaSmallGroup, calcola, applica, TESTI } from '../scripts/testi-17-agosto.mjs'; + +// Le modifiche chieste dal cliente il 17/08/2026 (MODIFICHE_PER_SITO.pdf). + +describe('Small Group è diventato Personalized Training Group', () => { + it('non resta traccia del vecchio nome nei dati né nel seed', () => { + const testi = [ + ...services.flatMap((s) => [s.title, s.subtitle, s.excerpt, s.longDescription, ...s.features, ...s.faq.flatMap((f) => [f.q, f.a])]), + ...trainings.flatMap((t) => [t.title, t.caption, t.description]), + ...site.footerTraining.map((v) => v.label), + ...contentSeed.map((e) => e.value), + ]; + expect(testi.filter((v) => /small group/i.test(v))).toEqual([]); + }); + + // Lo slug resta `small-group`: è l'indirizzo della scheda, e cambiarlo romperebbe i link + // già girati su WhatsApp senza che nessuno se ne accorga. + it('lo slug della scheda non cambia', () => { + expect(getService('small-group')?.title).toBe('Personalized Training Group'); + }); + + it('gli articoli restano corretti, e passarci due volte non cambia niente', () => { + const casi: [string, string][] = [ + ['Lo Small Group di InsanityLab è un allenamento', 'Il Personalized Training Group di InsanityLab è un allenamento'], + ['nuovi format come gli small group e una', 'nuovi format come i Personalized Training Group e una'], + ['One to One / Small Group / Rehab', 'One to One / Personalized Training Group / Rehab'], + ['Small Groups', 'Personalized Training Group'], + ['in formula small group (massimo 4 persone)', 'in formula Personalized Training Group (massimo 4 persone)'], + ]; + for (const [prima, dopo] of casi) { + expect(rinominaSmallGroup(prima)).toBe(dopo); + expect(rinominaSmallGroup(dopo)).toBe(dopo); + } + }); +}); + +describe('lo script porta i testi nuovi in un database già scritto', () => { + it('aggiorna le righe vecchie e al secondo giro non ha più niente da fare', () => { + const db = createDb(':memory:'); + const scrivi = db.prepare('INSERT OR REPLACE INTO content_blocks (tag, type, value) VALUES (?, ?, ?)'); + scrivi.run('service.one-to-one.subtitle', 'text', 'One to One / Small Group / Rehab'); + scrivi.run('service.profilazione.subtitle', 'text', 'Profilazione e valutazione fisica'); + scrivi.run('footer.services.4.label', 'text', 'Profilazione'); + + const cambi = applica(db); + const dopo = (tag: string) => (db.prepare('SELECT value FROM content_blocks WHERE tag = ?').get(tag) as { value: string }).value; + expect(dopo('service.one-to-one.subtitle')).toBe('One to One / Personalized Training Group / Rehab'); + expect(dopo('service.profilazione.subtitle')).toBe('Base e avanzato'); + expect(dopo('footer.services.4.label')).toBe('Check-up'); + expect(cambi.length).toBeGreaterThanOrEqual(3); + + expect(calcola(db)).toEqual([]); + }); + + // Un `--dry-run` che scrivesse renderebbe inutile guardare prima di toccare la produzione. + it('con --dry-run non tocca niente', () => { + const db = createDb(':memory:'); + db.prepare('INSERT OR REPLACE INTO content_blocks (tag, type, value) VALUES (?, ?, ?)') + .run('promo.card.2.title', 'text', 'Small Group'); + expect(applica(db, { dryRun: true }).length).toBe(1); + expect((db.prepare('SELECT value FROM content_blocks WHERE tag = ?').get('promo.card.2.title') as { value: string }).value) + .toBe('Small Group'); + }); + + it('i testi riscritti a mano corrispondono a quelli del seed', () => { + const seed = new Map(contentSeed.map((e) => [e.tag, e.value])); + for (const [tag, valore] of Object.entries(TESTI)) expect(seed.get(tag)).toBe(valore); + }); +}); + +describe('viste di /training', () => { + it('senza vista si vedono tutti i training', () => { + expect(trainingDaMostrare()?.map((t) => t.id)).toEqual(trainings.map((t) => t.id)); + }); + + it('One to One porta a One to One, Personalized Training Group e Rehab', () => { + expect(trainingDaMostrare('one-to-one')?.map((t) => t.id)).toEqual(['one-to-one', 'small-groups', 'rehab']); + }); + + it('Class porta a Holistic e Reformer', () => { + expect(trainingDaMostrare('class')?.map((t) => t.id)).toEqual(['holistic-class', 'reformer-class']); + }); + + // Un filtro sbagliato che mostra tutto farebbe passare per buono un indirizzo rotto. + it('una vista inventata è 404, non l\'elenco intero', () => { + expect(trainingDaMostrare('inesistente')).toBeNull(); + }); + + it('ogni vista nomina solo training che esistono', () => { + for (const v of Object.values(VISTE_TRAINING)) { + for (const id of v.blocchi) expect(trainings.some((t) => t.id === id)).toBe(true); + } + }); +}); + +describe('instradamento delle card di /services', () => { + const href = (slug: string) => { const s = getService(slug)!; return s.href ?? `/services/${s.slug}`; }; + + it('One to One e Class portano alle viste di training', () => { + expect(href('one-to-one')).toBe('/training/one-to-one'); + expect(href('performance-class')).toBe('/training/class'); + }); + + it('Check-up porta alla propria scheda', () => { + expect(href('profilazione')).toBe('/services/profilazione'); + }); + + // La Performance Class non si eroga più: la card resta, la scheda che la spiegava no. + it('la Performance Class non ha più una scheda ma resta in griglia', () => { + expect(getService('performance-class')?.page).toBe(false); + expect(getService('performance-class')?.grid).not.toBe(false); + }); + + it('nessun link del sito manda a una scheda che non esiste', () => { + const schede = new Set(services.filter((s) => s.page !== false).map((s) => `/services/${s.slug}`)); + const interni = [...site.footerServices, ...site.footerTraining, ...site.navigation] + .map((v) => v.href) + .filter((h) => h.startsWith('/services/')); + for (const h of interni) expect(schede.has(h)).toBe(true); + }); +}); + +describe('Check-up', () => { + const checkup = getService('profilazione')!; + + it('il trafiletto della card racconta Base e Avanzato', () => { + expect(checkup.title).toBe('Check-up'); + expect(checkup.subtitle).toBe('Base e avanzato'); + expect(checkup.excerpt).toContain('Check-up Base'); + expect(checkup.excerpt).toContain('Check-up Avanzato'); + }); + + it('la scheda porta la sezione avanzata con i suoi cinque punti', () => { + expect(checkup.extra?.title).toBe('Check-up Avanzato'); + expect(checkup.extra?.body).toContain('Wellness Tower'); + expect(checkup.extra?.items).toHaveLength(5); + }); + + it('i testi della sezione avanzata sono modificabili dal pannello', () => { + const tags = new Set(contentSeed.map((e) => e.tag)); + expect(tags.has('service.profilazione.extra.title')).toBe(true); + expect(tags.has('service.profilazione.extra.body')).toBe(true); + for (let i = 1; i <= 5; i++) expect(tags.has(`service.profilazione.extra.item.${i}`)).toBe(true); + }); +}); + +describe('calendario delle class', () => { + // Il calendario si apre da due punti diversi: le schede delle class che si fanno, e la + // card «Class» della griglia servizi, che ha slug `performance-class`. + it('le schede sono Holistic e Reformer, la Performance Class è fuori', () => { + expect(PAGINE_CON_CALENDARIO).toEqual(['holistic-class', 'reformer-class']); + }); + + it('in griglia il bottone sta sulla card Class', () => { + expect(CARD_CON_CALENDARIO).toEqual(['performance-class']); + }); + + it('ogni slug nominato esiste davvero', () => { + for (const slug of [...PAGINE_CON_CALENDARIO, ...CARD_CON_CALENDARIO]) expect(getService(slug)).toBeDefined(); + }); +}); + +describe('promo ed eventi', () => { + it('i testi della pagina di scelta e degli eventi sono nel seed', () => { + const tags = new Set(contentSeed.map((e) => e.tag)); + for (const tag of ['promo-eventi.hero.title', 'promo-eventi.promo.label', 'promo-eventi.eventi.label', + 'eventi.hero.title', 'eventi.inaugurazione.title', 'eventi.inaugurazione.body']) { + expect(tags.has(tag)).toBe(true); + } + }); + + it('l\'inaugurazione dice la data e che il programma manca ancora', () => { + const testo = contentSeed.find((e) => e.tag === 'eventi.inaugurazione.body')!.value; + expect(contentSeed.find((e) => e.tag === 'eventi.inaugurazione.title')!.value).toContain('19 settembre 2026'); + expect(testo).toContain('programma completo'); + }); + + // La voce di menu continua a puntare qui: è l'indirizzo già girato ai clienti. + it('il menu punta alla pagina di scelta', () => { + expect(site.navigation.some((v) => v.href === '/promo-eventi')).toBe(true); + }); +});