Promo: le edizioni si scambiano da sole alla data stabilita
La pagina non mostra piu' una promozione sola scritta nel codice: sceglie l'edizione la cui finestra contiene l'istante della richiesta. Il 20 agosto alle 00:00 la promo agosto lascia il posto agli Opening Bundles senza deploy, e dopo il 4 settembre la pagina dichiara che non c'e' nessuna promozione invece di lasciare online prezzi scaduti. Le edizioni sono dato (data/promo-edizioni.ts): finestra, quante card, quali campi ha ciascuna. I testi restano tag modificabili dal pannello, col prefisso dell'edizione. I bundle aggiungono domanda, descrizione e riquadro del periodo di utilizzo, presi dal volantino del cliente. Le date portano l'offset esplicito: il server gira in UTC e senza offset lo scambio avverrebbe due ore prima di quanto scritto sul sito. La pagina esce con Cache-Control: no-store, perche' una copia in cache la congelerebbe all'edizione vecchia. Dieci test: le finestre non si sovrappongono e non lasciano buchi, il countdown scade quando finisce l'edizione, e ogni tag richiesto esiste nel seed - un tag mancante non romperebbe la pagina, lascerebbe una card senza prezzo. Anteprima con ?anteprima=<data>, solo per chi e' loggato. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <span class="hl">WhatsApp</span> 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.<br>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.<br>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 <strong>4 settembre</strong>.<br>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 <strong>4 settembre</strong>.<br>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 <strong>4 settembre</strong>.<br>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.<br><br>Una volta terminata l\'iscrizione, riceverai un\'email con le istruzioni per scaricare la nostra app <span class="hl">Bsport</span>. Dopo aver effettuato l\'accesso, entra nella sezione <span class="hl">Club</span>: qui troverai tutti i pacchetti disponibili. Seleziona il bundle che preferisci e completa l\'acquisto direttamente dall\'app effettuando il pagamento.<br><br>Se hai bisogno di aiuto scrivi su <span class="hl">WhatsApp ({{whatsapp}})</span> 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 <span class="hl">fino al 4 settembre 2026</span> e si utilizzano <span class="hl">dal 1° settembre al 1° dicembre 2026</span>. Dopo l\'acquisto scrivici su <span class="hl">WhatsApp ({{whatsapp}})</span> 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 <span class="hl">WhatsApp ({{whatsapp}})</span> 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 <span class="hl">WhatsApp ({{whatsapp}})</span> per organizzare le sessioni.'),
|
||||
T('bundle.faq.4.q', 'Il pagamento è sicuro?'),
|
||||
H('bundle.faq.4.a', 'Sì. Il pagamento avviene tramite <span class="hl">Stripe</span>, 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]));
|
||||
|
||||
@@ -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 (`<prefix>.*`, 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 `<prefix>.card.<id>.*`. */
|
||||
id: number;
|
||||
/** Quante voci ha l'elenco puntato (`feature.1` … `feature.N`). */
|
||||
features: number;
|
||||
/** Card in evidenza: cornice diversa e badge `card.<id>.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;
|
||||
}
|
||||
+155
-78
@@ -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')) : '';
|
||||
---
|
||||
<Base title="Promo ed eventi — InsanityLab" description="Edizione apertura: pacchetti a 5 incontri per One to One, Small Group, Reformer e Holistic Class. Acquisto online, pagamento sicuro via Stripe.">
|
||||
<Base title="Promo ed eventi — InsanityLab" description={ed ? t(tg('hero.desc')).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim() : 'Le promozioni di apertura sono terminate: scrivici su WhatsApp per i percorsi attivi.'}>
|
||||
<div class="lp">
|
||||
<div class="page">
|
||||
|
||||
<section class="hero">
|
||||
<T tag="promo.hero.badge" as="div" class="badge" />
|
||||
<T tag="promo.hero.title1" as="h1" />
|
||||
<T tag="promo.hero.accent" as="p" class="accent" />
|
||||
<T tag="promo.hero.title2" as="h1" />
|
||||
<T tag="promo.hero.desc" as="p" class="desc" />
|
||||
<a class="btn-sand" href="#proposte"><T tag="promo.hero.cta" as="span" /></a>
|
||||
</section>
|
||||
|
||||
<section class="launch">
|
||||
<T tag="promo.launch.badge" as="div" class="badge" />
|
||||
<T tag="promo.launch.title" as="h1" />
|
||||
<T tag="promo.launch.accent" as="p" class="accent" />
|
||||
<T tag="promo.launch.desc" as="p" class="desc" />
|
||||
</section>
|
||||
|
||||
<section class="why5">
|
||||
<T tag="promo.why5.text" as="p" />
|
||||
</section>
|
||||
|
||||
<section class="cards-wrap" id="proposte">
|
||||
<div class="countdown" data-deadline={countdownDeadline} hidden>
|
||||
<T tag="promo.countdown.label" as="div" class="cd-label" />
|
||||
<div class="cd-boxes">
|
||||
<div class="cd-box"><span class="cd-num" data-cd="days">–</span><T tag="promo.countdown.unit.days" as="span" class="cd-unit" /></div>
|
||||
<div class="cd-box"><span class="cd-num" data-cd="hours">–</span><T tag="promo.countdown.unit.hours" as="span" class="cd-unit" /></div>
|
||||
<div class="cd-box"><span class="cd-num" data-cd="minutes">–</span><T tag="promo.countdown.unit.minutes" as="span" class="cd-unit" /></div>
|
||||
<div class="cd-box"><span class="cd-num" data-cd="seconds">–</span><T tag="promo.countdown.unit.seconds" as="span" class="cd-unit" /></div>
|
||||
</div>
|
||||
{anteprima && (
|
||||
<div class="anteprima">
|
||||
Anteprima al {quando.toLocaleString('it-IT', { timeZone: 'Europe/Rome' })} —
|
||||
{ed ? ` edizione «${ed.id}»` : ' nessuna edizione attiva'}
|
||||
</div>
|
||||
<div class="cards">
|
||||
{cards.map((c) => (
|
||||
<div class:list={['card', c.featured && 'featured']}>
|
||||
{c.featured && <T tag={`promo.card.${c.id}.badge`} as="div" class="featured-badge" />}
|
||||
<T tag={`promo.card.${c.id}.eyebrow`} as="div" class="eyebrow" />
|
||||
<T tag={`promo.card.${c.id}.title`} as="div" class="title" />
|
||||
<div class="price-row">
|
||||
<T tag={`promo.card.${c.id}.price`} as="div" class="price" />
|
||||
<T tag={`promo.card.${c.id}.price-old`} as="div" class="price-old" />
|
||||
)}
|
||||
|
||||
{ed ? (
|
||||
<Fragment>
|
||||
<section class="hero">
|
||||
<T tag={tg('hero.badge')} as="div" class="badge" />
|
||||
<T tag={tg('hero.title1')} as="h1" />
|
||||
<T tag={tg('hero.accent')} as="p" class="accent" />
|
||||
<T tag={tg('hero.title2')} as="h1" />
|
||||
<T tag={tg('hero.desc')} as="p" class="desc" />
|
||||
<a class="btn-sand" href="#proposte"><T tag={tg('hero.cta')} as="span" /></a>
|
||||
</section>
|
||||
|
||||
{ed.launch && (
|
||||
<Fragment>
|
||||
<section class="launch">
|
||||
<T tag={tg('launch.badge')} as="div" class="badge" />
|
||||
<T tag={tg('launch.title')} as="h1" />
|
||||
<T tag={tg('launch.accent')} as="p" class="accent" />
|
||||
<T tag={tg('launch.desc')} as="p" class="desc" />
|
||||
</section>
|
||||
|
||||
<section class="why5">
|
||||
<T tag={tg('why5.text')} as="p" />
|
||||
</section>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<section class="cards-wrap" id="proposte">
|
||||
<div class="countdown" data-deadline={countdownDeadline} hidden>
|
||||
<T tag={tg('countdown.label')} as="div" class="cd-label" />
|
||||
<div class="cd-boxes">
|
||||
<div class="cd-box"><span class="cd-num" data-cd="days">–</span><T tag={tg('countdown.unit.days')} as="span" class="cd-unit" /></div>
|
||||
<div class="cd-box"><span class="cd-num" data-cd="hours">–</span><T tag={tg('countdown.unit.hours')} as="span" class="cd-unit" /></div>
|
||||
<div class="cd-box"><span class="cd-num" data-cd="minutes">–</span><T tag={tg('countdown.unit.minutes')} as="span" class="cd-unit" /></div>
|
||||
<div class="cd-box"><span class="cd-num" data-cd="seconds">–</span><T tag={tg('countdown.unit.seconds')} as="span" class="cd-unit" /></div>
|
||||
</div>
|
||||
<T tag={`promo.card.${c.id}.unit`} as="div" class="unit" />
|
||||
<ul>
|
||||
{Array.from({ length: c.features }, (_, i) => i + 1).map((m) => (
|
||||
<T tag={`promo.card.${c.id}.feature.${m}`} as="li" />
|
||||
))}
|
||||
</ul>
|
||||
<a class="buy" href={BSPORT_URL} target="_blank" rel="noopener"><T tag="promo.buy-label" as="span" /></a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<T tag="promo.validity-note" as="p" class="validity-note" />
|
||||
</section>
|
||||
<div class:list={['cards', `cards-${ed.cards.length}`]}>
|
||||
{ed.cards.map((c) => (
|
||||
<div class:list={['card', c.featured && 'featured']}>
|
||||
{c.featured && <T tag={tg(`card.${c.id}.badge`)} as="div" class="featured-badge" />}
|
||||
<T tag={tg(`card.${c.id}.eyebrow`)} as="div" class="eyebrow" />
|
||||
<T tag={tg(`card.${c.id}.title`)} as="div" class="title" />
|
||||
{c.question && <T tag={tg(`card.${c.id}.question`)} as="p" class="question" />}
|
||||
<div class="price-row">
|
||||
<T tag={tg(`card.${c.id}.price`)} as="div" class="price" />
|
||||
<T tag={tg(`card.${c.id}.price-old`)} as="div" class="price-old" />
|
||||
</div>
|
||||
{c.unit && <T tag={tg(`card.${c.id}.unit`)} as="div" class="unit" />}
|
||||
<ul>
|
||||
{Array.from({ length: c.features }, (_, i) => i + 1).map((m) => (
|
||||
<T tag={tg(`card.${c.id}.feature.${m}`)} as="li" />
|
||||
))}
|
||||
</ul>
|
||||
{c.desc && <T tag={tg(`card.${c.id}.desc`)} as="p" class="card-desc" />}
|
||||
{c.note && (
|
||||
<div class="note-box">
|
||||
<T tag={tg(`card.${c.id}.note-label`)} as="div" class="note-label" />
|
||||
<T tag={tg(`card.${c.id}.note`)} as="div" class="note-text" />
|
||||
</div>
|
||||
)}
|
||||
<a class="buy" href={BSPORT_URL} target="_blank" rel="noopener"><T tag={tg('buy-label')} as="span" /></a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<T tag={tg('validity-note')} as="p" class="validity-note" />
|
||||
</section>
|
||||
|
||||
<section class="faq-wrap">
|
||||
<T tag="promo.faq.title" as="h1" />
|
||||
{faqs.map((n) => {
|
||||
const a = resolveContent(`promo.faq.${n}.a`);
|
||||
return (
|
||||
<div class="faq-block">
|
||||
<div class="faq-q"><T tag={`promo.faq.${n}.q`} as="p" /><div class="faq-toggle">+</div></div>
|
||||
<p class:list={['faq-a', a.classes]} data-tag={`promo.faq.${n}.a`} data-guid={a.guid || undefined} set:html={a.value.replaceAll('{{whatsapp}}', wa)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<section class="faq-wrap">
|
||||
<T tag={tg('faq.title')} as="h1" />
|
||||
{ed.faq.map((n) => {
|
||||
const a = resolveContent(tg(`faq.${n}.a`));
|
||||
return (
|
||||
<div class="faq-block">
|
||||
<div class="faq-q"><T tag={tg(`faq.${n}.q`)} as="p" /><div class="faq-toggle">+</div></div>
|
||||
<p class:list={['faq-a', a.classes]} data-tag={tg(`faq.${n}.a`)} data-guid={a.guid || undefined} set:html={a.value.replaceAll('{{whatsapp}}', wa)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</Fragment>
|
||||
) : (
|
||||
<section class="hero chiusa">
|
||||
<T tag="promo.chiusa.title" as="h1" />
|
||||
<T tag="promo.chiusa.desc" as="p" class="desc" />
|
||||
<a class="btn-sand" href={waLink} target="_blank" rel="noopener"><T tag="promo.chiusa.cta" as="span" /></a>
|
||||
</section>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user