diff --git a/src/components/CalendarioModal.astro b/src/components/CalendarioModal.astro index 92d495a..248f51b 100644 --- a/src/components/CalendarioModal.astro +++ b/src/components/CalendarioModal.astro @@ -1,42 +1,40 @@ --- -// Popup a pieno schermo con la griglia delle class in Sala Piccola. +// Popup a pieno schermo con gli orari delle class in Sala Piccola. // -// Il calendario è ricostruito dal dato (`data/calendari.ts`) e non è l'immagine ricevuta dal -// cliente: si legge da telefono, si stampa, e per correggere una cella non serve rifare uno -// screenshot. Quale griglia compare lo decide la data — quella in vigore, o la prossima -// dichiarando da quando entra. +// I calendari sono ricostruiti dal dato (`data/calendari.ts`) e non sono le immagini ricevute +// dal cliente: si leggono da telefono, si scaricano in PDF, e per correggere una cella non +// serve rifare uno screenshot. +// +// Si vedono tutti gli orari non ancora scaduti, uno sotto l'altro: chi guarda ad agosto vuole +// sapere com'è settembre per intero. Quelli passati spariscono da soli. // // Si apre da qualunque elemento con `data-apri-calendario` nella stessa pagina. -import { calendarioDaMostrare, avvisiAttivi, GIORNI, DISCIPLINE, type Giorno } from '../data/calendari'; +import { calendariDaMostrare, avvisiAttivi, GIORNI, DISCIPLINE, type Calendario, type Giorno } from '../data/calendari'; import { t } from '../lib/content'; const adesso = new Date(); -const scelta = calendarioDaMostrare(adesso); +const mostrati = calendariDaMostrare(adesso); // Giornate fuori griglia (eventi, chiusure): spariscono da sole quando sono passate. const avvisi = avvisiAttivi(adesso); -const cal = scelta?.cal ?? null; -const dal = cal ? new Date(cal.from).toLocaleDateString('it-IT', { + +const dataDi = (iso: string) => new Date(iso).toLocaleDateString('it-IT', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'Europe/Rome', -}) : ''; -// Discipline davvero presenti in questa griglia: una legenda che elenca anche ciò che non c'è -// fa cercare lezioni che non esistono. -const presenti = cal - ? DISCIPLINE.filter((d) => cal.righe.some((r) => Object.values(r.celle).includes(d))) - : []; +}); +// Legenda per griglia: elencare discipline che lì non compaiono farebbe cercare lezioni +// che quel periodo non ha. +const disciplinePresenti = (cal: Calendario) => + DISCIPLINE.filter((d) => cal.righe.some((r) => Object.values(r.celle).includes(d))); --- -{cal && ( - +{mostrati.length > 0 && ( +
-

{cal.sala}

-

{cal.titolo}

- {scelta!.futuro && ( -

{t('calendario.futuro-label')} {dal}

- )} +

{mostrati[0].cal.sala}

+

Calendario delle class

