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