Compare commits

..

25 Commits

Author SHA1 Message Date
Adriano Dal Pastro 2b71cbb49e L'inaugurazione e' di sabato, e la fascia morta fra 700 e 701 px
Due rilievi della revisione, uno dei quali fuori dalla promo.

**`eventi.astro`: "venerdi' 19 settembre 2026"**, ma il 19/09/2026 e' **sabato**
(`calendari.ts:136` lo diceva gia' giusto). E' la meta description della pagina
dell'inaugurazione: e' cio' che si legge nell'anteprima di un link su WhatsApp,
che e' il canale con cui questo cliente parla ai suoi. Una riga, ma pubblica e
falsa.

**`.cards-5` fra 700 e 701 px non era coperta da nessuna regola**: la fascia
intermedia aveva `and (min-width: 701px)`, e a larghezze frazionarie - uno zoom
del browser dà 700,5 px - non valeva ne' quella ne' il blocco <=700, quindi le
cinque card tornavano a sei colonne dentro ~570 px. Tolto il `min-width`: ci
pensa il blocco <=700, che viene dopo e a pari specificita' vince.

Tolta anche la riga ridondante `.cards-5 { 1fr }` nel blocco <=700: `.lp .cards`
copre gia' tutte le griglie a una colonna, ed e' lo stesso meccanismo per cui
`.cards-3` funziona senza una regola sua.

Restano non toccati, perche' verificati corretti: la virgola di "composizione
corporea, e analisi HRV" e' **nella creativa** (a fine riga, nel layout su tre
righe), e le code esplicative delle card 1 e 2 vengono dai testi di agosto
gia' approvati dal cliente, non dalle creative compresse per il formato social.

186 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKFABRT3y26dFNMquF3CS
2026-09-04 06:53:30 +00:00
Adriano Dal Pastro f914d0699a La promo di settembre subentra ai bundle il 5, cinque pacchetti
Gli Opening Bundles scadono il 04/09 alle 23:59:59 e dopo di loro non c'era
nessuna edizione: dal 5 il sito avrebbe mostrato "Nessuna promozione in corso"
mentre le promo erano vive. Adriano ha consegnato le sei creative il 03/09.