@@ -50,43 +48,55 @@ const presenti = cal )}
- - - - - {GIORNI.map((g) => ( - + {mostrati.map(({ cal, stato }) => ( +
+
+

{cal.titolo}

+ + {stato === 'in-corso' ? 'In vigore' : `${t('calendario.futuro-label')} ${dataDi(cal.from)}`} + +
+ +
{cal.sala} - {g} - {cal.noteGiorno?.[g] && {cal.noteGiorno[g]}} -
+ + + + {GIORNI.map((g) => ( + + ))} + + + + {cal.righe.map((r) => ( + + + {GIORNI.map((g: Giorno) => { + const chiuso = cal.chiusi.includes(g); + const lezione = r.celle[g]; + return ( + + ); + })} + + ))} + +
Ora + {g} + {cal.noteGiorno?.[g] && {cal.noteGiorno[g]}} +
{r.ora} + {chiuso ? {t('calendario.chiuso-label')} : lezione ?? ''} +
+ +
    + {disciplinePresenti(cal).map((d) => ( +
  • {d}
  • ))} - - - - {cal.righe.map((r) => ( - - {r.ora} - {GIORNI.map((g: Giorno) => { - const chiuso = cal.chiusi.includes(g); - const lezione = r.celle[g]; - return ( - - {chiuso ? {t('calendario.chiuso-label')} : lezione ?? ''} - - ); - })} - - ))} - - +
+ + ))}
-
    - {presenti.map((d) => ( -
  • {d}
  • - ))} -

{t('calendario.nota')}

@@ -104,9 +114,9 @@ const presenti = cal dlg.querySelector('[data-chiudi-calendario]')?.addEventListener('click', () => dlg.close()); // Click sul fondale: il target è il dialog stesso solo quando si preme fuori dal riquadro. dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); }); - // "Scarica in PDF" è un link a `/calendario/.pdf`, cioè un file vero generato dal - // server: si salva sul telefono e si gira su WhatsApp. Il CSS di stampa più sotto resta - // per chi preme Ctrl+P col popup aperto. + // "Scarica in PDF" è un link a /calendario/tutti.pdf, cioè un file vero generato dal + // server, un orario per pagina: si salva sul telefono e si gira su WhatsApp. Il CSS di + // stampa più sotto resta per chi preme Ctrl+P col popup aperto. } @@ -126,10 +136,6 @@ const presenti = cal letter-spacing: .25em; text-transform: uppercase; color: var(--c-accent-dark); } .cal-dlg__titolo { margin: 6px 0 0; font-size: 1.5rem; } - .cal-dlg__futuro { - margin: 6px 0 0; font-family: var(--font-heading); font-size: .72rem; letter-spacing: .12em; - text-transform: uppercase; color: var(--c-text-light); - } .cal-dlg__azioni { display: flex; align-items: center; gap: 12px; flex-shrink: 0; } .cal-dlg__btn { display: inline-block; font-family: var(--font-heading); font-size: .68rem; font-weight: 600; @@ -144,8 +150,8 @@ const presenti = cal } .cal-dlg__chiudi:hover { border-color: var(--c-accent); color: var(--c-accent-dark); } - /* Giornate fuori griglia: stanno sopra la tabella perché sono l'eccezione a ciò che la - tabella dice, e sotto passerebbero per una postilla. */ + /* Giornate fuori griglia: stanno sopra le tabelle perché sono l'eccezione a ciò che le + tabelle dicono, e sotto passerebbero per una postilla. */ .cal-avvisi { list-style: none; margin: 18px 0 0; padding: 12px 16px; background: var(--c-bg-alt); border-left: 3px solid var(--c-accent); display: flex; flex-direction: column; gap: 4px; @@ -153,13 +159,24 @@ const presenti = cal .cal-avvisi li { font-size: .84rem; color: var(--c-text); } .cal-avvisi strong { color: var(--c-heading); } + .cal-dlg__scroll { flex: 1; overflow: auto; margin: 18px 0 0; } + .cal-blocco + .cal-blocco { margin-top: 34px; padding-top: 26px; border-top: 1px solid #e4e0d9; } + .cal-blocco__top { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; } + .cal-blocco__titolo { margin: 0; font-size: 1.05rem; } + .cal-tag { + font-family: var(--font-heading); font-size: .62rem; font-weight: 600; letter-spacing: .14em; + text-transform: uppercase; padding: 4px 9px; background: var(--c-bg-alt); color: var(--c-text-light); + } + /* L'orario valido adesso si distingue a colpo d'occhio da quello che comincerà dopo: senza, + due griglie affiancate si leggono come alternative fra cui scegliere. */ + .cal-tag.is-ora { background: var(--c-accent); color: #fff; } + /* La griglia ha otto colonne: su telefono scorre in orizzontale invece di comprimersi fino a diventare illeggibile, e la colonna delle ore resta ferma. */ - .cal-dlg__scroll { flex: 1; overflow: auto; margin: 22px 0 0; } .cal-tab { border-collapse: collapse; width: 100%; min-width: 720px; } .cal-tab th, .cal-tab td { border: 1px solid #e4e0d9; padding: 9px 10px; text-align: left; } .cal-tab thead th { - position: sticky; top: 0; z-index: 2; background: var(--c-bg-alt); + background: var(--c-bg-alt); font-family: var(--font-heading); font-size: .7rem; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: var(--c-heading); } @@ -169,7 +186,7 @@ const presenti = cal } .cal-tab__ora-h { color: var(--c-accent-dark) !important; } .cal-tab__ora { - position: sticky; left: 0; z-index: 1; background: var(--c-bg); + background: var(--c-bg); font-family: var(--font-heading); font-size: .78rem; font-weight: 600; color: var(--c-heading); white-space: nowrap; } @@ -185,8 +202,7 @@ const presenti = cal .cal-tab__chiuso-txt { font-size: .62rem; letter-spacing: .12em; color: var(--c-text-light); } .cal-tab thead th.is-chiuso { color: var(--c-text-light); } - .cal-dlg__giu { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 24px; padding-top: 16px; } - .cal-legenda { display: flex; flex-wrap: wrap; gap: 8px 16px; list-style: none; margin: 0; padding: 0; } + .cal-legenda { display: flex; flex-wrap: wrap; gap: 8px 16px; list-style: none; margin: 12px 0 0; padding: 0; } .cal-legenda li { font-family: var(--font-heading); font-size: .66rem; letter-spacing: .12em; padding: 5px 10px; color: var(--c-heading); @@ -195,11 +211,13 @@ const presenti = cal .cal-legenda li.is-pilates { background: #f6efe0; box-shadow: inset 3px 0 0 #cbb98f; } .cal-legenda li.is-yoga { background: #e3eadf; box-shadow: inset 3px 0 0 #8ba57f; } .cal-legenda li.is-mobility { background: #f0e3e3; box-shadow: inset 3px 0 0 #b78e8e; } + .cal-dlg__giu { padding-top: 16px; } .cal-dlg__nota { margin: 0; font-size: .8rem; color: var(--c-text-light); } - /* Stampa (ossia "Salva come PDF"): resta solo la griglia, in orizzontale e a piena pagina. - `visibility` invece di `display` perché il dialog è annidato nel corpo della pagina e - nascondere gli antenati nasconderebbe anche lui. */ + /* Stampa dalla pagina (Ctrl+P col popup aperto): resta solo il calendario, un orario per + foglio. Il bottone scarica invece il PDF generato dal server, che è il documento buono da + girare. `visibility` invece di `display` perché il dialog è annidato nel corpo della + pagina e nascondere gli antenati nasconderebbe anche lui. */ @media print { @page { size: A4 landscape; margin: 10mm; } body { visibility: hidden; } @@ -209,8 +227,9 @@ const presenti = cal .cal-dlg__box { padding: 0; height: auto; } .cal-dlg__azioni { display: none; } .cal-dlg__scroll { overflow: visible; } + .cal-blocco { break-inside: avoid; } + .cal-blocco + .cal-blocco { break-before: page; border-top: 0; padding-top: 0; } .cal-tab { min-width: 0; } - .cal-tab thead th, .cal-tab__ora { position: static; } .cal-tab th, .cal-tab td { padding: 5px 6px; } .cal-tab__cella { font-size: .62rem; } } diff --git a/src/data/calendari.ts b/src/data/calendari.ts index 28bca28..502d295 100644 --- a/src/data/calendari.ts +++ b/src/data/calendari.ts @@ -149,20 +149,26 @@ export function avvisiAttivi(now: Date, list: AvvisoCalendario[] = avvisi): Avvi /** Servizi dalla cui pagina si apre il calendario della sala. */ export const SERVIZI_CON_CALENDARIO = ['performance-class', 'holistic-class']; -/** - * Il calendario da mostrare: quello in vigore adesso, oppure — se nessuno lo è ancora — il - * primo che entrerà in vigore, segnalato come futuro. `null` solo se non ce n'è nessuno da - * mostrare, e in quel caso la voce non compare in pagina. - * - * Serve il ripiego sul futuro perché il link esiste **prima** che il primo orario cominci: un - * "nessun calendario disponibile" ad agosto non direbbe niente a chi vuole sapere gli orari - * di settembre. - */ -export function calendarioDaMostrare(now: Date, list: Calendario[] = calendari): - { cal: Calendario; futuro: boolean } | null { - const t = now.getTime(); - const attivo = list.find((c) => t >= Date.parse(c.from) && (c.to === null || t <= Date.parse(c.to))); - if (attivo) return { cal: attivo, futuro: false }; - const prossimi = list.filter((c) => Date.parse(c.from) > t).sort((a, b) => Date.parse(a.from) - Date.parse(b.from)); - return prossimi.length ? { cal: prossimi[0], futuro: true } : null; +export interface CalendarioMostrato { + cal: Calendario; + stato: 'in-corso' | 'futuro'; +} + +/** + * I calendari da mostrare: quello in vigore e quelli che verranno, in ordine di partenza. + * + * Si vedono **tutti insieme**, non solo quello del momento: chi guarda a fine agosto vuole + * sapere com'è settembre per intero, e vedere una griglia sola gli farebbe credere che sia + * quella per sempre. Ognuna porta il proprio stato, così l'orario in vigore adesso non si + * confonde con quello che comincerà dopo. + * + * Gli orari **scaduti spariscono da soli**: dal 21 settembre resta il solo foglio valido, e + * nessuno rischia di leggere le lezioni di un periodo finito. + */ +export function calendariDaMostrare(now: Date, list: Calendario[] = calendari): CalendarioMostrato[] { + const t = now.getTime(); + return list + .filter((c) => c.to === null || t <= Date.parse(c.to)) + .sort((a, b) => Date.parse(a.from) - Date.parse(b.from)) + .map((cal) => ({ cal, stato: t >= Date.parse(cal.from) ? 'in-corso' : 'futuro' })); } diff --git a/src/lib/calendario-pdf.ts b/src/lib/calendario-pdf.ts index 8be8fc6..4bea433 100644 --- a/src/lib/calendario-pdf.ts +++ b/src/lib/calendario-pdf.ts @@ -24,21 +24,29 @@ const C_BORDO = '#cfc7b8'; // più scuro del fondo delle celle: due lezioni ugua const C_ACCENTO = '#9c8b70'; const C_ALT = '#f5f3f0'; -export const nomeFilePdf = (cal: Calendario): string => - `insanitylab-calendario-${cal.id}.pdf`; +/** Un solo orario: il nome porta il suo id. Più orari insieme: il nome è generico. */ +export const nomeFilePdf = (cals: Calendario[]): string => + cals.length === 1 ? `insanitylab-calendario-${cals[0].id}.pdf` : 'insanitylab-calendario-class.pdf'; /** - * Genera il PDF e restituisce il file completo. Il documento è una pagina sola: la griglia sta - * in A4 orizzontale, e spezzarla su due pagine renderebbe illeggibile proprio ciò che si va a - * cercare — l'incrocio fra un giorno e un'ora. + * Genera il PDF e restituisce il file completo: **un orario per pagina**. + * + * Ogni griglia sta in una pagina intera perché spezzarla renderebbe illeggibile proprio ciò + * che si va a cercare — l'incrocio fra un giorno e un'ora; e due orari sullo stesso foglio si + * confonderebbero fra loro. Le giornate fuori griglia si ripetono su ogni pagina: un foglio + * stampato da solo deve bastare a sé stesso. */ -export function generaCalendarioPdf(cal: Calendario, avvisi: AvvisoCalendario[] = []): Promise { +export function generaCalendariPdf(cals: Calendario[], avvisi: AvvisoCalendario[] = []): Promise { const doc = new PDFDocument({ size: 'A4', layout: 'landscape', margin: 32, info: { - Title: `InsanityLab — ${cal.sala}, ${cal.titolo}`, + // Il titolo è quello che compare nella scheda del lettore PDF e nel nome suggerito dai + // telefoni: con un orario solo dice quale, con più orari dice il periodo coperto. + Title: cals.length === 1 + ? `InsanityLab — ${cals[0].sala}, ${cals[0].titolo}` + : `InsanityLab — Calendario delle class (${cals.map((c) => c.titolo).join('; ')})`, Author: 'InsanityLab', Subject: 'Calendario delle class', }, @@ -51,6 +59,17 @@ export function generaCalendarioPdf(cal: Calendario, avvisi: AvvisoCalendario[] doc.on('error', rifiuta); }); + cals.forEach((cal, indice) => { + if (indice > 0) doc.addPage(); + disegnaPagina(doc, cal, avvisi); + }); + + doc.end(); + return finito; +} + +/** Una pagina: testata, griglia, legenda, giornate fuori griglia, piè di pagina. */ +function disegnaPagina(doc: PDFKit.PDFDocument, cal: Calendario, avvisi: AvvisoCalendario[]): void { const x0 = doc.page.margins.left; const larghezzaUtile = doc.page.width - doc.page.margins.left - doc.page.margins.right; @@ -165,7 +184,4 @@ export function generaCalendarioPdf(cal: Calendario, avvisi: AvvisoCalendario[] ); doc.font('Helvetica').fontSize(7).fillColor(C_ACCENTO) .text('insanitylab.it', x0, yAvvisi + 14, { width: larghezzaUtile, align: 'right' }); - - doc.end(); - return finito; } diff --git a/src/pages/calendario/[id].pdf.ts b/src/pages/calendario/[id].pdf.ts index 20dd16b..3bfedcb 100644 --- a/src/pages/calendario/[id].pdf.ts +++ b/src/pages/calendario/[id].pdf.ts @@ -1,29 +1,37 @@ import type { APIRoute } from 'astro'; -import { avvisiAttivi, calendari } from '../../data/calendari'; -import { generaCalendarioPdf, nomeFilePdf } from '../../lib/calendario-pdf'; +import { avvisiAttivi, calendari, calendariDaMostrare } from '../../data/calendari'; +import { generaCalendariPdf, nomeFilePdf } from '../../lib/calendario-pdf'; export const prerender = false; -// Il PDF del calendario, come file scaricabile: `/calendario/.pdf`. +// Il PDF degli orari, come file scaricabile. +// +// /calendario/tutti.pdf tutti gli orari validi, uno per pagina +// /calendario/settembre-1-18.pdf un orario solo // // Pubblico come il resto del sito — è lo stesso orario che chiunque legge nel popup, e un file -// dietro autenticazione non si potrebbe girare su WhatsApp, che è come questo centro -// comunica coi clienti. +// dietro autenticazione non si potrebbe girare su WhatsApp, che è come questo centro comunica +// coi clienti. export const GET: APIRoute = async ({ params }) => { - const cal = calendari.find((c) => c.id === params.id); + // "tutti" segue la stessa regola della pagina: gli orari scaduti non ci sono. Un PDF che si + // porta dietro il periodo finito farebbe cercare lezioni che non esistono più. + const cals = params.id === 'tutti' + ? calendariDaMostrare(new Date()).map((m) => m.cal) + : calendari.filter((c) => c.id === params.id); + // Un id sconosciuto è 404, non un PDF vuoto: un file che si apre e non contiene niente fa // credere che il calendario sia vuoto. - if (!cal) return new Response('Calendario inesistente.', { status: 404 }); + if (cals.length === 0) return new Response('Calendario inesistente.', { status: 404 }); // Le giornate fuori griglia stanno anche nel PDF: è il file che gira su WhatsApp, e chi lo // riceve non ha davanti la pagina dove sono scritte. - const pdf = await generaCalendarioPdf(cal, avvisiAttivi(new Date())); + const pdf = await generaCalendariPdf(cals, avvisiAttivi(new Date())); return new Response(new Uint8Array(pdf), { headers: { 'Content-Type': 'application/pdf', // `attachment` perché il bottone dice "scarica": senza, il browser lo aprirebbe in una // scheda e su telefono resterebbe lì, senza finire fra i file. - 'Content-Disposition': `attachment; filename="${nomeFilePdf(cal)}"`, + 'Content-Disposition': `attachment; filename="${nomeFilePdf(cals)}"`, 'Content-Length': String(pdf.length), // Un'ora: basta a non rigenerarlo a ogni click, e non abbastanza perché un avviso ormai // scaduto (le giornate fuori griglia spariscono da sole) resti in giro un giorno intero. diff --git a/src/pages/services/[slug].astro b/src/pages/services/[slug].astro index 8b5ef1c..231c648 100644 --- a/src/pages/services/[slug].astro +++ b/src/pages/services/[slug].astro @@ -8,7 +8,7 @@ import { serviceIcons } from '../../data/service-icons'; import T from '../../components/content/T.astro'; import TImg from '../../components/content/TImg.astro'; import CalendarioModal from '../../components/CalendarioModal.astro'; -import { calendarioDaMostrare, SERVIZI_CON_CALENDARIO } from '../../data/calendari'; +import { calendariDaMostrare, SERVIZI_CON_CALENDARIO } from '../../data/calendari'; import { t } from '../../lib/content'; export const prerender = false; @@ -18,7 +18,7 @@ const slug = service.slug; // Il calendario della sala si apre dalle pagine delle class. La voce compare solo se c'è // davvero una griglia da mostrare (in vigore o prossima): un link che apre un popup vuoto è // peggio di nessun link. -const conCalendario = SERVIZI_CON_CALENDARIO.includes(slug) && calendarioDaMostrare(new Date()) !== null; +const conCalendario = SERVIZI_CON_CALENDARIO.includes(slug) && calendariDaMostrare(new Date()).length > 0; const others = services.filter((p) => p.slug !== service.slug && p.grid !== false); const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/program-*.jpg', { eager: true }); const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default; diff --git a/tests/calendari.test.ts b/tests/calendari.test.ts index 5ae6232..b37968b 100644 --- a/tests/calendari.test.ts +++ b/tests/calendari.test.ts @@ -1,48 +1,47 @@ import { describe, it, expect } from 'vitest'; -import { avvisi, avvisiAttivi, calendari, calendarioDaMostrare, DISCIPLINE, GIORNI, type Giorno } from '../src/data/calendari'; +import { avvisi, avvisiAttivi, calendari, calendariDaMostrare, DISCIPLINE, GIORNI, type Giorno } from '../src/data/calendari'; const d = (iso: string) => new Date(iso); -describe('calendarioDaMostrare', () => { - it('in vigore: mostra quello del periodo', () => { - expect(calendarioDaMostrare(d('2026-09-05T10:00:00+02:00'))).toMatchObject({ - cal: { id: 'settembre-1-18' }, futuro: false, - }); - expect(calendarioDaMostrare(d('2026-09-25T10:00:00+02:00'))).toMatchObject({ - cal: { id: 'settembre-21-in-poi' }, futuro: false, - }); - // `to: null` = a tempo indeterminato: fra un anno vale ancora quello. - expect(calendarioDaMostrare(d('2027-03-01T10:00:00+02:00'))).toMatchObject({ - cal: { id: 'settembre-21-in-poi' }, futuro: false, - }); +describe('calendariDaMostrare', () => { + const idDi = (quando: string) => calendariDaMostrare(d(quando)).map((m) => `${m.cal.id}:${m.stato}`); + + // Si vedono tutti insieme, non solo quello del momento: chi guarda ad agosto vuole sapere + // com'è settembre per intero, e una griglia sola gli farebbe credere che valga per sempre. + it('prima dell\'inizio: entrambi, marcati futuri', () => { + expect(idDi('2026-08-17T10:00:00+02:00')) + .toEqual(['settembre-1-18:futuro', 'settembre-21-in-poi:futuro']); }); - // Il link esiste prima che il primo orario cominci: ad agosto deve mostrare gli orari di - // settembre dichiarando che entrano dopo, non un "nessun calendario" che non dice niente. - it('prima dell\'inizio: mostra il prossimo, marcato come futuro', () => { - expect(calendarioDaMostrare(d('2026-08-17T10:00:00+02:00'))).toMatchObject({ - cal: { id: 'settembre-1-18' }, futuro: true, - }); + it('durante il primo periodo: il primo è in corso, il secondo resta annunciato', () => { + expect(idDi('2026-09-05T10:00:00+02:00')) + .toEqual(['settembre-1-18:in-corso', 'settembre-21-in-poi:futuro']); + }); + + // Un orario scaduto non deve restare in pagina: farebbe cercare lezioni che non ci sono più. + it('finito il primo periodo, il primo orario sparisce', () => { + expect(idDi('2026-09-25T10:00:00+02:00')).toEqual(['settembre-21-in-poi:in-corso']); + expect(idDi('2027-03-01T10:00:00+02:00')).toEqual(['settembre-21-in-poi:in-corso']); }); // 19 e 20 settembre non stanno in nessuna griglia perché quei due giorni non hanno class: // il 19 c'è la presentazione della nuova apertura, il 20 si sta chiusi (Adriano, 17/08). - // La griglia mostrata è quindi quella del 21, marcata futura, e l'informazione sui due - // giorni arriva dagli avvisi qui sotto. - it('nei due giorni fuori griglia (19-20 settembre) mostra il prossimo', () => { - expect(calendarioDaMostrare(d('2026-09-19T10:00:00+02:00'))).toMatchObject({ - cal: { id: 'settembre-21-in-poi' }, futuro: true, - }); - expect(calendarioDaMostrare(d('2026-09-20T23:00:00+02:00'))).toMatchObject({ - cal: { id: 'settembre-21-in-poi' }, futuro: true, - }); + it('nei due giorni fuori griglia resta il solo orario del 21, annunciato', () => { + expect(idDi('2026-09-19T10:00:00+02:00')).toEqual(['settembre-21-in-poi:futuro']); + expect(idDi('2026-09-20T23:00:00+02:00')).toEqual(['settembre-21-in-poi:futuro']); }); it('confini al secondo, in ora italiana', () => { - expect(calendarioDaMostrare(d('2026-09-18T23:59:59+02:00'))?.cal.id).toBe('settembre-1-18'); - expect(calendarioDaMostrare(d('2026-09-21T00:00:00+02:00'))).toMatchObject({ - cal: { id: 'settembre-21-in-poi' }, futuro: false, - }); + expect(idDi('2026-09-18T23:59:59+02:00')) + .toEqual(['settembre-1-18:in-corso', 'settembre-21-in-poi:futuro']); + expect(idDi('2026-09-19T00:00:00+02:00')).toEqual(['settembre-21-in-poi:futuro']); + expect(idDi('2026-09-21T00:00:00+02:00')).toEqual(['settembre-21-in-poi:in-corso']); + }); + + it('l\'ordine dell\'elenco di partenza non cambia il risultato', () => { + const rovescio = [...calendari].reverse(); + expect(calendariDaMostrare(d('2026-09-05T10:00:00+02:00'), rovescio).map((m) => m.cal.id)) + .toEqual(['settembre-1-18', 'settembre-21-in-poi']); }); }); diff --git a/tests/calendario-pdf.test.ts b/tests/calendario-pdf.test.ts index 676fd82..a4353f2 100644 --- a/tests/calendario-pdf.test.ts +++ b/tests/calendario-pdf.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect } from 'vitest'; import { avvisi, calendari } from '../src/data/calendari'; -import { generaCalendarioPdf, nomeFilePdf } from '../src/lib/calendario-pdf'; +import { generaCalendariPdf, nomeFilePdf } from '../src/lib/calendario-pdf'; describe('PDF del calendario', () => { for (const cal of calendari) { it(`${cal.id}: produce un PDF valido`, async () => { - const pdf = await generaCalendarioPdf(cal); + const pdf = await generaCalendariPdf([cal]); // Un PDF comincia sempre con %PDF- e finisce con %%EOF: se il documento non venisse // chiuso (`doc.end()` mancante, o un errore a metà) il file arriverebbe monco al // browser, che mostrerebbe una pagina bianca invece di un errore. @@ -20,7 +20,7 @@ describe('PDF del calendario', () => { // /Type /Pages, che dichiara quante pagine contiene. it('sta in una pagina sola, anche con le giornate fuori griglia in fondo', async () => { for (const cal of calendari) { - const pdf = await generaCalendarioPdf(cal, avvisi); + const pdf = await generaCalendariPdf([cal], avvisi); const conteggio = /\/Type\s*\/Pages[^>]*?\/Count\s+(\d+)/s.exec(pdf.toString('latin1')); expect(conteggio, `${cal.id}: numero di pagine non dichiarato`).not.toBeNull(); expect(Number(conteggio![1]), `${cal.id}: il calendario occupa più di una pagina`).toBe(1); @@ -28,8 +28,20 @@ describe('PDF del calendario', () => { }); it('il nome del file distingue i due periodi', () => { - const nomi = calendari.map(nomeFilePdf); + const nomi = calendari.map((c) => nomeFilePdf([c])); expect(new Set(nomi).size).toBe(nomi.length); for (const n of nomi) expect(n).toMatch(/^insanitylab-calendario-[a-z0-9-]+\.pdf$/); + // Più orari insieme: un nome che nominasse solo il primo farebbe credere che dentro ci + // sia quello e basta. + expect(nomeFilePdf(calendari)).toBe('insanitylab-calendario-class.pdf'); + }); + + // Un orario per pagina: due griglie sullo stesso foglio si confonderebbero fra loro, e + // spezzarne una a metà toglie proprio ciò che si va a cercare, l'incrocio giorno/ora. + it('con più orari fa una pagina per ciascuno', async () => { + const pdf = await generaCalendariPdf(calendari, avvisi); + const conteggio = /\/Type\s*\/Pages[^>]*?\/Count\s+(\d+)/s.exec(pdf.toString('latin1')); + expect(conteggio).not.toBeNull(); + expect(Number(conteggio![1])).toBe(calendari.length); }); });