Mostra anche la classifica di Coppa in Campionato > Classifica

parseClassifica() legge project-sheets.php?project_id=848 con lo stesso
parsing del girone di campionato (stessa struttura di tabella), quindi
nessun parser dedicato. La Coppa resta limitata alla fase a gironi: la
finale secca vive in un project_id figlio non tracciato dal codice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 11:29:34 +02:00
co-authored by Claude Sonnet 5
parent 8353761b6e
commit c628997934
6 changed files with 186 additions and 70 deletions
+8
View File
@@ -10,6 +10,12 @@ import type { RigaClassifica } from "./crapp-data";
export const CSI_BASE = "https://livescore.csibologna.it";
/** Campionato Open Misto Eccellenza 2025/26. Cambia a ogni stagione. */
export const CSI_PROJECT_ID = 767;
/**
* Coppa CSI Misto Silver 2025/26. Stesso formato di tabella del campionato (girone
* all'italiana), solo con meno squadre: `parseClassifica()` funziona invariata. Cambia a
* ogni stagione come CSI_PROJECT_ID, vedi docs/modules/collegamento-csi.md.
*/
export const CSI_COPPA_PROJECT_ID = 848;
/** C.R.A.P. Volley sul portale CSI. */
export const CSI_TEAM_ID = 3359;
export const CSI_GIRONE = "Girone B";
@@ -36,6 +42,8 @@ export type PartitaCsi = {
export type DatiCsi = {
classifica: RigaClassifica[];
/** Classifica del girone di Coppa (project_id 848). Vuota se il parsing fallisce. */
classificaCoppa: RigaClassifica[];
partite: PartitaCsi[];
girone: string;
aggiornato: string;
+10 -1
View File
@@ -1,5 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import {
CSI_COPPA_PROJECT_ID,
CSI_GIRONE,
parseClassifica,
partiteDaEventi,
@@ -28,8 +29,15 @@ async function scarica(url: string): Promise<string> {
}
async function leggiCsi(): Promise<DatiCsi> {
const [html, json] = await Promise.all([scarica(urlClassifica()), scarica(urlPartite())]);
const [html, htmlCoppa, json] = await Promise.all([
scarica(urlClassifica()),
// Non blocca la risposta se fallisce: la Coppa è un dato supplementare, non critico
// come classifica/partite del girone (vedi il controllo sotto).
scarica(urlClassifica(CSI_COPPA_PROJECT_ID)).catch(() => ""),
scarica(urlPartite()),
]);
const classifica = parseClassifica(html);
const classificaCoppa = parseClassifica(htmlCoppa);
const eventiGrezzi = JSON.parse(json);
const partite = partiteDaEventi(eventiGrezzi);
if (classifica.length === 0 && partite.length === 0) {
@@ -48,6 +56,7 @@ async function leggiCsi(): Promise<DatiCsi> {
}
return {
classifica,
classificaCoppa,
partite,
girone: CSI_GIRONE,
aggiornato: new Date().toISOString(),
+49 -41
View File
@@ -2,7 +2,7 @@ import { useMemo } from "react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { AlertTriangle, ChevronRight, RefreshCw } from "lucide-react";
import { cn } from "@/lib/utils";
import { formatData } from "@/lib/crapp-data";
import { formatData, type RigaClassifica } from "@/lib/crapp-data";
import { PageHeader } from "@/components/crapp/ui-bits";
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
import { useScoutMatches } from "@/lib/scout-store";
@@ -37,6 +37,44 @@ export const Route = createFileRoute("/classifica")({
component: Classifica,
});
function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: string }) {
return (
<div className="overflow-hidden rounded-3xl bg-card shadow-card">
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-xs font-bold uppercase text-muted-foreground">
<span>#</span>
<span>Squadra</span>
<span className="text-center">G</span>
<span className="text-center">Set</span>
<span className="text-center">Pt</span>
</div>
{righe.length === 0 ? (
<p className="px-3 py-4 text-center text-xs text-muted-foreground">{vuoto}</p>
) : (
righe.map((r) => {
const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley";
return (
<div
key={r.pos}
className={cn(
"grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] items-center gap-2 border-b border-border px-3 py-2.5 text-sm last:border-0",
noi && "bg-accent/10",
)}
>
<span className={cn("font-display text-base", noi && "text-accent")}>{r.pos}</span>
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>{r.squadra}</span>
<span className="text-center text-xs text-muted-foreground">{r.giocate}</span>
<span className="text-center text-xs tabular-nums text-muted-foreground">
{r.setFatti}:{r.setSubiti}
</span>
<span className="text-center font-bold tabular-nums">{r.punti}</span>
</div>
);
})
)}
</div>
);
}
function formatAggiornamento(iso: string) {
const d = new Date(iso);
const oggi = new Date().toDateString() === d.toDateString();
@@ -52,6 +90,7 @@ function Classifica() {
const { eventi } = useEventi();
const classifica = useMemo(() => csi?.classifica ?? [], [csi]);
const classificaCoppa = useMemo(() => csi?.classificaCoppa ?? [], [csi]);
const mvpPerMatch = useMemo(() => vincitoriMvp(votiMvp.data ?? []), [votiMvp.data]);
const eventoIdPerData = useMemo(
@@ -104,48 +143,17 @@ function Classifica() {
? `Dati CSI aggiornati ${formatAggiornamento(csi.aggiornato)}`
: "Dati CSI in arrivo"}
</div>
<div className="overflow-hidden rounded-3xl bg-card shadow-card">
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-xs font-bold uppercase text-muted-foreground">
<span>#</span>
<span>Squadra</span>
<span className="text-center">G</span>
<span className="text-center">Set</span>
<span className="text-center">Pt</span>
</div>
{classifica.length === 0 ? (
<p className="px-3 py-4 text-center text-xs text-muted-foreground">
Classifica non ancora disponibile.
</p>
) : (
classifica.map((r) => {
const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley";
return (
<div
key={r.pos}
className={cn(
"grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] items-center gap-2 border-b border-border px-3 py-2.5 text-sm last:border-0",
noi && "bg-accent/10",
)}
>
<span className={cn("font-display text-base", noi && "text-accent")}>
{r.pos}
</span>
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>
{r.squadra}
</span>
<span className="text-center text-xs text-muted-foreground">{r.giocate}</span>
<span className="text-center text-xs tabular-nums text-muted-foreground">
{r.setFatti}:{r.setSubiti}
</span>
<span className="text-center font-bold tabular-nums">{r.punti}</span>
</div>
);
})
)}
</div>
{classificaCoppa.length > 0 ? (
<>
<h3 className="mb-2 px-1 text-sm font-bold text-foreground">Coppa</h3>
<TabellaClassifica righe={classificaCoppa} vuoto="Classifica non disponibile." />
<h3 className="mb-2 mt-4 px-1 text-sm font-bold text-foreground">Girone</h3>
</>
) : null}
<TabellaClassifica righe={classifica} vuoto="Classifica non ancora disponibile." />
</>
),
[csi, classifica],
[csi, classifica, classificaCoppa],
);
const contenutoStorico = useMemo(