diff --git a/src/data/content-seed.ts b/src/data/content-seed.ts index d55f900..c8396a9 100644 --- a/src/data/content-seed.ts +++ b/src/data/content-seed.ts @@ -386,6 +386,83 @@ export const contentSeed: SeedEntry[] = [ T('promo.faq.6.q', 'Posso chiedere un rimborso?'), H('promo.faq.6.a', 'Per richieste di rimborso o problemi con l\'acquisto scrivici su WhatsApp o via email. Valutiamo ogni situazione in modo diretto e umano.'), + // --- Pagina promo, stato "nessuna promozione in corso" --- + // Fra un'edizione e la successiva la pagina resta raggiungibile (è nel menu e nei link già + // girati ai clienti): mostra questo invece dei prezzi di una promozione finita. + T('promo.chiusa.title', 'Nessuna promozione in corso'), + H('promo.chiusa.desc', 'Le offerte di apertura sono terminate.
Scrivici su WhatsApp: ti raccontiamo i percorsi attivi e troviamo quello giusto per te.'), + T('promo.chiusa.cta', 'Scrivici su WhatsApp', []), + + // --- Opening Bundles (edizione `bundle-apertura`, dal 20 agosto al 4 settembre 2026) --- + // Testi dal volantino fornito dal cliente il 13/08. La struttura delle card (domanda, + // descrizione, riquadro date) è dichiarata in `data/promo-edizioni.ts`. + T('bundle.hero.badge', 'New Opening · Offerta esclusiva'), + T('bundle.hero.title1', 'Opening Bundles.'), + T('bundle.hero.accent', 'Non è uno sconto.'), + T('bundle.hero.title2', 'È un accesso privilegiato.'), + H('bundle.hero.desc', 'Tre percorsi costruiti su misura, disponibili solo per la nuova apertura.
Acquistabili fino al 4 settembre, utilizzabili dal 1° settembre al 1° dicembre.'), + T('bundle.hero.cta', 'Scopri i tre percorsi', []), + + T('bundle.buy-label', 'Acquista', []), + + T('bundle.card.1.eyebrow', 'Personal Training', []), + T('bundle.card.1.title', 'Intensive'), + T('bundle.card.1.question', 'Ti alleni ma non vedi risultati?'), + T('bundle.card.1.price', '450 €', []), + T('bundle.card.1.price-old', '770 €', []), + T('bundle.card.1.feature.1', 'Profilazione Avanzata'), + T('bundle.card.1.feature.2', '8 sessioni PT 1:1'), + T('bundle.card.1.feature.3', '4 Community Class'), + T('bundle.card.1.desc', 'Un check-up avanzato per conoscere davvero il tuo corpo — poi 2 sessioni di Personal Training e una Community Class a settimana. Un mese per capire di cosa sei capace.'), + T('bundle.card.1.note-label', 'Periodo di utilizzo', []), + H('bundle.card.1.note', 'Acquistabile fino al 4 settembre.
Utilizzabile dal 1° settembre al 1° dicembre.'), + + T('bundle.card.2.eyebrow', 'Pilates & Holistic', []), + T('bundle.card.2.title', 'Restore'), + T('bundle.card.2.question', 'Hai dolori, rigidità o ti senti bloccato?'), + T('bundle.card.2.price', '170 €', []), + T('bundle.card.2.price-old', '220 €', []), + T('bundle.card.2.feature.1', '4 Pilates Reformer'), + T('bundle.card.2.feature.2', '4 Holistic Class'), + T('bundle.card.2.desc', 'Se hai dolori, rigidità, o semplicemente vuoi prenderti cura di te con dolcezza. Reformer e Holistic Class lavorano insieme per rimettere il corpo in equilibrio — mobilità, core, respiro. Senza forzare, senza improvvisare.'), + T('bundle.card.2.note-label', 'Periodo di utilizzo', []), + H('bundle.card.2.note', 'Acquistabile fino al 4 settembre.
Utilizzabile dal 1° settembre al 1° dicembre.'), + + T('bundle.card.3.eyebrow', 'Training & Wellness', []), + T('bundle.card.3.title', 'Recharge'), + T('bundle.card.3.question', 'Sei stanco e svuotato anche dopo una notte di sonno?'), + T('bundle.card.3.price', '250 €', []), + T('bundle.card.3.price-old', '310 €', []), + T('bundle.card.3.feature.1', 'Analisi HRV'), + T('bundle.card.3.feature.2', '4 Pilates Reformer'), + T('bundle.card.3.feature.3', '3 Training Group'), + T('bundle.card.3.desc', 'Stanco, svuotato, fuori ritmo. Attraverso l\'analisi HRV misuriamo il tuo livello di stress e il tuo stato energetico reale — poi Reformer e Training Group per farti ripartire davvero.'), + T('bundle.card.3.note-label', 'Periodo di utilizzo', []), + H('bundle.card.3.note', 'Acquistabile fino al 4 settembre.
Utilizzabile dal 1° settembre al 1° dicembre.'), + + // La deadline è la stessa fine dell'edizione in `promo-edizioni.ts`: un test lo verifica, + // perché un countdown che scade a un'ora diversa dallo scambio fa contraddire il sito. + T('bundle.countdown.label', 'I bundle scadono tra', []), + T('bundle.countdown.deadline', '2026-09-04T23:59:59+02:00', []), + T('bundle.countdown.unit.days', 'giorni', []), + T('bundle.countdown.unit.hours', 'ore', []), + T('bundle.countdown.unit.minutes', 'minuti', []), + T('bundle.countdown.unit.seconds', 'secondi', []), + + T('bundle.validity-note', 'Acquistabili fino al 4 settembre 2026 · Utilizzabili dal 1° settembre al 1° dicembre 2026'), + + T('bundle.faq.title', 'Tutto quello che vuoi sapere prima di acquistare.'), + T('bundle.faq.0.q', 'Come faccio ad acquistare un bundle?'), + H('bundle.faq.0.a', 'Clicca sul bundle che desideri 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 il bundle 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('bundle.faq.1.q', 'Entro quando posso acquistarlo, e entro quando devo usarlo?'), + H('bundle.faq.1.a', 'I bundle si acquistano fino al 4 settembre 2026 e si utilizzano dal 1° settembre al 1° dicembre 2026. Dopo l\'acquisto scrivici su WhatsApp ({{whatsapp}}) per fissare il primo incontro: verrai ricontattato subito tramite messaggio o chiamata.'), + T('bundle.faq.2.q', 'Posso scambiare gli incontri fra un bundle e l\'altro?'), + H('bundle.faq.2.a', 'No: ogni bundle è un percorso costruito su un obiettivo preciso, e le attività che contiene lavorano insieme. Se ti interessano servizi di bundle diversi puoi acquistarne più di uno, oppure scrivici su WhatsApp ({{whatsapp}}) e ti aiutiamo a scegliere.'), + T('bundle.faq.3.q', 'Posso regalare un bundle a qualcun altro?'), + H('bundle.faq.3.a', 'Sì. Puoi acquistare il bundle a tuo nome — per la fatturazione — e farlo utilizzare a un\'altra persona. Ti basterà comunicarci chi userà gli incontri quando ci scriverai su WhatsApp ({{whatsapp}}) per organizzare le sessioni.'), + T('bundle.faq.4.q', 'Il pagamento è sicuro?'), + H('bundle.faq.4.a', 'Sì. Il pagamento avviene tramite Stripe, una delle piattaforme di pagamento online più sicure al mondo. I tuoi dati bancari non vengono mai condivisi con insanity.lab — tutto è gestito in modo cifrato da Stripe.'), + ]; export const seedByTag = new Map(contentSeed.map((e) => [e.tag, e])); diff --git a/src/data/promo-edizioni.ts b/src/data/promo-edizioni.ts new file mode 100644 index 0000000..8119db0 --- /dev/null +++ b/src/data/promo-edizioni.ts @@ -0,0 +1,94 @@ +// Edizioni della pagina "Promo ed eventi". +// +// Una promozione ha una fine, e la pagina che la mostra deve saperla: qui sta scritto quale +// set di contenuti è in vetrina e in quale finestra di tempo. La pagina non decide niente, +// legge l'edizione attiva nell'istante della richiesta — così il passaggio da una promozione +// alla successiva avviene da solo, alla mezzanotte giusta, senza deploy e senza che qualcuno +// debba ricordarsene. +// +// I testi restano nei tag (`.*`, modificabili senza deploy). Qui c'è solo la +// struttura: quante card, quali campi ha ciascuna, quali FAQ. Aggiungere un'edizione = +// una voce in questo elenco + i suoi tag nel seed; i test in `tests/promo-edizioni.test.ts` +// verificano che non manchi niente e che le finestre non si sovrappongano né lascino buchi. + +export interface CardSpec { + /** Numero della card: forma i tag `.card..*`. */ + id: number; + /** Quante voci ha l'elenco puntato (`feature.1` … `feature.N`). */ + features: number; + /** Card in evidenza: cornice diversa e badge `card..badge`. */ + featured?: boolean; + /** Riga sotto il prezzo, es. "5 incontri". */ + unit?: boolean; + /** Domanda in testa alla card, es. "Ti alleni ma non vedi risultati?". */ + question?: boolean; + /** Paragrafo descrittivo sotto l'elenco. */ + desc?: boolean; + /** Riquadro in fondo col periodo di acquisto e di utilizzo. */ + note?: boolean; +} + +export interface Edizione { + id: string; + /** Prefisso dei tag di contenuto di questa edizione. */ + prefix: string; + /** Inizio e fine validità, ISO **con offset esplicito**: il server gira in UTC e senza + * offset lo scambio avverrebbe due ore prima di quanto scritto sul sito. */ + from: string; + to: string; + cards: CardSpec[]; + /** Numeri delle FAQ, nell'ordine di comparsa. */ + faq: number[]; + /** Sezioni "launch" e "why5" fra l'hero e le card. */ + launch?: boolean; +} + +export const edizioni: Edizione[] = [ + { + // Promo di lancio: 4 pacchetti da 5 incontri, online dal 1° agosto. + id: 'agosto-2026', + prefix: 'promo', + from: '2026-08-01T00:00:00+02:00', + to: '2026-08-19T23:59:59+02:00', + launch: true, + cards: [ + { id: 1, features: 4, unit: true }, + { id: 2, features: 4, unit: true, featured: true }, + { id: 3, features: 3, unit: true }, + { id: 4, features: 3, unit: true }, + ], + faq: [0, 1, 2, 3, 4, 5, 6], + }, + { + // Opening Bundles: subentra alla promo agosto il giorno dopo la sua scadenza, si + // acquistano fino al 4 settembre e si usano dal 1° settembre al 1° dicembre. + id: 'bundle-apertura', + prefix: 'bundle', + from: '2026-08-20T00:00:00+02:00', + to: '2026-09-04T23:59:59+02:00', + cards: [ + { id: 1, features: 3, question: true, desc: true, note: true }, + { id: 2, features: 2, question: true, desc: true, note: true }, + // Nessuna card "in evidenza": sono tre percorsi per esigenze diverse, non tre livelli + // dello stesso prodotto, e un "più scelto" su un'offerta appena nata sarebbe inventato. + { id: 3, features: 3, question: true, desc: true, note: true }, + ], + faq: [0, 1, 2, 3, 4], + }, +]; + +/** Tag di contenuto di un'edizione: `tagEdizione(e, 'card.1.price')` → `bundle.card.1.price`. */ +export const tagEdizione = (e: Edizione, suffisso: string): string => `${e.prefix}.${suffisso}`; + +/** + * L'edizione valida in un istante, o `null` se non ce n'è nessuna (e allora la pagina mostra + * lo stato "nessuna promozione in corso" invece di prezzi scaduti). + * + * Il confronto è fra istanti assoluti: `Date.parse` interpreta l'offset scritto nel dato, e + * `now` arriva dall'orologio del server. Nessuna delle due parti dipende dal fuso della + * macchina, che in produzione è UTC e in sviluppo no. + */ +export function edizioneAttiva(now: Date, list: Edizione[] = edizioni): Edizione | null { + const t = now.getTime(); + return list.find((e) => t >= Date.parse(e.from) && t <= Date.parse(e.to)) ?? null; +} diff --git a/src/pages/promo-eventi.astro b/src/pages/promo-eventi.astro index 2c9de55..25449ee 100644 --- a/src/pages/promo-eventi.astro +++ b/src/pages/promo-eventi.astro @@ -2,99 +2,145 @@ 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, -// inserisce i dati e paga via Stripe dentro la piattaforma. Link centralizzato qui: -// quando avremo gli ID membership per singolo prodotto basterà mappare BSPORT_URL card per card. +// 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: +// il catalogo lo presenta bsport dopo l'accesso. const BSPORT_URL = 'https://backoffice.bsport.io/login/signup?membership=3428'; export const prerender = false; -// I testi sono editabili dal pannello contenuti (tag `promo.*`). Qui resta solo la -// struttura: quante card, quante feature ciascuna e quale è "in evidenza". -const cards = [ - { id: 1, features: 4, featured: false }, - { id: 2, features: 4, featured: true }, - { id: 3, features: 3, featured: false }, - { id: 4, features: 3, featured: false }, -]; -const faqs = [0, 1, 2, 3, 4, 5, 6]; +// Quale promozione è in vetrina lo decide il calendario, non un deploy: l'edizione attiva è +// quella la cui finestra contiene questo istante (vedi `data/promo-edizioni.ts`). Alla +// scadenza dell'una subentra l'altra da sola, e quando non ce n'è nessuna la pagina lo dice +// invece di lasciare online i prezzi di un'offerta finita. +// +// Anteprima: `?anteprima=2026-08-20` mostra l'edizione di quella data. Solo per chi è loggato +// — un visitatore non deve poter vedere prezzi non ancora validi, o non più. +const param = Astro.url.searchParams.get('anteprima'); +const token = Astro.cookies.get(SESSION_COOKIE)?.value; +const utente = token ? getSessionUser(getDb(), token) : null; +// Una data secca (`2026-08-20`) sarebbe letta come UTC: la si porta a mezzogiorno italiano, +// lontano dai confini di mezzanotte dove sta la differenza fra un'edizione e l'altra. +const quandoParam = param && /^\d{4}-\d{2}-\d{2}$/.test(param) ? `${param}T12:00:00+02:00` : param; +const anteprima = Boolean(utente && quandoParam && !Number.isNaN(Date.parse(quandoParam))); +const quando = anteprima ? new Date(quandoParam!) : new Date(); +const ed = edizioneAttiva(quando); +// La pagina cambia contenuto col passare del tempo: una copia in cache la congelerebbe alla +// vecchia edizione, e lo scambio avverrebbe quando pare al proxy invece che a mezzanotte. +Astro.response.headers.set('Cache-Control', 'no-store'); + +// Tag dell'edizione attiva: `tg('card.1.price')` → `promo.card.1.price` o `bundle.card.1.price`. +const tg = (suffisso: string) => (ed ? tagEdizione(ed, suffisso) : suffisso); // Numero WhatsApp centralizzato: le risposte FAQ contengono il segnaposto {{whatsapp}} // che qui viene sostituito col valore del tag globale, così si aggiorna in un punto solo. const wa = t('global.whatsapp'); -// Scadenza della promo (tag editabile dal pannello, formato ISO con offset). Viene solo passata -// al client: è il JS a decidere se mostrare il countdown, perché il confronto va fatto col tempo -// del visitatore e non con quello del render (la pagina è SSR ma può essere servita da cache). -const countdownDeadline = t('promo.countdown.deadline'); +const waLink = `https://wa.me/39${wa.replace(/\D/g, '')}`; +// Scadenza dell'edizione in corso (tag editabile dal pannello, ISO con offset). Viene solo +// passata al client: è il JS a decidere se mostrare il countdown, perché il confronto va fatto +// col tempo del visitatore. +const countdownDeadline = ed ? t(tg('countdown.deadline')) : ''; --- - +]*>/g, ' ').replace(/\s+/g, ' ').trim() : 'Le promozioni di apertura sono terminate: scrivici su WhatsApp per i percorsi attivi.'}>
-
- - - - - - -
- -
- - - - -
- -
- -
- -
-
+
+ {ed.cards.map((c) => ( +
+ {c.featured && } + + + {c.question && } +
+ + +
+ {c.unit && } +
    + {Array.from({ length: c.features }, (_, i) => i + 1).map((m) => ( + + ))} +
+ {c.desc && } + {c.note && ( +
+ + +
+ )} + +
+ ))} +
+ + -
- - {faqs.map((n) => { - const a = resolveContent(`promo.faq.${n}.a`); - return ( -
-
+
-

-

- ); - })} -
+
+ + {ed.faq.map((n) => { + const a = resolveContent(tg(`faq.${n}.a`)); + return ( +
+
+
+

+

+ ); + })} +
+ + ) : ( +
+ + + +
+ )}
@@ -229,6 +275,37 @@ const countdownDeadline = t('promo.countdown.deadline'); .lp .validity-note { text-align: center; font-size: 12px; color: #8A8980; margin: 22px 0 0; } + /* CARD DELLE EDIZIONI A TRE (Opening Bundles): domanda in testa, descrizione e riquadro + col periodo di utilizzo. Le card restano in colonna, quindi il bottone finisce ad altezze + diverse se i testi hanno lunghezze diverse: `flex` + `margin-top:auto` lo tiene in fondo. */ + .lp .cards-3 { grid-template-columns: repeat(3, 1fr); } + .lp .cards-3 .card { display: flex; flex-direction: column; } + .lp .cards-3 .card .buy { margin-top: auto; } + .lp .card .question { + margin: 0 0 14px; font-size: 15px; line-height: 1.4; font-weight: 700; color: #2D2D2D; + } + .lp .card .card-desc { + margin: 0 0 18px; font-size: 13px; line-height: 1.6; color: #6E685C; font-style: italic; + } + .lp .card .note-box { background: #F2ECE1; border-radius: 8px; padding: 10px 12px; margin: 0 0 18px; } + .lp .card .note-label { + font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700; + color: #8A6E3F; margin-bottom: 3px; + } + .lp .card .note-text { font-size: 12px; line-height: 1.5; color: #5C564A; } + + /* Pagina senza nessuna promozione attiva: fra un'edizione e la successiva. */ + .lp .hero.chiusa { padding-bottom: 72px; } + .lp .hero.chiusa h1 { font-size: 34px; } + .lp .hero.chiusa p.desc { margin-bottom: 28px; } + + /* Barra di anteprima: visibile solo a chi è loggato e ha chiesto una data (?anteprima=…). + Deve essere impossibile scambiarla per la pagina vera. */ + .lp .anteprima { + background: #8A6E3F; color: #FFFFFF; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; + padding: 10px 16px; text-align: center; + } + /* FAQ */ .lp .faq-wrap { padding: 72px 64px; } .lp .faq-wrap h1 { margin: 0 0 40px; font-size: 34px; line-height: 1.25; font-weight: 700; color: #2D2D2D; max-width: 480px; } diff --git a/tests/promo-edizioni.test.ts b/tests/promo-edizioni.test.ts new file mode 100644 index 0000000..82a6d14 --- /dev/null +++ b/tests/promo-edizioni.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from 'vitest'; +import { edizioni, edizioneAttiva, tagEdizione, type Edizione } from '../src/data/promo-edizioni'; +import { seedByTag } from '../src/data/content-seed'; + +const d = (iso: string) => new Date(iso); + +describe('edizioneAttiva', () => { + it('sceglie l\'edizione la cui finestra contiene l\'istante', () => { + expect(edizioneAttiva(d('2026-08-15T12:00:00+02:00'))?.id).toBe('agosto-2026'); + expect(edizioneAttiva(d('2026-08-25T12:00:00+02:00'))?.id).toBe('bundle-apertura'); + }); + + it('fuori da ogni finestra non c\'è edizione attiva', () => { + expect(edizioneAttiva(d('2026-07-15T12:00:00+02:00'))).toBeNull(); + expect(edizioneAttiva(d('2026-09-10T12:00:00+02:00'))).toBeNull(); + }); + + // Il passaggio è il momento in cui il sito cambia da solo: va provato al secondo, e in ora + // italiana. Il server gira in UTC, quindi un confronto fatto sull'orologio locale della + // macchina anticiperebbe (o ritarderebbe) lo scambio di due ore. + it('scambia le edizioni al secondo giusto, in ora italiana', () => { + expect(edizioneAttiva(d('2026-08-19T23:59:59+02:00'))?.id).toBe('agosto-2026'); + expect(edizioneAttiva(d('2026-08-20T00:00:00+02:00'))?.id).toBe('bundle-apertura'); + expect(edizioneAttiva(d('2026-09-04T23:59:59+02:00'))?.id).toBe('bundle-apertura'); + expect(edizioneAttiva(d('2026-09-05T00:00:00+02:00'))).toBeNull(); + }); + + it('l\'ordine dell\'elenco non cambia il risultato', () => { + const rovescio = [...edizioni].reverse(); + expect(edizioneAttiva(d('2026-08-15T12:00:00+02:00'), rovescio)?.id).toBe('agosto-2026'); + expect(edizioneAttiva(d('2026-08-25T12:00:00+02:00'), rovescio)?.id).toBe('bundle-apertura'); + }); +}); + +describe('coerenza delle edizioni configurate', () => { + const ordinate = [...edizioni].sort((a, b) => Date.parse(a.from) - Date.parse(b.from)); + + it('ogni finestra è valida e ha un verso', () => { + for (const e of edizioni) { + expect(Number.isNaN(Date.parse(e.from)), `${e.id}: from illeggibile`).toBe(false); + expect(Number.isNaN(Date.parse(e.to)), `${e.id}: to illeggibile`).toBe(false); + expect(Date.parse(e.to), `${e.id}: finisce prima di cominciare`).toBeGreaterThan(Date.parse(e.from)); + } + }); + + // Due promozioni insieme sul sito è il difetto peggiore: prezzi diversi per la stessa cosa, + // e nessuno sa quale vale. Nessun buco, invece, evita il giorno in cui la pagina non offre + // niente senza che nessuno se ne accorga: se un buco è voluto va scritto qui, non subìto. + it('le finestre non si sovrappongono e non lasciano buchi', () => { + for (let i = 1; i < ordinate.length; i++) { + const prima = ordinate[i - 1]; + const dopo = ordinate[i]; + const stacco = Date.parse(dopo.from) - Date.parse(prima.to); + expect(stacco, `${prima.id} e ${dopo.id} si sovrappongono`).toBeGreaterThan(0); + expect(stacco, `fra ${prima.id} e ${dopo.id} c'è un buco`).toBeLessThanOrEqual(1000); + } + }); + + // Il countdown è la promessa fatta al visitatore. Se dice una data e lo scambio ne usa + // un'altra, il sito si contraddice da solo: o cambia mentre il contatore corre ancora, o + // resta lì a zero. Sono lo stesso numero e devono restare lo stesso numero. + it('il countdown scade quando finisce l\'edizione', () => { + for (const e of edizioni) { + const deadline = seedByTag.get(tagEdizione(e, 'countdown.deadline'))?.value; + expect(deadline, `${e.id}: manca il tag della deadline`).toBeTruthy(); + expect(Date.parse(deadline!), `${e.id}: countdown e fine edizione non coincidono`) + .toBe(Date.parse(e.to)); + } + }); +}); + +describe('i testi di ogni edizione esistono', () => { + // Un tag che manca non rompe la pagina: `resolve` restituisce stringa vuota e logga un warn + // che nessuno legge. In vetrina diventa una card senza prezzo, o un titolo sparito. Meglio + // scoprirlo qui che dal cliente. + const richiesti = (e: Edizione): string[] => { + const base = ['hero.badge', 'hero.title1', 'hero.accent', 'hero.title2', 'hero.desc', 'hero.cta', + 'buy-label', 'validity-note', 'faq.title', + 'countdown.label', 'countdown.deadline', + 'countdown.unit.days', 'countdown.unit.hours', 'countdown.unit.minutes', 'countdown.unit.seconds']; + if (e.launch) base.push('launch.badge', 'launch.title', 'launch.accent', 'launch.desc', 'why5.text'); + for (const c of e.cards) { + base.push(`card.${c.id}.eyebrow`, `card.${c.id}.title`, `card.${c.id}.price`, `card.${c.id}.price-old`); + if (c.featured) base.push(`card.${c.id}.badge`); + if (c.unit) base.push(`card.${c.id}.unit`); + if (c.question) base.push(`card.${c.id}.question`); + if (c.desc) base.push(`card.${c.id}.desc`); + if (c.note) base.push(`card.${c.id}.note-label`, `card.${c.id}.note`); + for (let i = 1; i <= c.features; i++) base.push(`card.${c.id}.feature.${i}`); + } + for (const n of e.faq) base.push(`faq.${n}.q`, `faq.${n}.a`); + return base.map((s) => tagEdizione(e, s)); + }; + + for (const e of edizioni) { + it(`${e.id}: tutti i tag sono nel seed`, () => { + const mancanti = richiesti(e).filter((tag) => !seedByTag.has(tag)); + expect(mancanti, `tag mancanti: ${mancanti.join(', ')}`).toEqual([]); + }); + } + + it('lo stato "nessuna promozione" ha i suoi testi', () => { + for (const tag of ['promo.chiusa.title', 'promo.chiusa.desc', 'promo.chiusa.cta']) { + expect(seedByTag.has(tag), `manca ${tag}`).toBe(true); + } + }); +});