Edizione `settembre-2026` (5 -> 19 settembre, chiude il giorno
dell'inaugurazione): tornano i pacchetti da 5 incontri, con in piu' il
Check-up avanzato, che e' una valutazione singola e per questo dice
"1 valutazione" invece di "5 incontri". Prezzi 250/150/130/80/70.

Due cose che la struttura non prevedeva:

- **Nessun prezzo barrato.** Le creative danno il solo prezzo promo, e i
  listini pieni di agosto (300/220/140/100) sono numeri che nessuno ha
  riconfermato - riportarli accanto ai nuovi sarebbe stato inventarli.
  `price-old` diventa opzionale nella CardSpec (`priceOld`) invece di essere
  un tag vuoto: cosi' la spec dichiara che quella card non ne ha, e il test
  lo pretende solo dove c'e'. Agosto e i bundle sono marcati `priceOld: true`,
  quindi restano sotto controllo come prima.

- **Cinque card non stanno in una griglia da due o da tre** senza lasciare un
  buco in fondo. `.cards-5` va a sei colonne con span due, cosi' le ultime due
  si spostano al centro; a due card per riga sotto i 1000px, a una sotto i 700.

Testi ripresi dall'edizione di agosto, gia' approvata dal cliente, cambiando
solo cio' che le date rendevano falso: la FAQ delle class diceva "gli orari
saranno pubblicati entro Settembre" e adesso e' settembre, quindi rimanda al
calendario che esiste. La FAQ 7 sul Check-up e' nuova e va fatta leggere a
Nicola prima della pubblicazione.

186 test verdi, `astro check` invariato (i 3 errori della pagina promo sono
preesistenti e non toccati).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKFABRT3y26dFNMquF3CS
2026-09-04 06:43:50 +00:00
AdrianoDev f5fac89c1b Via dal router il vecchio sottodominio, tolto dal DNS
Adriano ha rimosso il record insanitylab.tielogic.xyz il 03/09/2026. Una
regola Traefik che nomina un host inesistente non e' inerte: il router chiede
un certificato per i nomi della sua regola, e la sfida ACME su un nome che
non risolve fallisce a ogni tentativo - proprio cio' che Let's Encrypt limita.

Resta il redirect di www verso l'apex, che e' l'unico ancora vero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:41:52 +00:00
AdrianoDev 665c598462 Il vecchio sottodominio torna al suo redirect
Adriano ha creato piattaforme.tielogic.xyz, che e' il nome giusto per le aree
riservate: il prestito di insanitylab.tielogic.xyz e' durato un'ora e finisce
qui. Il sito torna esattamente come stamattina - apex, www e vecchio
sottodominio - e le piattaforme stanno su un host loro.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:39:29 +00:00
AdrianoDev 4103376eb1 Il vecchio sottodominio esce dal redirect: serve alle aree riservate
insanitylab.tielogic.xyz faceva 308 verso l'apex dal 12/07. Dal 03/09/2026
ospita le aree riservate (Campus, Stress Index, Longevity) mentre si aspetta
il record A di apps.insanitylab.it su Aruba: il dominio del cliente resta
dov'e', le piattaforme partono da un sottodominio nostro.

www.insanitylab.it continua a reindirizzare come prima.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:34:59 +00:00
Adriano Dal Pastro b75e12de86 Calendario: alzato il min-width a 780px, che il layout fisso rende necessario
Coda della modifica precedente, e senza questa quella era una regressione sul
telefono. A colonne uguali la piu' stretta deve contenere la parola piu'
lunga; in layout automatico il browser allargava da solo le colonne piene e
stringeva quelle vuote, quindi il problema non si vedeva.

Il conto, misurato sul Montserrat 400 che il sito serve davvero (non
stimato): REFORMER a .7rem con letter-spacing .1em e' 75,4 px, mentre la
colonna a 720px ne offriva 72,7 di spazio utile — 2,7 in meno. La parola
sarebbe uscita dalla cella proprio dove il minimo si vede, cioe' sul
telefono, che quella tabella la fa scorrere in orizzontale apposta.

A 780px la colonna da' 81,3 px, ~6 di margine. In stampa il conto era gia'
buono (corpo .62rem e padding ridotto: 66,8 px in 77,9).

Il conto e' scritto nel CSS accanto alla regola: chi tocca padding, corpo o
spaziatura deve rifarlo.

185 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:57:41 +00:00
Adriano Dal Pastro 718ca499c4 Calendario: le sette colonne dei giorni tutte della stessa larghezza
Chiesto da Adriano. La tabella era in layout automatico, quindi il browser
dimensionava ogni colonna sul contenuto: il sabato, che porta la nota «solo
il 12 settembre», veniva largo quasi il doppio, e i giorni senza class si
stringevano. Una griglia oraria con le colonne di misure diverse si legge
male proprio dove serve — nell'incrocio fra un giorno e un'ora.

`table-layout: fixed` piu' una larghezza sulla sola colonna delle ore
(64px): tutto il resto viene diviso in parti uguali fra le sette dei giorni.

E' lo stesso criterio che il PDF applica da sempre — la' le colonne sono
`(larghezzaUtile - larghezzaOra) / GIORNI.length` — quindi adesso le due
versioni dello stesso foglio si somigliano invece di divergere.

La colonna delle ore resta piu' stretta dei giorni, come nel PDF: ci sta
dentro «12.15» e nient'altro.

185 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:55:14 +00:00
Adriano Dal Pastro 4f031cf571 Calendario: niente class il venerdi' dall'1 al 18 settembre
Chiesto da Adriano l'01/09. Tolte le tre celle del venerdi' dal calendario
'settembre-1-18': MOBILITY alle 8.30, REFORMER alle 9.15 e alle 10.00.

Erano tutte di mattina, quindi in quel periodo il venerdi' resta una colonna
completamente vuota. Resta VUOTA e non barrata: togliere le class non vuol
dire che la sala sia chiusa, e nessuno l'ha detto — il dato distingue le due
cose apposta (`chiusi`), e sceglierne una che non ci e' stata comunicata
sarebbe inventare un'informazione.

⚠️ L'edit e' confinato al primo calendario. Le tre righe del venerdi' sono
IDENTICHE, carattere per carattere, in 'settembre-21-in-poi' — dove il
venerdi' deve restare — quindi una sostituzione sul file intero ne avrebbe
prese sei invece di tre. Verificato dopo: dal 21 settembre il venerdi' ha
ancora PILATES 7.45, MOBILITY 8.30, REFORMER 9.15 e 10.00.

Aggiunto un commento nel dato, perche' confrontando le due griglie il buco
sembra una trascrizione monca e il prossimo che passa lo "corregge".

185 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:51:13 +00:00
Adriano 6ba8794770 Fuori dal versionamento la cache di Python
Nel repo ora c'e' anche Python — lo script che rigenera la locandina, e i
riferimenti dei test longevity — e ogni esecuzione lascia dietro __pycache__.
Comparivano come file non tracciati a ogni git status, senza essere codice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 13:16:29 +02:00
Adriano Dal Pastro 0ea1cb5e77 Eventi: la citazione di chiusura fra virgolette e piu' grande
Caporali «» attorno alla frase — sono le virgolette italiane, e su un corsivo
di quel corpo reggono meglio delle doppie curve.

Corpo da clamp(1.05rem, 2.4vw, 1.3rem) a clamp(1.35rem, 3.4vw, 1.9rem): ~+45%
sia sul minimo che sul massimo, quindi cresce anche su mobile. Interlinea da
1.8 a 1.5, altrimenti a quel corpo le due righe galleggiavano.

Il <br /> che spezza la frase in due versi resta dov'era.
2026-08-28 11:12:33 +00:00
Adriano 7f96b95e04 Eventi: via il piano dai blocchi, la locandina scende in fondo
Tre ritocchi chiesti dal cliente: il "Primo piano" sotto "Prova i nostri
servizi" spariva il messaggio piu' che aiutare, il download della locandina
sta meglio a fine lettura che nell'apertura, e il titolo del programma va
a capo dopo "Come si svolge".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:55:14 +02:00
Adriano 13d73f90a8 Eventi: i test seguono la data separata e l'URL stampato nel QR
Il test dell'inaugurazione cercava la data dentro il titolo, ma i due sono
stati separati in tag distinti il 19 agosto: era rosso da allora, anche in
produzione.

Il nuovo test tiene insieme le tre cose che devono restare d'accordo, perche'
il QR delle locandine affisse non si corregge piu': la rotta servita dal sito,
l'URL cucito nel codice e il PDF che la pagina offre in download.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:43:13 +02:00
Adriano 7fd3961469 Eventi: la landing dell'In-Sanity Hour, con la locandina da scaricare
La pagina dell'inaugurazione del 19 settembre: il programma completo della
giornata, gli slot di prova del mattino e del pomeriggio, la festa serale.

Il blocco del titolo è il ritaglio della locandina vera, non una sua
riproduzione: i caratteri originali non li abbiamo, e ricostruirli a occhio
è già stato provato senza riuscirci. Il resto della pagina usa la tipografia
del sito e i colori campionati dalla locandina, così volantino e schermo si
riconoscono come la stessa cosa.

La locandina scaricabile è ricalcata in vettoriale dallo stesso JPEG — tre
colori piatti, quindi ogni forma diventa un tracciato — e monta il QR code
definitivo verso questa pagina, un filo più piccolo dell'originale per
lasciargli attorno il margine muto che lo standard richiede.

I testi non passano più dal pannello contenuti: la pagina è in codice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:41:47 +02:00
Adriano ecab67805b Data dell'inaugurazione piu' grande 2026-08-19 10:34:38 +02:00
Adriano cdfbf194c2 Eventi: la data dell'inaugurazione si legge da lontano
Chiesto da Adriano il 19/08: era 0,82rem sotto un titolo che arriva a 2,6rem, e
sembrava una didascalia. Ora cresce con la finestra come il titolo, restando un
gradino sotto.
2026-08-19 10:34:12 +02:00
Adriano 76adef3fc7 Titolo e data dell'inaugurazione su due righe 2026-08-19 10:31:49 +02:00
Adriano d079019dcf Eventi: titolo e data dell'inaugurazione su due righe
Chiesto da Adriano il 19/08. Sono due tag e non un testo con un a capo dentro:
la data si cambia da sola se l'evento si sposta, e un ritorno a capo dentro un
valore di testo non sopravvive all'editor del pannello.

Il titolo vive anche nel database: in produzione va aggiornato
eventi.inaugurazione.title a "Inaugurazione", altrimenti resta la riga unica.
Il tag nuovo (.date) non e' in tabella e viene dal seed.
2026-08-19 10:30:07 +02:00
Adriano 48909e8c8b Foto del team senza logo, e la foto di Eva torna nel repo 2026-08-19 10:25:18 +02:00
Adriano cc6945c3d1 Eva: la foto della pagina About torna nel repo, senza logo
La foto di Eva in pagina non veniva dal repo ma da un file caricato dal
pannello il 12/07 (tag team.eva.image-about): lo scatto giusto, ritagliato piu'
largo, ma con il logo ancora nell'angolo. Il file del repo era l'altro ritaglio,
piu' stretto, e non lo vedeva nessuno.

Ora team-eva-dark.jpg E' quella foto, col logo rimosso: maschera dei pixel del
logo allargata di 4 px per prendere i bordi sfumati, ricostruzione biharmonic
(scikit-image) e grana del tessuto ripresa dalla stoffa 80 px piu' in alto —
una toppa liscia si sarebbe notata.

Cosi' tutte le foto stanno in un posto solo e passano dall'ottimizzazione della
build (54 KB webp invece di 177 KB jpeg servito com'e'). In produzione va
svuotato il tag team.eva.image-about, altrimenti continua a vincere il file
caricato. Originale col logo su
wasabi://.../foto-team/team-eva-dark-ORIGINALE-col-logo.jpeg; il file su
uploads/ resta dov'e'.

Il ritaglio e' 864x1240 (0.697) invece di 1547x1920 (0.806): le card ritagliano
a 3:4, quindi qui si perde qualcosa sopra e sotto invece che ai lati. Verificato
sulla card renderizzata.
2026-08-19 10:24:13 +02:00
Adriano f7a9ae47d1 Foto del team senza il logo nell'angolo
Adriano ha tolto un piccolo logo dall'angolo in basso a destra delle sette foto
scure della pagina About: alessandro, alice, donata, eleonora, eva, giulia,
nicola. Originali in wasabi://.../Sito_Web/foto-team-originali-20260819.zip.

Stesso formato di prima (JPEG 1547x1920, RGB): l'unica differenza e' un riquadro
di ~80x80 px nell'angolo, verificato confrontando le immagini. I file pesano il
doppio perche' risalvati a qualita' piu' alta, ma in pagina non cambia nulla: la
build li ricomprime in webp attorno agli 85 KB.

Nota: la foto di Eva in pagina NON e' questa, e' un ritaglio piu' largo dello
stesso scatto caricato dal pannello il 12/07 (tag team.eva.image-about), che il
logo ce l'ha ancora.
2026-08-19 10:16:29 +02:00
Adriano 9772b3a3f6 Il titolo della pagina About us 2026-08-19 09:43:49 +02:00
Adriano 32c8a510bb About us: il titolo in cima alla pagina dice About us, non Insanitylab
Segnalato da Adriano il 19/08. Il valore vive anche nel database dei contenuti,
dove viene letto per primo: il seed da solo non avrebbe cambiato la pagina, e
l'aggiornamento del tag about.hero.title va fatto in produzione.
2026-08-19 08:59:44 +02:00
Adriano 20f0c2120b Piu' aria fra l'immagine e il titolo Check-up Avanzato 2026-08-19 08:54:46 +02:00
Adriano 6e002d4dc1 Check-up: il titolo della sezione Avanzato si stacca dall'immagine
Segnalato da Adriano il 19/08: "Check-up Avanzato" era appiccicato alla foto
sopra e si leggeva come una didascalia invece che come l'inizio di una parte
nuova.

Lo stacco e' un padding e non un margine: sopra c'e' gia' il margine
dell'immagine, e due margini verticali adiacenti collassano fra loro — con
margin-top lo spazio sarebbe rimasto quello di prima.
2026-08-19 08:49:55 +02:00
Adriano 3790d6d506 Check-up senza la domanda "Che cosa comprende?" 2026-08-19 08:46:19 +02:00
23 changed files with 660 additions and 46 deletions
+2
View File
@@ -7,3 +7,5 @@ data/*.db*
uploads/* uploads/*
!uploads/.gitkeep !uploads/.gitkeep
assets-src/ assets-src/
__pycache__/
*.pyc
+6 -1
View File
@@ -23,7 +23,12 @@ services:
- traefik.http.routers.insanitylab.tls.certresolver=mytlschallenge - traefik.http.routers.insanitylab.tls.certresolver=mytlschallenge
- traefik.http.services.insanitylab.loadbalancer.server.port=4321 - traefik.http.services.insanitylab.loadbalancer.server.port=4321
# Router redirect: www e vecchio sottodominio tielogic.xyz → 301 apex # Router redirect: www e vecchio sottodominio tielogic.xyz → 301 apex
- traefik.http.routers.insanitylab-redir.rule=Host(`www.insanitylab.it`) || Host(`insanitylab.tielogic.xyz`) # ⚠️ `insanitylab.tielogic.xyz` tolto dal DNS da Adriano il 03/09/2026: il record non
# esiste piu'. Lasciarlo qui non e' inerte — il router chiede un certificato per i
# nomi della sua regola, e una sfida ACME su un nome che non risolve fallisce a ogni
# tentativo, che e' esattamente cio' che Let's Encrypt limita. Il vecchio dominio ha
# smesso di essere raggiungibile: da qui in poi vale solo il redirect di `www`.
- traefik.http.routers.insanitylab-redir.rule=Host(`www.insanitylab.it`)
- traefik.http.routers.insanitylab-redir.tls=true - traefik.http.routers.insanitylab-redir.tls=true
- traefik.http.routers.insanitylab-redir.entrypoints=websecure - traefik.http.routers.insanitylab-redir.entrypoints=websecure
- traefik.http.routers.insanitylab-redir.tls.certresolver=mytlschallenge - traefik.http.routers.insanitylab-redir.tls.certresolver=mytlschallenge
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Genera la locandina dell'In-Sanity Hour in PDF vettoriale, con il QR code vero.
Perché non si parte da un sorgente grafico: l'unico file disponibile è un JPEG da
612x751 px, che stampato su A4 darebbe 70 dpi. La locandina però è fatta di tre soli
colori piatti, quindi si può ricalcare: ogni forma — testi, calligrafia, bicchieri —
diventa un tracciato vettoriale che si stampa nitido a qualsiasi formato, senza dover
reinstallare i font originali (che non abbiamo) né riprodurli a occhio.
Del JPEG resta solo la geometria. Il QR di prova viene coperto e ridisegnato con
l'URL vero della pagina evento, anch'esso vettoriale.
Uso: python3 scripts/locandina-evento.py
Serve: pip install --user potracer qrcode cairosvg
"""
from pathlib import Path
import cairosvg
import numpy as np
import potrace
import qrcode
from PIL import Image
URL_EVENTO = 'https://insanitylab.it/promo-eventi/eventi'
RADICE = Path(__file__).resolve().parent.parent
SORGENTE = RADICE.parent / 'Scaricati' / 'Locandina.jpeg'
USCITA_PDF = RADICE / 'public' / 'locandina-insanity-hour.pdf'
USCITA_PNG = RADICE / 'public' / 'img' / 'locandina-insanity-hour.png'
USCITA_TITOLO = RADICE / 'public' / 'img' / 'insanity-hour-titolo.png'
# Riquadro stampato dentro il JPEG, senza il bordo bianco dello screenshot.
RIQUADRO = (22, 13, 597, 732)
# Blocco "SAVE THE DATE / IN-SANITY / Hour" usato come immagine nella pagina web.
TITOLO_WEB = (23, 24, 596, 275)
# Quadrato del QR di prova da sostituire, in coordinate del JPEG originale.
QR_PROVA = (387, 632, 475, 720)
# Bordo superiore della fascia beige: la toppa sul QR non deve sconfinare nel marrone.
FASCIA_Y = 627
FONDO, BEIGE, CREMA = '#4F3126', '#D0B49E', '#F6F1E9'
CENTRI = [(79, 49, 38), (208, 180, 158), (246, 241, 233)]
UPSCALE = 4 # si traccia sull'immagine ingrandita: bordi piu' morbidi, curve piu' fedeli
MODULI_QUIET = 4 # margine muto richiesto dallo standard QR, in moduli
def classifica(immagine: Image.Image) -> np.ndarray:
"""Assegna a ogni pixel il piu' vicino dei tre colori piatti della locandina."""
grande = immagine.resize((immagine.width * UPSCALE, immagine.height * UPSCALE), Image.LANCZOS)
px = np.asarray(grande).astype(np.int16)
distanze = np.stack([((px - np.array(c)) ** 2).sum(axis=2) for c in CENTRI])
return np.argmin(distanze, axis=0)
def traccia(maschera: np.ndarray) -> str:
"""Contorni della maschera come path SVG. potracer prende per foreground i pixel
falsi, quindi la maschera va negata."""
percorso = potrace.Bitmap(~maschera).trace(turdsize=6, alphamax=1.0, opticurve=True, opttolerance=0.2)
d = []
for curva in percorso.curves:
avvio = curva.start_point
d.append(f'M{avvio.x / UPSCALE:.2f},{avvio.y / UPSCALE:.2f}')
for seg in curva.segments:
fine = seg.end_point
if seg.is_corner:
d.append(f'L{seg.c.x / UPSCALE:.2f},{seg.c.y / UPSCALE:.2f}L{fine.x / UPSCALE:.2f},{fine.y / UPSCALE:.2f}')
else:
d.append(
f'C{seg.c1.x / UPSCALE:.2f},{seg.c1.y / UPSCALE:.2f}'
f' {seg.c2.x / UPSCALE:.2f},{seg.c2.y / UPSCALE:.2f}'
f' {fine.x / UPSCALE:.2f},{fine.y / UPSCALE:.2f}'
)
d.append('Z')
return ''.join(d)
def qr_svg(x0: float, y0: float, x1: float, y1: float) -> str:
"""QR dell'URL evento, disegnato a rettangoli dentro il riquadro dato.
Il QR di prova sfiorava il bordo marrone: qui il modulo viene calcolato includendo
il margine muto, cosi' il codice resta leggibile anche stampato in piccolo.
"""
codice = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=1, border=0)
codice.add_data(URL_EVENTO)
codice.make(fit=True)
matrice = codice.get_matrix()
n = len(matrice)
lato = min(x1 - x0, y1 - y0)
modulo = lato / (n + 2 * MODULI_QUIET)
off_x = x0 + (x1 - x0 - lato) / 2 + MODULI_QUIET * modulo
off_y = y0 + (y1 - y0 - lato) / 2 + MODULI_QUIET * modulo
quadrati = [
f'<rect x="{off_x + c * modulo:.3f}" y="{off_y + r * modulo:.3f}" '
f'width="{modulo:.3f}" height="{modulo:.3f}"/>'
for r, riga in enumerate(matrice)
for c, pieno in enumerate(riga)
if pieno
]
return f'<g fill="{FONDO}" shape-rendering="crispEdges">{"".join(quadrati)}</g>', n
def main() -> None:
originale = Image.open(SORGENTE).convert('RGB')
locandina = originale.crop(RIQUADRO)
larghezza, altezza = locandina.size
etichette = classifica(locandina)
dx, dy = RIQUADRO[0], RIQUADRO[1]
qx0, qy0, qx1, qy1 = QR_PROVA[0] - dx, QR_PROVA[1] - dy, QR_PROVA[2] - dx, QR_PROVA[3] - dy
qr, moduli = qr_svg(qx0, qy0, qx1, qy1)
toppa_y = max(qy0 - 6, FASCIA_Y - dy + 1)
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {larghezza} {altezza}" width="{larghezza}" height="{altezza}">
<rect width="{larghezza}" height="{altezza}" fill="{FONDO}"/>
<path fill="{BEIGE}" d="{traccia(etichette == 1)}"/>
<path fill="{CREMA}" d="{traccia(etichette == 2)}"/>
<rect x="{qx0 - 6}" y="{toppa_y}" width="{qx1 - qx0 + 12}" height="{qy1 + 6 - toppa_y}" fill="{BEIGE}"/>
{qr}
</svg>'''
USCITA_PDF.parent.mkdir(parents=True, exist_ok=True)
# Proporzioni native (575x719). cairosvg legge i px a 96 dpi: per una base di 210 mm
# (la larghezza di un A4) la pagina va chiesta in px, non in punti.
px_per_mm = 96 / 25.4
scala = 210 * px_per_mm / larghezza
cairosvg.svg2pdf(bytestring=svg.encode(), write_to=str(USCITA_PDF),
output_width=larghezza * scala, output_height=altezza * scala)
cairosvg.svg2png(bytestring=svg.encode(), write_to=str(USCITA_PNG),
output_width=larghezza * 3, output_height=altezza * 3)
originale.crop(TITOLO_WEB).save(USCITA_TITOLO)
print(f'locandina {larghezza}x{altezza} px -> {USCITA_PDF.relative_to(RADICE)} '
f'({210:.0f}x{210 * altezza / larghezza:.0f} mm)')
print(f'anteprima {larghezza * 3}x{altezza * 3} -> {USCITA_PNG.relative_to(RADICE)}')
print(f'QR {moduli}x{moduli} moduli -> {URL_EVENTO}')
if __name__ == '__main__':
main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 554 KiB

+20 -1
View File
@@ -173,7 +173,23 @@ const disciplinePresenti = (cal: Calendario) =>
/* La griglia ha otto colonne: su telefono scorre in orizzontale invece di comprimersi /* La griglia ha otto colonne: su telefono scorre in orizzontale invece di comprimersi
fino a diventare illeggibile, e la colonna delle ore resta ferma. */ fino a diventare illeggibile, e la colonna delle ore resta ferma. */
.cal-tab { border-collapse: collapse; width: 100%; min-width: 720px; } /* `table-layout: fixed` e' quello che rende le sette colonne dei giorni ESATTAMENTE
uguali: in automatico il browser le dimensiona sul contenuto, cosi' il sabato con la
nota «solo il 12 settembre» diventava largo il doppio e i giorni vuoti si stringevano.
Con il layout fisso basta dare una larghezza alla colonna delle ore: tutto il resto
viene diviso in parti uguali fra le sette rimanenti. E' lo stesso criterio del PDF,
dove le colonne sono `(larghezzaUtile - larghezzaOra) / 7` da sempre.
⚠️ Il `min-width` e' passato da 720 a 780px INSIEME al layout fisso, e i due sono
legati: a colonne uguali la piu' stretta deve contenere la parola piu' lunga, mentre
in layout automatico il browser allargava da solo le colonne piene e stringeva quelle
vuote. Il conto, misurato sul Montserrat 400 che il sito serve davvero:
REFORMER a .7rem con letter-spacing .1em = 75,4 px
colonna a 720px = (720 - 64) / 7 = 93,7 px, meno 20 di padding e 1 di bordo = 72,7
cioe' 2,7 px in meno del necessario: la parola sarebbe uscita dalla cella sul
telefono, dove il minimo e' quello che si vede. A 780px la colonna da' 81,3 px di
spazio utile, ~6 di margine. Chi tocca padding, corpo o spaziatura rifaccia il conto. */
.cal-tab { border-collapse: collapse; width: 100%; min-width: 780px; table-layout: fixed; }
.cal-tab th, .cal-tab td { border: 1px solid #e4e0d9; padding: 9px 10px; text-align: left; } .cal-tab th, .cal-tab td { border: 1px solid #e4e0d9; padding: 9px 10px; text-align: left; }
.cal-tab thead th { .cal-tab thead th {
background: var(--c-bg-alt); background: var(--c-bg-alt);
@@ -184,6 +200,9 @@ const disciplinePresenti = (cal: Calendario) =>
display: block; font-size: .62rem; letter-spacing: .06em; text-transform: none; display: block; font-size: .62rem; letter-spacing: .06em; text-transform: none;
color: var(--c-text-light); font-weight: 400; color: var(--c-text-light); font-weight: 400;
} }
/* L'unica larghezza dichiarata: la colonna delle ore. Resta piu' stretta dei giorni,
come nel PDF — ci sta dentro «12.15» e nient'altro. */
.cal-tab__ora-h, .cal-tab__ora { width: 64px; }
.cal-tab__ora-h { color: var(--c-accent-dark) !important; } .cal-tab__ora-h { color: var(--c-accent-dark) !important; }
.cal-tab__ora { .cal-tab__ora {
background: var(--c-bg); background: var(--c-bg);
+10 -3
View File
@@ -47,13 +47,20 @@ export const calendari: Calendario[] = [
to: '2026-09-18T23:59:59+02:00', to: '2026-09-18T23:59:59+02:00',
chiusi: ['DOM'], chiusi: ['DOM'],
noteGiorno: { SAB: 'solo il 12 settembre' }, noteGiorno: { SAB: 'solo il 12 settembre' },
// ⚠️ Il VENERDI' non ha nessuna class in questo periodo, ed e' voluto: le tre che
// c'erano (MOBILITY 8.30, REFORMER 9.15 e 10.00) sono state tolte su richiesta del
// cliente l'01/09/2026, ed erano tutte di mattina. Non e' una trascrizione monca:
// confrontando questa griglia con quella dal 21 settembre — dove il venerdi' c'e' —
// la differenza salta all'occhio e sembra un errore. Non lo e'.
// La colonna resta VUOTA e non barrata: togliere le class non vuol dire che la sala
// sia chiusa, e nessuno ce l'ha detto (scelta di Adriano, 01/09).
righe: [ righe: [
{ ora: '6.15', celle: {} }, { ora: '6.15', celle: {} },
{ ora: '7.00', clou: true, celle: { MAR: 'YOGA' } }, { ora: '7.00', clou: true, celle: { MAR: 'YOGA' } },
{ ora: '7.45', clou: true, celle: { MAR: 'PILATES', MER: 'REFORMER' } }, { ora: '7.45', clou: true, celle: { MAR: 'PILATES', MER: 'REFORMER' } },
{ ora: '8.30', celle: { MER: 'REFORMER', VEN: 'MOBILITY' } }, { ora: '8.30', celle: { MER: 'REFORMER' } },
{ ora: '9.15', celle: { VEN: 'REFORMER', SAB: 'MOBILITY' } }, { ora: '9.15', celle: { SAB: 'MOBILITY' } },
{ ora: '10.00', celle: { VEN: 'REFORMER', SAB: 'PILATES' } }, { ora: '10.00', celle: { SAB: 'PILATES' } },
{ ora: '10.45', celle: { SAB: 'REFORMER' } }, { ora: '10.45', celle: { SAB: 'REFORMER' } },
{ ora: '11.30', celle: { SAB: 'REFORMER' } }, { ora: '11.30', celle: { SAB: 'REFORMER' } },
{ ora: '12.15', clou: true, celle: { MAR: 'REFORMER', MER: 'REFORMER', GIO: 'REFORMER' } }, { ora: '12.15', clou: true, celle: { MAR: 'REFORMER', MER: 'REFORMER', GIO: 'REFORMER' } },
+99 -2
View File
@@ -165,7 +165,7 @@ export const contentSeed: SeedEntry[] = [
// --- About: meta, hero --- // --- About: meta, hero ---
T('about.meta.title', 'Insanitylab', []), T('about.meta.title', 'Insanitylab', []),
T('about.meta.description', 'Mission, storia, team e metodo di InsanityLab: un laboratorio esclusivo dove movimento, prevenzione e performance convivono in un approccio personalizzato.', []), T('about.meta.description', 'Mission, storia, team e metodo di InsanityLab: un laboratorio esclusivo dove movimento, prevenzione e performance convivono in un approccio personalizzato.', []),
T('about.hero.title', 'Insanitylab', []), T('about.hero.title', 'About us', []),
// --- About: mission --- // --- About: mission ---
T('about.mission.title', 'Mission'), T('about.mission.title', 'Mission'),
@@ -318,7 +318,8 @@ export const contentSeed: SeedEntry[] = [
T('eventi.meta.title', '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.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.hero.title', 'Eventi', []),
T('eventi.inaugurazione.title', 'Inaugurazione — 19 settembre 2026'), T('eventi.inaugurazione.title', 'Inaugurazione'),
T('eventi.inaugurazione.date', '19 settembre 2026'),
T('eventi.inaugurazione.body', 'Apriamo le porte del nuovo spazio con un aperitivo e dj set. Sarà loccasione 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.inaugurazione.body', 'Apriamo le porte del nuovo spazio con un aperitivo e dj set. Sarà loccasione 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.cta.label', 'Scrivici su WhatsApp', []),
T('eventi.torna.label', '← Torna a promo ed eventi', []), T('eventi.torna.label', '← Torna a promo ed eventi', []),
@@ -502,6 +503,102 @@ export const contentSeed: SeedEntry[] = [
T('bundle.faq.4.q', 'Il pagamento è sicuro?'), 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.'), 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.'),
// --- Promo settembre (edizione `settembre-2026`, 5 → 19 settembre) ---
// Testi ripresi dall'edizione di agosto, che il cliente ha già approvato: cambia solo ciò che
// le date o le nuove creative rendono falso. I prezzi e le voci degli elenchi vengono dalle
// sei creative consegnate il 03/09.
T('settembre.hero.badge', 'insanity.lab · bologna savena'),
T('settembre.hero.title1', 'Allenati con metodo.'),
T('settembre.hero.accent', 'Scegli come'),
T('settembre.hero.title2', 'vuoi farlo.'),
H('settembre.hero.desc', 'Allenati con flessibilità, senza vincoli, da subito.<br>Acquisto online in pochi secondi, pagamento sicuro via Stripe.'),
T('settembre.hero.cta', 'Le nostre proposte', []),
T('settembre.launch.badge', 'solo per apertura nuova sede Settembre 2026'),
T('settembre.launch.title', 'Il nostro metodo nasce per durare.'),
T('settembre.launch.accent', "Per l'apertura, un'eccezione. Solo per poco."),
T('settembre.launch.desc', "In-Sanity Lab lavora per percorsi strutturati — trimestrali, semestrali — perché i risultati veri si costruiscono nel tempo. Per la nuova apertura, ci sarà un'eccezione: 5 incontri pensati come un vero mini-percorso, non semplici lezioni sciolte — e una valutazione singola, per chi vuole partire dai propri numeri."),
T('settembre.why5.text', 'Non è una prova. È un percorso in miniatura — per capire come lavoriamo prima di impegnarti su un programma trimestrale o semestrale.'),
T('settembre.buy-label', 'Acquista', []),
T('settembre.card.1.eyebrow', 'Personal training', []),
T('settembre.card.1.title', 'One to One'),
T('settembre.card.1.price', '250 €', []),
T('settembre.card.1.unit', '5 incontri', []),
T('settembre.card.1.feature.1', 'Screening iniziale, definizione dei tuoi obiettivi'),
T('settembre.card.1.feature.2', 'Check-up con profilazione fisica, il tuo punto di partenza'),
T('settembre.card.1.feature.3', 'PT dedicato, presente a ogni sessione'),
T('settembre.card.1.feature.4', 'In-sanity plan, il tuo programma personalizzato'),
T('settembre.card.2.eyebrow', 'Personal training', []),
T('settembre.card.2.title', 'Personalized Training Group'),
T('settembre.card.2.price', '150 €', []),
T('settembre.card.2.unit', '5 incontri', []),
T('settembre.card.2.feature.1', 'Screening iniziale, definizione dei tuoi obiettivi'),
T('settembre.card.2.feature.2', 'Check-up con profilazione fisica, il tuo punto di partenza'),
T('settembre.card.2.feature.3', 'PT dedicato, presente a ogni sessione'),
T('settembre.card.2.feature.4', 'In-sanity plan, il tuo programma personalizzato'),
T('settembre.card.3.eyebrow', 'Pilates class', []),
T('settembre.card.3.title', 'Reformer Class'),
T('settembre.card.3.price', '130 €', []),
T('settembre.card.3.unit', '5 incontri', []),
T('settembre.card.3.feature.1', 'Core, postura e mobilità su macchina Reformer'),
T('settembre.card.3.feature.2', '40 minuti, massimo 5 utenti'),
T('settembre.card.3.feature.3', 'Lavoro a basso impatto sulle articolazioni'),
T('settembre.card.4.eyebrow', 'Holistic class', []),
T('settembre.card.4.title', 'Pilates Mat, Mobility, Yoga'),
T('settembre.card.4.price', '80 €', []),
T('settembre.card.4.unit', '5 incontri', []),
T('settembre.card.4.feature.1', 'Mobilità articolare, respirazione, equilibrio'),
T('settembre.card.4.feature.2', 'Da solo, o integrato nei tuoi allenamenti'),
T('settembre.card.4.feature.3', 'Adatto a tutti i livelli'),
// Il Check-up avanzato non è un percorso: si compra una volta e si fa in un appuntamento.
// Per questo l'unità dice "1 valutazione" e non "5 incontri".
T('settembre.card.5.eyebrow', 'Valutazione', []),
T('settembre.card.5.title', 'Check-up avanzato'),
T('settembre.card.5.price', '70 €', []),
T('settembre.card.5.unit', '1 valutazione', []),
T('settembre.card.5.feature.1', 'Analisi completa di composizione corporea, e analisi HRV'),
T('settembre.card.5.feature.2', 'Spirometria e scansione posturale 3D'),
T('settembre.card.5.feature.3', 'Report dettagliato e consigli personalizzati'),
// La deadline è lo stesso istante in cui finisce l'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('settembre.countdown.label', 'La promo scade tra', []),
T('settembre.countdown.deadline', '2026-09-19T23:59:59+02:00', []),
T('settembre.countdown.unit.days', 'giorni', []),
T('settembre.countdown.unit.hours', 'ore', []),
T('settembre.countdown.unit.minutes', 'minuti', []),
T('settembre.countdown.unit.seconds', 'secondi', []),
T('settembre.validity-note', 'Promo valida dal 5 al 19 settembre 2026 · Incontri validi entro 2 mesi dal primo incontro e comunque non oltre il 1° dicembre 2026', []),
T('settembre.faq.title', 'Tutto quello che vuoi sapere prima di acquistare.'),
T('settembre.faq.0.q', 'Come faccio ad acquistare i pacchetti?'),
H('settembre.faq.0.a', 'Clicca sull\'icona del pacchetto che desideri acquistare 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 quello 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('settembre.faq.1.q', 'Come funziona il pacchetto a incontri per One to One e Personalized Training Group?'),
H('settembre.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 <span class="hl">WhatsApp ({{whatsapp}})</span> 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.'),
// Ad agosto qui c'era scritto "gli orari saranno pubblicati entro Settembre": adesso è
// settembre e il calendario esiste, quindi la frase andava riscritta o diventava falsa.
T('settembre.faq.2.q', 'Come funziona il percorso Reformer Class o Holistic Class?'),
H('settembre.faq.2.a', 'Per acquistare il pacchetto online segui le istruzioni riportate nella risposta della prima domanda. Una volta scaricata l\'app <span class="hl">Bsport</span> e acquistato il pacchetto potrai procedere a selezionare autonomamente la class che preferisci nel giorno e nell\'orario che desideri. Gli orari delle class li trovi nel <span class="hl">calendario</span>, che puoi consultare e scaricare in PDF dalla pagina <span class="hl">Servizi</span> del sito. In caso di problemi contattaci su <span class="hl">WhatsApp ({{whatsapp}})</span>! Gli incontri sono validi 2 mesi dalla data del primo incontro e comunque non oltre il 1° dicembre 2026.'),
T('settembre.faq.7.q', "Che cos'è il Check-up avanzato?"),
H('settembre.faq.7.a', 'È una <span class="hl">valutazione singola</span>, non un percorso a incontri: analisi completa della composizione corporea e analisi HRV, spirometria e scansione posturale 3D. Al termine ricevi un report dettagliato con i consigli che ne derivano. Puoi farlo da solo, oppure come punto di partenza prima di scegliere un percorso. Per fissare l\'appuntamento scrivici su <span class="hl">WhatsApp ({{whatsapp}})</span>.'),
T('settembre.faq.3.q', 'Posso usare gli incontri su servizi diversi?'),
H('settembre.faq.3.a', 'Ogni pacchetto è dedicato a un singolo servizio (<span class="hl">One to One</span>, <span class="hl">Personalized Training Group</span>, <span class="hl">Reformer Class</span> o <span class="hl">Pilates Mat, Mobility, Yoga</span>). Se vuoi accedere a più servizi puoi acquistare pacchetti separati per ciascuno. Il <span class="hl">Check-up avanzato</span> è a sé: è una valutazione singola e non un pacchetto di incontri.'),
T('settembre.faq.4.q', 'Posso regalare un pacchetto a qualcun altro?'),
H('settembre.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 <span class="hl">WhatsApp ({{whatsapp}})</span> per organizzare le sessioni.'),
T('settembre.faq.5.q', 'Il pagamento è sicuro?'),
H('settembre.faq.5.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.'),
T('settembre.faq.6.q', 'Posso chiedere un rimborso?'),
H('settembre.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.'),
]; ];
export const seedByTag = new Map(contentSeed.map((e) => [e.tag, e])); export const seedByTag = new Map(contentSeed.map((e) => [e.tag, e]));
+35 -7
View File
@@ -20,6 +20,9 @@ export interface CardSpec {
featured?: boolean; featured?: boolean;
/** Riga sotto il prezzo, es. "5 incontri". */ /** Riga sotto il prezzo, es. "5 incontri". */
unit?: boolean; unit?: boolean;
/** Prezzo pieno barrato accanto a quello promo. Senza, la card mostra il solo prezzo di
* vendita: un listino barrato non dichiarato dal cliente sarebbe un numero inventato. */
priceOld?: boolean;
/** Domanda in testa alla card, es. "Ti alleni ma non vedi risultati?". */ /** Domanda in testa alla card, es. "Ti alleni ma non vedi risultati?". */
question?: boolean; question?: boolean;
/** Paragrafo descrittivo sotto l'elenco. */ /** Paragrafo descrittivo sotto l'elenco. */
@@ -52,10 +55,10 @@ export const edizioni: Edizione[] = [
to: '2026-08-19T23:59:59+02:00', to: '2026-08-19T23:59:59+02:00',
launch: true, launch: true,
cards: [ cards: [
{ id: 1, features: 4, unit: true }, { id: 1, features: 4, unit: true, priceOld: true },
{ id: 2, features: 4, unit: true, featured: true }, { id: 2, features: 4, unit: true, priceOld: true, featured: true },
{ id: 3, features: 3, unit: true }, { id: 3, features: 3, unit: true, priceOld: true },
{ id: 4, features: 3, unit: true }, { id: 4, features: 3, unit: true, priceOld: true },
], ],
faq: [0, 1, 2, 3, 4, 5, 6], faq: [0, 1, 2, 3, 4, 5, 6],
}, },
@@ -67,14 +70,39 @@ export const edizioni: Edizione[] = [
from: '2026-08-20T00:00:00+02:00', from: '2026-08-20T00:00:00+02:00',
to: '2026-09-04T23:59:59+02:00', to: '2026-09-04T23:59:59+02:00',
cards: [ cards: [
{ id: 1, features: 3, question: true, desc: true, note: true }, { id: 1, features: 3, question: true, desc: true, note: true, priceOld: true },
{ id: 2, features: 2, question: true, desc: true, note: true }, { id: 2, features: 2, question: true, desc: true, note: true, priceOld: true },
// Nessuna card "in evidenza": sono tre percorsi per esigenze diverse, non tre livelli // 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. // dello stesso prodotto, e un "più scelto" su un'offerta appena nata sarebbe inventato.
{ id: 3, features: 3, question: true, desc: true, note: true }, { id: 3, features: 3, question: true, desc: true, note: true, priceOld: true },
], ],
faq: [0, 1, 2, 3, 4], faq: [0, 1, 2, 3, 4],
}, },
{
// Promo di settembre: subentra ai bundle il 5 e chiude il 19, il giorno dell'inaugurazione.
// Torna la forma dei pacchetti da 5 incontri, con in più il Check-up avanzato, che è una
// valutazione singola e non un percorso — per questo ha "1 valutazione" al posto di
// "5 incontri".
// Nessun prezzo barrato: le creative del cliente danno il solo prezzo promo, e un listino
// pieno ripreso dall'edizione di agosto sarebbe un numero che nessuno ha confermato.
id: 'settembre-2026',
prefix: 'settembre',
from: '2026-09-05T00:00:00+02:00',
to: '2026-09-19T23:59:59+02:00',
launch: true,
cards: [
{ id: 1, features: 4, unit: true },
{ id: 2, features: 4, unit: true },
{ id: 3, features: 3, unit: true },
{ id: 4, features: 3, unit: true },
// Nessuna card "in evidenza": cinque proposte per esigenze diverse, non cinque livelli
// dello stesso prodotto.
{ id: 5, features: 3, unit: true },
],
// La 7 è la domanda nuova sul Check-up: va letta dopo le altre due "come funziona", non
// in fondo dopo il rimborso, quindi l'ordine di comparsa non segue la numerazione.
faq: [0, 1, 2, 7, 3, 4, 5, 6],
},
]; ];
/** Tag di contenuto di un'edizione: `tagEdizione(e, 'card.1.price')` → `bundle.card.1.price`. */ /** Tag di contenuto di un'edizione: `tagEdizione(e, 'card.1.price')` → `bundle.card.1.price`. */
+275 -23
View File
@@ -1,36 +1,288 @@
--- ---
import Base from '../../layouts/Base.astro'; 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; export const prerender = false;
// Gli eventi del centro. Oggi ce n'è uno solo — l'inaugurazione del 19 settembre — e il // La landing dell'inaugurazione del 19 settembre. Il blocco del titolo è il ritaglio
// programma della giornata non c'è ancora: il testo lo dichiara invece di lasciare una // della locandina vera (font e calligrafia originali, non riprodotti): il resto della
// pagina vuota, ed è editabile dal pannello, così il cliente lo completa senza un deploy. // pagina usa la tipografia del sito e i colori campionati dalla locandina stessa.
const wa = t('global.whatsapp'); // L'URL di questa pagina è quello stampato nel QR code, quindi non va cambiato.
const waLink = `https://wa.me/39${wa.replace(/\D/g, '')}`; const WHATSAPP = '351 753 5977';
--- const waLink = `https://wa.me/39${WHATSAPP.replace(/\D/g, '')}`;
<Base title={t('eventi.meta.title')} description={t('eventi.meta.description')}>
<PageHero title={t('eventi.hero.title')} />
<section class="section"> type Slot = { ora: string; nome?: string };
type Gruppo = { label: string; slots: Slot[] };
const mattino: Gruppo[] = [
{ label: "Corsi Olistici — 20' a slot", slots: [
{ ora: '10:0010:20', nome: 'Mobility' },
{ ora: '10:3010:50', nome: 'Yoga' },
{ ora: '11:0011:20', nome: 'Pilates Mat' },
{ ora: '11:3011:50', nome: 'Reformer' },
{ ora: '12:0012:20', nome: 'Reformer' },
{ ora: '12:3012:50', nome: 'Reformer' },
] },
{ label: "Personal Training — 30' a slot", slots: ['10:30', '11:00', '11:30', '12:00', '12:30'].map((ora) => ({ ora })) },
{ label: "Check-up e Consulenze — 30' a slot", slots: ['10:30', '11:00', '11:30', '12:00', '12:30'].map((ora) => ({ ora })) },
];
const pomeriggio: Gruppo[] = [
{ label: "Corsi Olistici — 20' a slot", slots: [
{ ora: '16:0016:20', nome: 'Reformer' },
{ ora: '16:3016:50', nome: 'Reformer' },
{ ora: '17:0017:20', nome: 'Pilates' },
{ ora: '17:3017:50', nome: 'Mobility' },
] },
{ label: "Personal Training — 30' a slot", slots: ['16:00', '16:30', '17:00', '17:30'].map((ora) => ({ ora })) },
{ label: "Check-up e Consulenze — 30' a slot", slots: ['16:00', '16:30', '17:00', '17:30'].map((ora) => ({ ora })) },
];
---
<Base
title="In-Sanity Hour — 19 settembre 2026"
description="L'inaugurazione di InsanityLab: sabato 19 settembre 2026 a Bologna. Mini class, personal training, check-up e consulenze gratuite, poi dj set e aperitivo per tutti."
>
<section class="ev-hero">
<div class="ev-hero__inner" data-reveal>
<img
class="ev-hero__titolo"
src="/img/insanity-hour-titolo.png"
width="580" height="251"
alt="Save the date — In-Sanity Hour"
/>
<p class="ev-hero__giorno">19</p>
<p class="ev-hero__mese">Settembre</p>
<p class="ev-hero__inaug">Inaugurazione</p>
<p class="ev-hero__claim">Un giorno solo per vivere il Lab da vicino.</p>
<p class="ev-hero__gusto">DJ set · Aperitivo</p>
<p class="ev-hero__luogo">Via Pietro di Mattiolo, 5 — Bologna</p>
</div>
</section>
<section class="section ev-intro">
<div class="container" data-reveal>
<p class="ev-intro__testo">
Una giornata aperta a tutti — clienti e non — per scoprire la nuova struttura e conoscere
il nostro staff. Vivi l'esperienza In-Sanity Lab da vicino: mini class, allenamenti,
consulenze e check up avanzato.
</p>
<p class="ev-intro__gratis">Solo per oggi, tutto gratuito</p>
<p class="ev-intro__testo">
L'ingresso e l'aperitivo serale sono liberi, porta chi vuoi. La prenotazione serve solo
per assicurarti il posto nei singoli slot di prova.
</p>
<a class="btn" href={waLink} target="_blank" rel="noopener">Prenotati su WhatsApp</a>
<p class="ev-nota">{WHATSAPP} — rispondiamo entro la giornata</p>
</div>
</section>
<section class="section ev-programma">
<div class="container"> <div class="container">
<article class="evento" data-reveal> <p class="eyebrow" data-reveal>Programma della giornata</p>
<T tag="eventi.inaugurazione.title" as="h2" /> <h2 data-reveal>Come si svolge<br />l'In-Sanity Hour</h2>
<T tag="eventi.inaugurazione.body" as="p" />
<a class="btn" href={waLink} target="_blank" rel="noopener">{t('eventi.cta.label')}</a> <article class="ev-blocco" data-reveal>
<p class="ev-blocco__ora">10:00</p>
<h3>Apertura</h3>
<p class="ev-blocco__desc">
Apertura ufficiale del Lab. Visita libera degli spazi, promozioni dedicate attive
per tutta la giornata.
</p>
</article> </article>
<p class="evento__torna"><a href="/promo-eventi">{t('eventi.torna.label')}</a></p>
<article class="ev-blocco" data-reveal>
<p class="ev-blocco__ora">10:00 13:00</p>
<h3>Prova i nostri servizi</h3>
{mattino.map((gruppo) => (
<div class="ev-gruppo">
<p class="ev-gruppo__label">{gruppo.label}</p>
<ul class="ev-slots">
{gruppo.slots.map((slot) => (
<li class:list={['ev-slot', { 'ev-slot--corso': slot.nome }]}>
<span class="ev-slot__ora">{slot.ora}</span>
{slot.nome && <span class="ev-slot__nome">{slot.nome}</span>}
</li>
))}
</ul>
</div>
))}
<a class="btn btn--chiaro" href={waLink} target="_blank" rel="noopener">Prenota uno slot</a>
<p class="ev-nota">{WHATSAPP} — scrivici per riservare uno slot del mattino</p>
</article>
<article class="ev-blocco ev-blocco--pausa" data-reveal>
<p class="ev-blocco__ora">13:00 16:00</p>
<h3>Pausa</h3>
</article>
<article class="ev-blocco" data-reveal>
<p class="ev-blocco__ora">16:00 18:00</p>
<h3>Prova i nostri servizi</h3>
{pomeriggio.map((gruppo) => (
<div class="ev-gruppo">
<p class="ev-gruppo__label">{gruppo.label}</p>
<ul class="ev-slots">
{gruppo.slots.map((slot) => (
<li class:list={['ev-slot', { 'ev-slot--corso': slot.nome }]}>
<span class="ev-slot__ora">{slot.ora}</span>
{slot.nome && <span class="ev-slot__nome">{slot.nome}</span>}
</li>
))}
</ul>
</div>
))}
<a class="btn btn--chiaro" href={waLink} target="_blank" rel="noopener">Prenota uno slot</a>
<p class="ev-nota">{WHATSAPP} — scrivici per riservare uno slot del pomeriggio</p>
</article>
<article class="ev-blocco ev-blocco--festa" data-reveal>
<p class="ev-blocco__ora">18:00 22:00</p>
<h3>Si festeggia!</h3>
<p class="ev-blocco__desc">
DJ set e aperitivo per tutti. Brindisi e la nuova sede finalmente aperta a tutti.
Tra un drink e l'altro, la serata avrà anche i suoi momenti a sorpresa.
Promozioni esclusive valide solo durante l'evento.
</p>
</article>
</div>
</section>
<section class="ev-chiusura">
<div class="container" data-reveal>
<blockquote class="ev-citazione">
«Qui non ti alleni per somigliare a nessuno,<br />ti muovi per ricordare chi sei.»
</blockquote>
<p class="ev-chiusura__luogo">Via Pietro di Mattiolo, 5 — 40139, Bologna</p>
<a class="ev-locandina" href="/locandina-insanity-hour.pdf" download>Scarica la locandina</a>
<p class="ev-torna"><a href="/promo-eventi">← Torna a promo ed eventi</a></p>
</div> </div>
</section> </section>
</Base> </Base>
<style> <style>
.evento { max-width: 760px; margin: 0 auto; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 20px; } /* Colori campionati dalla locandina: il marrone e il beige sono quelli veri, non quelli
.evento :global(h2) { text-transform: uppercase; letter-spacing: .1em; margin: 0; } del sito, così la pagina e il volantino stampato si riconoscono come la stessa cosa. */
.evento :global(p) { margin: 0; } .ev-hero, .ev-intro, .ev-programma, .ev-chiusura {
.evento__torna { text-align: center; margin-top: 46px; } --ev-brown: #4f3126;
.evento__torna a { font-family: var(--font-heading); font-size: .72rem; letter-spacing: .18em; text-transform: uppercase; color: var(--c-heading); text-decoration: none; } --ev-brown-deep: #3d251c;
.evento__torna a:hover { color: var(--c-accent-dark); } --ev-beige: #d0b49e;
--ev-cream: #f6f1e9;
--ev-line: rgba(208, 180, 158, .35);
}
/* --- Hero: il ritaglio della locandina su fondo marrone identico, così l'immagine
non ha bordi visibili e sembra composta con il resto della pagina. --- */
.ev-hero {
background: radial-gradient(circle at 50% -10%, #5c3a2d 0%, var(--ev-brown) 55%, var(--ev-brown-deep) 100%);
color: var(--ev-cream);
padding: 64px 24px 72px;
text-align: center;
position: relative;
}
.ev-hero::before {
content: ''; position: absolute; inset: 14px;
border: 1px solid var(--ev-line); pointer-events: none;
}
.ev-hero__inner { max-width: 640px; margin: 0 auto; position: relative; }
.ev-hero__titolo { width: min(100%, 540px); margin: 0 auto; mix-blend-mode: lighten; }
.ev-hero__giorno {
font-family: var(--font-heading); font-size: clamp(3.4rem, 11vw, 4.6rem);
font-weight: 300; line-height: 1; letter-spacing: .04em; margin: 26px 0 0;
}
.ev-hero__mese {
font-family: var(--font-heading); font-size: .82rem; font-weight: 600;
letter-spacing: .35em; text-transform: uppercase; color: var(--ev-beige); margin: 10px 0 0;
}
.ev-hero__inaug {
display: inline-block; margin: 20px 0 0; padding: 7px 16px;
border: 1px solid var(--ev-line);
font-family: var(--font-heading); font-size: .68rem; font-weight: 600;
letter-spacing: .24em; text-transform: uppercase; color: var(--ev-cream);
}
.ev-hero__claim { margin: 26px 0 0; font-size: 1.05rem; line-height: 1.7; color: rgba(246, 241, 233, .9); }
.ev-hero__gusto {
margin: 18px 0 0; font-family: var(--font-heading); font-size: .7rem; font-weight: 600;
letter-spacing: .28em; text-transform: uppercase; color: var(--ev-beige);
}
.ev-hero__luogo {
margin: 26px 0 0; font-family: var(--font-heading); font-size: .68rem;
letter-spacing: .18em; text-transform: uppercase; color: rgba(246, 241, 233, .6);
}
.ev-locandina {
display: inline-block; margin-top: 34px; padding: 12px 22px;
border: 1px solid var(--ev-beige); color: var(--ev-beige); text-decoration: none;
font-family: var(--font-heading); font-size: .68rem; font-weight: 600;
letter-spacing: .2em; text-transform: uppercase; transition: background .2s, color .2s;
}
.ev-locandina:hover { background: var(--ev-beige); color: var(--ev-brown); }
/* --- Intro --- */
.ev-intro .container { max-width: 720px; text-align: center; }
.ev-intro__testo { margin: 0 0 18px; }
.ev-intro__gratis {
font-family: var(--font-heading); font-size: clamp(1.15rem, 2.6vw, 1.5rem);
font-weight: 600; letter-spacing: .06em; color: var(--ev-brown); margin: 26px 0;
}
.ev-nota {
margin: 14px 0 0; font-size: .84rem; color: var(--c-text-light);
}
/* --- Programma --- */
.ev-programma { background: var(--c-bg-alt); }
.ev-programma .container { max-width: 800px; }
.ev-programma .eyebrow, .ev-programma h2 { text-align: center; }
.ev-programma h2 { margin-bottom: 46px; }
.ev-blocco {
background: #fff; border-left: 3px solid var(--ev-beige);
padding: 30px 30px 34px; margin-bottom: 20px; text-align: center;
}
.ev-blocco__ora {
font-family: var(--font-heading); font-size: .78rem; font-weight: 600;
letter-spacing: .2em; color: var(--ev-brown); margin: 0 0 8px;
}
.ev-blocco h3 { text-transform: uppercase; letter-spacing: .08em; margin: 0 0 12px; }
.ev-blocco__desc { margin: 0; }
.ev-blocco--pausa { border-left-color: var(--c-text-light); opacity: .75; }
.ev-blocco--festa { border-left-color: var(--ev-brown); }
.ev-gruppo { margin-bottom: 24px; }
.ev-gruppo__label {
font-family: var(--font-heading); font-size: .72rem; font-weight: 600;
letter-spacing: .14em; text-transform: uppercase; color: var(--c-text-light);
margin: 0 0 12px;
}
.ev-slots {
list-style: none; margin: 0; padding: 0;
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
}
.ev-slot {
border: 1px solid var(--ev-line); padding: 8px 14px;
font-family: var(--font-heading); font-size: .78rem; color: var(--ev-brown);
}
.ev-slot--corso { display: flex; flex-direction: column; gap: 2px; }
.ev-slot__nome { font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; color: var(--c-text-light); }
.btn--chiaro { margin-top: 10px; }
/* --- Chiusura --- */
.ev-chiusura {
background: var(--ev-brown); color: var(--ev-cream);
padding: 64px 24px; text-align: center;
}
.ev-citazione {
margin: 0; font-size: clamp(1.35rem, 3.4vw, 1.9rem); line-height: 1.5;
font-style: italic; color: var(--ev-cream);
}
.ev-chiusura__luogo {
margin: 30px 0 0; font-family: var(--font-heading); font-size: .68rem;
letter-spacing: .18em; text-transform: uppercase; color: var(--ev-beige);
}
.ev-torna { margin: 34px 0 0; }
.ev-torna a {
font-family: var(--font-heading); font-size: .72rem; letter-spacing: .18em;
text-transform: uppercase; color: rgba(246, 241, 233, .7); text-decoration: none;
}
.ev-torna a:hover { color: var(--ev-beige); }
@media (max-width: 767px) {
.ev-hero { padding: 44px 18px 54px; }
.ev-blocco { padding: 24px 18px 28px; }
}
</style> </style>
+27 -1
View File
@@ -114,7 +114,7 @@ const countdownDeadline = ed ? t(tg('countdown.deadline')) : '';
{c.question && <T tag={tg(`card.${c.id}.question`)} as="p" class="question" />} {c.question && <T tag={tg(`card.${c.id}.question`)} as="p" class="question" />}
<div class="price-row"> <div class="price-row">
<T tag={tg(`card.${c.id}.price`)} as="div" class="price" /> <T tag={tg(`card.${c.id}.price`)} as="div" class="price" />
<T tag={tg(`card.${c.id}.price-old`)} as="div" class="price-old" /> {c.priceOld && <T tag={tg(`card.${c.id}.price-old`)} as="div" class="price-old" />}
</div> </div>
{c.unit && <T tag={tg(`card.${c.id}.unit`)} as="div" class="unit" />} {c.unit && <T tag={tg(`card.${c.id}.unit`)} as="div" class="unit" />}
<ul> <ul>
@@ -296,6 +296,16 @@ const countdownDeadline = ed ? t(tg('countdown.deadline')) : '';
.lp .cards-3 { grid-template-columns: repeat(3, 1fr); } .lp .cards-3 { grid-template-columns: repeat(3, 1fr); }
.lp .cards-3 .card { display: flex; flex-direction: column; } .lp .cards-3 .card { display: flex; flex-direction: column; }
.lp .cards-3 .card .buy { margin-top: auto; } .lp .cards-3 .card .buy { margin-top: auto; }
/* CARD DELLE EDIZIONI A CINQUE (promo settembre). Cinque non si divide in una griglia a due
o a tre colonne senza lasciare un buco in fondo: la griglia è a sei colonne e ogni card ne
occupa due, così le ultime due si spostano al centro invece di restare allineate a sinistra
con un vuoto accanto. */
.lp .cards-5 { grid-template-columns: repeat(6, 1fr); }
.lp .cards-5 .card { grid-column: span 2; display: flex; flex-direction: column; }
.lp .cards-5 .card .buy { margin-top: auto; }
.lp .cards-5 .card:nth-child(4) { grid-column: 2 / span 2; }
.lp .cards-5 .card:nth-child(5) { grid-column: 4 / span 2; }
.lp .card .question { .lp .card .question {
margin: 0 0 14px; font-size: 15px; line-height: 1.4; font-weight: 700; color: #2D2D2D; margin: 0 0 14px; font-size: 15px; line-height: 1.4; font-weight: 700; color: #2D2D2D;
} }
@@ -343,9 +353,25 @@ const countdownDeadline = ed ? t(tg('countdown.deadline')) : '';
.lp .faq-block.is-open .faq-a { display: block; } .lp .faq-block.is-open .faq-a { display: block; }
.lp .faq-a .hl { color: #8A6E3F; } .lp .faq-a .hl { color: #8A6E3F; }
/* Sotto i 1000px tre colonne diventano troppo strette per un prezzo e un elenco: le cinque
card passano a due per riga (griglia a quattro, span due), con la quinta al centro.
Nessun `min-width` a chiudere la fascia in basso: a viewport frazionari (uno zoom del
browser dà larghezze come 700,5px) fra 700 e 701 non varrebbe nessuna delle due regole e
le card tornerebbero a sei colonne. Ci pensa il blocco ≤700px, che viene dopo e a pari
specificità vince. */
@media (max-width: 1000px) {
.lp .cards-5 { grid-template-columns: repeat(4, 1fr); }
.lp .cards-5 .card:nth-child(4) { grid-column: span 2; }
.lp .cards-5 .card:nth-child(5) { grid-column: 2 / span 2; }
}
@media (max-width: 700px) { @media (max-width: 700px) {
.lp .hero, .lp .launch, .lp .why5, .lp .cards-wrap, .lp .faq-wrap { padding-left: 28px; padding-right: 28px; } .lp .hero, .lp .launch, .lp .why5, .lp .cards-wrap, .lp .faq-wrap { padding-left: 28px; padding-right: 28px; }
.lp .cards { grid-template-columns: 1fr; } .lp .cards { grid-template-columns: 1fr; }
/* A una colonna gli span e le collocazioni delle ultime due card lascerebbero righe vuote. */
.lp .cards-5 .card,
.lp .cards-5 .card:nth-child(4),
.lp .cards-5 .card:nth-child(5) { grid-column: auto; }
.lp .countdown .cd-boxes { gap: 6px; } .lp .countdown .cd-boxes { gap: 6px; }
.lp .countdown .cd-box { min-width: 0; flex: 1; padding: 10px 4px; } .lp .countdown .cd-box { min-width: 0; flex: 1; padding: 10px 4px; }
.lp .countdown .cd-num { font-size: 22px; } .lp .countdown .cd-num { font-size: 22px; }
+1 -1
View File
@@ -130,7 +130,7 @@ const tones = ['tan', 'taupe', 'dark'];
.prog { display: grid; grid-template-columns: 2.2fr 1fr; gap: 60px; } .prog { display: grid; grid-template-columns: 2.2fr 1fr; gap: 60px; }
.prog :global(.prog__desc) { white-space: pre-line; } .prog :global(.prog__desc) { white-space: pre-line; }
.prog__features { padding-left: 20px; display: grid; gap: 8px; margin-bottom: 30px; } .prog__features { padding-left: 20px; display: grid; gap: 8px; margin-bottom: 30px; }
.prog__extra { margin-bottom: 10px; } .prog__extra { padding-top: 30px; margin-bottom: 10px; }
.prog__extra h2 { letter-spacing: .1em; margin-bottom: .5em; } .prog__extra h2 { letter-spacing: .1em; margin-bottom: .5em; }
.prog__features li::marker { color: var(--c-accent); } .prog__features li::marker { color: var(--c-accent); }
.prog__img { width: 100%; height: auto; margin-bottom: 30px; } .prog__img { width: 100%; height: auto; margin-bottom: 30px; }
+31
View File
@@ -0,0 +1,31 @@
import { existsSync, readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
// L'indirizzo della pagina evento e' stampato dentro il QR code delle locandine affisse
// in giro per Bologna: una volta in strada non si corregge piu'. Questi test tengono
// insieme le tre cose che devono restare d'accordo — la rotta servita dal sito, l'URL
// cucito nel QR e il file che la pagina offre in download.
const ROTTA = 'src/pages/promo-eventi/eventi.astro';
const URL_STAMPATO = 'https://insanitylab.it/promo-eventi/eventi';
describe('locandina In-Sanity Hour', () => {
it('la rotta che il QR indica esiste', () => {
expect(existsSync(ROTTA)).toBe(true);
});
it('il generatore della locandina punta a quella rotta', () => {
const script = readFileSync('scripts/locandina-evento.py', 'utf-8');
expect(script).toContain(`URL_EVENTO = '${URL_STAMPATO}'`);
// La rotta del file deve corrispondere alla coda dell'URL, altrimenti il QR
// porterebbe altrove: /promo-eventi/eventi <- src/pages/promo-eventi/eventi.astro
const daFile = '/' + ROTTA.replace('src/pages/', '').replace('.astro', '');
expect(URL_STAMPATO.endsWith(daFile)).toBe(true);
});
it('la locandina che la pagina offre in download esiste', () => {
const pagina = readFileSync(ROTTA, 'utf-8');
const link = pagina.match(/href="(\/locandina-[^"]+\.pdf)"/);
expect(link).not.toBeNull();
expect(existsSync(`public${link![1]}`)).toBe(true);
});
});
+5 -4
View File
@@ -183,10 +183,11 @@ describe('promo ed eventi', () => {
} }
}); });
it('l\'inaugurazione dice la data e che il programma manca ancora', () => { // I tag `eventi.*` restano nel seed — in produzione le righe esistono già — ma dal
const testo = contentSeed.find((e) => e.tag === 'eventi.inaugurazione.body')!.value; // commit della landing dell'In-Sanity Hour non alimentano piu' nessuna pagina: la data
expect(contentSeed.find((e) => e.tag === 'eventi.inaugurazione.title')!.value).toContain('19 settembre 2026'); // vive nel suo tag, non nel titolo, da quando i due sono stati separati.
expect(testo).toContain('programma completo'); it('la data dell\'inaugurazione sta nel tag della data', () => {
expect(contentSeed.find((e) => e.tag === 'eventi.inaugurazione.date')!.value).toContain('19 settembre 2026');
}); });
// La voce di menu continua a puntare qui: è l'indirizzo già girato ai clienti. // La voce di menu continua a puntare qui: è l'indirizzo già girato ai clienti.
+8 -3
View File
@@ -8,11 +8,12 @@ describe('edizioneAttiva', () => {
it('sceglie l\'edizione la cui finestra contiene l\'istante', () => { 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-15T12:00:00+02:00'))?.id).toBe('agosto-2026');
expect(edizioneAttiva(d('2026-08-25T12:00:00+02:00'))?.id).toBe('bundle-apertura'); expect(edizioneAttiva(d('2026-08-25T12:00:00+02:00'))?.id).toBe('bundle-apertura');
expect(edizioneAttiva(d('2026-09-10T12:00:00+02:00'))?.id).toBe('settembre-2026');
}); });
it('fuori da ogni finestra non c\'è edizione attiva', () => { it('fuori da ogni finestra non c\'è edizione attiva', () => {
expect(edizioneAttiva(d('2026-07-15T12:00:00+02:00'))).toBeNull(); expect(edizioneAttiva(d('2026-07-15T12:00:00+02:00'))).toBeNull();
expect(edizioneAttiva(d('2026-09-10T12:00:00+02:00'))).toBeNull(); expect(edizioneAttiva(d('2026-09-25T12:00:00+02:00'))).toBeNull();
}); });
// Il passaggio è il momento in cui il sito cambia da solo: va provato al secondo, e in ora // Il passaggio è il momento in cui il sito cambia da solo: va provato al secondo, e in ora
@@ -22,13 +23,16 @@ describe('edizioneAttiva', () => {
expect(edizioneAttiva(d('2026-08-19T23:59:59+02:00'))?.id).toBe('agosto-2026'); 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-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-04T23:59:59+02:00'))?.id).toBe('bundle-apertura');
expect(edizioneAttiva(d('2026-09-05T00:00:00+02:00'))).toBeNull(); expect(edizioneAttiva(d('2026-09-05T00:00:00+02:00'))?.id).toBe('settembre-2026');
expect(edizioneAttiva(d('2026-09-19T23:59:59+02:00'))?.id).toBe('settembre-2026');
expect(edizioneAttiva(d('2026-09-20T00:00:00+02:00'))).toBeNull();
}); });
it('l\'ordine dell\'elenco non cambia il risultato', () => { it('l\'ordine dell\'elenco non cambia il risultato', () => {
const rovescio = [...edizioni].reverse(); const rovescio = [...edizioni].reverse();
expect(edizioneAttiva(d('2026-08-15T12:00:00+02:00'), rovescio)?.id).toBe('agosto-2026'); 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'); expect(edizioneAttiva(d('2026-08-25T12:00:00+02:00'), rovescio)?.id).toBe('bundle-apertura');
expect(edizioneAttiva(d('2026-09-10T12:00:00+02:00'), rovescio)?.id).toBe('settembre-2026');
}); });
}); });
@@ -80,7 +84,8 @@ describe('i testi di ogni edizione esistono', () => {
'countdown.unit.days', 'countdown.unit.hours', 'countdown.unit.minutes', 'countdown.unit.seconds']; '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'); if (e.launch) base.push('launch.badge', 'launch.title', 'launch.accent', 'launch.desc', 'why5.text');
for (const c of e.cards) { 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`); base.push(`card.${c.id}.eyebrow`, `card.${c.id}.title`, `card.${c.id}.price`);
if (c.priceOld) base.push(`card.${c.id}.price-old`);
if (c.featured) base.push(`card.${c.id}.badge`); if (c.featured) base.push(`card.${c.id}.badge`);
if (c.unit) base.push(`card.${c.id}.unit`); if (c.unit) base.push(`card.${c.id}.unit`);
if (c.question) base.push(`card.${c.id}.question`); if (c.question) base.push(`card.${c.id}.question`);