510d7ca4ec
Primo passo della separazione sito / piattaforme decisa da Adriano il 03/09: due applicazioni autosufficienti, senza workspace di root, cosi' che ognuna resti un progetto Astro completo ed estraibile. Qui non cambia niente del codice: solo la radice. I pattern del .gitignore che avevano una barra erano ancorati alla radice e dopo lo spostamento non avrebbero piu' coperto niente (data/*.db*, uploads/*): resi validi a qualunque profondita'. Suite: 45 file, 347 test verdi prima e dopo lo spostamento. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
262 lines
13 KiB
Plaintext
262 lines
13 KiB
Plaintext
---
|
||
// Popup a pieno schermo con gli orari delle class in Sala Piccola.
|
||
//
|
||
// 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 { calendariDaMostrare, avvisiAttivi, GIORNI, DISCIPLINE, type Calendario, type Giorno } from '../data/calendari';
|
||
import { t } from '../lib/content';
|
||
|
||
const adesso = new Date();
|
||
const mostrati = calendariDaMostrare(adesso);
|
||
// Giornate fuori griglia (eventi, chiusure): spariscono da sole quando sono passate.
|
||
const avvisi = avvisiAttivi(adesso);
|
||
|
||
const dataDi = (iso: string) => new Date(iso).toLocaleDateString('it-IT', {
|
||
day: 'numeric', month: 'long', year: 'numeric', timeZone: 'Europe/Rome',
|
||
});
|
||
// 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)));
|
||
---
|
||
{mostrati.length > 0 && (
|
||
<dialog class="cal-dlg" aria-label="Calendario delle class">
|
||
<div class="cal-dlg__box">
|
||
<header class="cal-dlg__top">
|
||
<div>
|
||
<p class="cal-dlg__sala">{mostrati[0].cal.sala}</p>
|
||
<h2 class="cal-dlg__titolo">Calendario delle class</h2>
|
||
</div>
|
||
<div class="cal-dlg__azioni">
|
||
<a class="cal-dlg__btn" href="/calendario/tutti.pdf" download>{t('calendario.pdf-label')}</a>
|
||
<button type="button" class="cal-dlg__chiudi" data-chiudi-calendario aria-label={t('calendario.chiudi-label')}>×</button>
|
||
</div>
|
||
</header>
|
||
|
||
{avvisi.length > 0 && (
|
||
<ul class="cal-avvisi">
|
||
{avvisi.map((a) => (
|
||
<li><strong>{a.quando}</strong> — {a.testo}</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
|
||
<div class="cal-dlg__scroll">
|
||
{mostrati.map(({ cal, stato }) => (
|
||
<section class="cal-blocco">
|
||
<div class="cal-blocco__top">
|
||
<h3 class="cal-blocco__titolo">{cal.titolo}</h3>
|
||
<span class:list={['cal-tag', stato === 'in-corso' && 'is-ora']}>
|
||
{stato === 'in-corso' ? 'In vigore' : `${t('calendario.futuro-label')} ${dataDi(cal.from)}`}
|
||
</span>
|
||
</div>
|
||
|
||
<table class="cal-tab">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col" class="cal-tab__ora-h">Ora</th>
|
||
{GIORNI.map((g) => (
|
||
<th scope="col" class:list={['cal-tab__gg', cal.chiusi.includes(g) && 'is-chiuso']}>
|
||
{g}
|
||
{cal.noteGiorno?.[g] && <span class="cal-tab__nota">{cal.noteGiorno[g]}</span>}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{cal.righe.map((r) => (
|
||
<tr class:list={[r.clou && 'is-clou']}>
|
||
<th scope="row" class="cal-tab__ora">{r.ora}</th>
|
||
{GIORNI.map((g: Giorno) => {
|
||
const chiuso = cal.chiusi.includes(g);
|
||
const lezione = r.celle[g];
|
||
return (
|
||
<td class:list={['cal-tab__cella', chiuso && 'is-chiuso', lezione && `is-${lezione.toLowerCase()}`]}>
|
||
{chiuso ? <span class="cal-tab__chiuso-txt">{t('calendario.chiuso-label')}</span> : lezione ?? ''}
|
||
</td>
|
||
);
|
||
})}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
<ul class="cal-legenda">
|
||
{disciplinePresenti(cal).map((d) => (
|
||
<li class={`is-${d.toLowerCase()}`}>{d}</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
))}
|
||
</div>
|
||
|
||
<footer class="cal-dlg__giu">
|
||
<p class="cal-dlg__nota">{t('calendario.nota')}</p>
|
||
</footer>
|
||
</div>
|
||
</dialog>
|
||
)}
|
||
|
||
<script>
|
||
// Apertura/chiusura del popup. Nessuna libreria: <dialog> nativo dà da solo il fondale, il
|
||
// focus trap e la chiusura con Esc.
|
||
const dlg = document.querySelector<HTMLDialogElement>('.cal-dlg');
|
||
if (dlg) {
|
||
document.querySelectorAll<HTMLElement>('[data-apri-calendario]').forEach((el) => {
|
||
el.addEventListener('click', (e) => { e.preventDefault(); dlg.showModal(); });
|
||
});
|
||
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/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.
|
||
}
|
||
</script>
|
||
|
||
<style is:global>
|
||
.cal-dlg {
|
||
width: 100%; max-width: none; height: 100%; max-height: none; margin: 0; padding: 0;
|
||
border: 0; background: transparent; color: var(--c-text);
|
||
}
|
||
.cal-dlg::backdrop { background: rgba(43,43,43,.75); }
|
||
.cal-dlg__box {
|
||
display: flex; flex-direction: column; height: 100%; background: var(--c-bg);
|
||
padding: 28px clamp(16px, 4vw, 48px) 24px;
|
||
}
|
||
.cal-dlg__top { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; }
|
||
.cal-dlg__sala {
|
||
margin: 0; font-family: var(--font-heading); font-size: .72rem; font-weight: 600;
|
||
letter-spacing: .25em; text-transform: uppercase; color: var(--c-accent-dark);
|
||
}
|
||
.cal-dlg__titolo { margin: 6px 0 0; font-size: 1.5rem; }
|
||
.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;
|
||
letter-spacing: .2em; text-transform: uppercase; text-decoration: none; border: 0;
|
||
cursor: pointer; padding: 13px 22px; background: var(--c-accent); color: #fff;
|
||
transition: background .2s;
|
||
}
|
||
.cal-dlg__btn:hover { background: var(--c-accent-dark); }
|
||
.cal-dlg__chiudi {
|
||
width: 42px; height: 42px; border: 1px solid #ddd; background: #fff; cursor: pointer;
|
||
font-size: 1.4rem; line-height: 1; color: var(--c-heading);
|
||
}
|
||
.cal-dlg__chiudi:hover { border-color: var(--c-accent); color: var(--c-accent-dark); }
|
||
|
||
/* 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;
|
||
}
|
||
.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. */
|
||
/* `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 thead th {
|
||
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);
|
||
}
|
||
.cal-tab__nota {
|
||
display: block; font-size: .62rem; letter-spacing: .06em; text-transform: none;
|
||
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 {
|
||
background: var(--c-bg);
|
||
font-family: var(--font-heading); font-size: .78rem; font-weight: 600; color: var(--c-heading);
|
||
white-space: nowrap;
|
||
}
|
||
.cal-tab tr.is-clou .cal-tab__ora { background: #f7f2e6; }
|
||
.cal-tab__cella { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .1em; color: var(--c-heading); }
|
||
/* Ogni disciplina ha un fondo tenue e una barra a sinistra: la barra si vede anche quando il
|
||
browser stampa senza sfondi. */
|
||
.cal-tab__cella.is-reformer { background: #ece5d7; box-shadow: inset 3px 0 0 var(--c-accent-dark); }
|
||
.cal-tab__cella.is-pilates { background: #f6efe0; box-shadow: inset 3px 0 0 #cbb98f; }
|
||
.cal-tab__cella.is-yoga { background: #e3eadf; box-shadow: inset 3px 0 0 #8ba57f; }
|
||
.cal-tab__cella.is-mobility { background: #f0e3e3; box-shadow: inset 3px 0 0 #b78e8e; }
|
||
.cal-tab__cella.is-chiuso { background: repeating-linear-gradient(135deg, #f5f3f0, #f5f3f0 6px, #ece8e2 6px, #ece8e2 12px); }
|
||
.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-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);
|
||
}
|
||
.cal-legenda li.is-reformer { background: #ece5d7; box-shadow: inset 3px 0 0 var(--c-accent-dark); }
|
||
.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 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; }
|
||
.cal-dlg, .cal-dlg * { visibility: visible; }
|
||
.cal-dlg { position: absolute; inset: 0; height: auto; }
|
||
.cal-dlg::backdrop { background: transparent; }
|
||
.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 th, .cal-tab td { padding: 5px 6px; }
|
||
.cal-tab__cella { font-size: .62rem; }
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.cal-dlg__box { padding: 20px 14px 18px; }
|
||
.cal-dlg__titolo { font-size: 1.2rem; }
|
||
.cal-dlg__btn { padding: 11px 16px; letter-spacing: .12em; }
|
||
}
|
||
</style>
|