Mostra la stagione CSI nell'header di Campionato e divide lo storico per stagione
Il portale CSI non espone il nome della stagione: stagioneDi() la ricava dalla data della gara (l'anno cambia ad agosto), stagioneDaPartite() dà quella del campionato e raggruppaPerStagione() divide lo storico partite. Coperte da test in csi-core.test.ts. Documentato che oggi Campionato mostra 2025/26 mentre Squadra e Calendario mostrano 2026/27: il CSI non ha ancora creato le pagine della nuova stagione. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
33ed2398ab
commit
ae5d790011
5 files changed
+218
-111
No files matched your search
@@ -21,6 +21,43 @@ export const CSI_TEAM_ID = 3359;
|
||||
export const CSI_GIRONE = "Girone B";
|
||||
export const CSI_NOME_SQUADRA = "C.R.A.P. Volley";
|
||||
|
||||
/**
|
||||
* Stagione sportiva di una data `YYYY-MM-DD`, nel formato "2025/26". Il portale CSI non
|
||||
* espone il nome della stagione (né nella classifica né nel calendario della squadra):
|
||||
* si ricava dalla data della gara, con l'anno che cambia ad agosto — le gare di
|
||||
* settembre-dicembre aprono la stagione, quelle di gennaio-luglio la chiudono.
|
||||
*/
|
||||
export function stagioneDi(dataIso: string): string {
|
||||
const anno = Number(dataIso.slice(0, 4));
|
||||
const mese = Number(dataIso.slice(5, 7));
|
||||
const inizio = mese >= 8 ? anno : anno - 1;
|
||||
return `${inizio}/${String((inizio + 1) % 100).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/** Stagione del campionato letto dal CSI: quella della prima gara, null se non ce ne sono. */
|
||||
export function stagioneDaPartite(partite: ReadonlyArray<{ data: string }>): string | null {
|
||||
const date = partite.map((p) => p.data).filter((d) => /^\d{4}-\d{2}/.test(d));
|
||||
if (date.length === 0) return null;
|
||||
return stagioneDi(date.reduce((a, b) => (a < b ? a : b)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Storico partite diviso per stagione, nell'ordine in cui arrivano: le gare scoutate in
|
||||
* locale possono essere di stagioni diverse, quelle CSI sono tutte dello stesso campionato.
|
||||
*/
|
||||
export function raggruppaPerStagione<T extends { data: string }>(
|
||||
match: readonly T[],
|
||||
): Array<{ stagione: string; match: T[] }> {
|
||||
const gruppi: Array<{ stagione: string; match: T[] }> = [];
|
||||
for (const m of match) {
|
||||
const stagione = stagioneDi(m.data);
|
||||
const gruppo = gruppi.find((g) => g.stagione === stagione);
|
||||
if (gruppo) gruppo.match.push(m);
|
||||
else gruppi.push({ stagione, match: [m] });
|
||||
}
|
||||
return gruppi;
|
||||
}
|
||||
|
||||
export const urlClassifica = (projectId = CSI_PROJECT_ID) =>
|
||||
`${CSI_BASE}/components/project-sheets.php?project_id=${projectId}`;
|
||||
export const urlPartite = (teamId = CSI_TEAM_ID) =>
|
||||
|
||||
+132
-110
@@ -7,7 +7,13 @@ import { PageHeader, TeamLogo } from "@/components/crapp/ui-bits";
|
||||
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
||||
import { useScoutMatches } from "@/lib/scout-store";
|
||||
import { useCsi } from "@/lib/csi";
|
||||
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||
import {
|
||||
isNostraSquadra,
|
||||
matchDaPartitaCsi,
|
||||
partiteGiocate,
|
||||
raggruppaPerStagione,
|
||||
stagioneDaPartite,
|
||||
} from "@/lib/csi-core";
|
||||
import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti";
|
||||
import { useEventi } from "@/lib/eventi";
|
||||
import { LogoSquadra } from "@/components/crapp/DettaglioCsi";
|
||||
@@ -127,6 +133,8 @@ function Classifica() {
|
||||
|
||||
const classifica = useMemo(() => csi?.classifica ?? [], [csi]);
|
||||
const classificaCoppa = useMemo(() => csi?.classificaCoppa ?? [], [csi]);
|
||||
// Tutte le gare, non solo quelle giocate: a inizio stagione il calendario c'è già.
|
||||
const stagioneCsi = useMemo(() => stagioneDaPartite(csi?.partite ?? []), [csi]);
|
||||
|
||||
const mvpPerMatch = useMemo(() => vincitoriMvp(votiMvp.data ?? []), [votiMvp.data]);
|
||||
const eventoIdPerData = useMemo(
|
||||
@@ -200,119 +208,127 @@ function Classifica() {
|
||||
Nessuna partita disponibile.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{tuttiMatch.map((m) => {
|
||||
const vinta = m.setNostri > m.setLoro;
|
||||
const eventoId = eventoIdPerData.get(m.data);
|
||||
const cliccabile = Boolean(eventoId) || !m.scout;
|
||||
const casa = {
|
||||
nome: m.casa ? NOME_NOI : m.avversario,
|
||||
nostro: m.casa,
|
||||
};
|
||||
const trasferta = {
|
||||
nome: m.casa ? m.avversario : NOME_NOI,
|
||||
nostro: !m.casa,
|
||||
};
|
||||
// Badge e parziali in ordine casa–ospite; il colore resta sulla vittoria CRAP.
|
||||
const setCasa = m.casa ? m.setNostri : m.setLoro;
|
||||
const setOspite = m.casa ? m.setLoro : m.setNostri;
|
||||
const contenuto = (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<LogoPartita
|
||||
nostro={casa.nostro}
|
||||
logoAvversario={m.logoAvversario}
|
||||
avversario={m.avversario}
|
||||
/>
|
||||
<p className="truncate text-sm font-bold">{casa.nome}</p>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<LogoPartita
|
||||
nostro={trasferta.nostro}
|
||||
logoAvversario={m.logoAvversario}
|
||||
avversario={m.avversario}
|
||||
/>
|
||||
<p className="truncate text-sm font-bold">{trasferta.nome}</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatData(m.data)} · MVP {m.mvp || "da votare"}
|
||||
{m.scout ? " · scoutata" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-xl px-2.5 py-1 font-display text-xl",
|
||||
vinta
|
||||
? "bg-success text-success-foreground"
|
||||
: "bg-destructive text-destructive-foreground",
|
||||
)}
|
||||
>
|
||||
{setCasa}-{setOspite}
|
||||
</span>
|
||||
{cliccabile ? (
|
||||
<ChevronRight className="h-5 w-5 shrink-0 text-muted-foreground" aria-hidden />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-1.5">
|
||||
{m.parziali.map((p, i) => {
|
||||
const puntiCasa = m.casa ? p[0] : p[1];
|
||||
const puntiOspite = m.casa ? p[1] : p[0];
|
||||
const setVintoDaNoi = p[0] > p[1];
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{raggruppaPerStagione(tuttiMatch).map(({ stagione, match }) => (
|
||||
<section key={stagione} className="space-y-3">
|
||||
<h3 className="px-1 text-sm font-bold text-foreground">Stagione {stagione}</h3>
|
||||
{match.map((m) => {
|
||||
const vinta = m.setNostri > m.setLoro;
|
||||
const eventoId = eventoIdPerData.get(m.data);
|
||||
const cliccabile = Boolean(eventoId) || !m.scout;
|
||||
const casa = {
|
||||
nome: m.casa ? NOME_NOI : m.avversario,
|
||||
nostro: m.casa,
|
||||
};
|
||||
const trasferta = {
|
||||
nome: m.casa ? m.avversario : NOME_NOI,
|
||||
nostro: !m.casa,
|
||||
};
|
||||
// Badge e parziali in ordine casa–ospite; il colore resta sulla vittoria CRAP.
|
||||
const setCasa = m.casa ? m.setNostri : m.setLoro;
|
||||
const setOspite = m.casa ? m.setLoro : m.setNostri;
|
||||
const contenuto = (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<LogoPartita
|
||||
nostro={casa.nostro}
|
||||
logoAvversario={m.logoAvversario}
|
||||
avversario={m.avversario}
|
||||
/>
|
||||
<p className="truncate text-sm font-bold">{casa.nome}</p>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<LogoPartita
|
||||
nostro={trasferta.nostro}
|
||||
logoAvversario={m.logoAvversario}
|
||||
avversario={m.avversario}
|
||||
/>
|
||||
<p className="truncate text-sm font-bold">{trasferta.nome}</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatData(m.data)} · MVP {m.mvp || "da votare"}
|
||||
{m.scout ? " · scoutata" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
key={i}
|
||||
className={cn(
|
||||
"rounded-lg px-2 py-1 text-xs font-semibold tabular-nums",
|
||||
setVintoDaNoi ? "bg-secondary" : "bg-muted text-muted-foreground",
|
||||
"shrink-0 rounded-xl px-2.5 py-1 font-display text-xl",
|
||||
vinta
|
||||
? "bg-success text-success-foreground"
|
||||
: "bg-destructive text-destructive-foreground",
|
||||
)}
|
||||
>
|
||||
{puntiCasa}-{puntiOspite}
|
||||
{setCasa}-{setOspite}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{eventoId && !m.mvp ? (
|
||||
<span className="ml-auto inline-flex items-center gap-0.5 text-xs font-bold uppercase text-accent">
|
||||
Vota MVP <ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
const aria = `${casa.nome} vs ${trasferta.nome}: dettaglio partita`;
|
||||
if (eventoId) {
|
||||
return (
|
||||
<Link
|
||||
key={m.id}
|
||||
to="/partita/$id"
|
||||
params={{ id: eventoId }}
|
||||
aria-label={aria}
|
||||
className="premi block rounded-3xl bg-card p-4 shadow-card ring-1 ring-transparent transition-[box-shadow,transform] active:scale-[0.99] hover:ring-accent/30"
|
||||
>
|
||||
{contenuto}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
// Senza evento CrAPP collegato (nessuna creazione automatica dal calendario
|
||||
// CSI, vedi "Evoluzioni possibili"): le gare scoutate localmente non hanno un
|
||||
// corrispettivo sul portale da mostrare, quelle CSI sì.
|
||||
return m.scout ? (
|
||||
<article key={m.id} className="rounded-3xl bg-card p-4 shadow-card">
|
||||
{contenuto}
|
||||
</article>
|
||||
) : (
|
||||
<Link
|
||||
key={m.id}
|
||||
to="/partita-csi/$id"
|
||||
params={{ id: m.id }}
|
||||
aria-label={aria}
|
||||
className="premi block rounded-3xl bg-card p-4 shadow-card ring-1 ring-transparent transition-[box-shadow,transform] active:scale-[0.99] hover:ring-accent/30"
|
||||
>
|
||||
{contenuto}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{cliccabile ? (
|
||||
<ChevronRight
|
||||
className="h-5 w-5 shrink-0 text-muted-foreground"
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-1.5">
|
||||
{m.parziali.map((p, i) => {
|
||||
const puntiCasa = m.casa ? p[0] : p[1];
|
||||
const puntiOspite = m.casa ? p[1] : p[0];
|
||||
const setVintoDaNoi = p[0] > p[1];
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={cn(
|
||||
"rounded-lg px-2 py-1 text-xs font-semibold tabular-nums",
|
||||
setVintoDaNoi ? "bg-secondary" : "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{puntiCasa}-{puntiOspite}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{eventoId && !m.mvp ? (
|
||||
<span className="ml-auto inline-flex items-center gap-0.5 text-xs font-bold uppercase text-accent">
|
||||
Vota MVP <ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
const aria = `${casa.nome} vs ${trasferta.nome}: dettaglio partita`;
|
||||
if (eventoId) {
|
||||
return (
|
||||
<Link
|
||||
key={m.id}
|
||||
to="/partita/$id"
|
||||
params={{ id: eventoId }}
|
||||
aria-label={aria}
|
||||
className="premi block rounded-3xl bg-card p-4 shadow-card ring-1 ring-transparent transition-[box-shadow,transform] active:scale-[0.99] hover:ring-accent/30"
|
||||
>
|
||||
{contenuto}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
// Senza evento CrAPP collegato (nessuna creazione automatica dal calendario
|
||||
// CSI, vedi "Evoluzioni possibili"): le gare scoutate localmente non hanno un
|
||||
// corrispettivo sul portale da mostrare, quelle CSI sì.
|
||||
return m.scout ? (
|
||||
<article key={m.id} className="rounded-3xl bg-card p-4 shadow-card">
|
||||
{contenuto}
|
||||
</article>
|
||||
) : (
|
||||
<Link
|
||||
key={m.id}
|
||||
to="/partita-csi/$id"
|
||||
params={{ id: m.id }}
|
||||
aria-label={aria}
|
||||
className="premi block rounded-3xl bg-card p-4 shadow-card ring-1 ring-transparent transition-[box-shadow,transform] active:scale-[0.99] hover:ring-accent/30"
|
||||
>
|
||||
{contenuto}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
[tuttiMatch, eventoIdPerData],
|
||||
@@ -322,7 +338,13 @@ function Classifica() {
|
||||
<>
|
||||
<PageHeader
|
||||
titolo="Campionato"
|
||||
sottotitolo={csi ? `${csi.girone} · CSI Bologna` : "CSI Bologna"}
|
||||
sottotitolo={
|
||||
csi
|
||||
? [csi.girone, "CSI Bologna", stagioneCsi && `Stagione ${stagioneCsi}`]
|
||||
.filter(Boolean)
|
||||
.join(" · ")
|
||||
: "CSI Bologna"
|
||||
}
|
||||
/>
|
||||
|
||||
<BarraSottosezioni
|
||||
|
||||
Reference in new issue
Block a user