Sistema tutti gli errori di lint (prettier) rimasti su main

npx eslint --fix su test/unit/obiettivi.test.ts, test/integration/obiettivi.test.ts,
BarraSottosezioni.tsx, EventoCard.tsx e calendario.tsx — solo formattazione,
nessuna modifica di comportamento. Verificato con la suite unit (32/32 verde).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 14:12:49 +02:00
co-authored by Claude Sonnet 5
parent 080379cc64
commit 1fdc6720a0
5 changed files with 188 additions and 178 deletions
+1 -3
View File
@@ -90,9 +90,7 @@ export function BarraSottosezioni({
aria-label="Sottosezioni" aria-label="Sottosezioni"
className={cn( className={cn(
"flex flex-nowrap", "flex flex-nowrap",
riempiLarghezza riempiLarghezza ? "w-full" : "w-max min-w-full",
? "w-full"
: "w-max min-w-full",
sottolineatura ? "gap-1 px-2 py-1.5" : "snap-x snap-mandatory gap-1.5 px-5", sottolineatura ? "gap-1 px-2 py-1.5" : "snap-x snap-mandatory gap-1.5 px-5",
)} )}
> >
+2 -6
View File
@@ -88,8 +88,7 @@ export function EventoCard({
const passato = evento.data < dataOggi(); const passato = evento.data < dataOggi();
const cliccabile = Boolean(linkTo); const cliccabile = Boolean(linkTo);
/** Solo gli eventi extra-campo non hanno scheda dedicata: le note restano sulla card. */ /** Solo gli eventi extra-campo non hanno scheda dedicata: le note restano sulla card. */
const noteCard = const noteCard = evento.tipo === "evento" ? evento.note.trim() : "";
evento.tipo === "evento" ? evento.note.trim() : "";
const stati = const stati =
evento.tipo === "evento" evento.tipo === "evento"
? // Se resta un vecchio "infortunato", mostra il bottone solo per poterlo togliere. ? // Se resta un vecchio "infortunato", mostra il bottone solo per poterlo togliere.
@@ -193,10 +192,7 @@ export function EventoCard({
{io ? ( {io ? (
<div <div
className={cn( className={cn("pointer-events-auto flex w-full gap-1", metaStato ? "mt-2.5" : "mt-3")}
"pointer-events-auto flex w-full gap-1",
metaStato ? "mt-2.5" : "mt-3",
)}
role="group" role="group"
aria-label="La tua presenza" aria-label="La tua presenza"
> >
+102 -102
View File
@@ -182,7 +182,7 @@ function Calendario() {
<span key={i}>{g}</span> <span key={i}>{g}</span>
))} ))}
</div> </div>
{/* {/*
Il mese si cambia anche con lo swipe: il punto d'arrivo si Il mese si cambia anche con lo swipe: il punto d'arrivo si
decide proiettando la velocità di rilascio (come la decide proiettando la velocità di rilascio (come la
decelerazione dello scroll iOS), non dalla posizione del dito. decelerazione dello scroll iOS), non dalla posizione del dito.
@@ -195,112 +195,112 @@ function Calendario() {
riga e sulle colonne di bordo. Il padding sta dentro il riquadro riga e sulle colonne di bordo. Il padding sta dentro il riquadro
di ritaglio, i margini negativi rimettono la griglia dov'era. di ritaglio, i margini negativi rimettono la griglia dov'era.
*/} */}
<div className="relative -mx-1 mt-1 overflow-hidden p-1"> <div className="relative -mx-1 mt-1 overflow-hidden p-1">
<AnimatePresence initial={false} mode="popLayout" custom={direzione}> <AnimatePresence initial={false} mode="popLayout" custom={direzione}>
<motion.div <motion.div
key={mesePrefix} key={mesePrefix}
custom={direzione} custom={direzione}
drag={ridotto ? false : "x"} drag={ridotto ? false : "x"}
dragConstraints={{ left: 0, right: 0 }} dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.18} dragElastic={0.18}
dragMomentum={false} dragMomentum={false}
onDragEnd={(_, info) => { onDragEnd={(_, info) => {
const arrivo = info.offset.x + proietta(info.velocity.x); const arrivo = info.offset.x + proietta(info.velocity.x);
if (arrivo < -60) successivo(); if (arrivo < -60) successivo();
else if (arrivo > 60) precedente(); else if (arrivo > 60) precedente();
}} }}
initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }} initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }} exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }}
transition={ridotto ? { duration: 0.2 } : molla.foglio} transition={ridotto ? { duration: 0.2 } : molla.foglio}
className="grid touch-pan-y grid-cols-7 gap-1" className="grid touch-pan-y grid-cols-7 gap-1"
> >
{Array.from({ length: offsetLunedi }).map((_, i) => ( {Array.from({ length: offsetLunedi }).map((_, i) => (
<span key={`v${i}`} /> <span key={`v${i}`} />
))} ))}
{Array.from({ length: giorni }).map((_, i) => { {Array.from({ length: giorni }).map((_, i) => {
const giorno = i + 1; const giorno = i + 1;
const eventiGiorno = eventiPerGiorno.get(giorno) ?? []; const eventiGiorno = eventiPerGiorno.get(giorno) ?? [];
const haEventi = eventiGiorno.length > 0; const haEventi = eventiGiorno.length > 0;
// Attenzione: si conta per **tipo**, non per numero di // Attenzione: si conta per **tipo**, non per numero di
// eventi. Due partite nello stesso giorno restano una cella // eventi. Due partite nello stesso giorno restano una cella
// rossa piena; si divide solo se i tipi sono diversi. // rossa piena; si divide solo se i tipi sono diversi.
const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo))); const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo)));
// Più tipi: la cella si divide in bande a taglio netto (gli // Più tipi: la cella si divide in bande a taglio netto (gli
// stop sono duplicati apposta, non è una sfumatura), una per // stop sono duplicati apposta, non è una sfumatura), una per
// tipo, in diagonale. // tipo, in diagonale.
const sfondo = const sfondo =
tipiGiorno.length > 1 tipiGiorno.length > 1
? `linear-gradient(135deg, ${tipiGiorno ? `linear-gradient(135deg, ${tipiGiorno
.map((t, idx) => { .map((t, idx) => {
const da = (idx / tipiGiorno.length) * 100; const da = (idx / tipiGiorno.length) * 100;
const a = ((idx + 1) / tipiGiorno.length) * 100; const a = ((idx + 1) / tipiGiorno.length) * 100;
return `${coloreTipo[t]} ${da}%, ${coloreTipo[t]} ${a}%`; return `${coloreTipo[t]} ${da}%, ${coloreTipo[t]} ${a}%`;
}) })
.join(", ")})` .join(", ")})`
: undefined; : undefined;
const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined; const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
const isOggi = const isOggi =
!!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno; !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
const Cella = haEventi ? "button" : "div"; const Cella = haEventi ? "button" : "div";
return ( return (
<Cella <Cella
key={giorno} key={giorno}
type={haEventi ? "button" : undefined} type={haEventi ? "button" : undefined}
onClick={haEventi ? () => apriGiorno(giorno) : undefined} onClick={haEventi ? () => apriGiorno(giorno) : undefined}
style={sfondo ? { backgroundImage: sfondo } : undefined} style={sfondo ? { backgroundImage: sfondo } : undefined}
className={cn( className={cn(
"relative grid aspect-square place-items-center rounded-xl text-sm font-semibold", "relative grid aspect-square place-items-center rounded-xl text-sm font-semibold",
tipo === "partita" && "bg-accent text-accent-foreground", tipo === "partita" && "bg-accent text-accent-foreground",
tipo === "allenamento" && "bg-training text-training-foreground", tipo === "allenamento" && "bg-training text-training-foreground",
tipo === "evento" && "bg-warning text-warning-foreground", tipo === "evento" && "bg-warning text-warning-foreground",
tipo === "compleanno" && "bg-success text-success-foreground", tipo === "compleanno" && "bg-success text-success-foreground",
!tipo && !haEventi && "text-muted-foreground", !tipo && !haEventi && "text-muted-foreground",
!tipo && haEventi && "text-foreground", !tipo && haEventi && "text-foreground",
haEventi && "cursor-pointer transition-transform active:scale-90", haEventi && "cursor-pointer transition-transform active:scale-90",
isOggi && "ring-2 ring-foreground ring-offset-1 ring-offset-card", isOggi && "ring-2 ring-foreground ring-offset-1 ring-offset-card",
)} )}
aria-label={ aria-label={
haEventi haEventi
? `${giorno} ${mesiIT[mese]}: ${eventiGiorno.length} ${eventiGiorno.length === 1 ? "evento" : "eventi"}` ? `${giorno} ${mesiIT[mese]}: ${eventiGiorno.length} ${eventiGiorno.length === 1 ? "evento" : "eventi"}`
: undefined : undefined
} }
aria-current={isOggi ? "date" : undefined} aria-current={isOggi ? "date" : undefined}
> >
{/* {/*
Sulle celle divise il numero sta direttamente sulle Sulle celle divise il numero sta direttamente sulle
bande, staccato dal fondo da un alone bianco: è la bande, staccato dal fondo da un alone bianco: è la
resa scelta, il colore deve restare pieno e visibile resa scelta, il colore deve restare pieno e visibile
fino al bordo. fino al bordo.
*/} */}
<span className="relative drop-shadow-[0_1px_1px_rgba(255,255,255,0.5)]"> <span className="relative drop-shadow-[0_1px_1px_rgba(255,255,255,0.5)]">
{giorno} {giorno}
</span> </span>
</Cella> </Cella>
); );
})} })}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</div> </div>
<p className="mt-2 text-xs text-muted-foreground"> <p className="mt-2 text-xs text-muted-foreground">
Scorri a destra o sinistra per cambiare mese. Scorri a destra o sinistra per cambiare mese.
</p> </p>
<div className="mt-3 flex flex-wrap gap-3 text-xs font-semibold text-muted-foreground"> <div className="mt-3 flex flex-wrap gap-3 text-xs font-semibold text-muted-foreground">
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita <i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita
</span> </span>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-training" /> Allenamento <i className="h-2.5 w-2.5 rounded-full bg-training" /> Allenamento
</span> </span>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-warning" /> Eventi <i className="h-2.5 w-2.5 rounded-full bg-warning" /> Eventi
</span> </span>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-success" /> Compleanni <i className="h-2.5 w-2.5 rounded-full bg-success" /> Compleanni
</span> </span>
</div> </div>
</Card> </Card>
</Section> </Section>
{admin ? ( {admin ? (
<div className="px-5 pt-4"> <div className="px-5 pt-4">
+78 -62
View File
@@ -96,7 +96,10 @@ if (!locale) {
giocatore_id: g.id, giocatore_id: g.id,
stato: presenti.includes(g.id) ? "presente" : "assente", stato: presenti.includes(g.id) ? "presente" : "assente",
})); }));
const inseritePresenze = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) }); const inseritePresenze = await rest("risposte_presenze", {
method: "POST",
body: JSON.stringify(righe),
});
if (!inseritePresenze.ok) { if (!inseritePresenze.ok) {
throw new Error(`inserimento presenze fallito: ${await inseritePresenze.text()}`); throw new Error(`inserimento presenze fallito: ${await inseritePresenze.text()}`);
} }
@@ -195,7 +198,11 @@ if (!locale) {
// Tutta la rosa presente all'allenamento, nessuno alla partita: 50% aggregato sui due. // Tutta la rosa presente all'allenamento, nessuno alla partita: 50% aggregato sui due.
const risposte = [ const risposte = [
...giocatori.map((g) => ({ evento_id: allenamentoId, giocatore_id: g.id, stato: "presente" })), ...giocatori.map((g) => ({
evento_id: allenamentoId,
giocatore_id: g.id,
stato: "presente",
})),
...giocatori.map((g) => ({ evento_id: partitaId, giocatore_id: g.id, stato: "assente" })), ...giocatori.map((g) => ({ evento_id: partitaId, giocatore_id: g.id, stato: "assente" })),
]; ];
const scritte = await rest("risposte_presenze", { const scritte = await rest("risposte_presenze", {
@@ -379,65 +386,68 @@ if (!locale) {
}, },
); );
await prova( await prova("o7 somma presenze calcolate da eventi/risposte reali del database", async () => {
"o7 somma presenze calcolate da eventi/risposte reali del database", // o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da
async () => { // `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita
// o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da // la stessa funzione pura sui dati appena scritti, per verificare l'intera catena
// `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita // DB -> contaPresenzeGiocatore -> o7, non solo la somma finale.
// la stessa funzione pura sui dati appena scritti, per verificare l'intera catena const allenamentoId = `${PREFISSO}-o7-allenamento`;
// DB -> contaPresenzeGiocatore -> o7, non solo la somma finale. const partitaId = `${PREFISSO}-o7-partita`;
const allenamentoId = `${PREFISSO}-o7-allenamento`; const OGGI_STR = "2099-01-20";
const partitaId = `${PREFISSO}-o7-partita`;
const OGGI_STR = "2099-01-20";
for (const [id, tipo, data] of [ for (const [id, tipo, data] of [
[allenamentoId, "allenamento", "2099-01-05"], [allenamentoId, "allenamento", "2099-01-05"],
[partitaId, "partita", "2099-01-08"], [partitaId, "partita", "2099-01-08"],
] as const) { ] as const) {
const inserito = await rest("eventi_app", { const inserito = await rest("eventi_app", {
method: "POST", method: "POST",
body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }), body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }),
}); });
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`); if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
} }
// g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze, // g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze,
// il ritardo conta). g3: assente a entrambi (0 presenze). // il ritardo conta). g3: assente a entrambi (0 presenze).
const [g1, g2, g3] = giocatori; const [g1, g2, g3] = giocatori;
const righe = [ const righe = [
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" }, { evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" }, { evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
{ evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" }, { evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
{ evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" }, { evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
{ evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" }, { evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
{ evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" }, { evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
]; ];
const inserite = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) }); const inserite = await rest("risposte_presenze", {
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`); method: "POST",
body: JSON.stringify(righe),
});
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
const eventiReali = (await leggiEventi()).filter( const eventiReali = (await leggiEventi()).filter(
(e) => e.id === allenamentoId || e.id === partitaId, (e) => e.id === allenamentoId || e.id === partitaId,
); );
const presenzeReali = { const presenzeReali = {
...(await leggiPresenze(allenamentoId)), ...(await leggiPresenze(allenamentoId)),
...(await leggiPresenze(partitaId)), ...(await leggiPresenze(partitaId)),
}; };
const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({ const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({
...g, ...g,
presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR), presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR),
})); }));
const o7 = obiettiviSquadra(rosaConPresenzeReali, { eventi: [], presenze: {}, pagelle: [] }) const o7 = obiettiviSquadra(rosaConPresenzeReali, {
.find((o) => o.id === "o7")!; eventi: [],
presenze: {},
pagelle: [],
}).find((o) => o.id === "o7")!;
assert.equal( assert.equal(
o7.valore, o7.valore,
4, 4,
"g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database", "g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database",
); );
}, });
);
await prova("o12/o13 media e conteggio pagelle vere lette da pagelle_voti", async () => { await prova("o12/o13 media e conteggio pagelle vere lette da pagelle_voti", async () => {
const matchId = `${PREFISSO}-o12-m1`; const matchId = `${PREFISSO}-o12-m1`;
@@ -522,20 +532,26 @@ if (!locale) {
}); });
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`); if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
const eventiReali = (await leggiEventi()).filter((e) => const eventiReali = (await leggiEventi()).filter((e) => eventi.some(([id]) => id === e.id));
eventi.some(([id]) => id === e.id),
);
const presenzeReali: MappaPresenze = {}; const presenzeReali: MappaPresenze = {};
for (const [id] of eventi) Object.assign(presenzeReali, await leggiPresenze(id)); for (const [id] of eventi) Object.assign(presenzeReali, await leggiPresenze(id));
const rosaConSerieReali = [g1!, g2!, g3!].map((g) => ({ const rosaConSerieReali = [g1!, g2!, g3!].map((g) => ({
...g, ...g,
serieAllenamenti: serieConsecutiva(g.id, eventiReali, presenzeReali, "allenamento", OGGI_STR), serieAllenamenti: serieConsecutiva(
g.id,
eventiReali,
presenzeReali,
"allenamento",
OGGI_STR,
),
})); }));
const o11 = obiettiviSquadra(rosaConSerieReali, { eventi: [], presenze: {}, pagelle: [] }).find( const o11 = obiettiviSquadra(rosaConSerieReali, {
(o) => o.id === "o11", eventi: [],
)!; presenze: {},
pagelle: [],
}).find((o) => o.id === "o11")!;
assert.equal( assert.equal(
o11.valore, o11.valore,
1, 1,
+5 -5
View File
@@ -126,7 +126,10 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
assert.equal(o1Agosto.scadenza, "2026-08-31", "scadenza = ultimo giorno del mese"); assert.equal(o1Agosto.scadenza, "2026-08-31", "scadenza = ultimo giorno del mese");
const o1Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o1"); const o1Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o1");
assert.ok(o1Settembre.titolo.includes("settembre"), "titolo o1 riflette il mese iniettato (settembre)"); assert.ok(
o1Settembre.titolo.includes("settembre"),
"titolo o1 riflette il mese iniettato (settembre)",
);
assert.equal(o1Settembre.scadenza, "2026-09-30", "scadenza = ultimo giorno di settembre (30 gg)"); assert.equal(o1Settembre.scadenza, "2026-09-30", "scadenza = ultimo giorno di settembre (30 gg)");
// La scadenza di o2 ("Tutti rispondono alle convocazioni") era una data fissa // La scadenza di o2 ("Tutti rispondono alle convocazioni") era una data fissa
@@ -197,10 +200,7 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
); );
const dueEventiRisposte: ContestoObiettivi = { const dueEventiRisposte: ContestoObiettivi = {
eventi: [ eventi: [evento("dr1", "2026-08-03", "allenamento"), evento("dr2", "2026-08-17", "partita")],
evento("dr1", "2026-08-03", "allenamento"),
evento("dr2", "2026-08-17", "partita"),
],
presenze: { presenze: {
dr1: Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])), dr1: Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])),
dr2: {}, dr2: {},