Read the official standings and results from the CSI portal.
The Campionato page showed hardcoded demo data. It now reads the real 2025/26 season (Campionato Open Misto Eccellenza, project 767, team 3359, Girone B) from the Livescore CSI Bologna portal. The portal has no documented API: we call the same endpoints its own pages call over ajax, so parsing must degrade gracefully. A single server route fetches them, caches for 6 hours and serves the last good payload on failure; the page falls back to the previous data when nothing is available. No browser ever contacts the portal, keeping the request count independent of how many players open the app. Also fixes the header, which claimed "Girone C - CSI Milano". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
CSI_GIRONE,
|
||||
parseClassifica,
|
||||
partiteDaEventi,
|
||||
urlClassifica,
|
||||
urlPartite,
|
||||
type DatiCsi,
|
||||
} from "@/lib/csi-core";
|
||||
|
||||
const SCADENZA_MS = 6 * 60 * 60 * 1000;
|
||||
|
||||
// ponytail: cache in memoria del processo, si perde ai cold start e non è
|
||||
// condivisa tra istanze. Basta per una squadra; se il portale CSI diventa
|
||||
// lento o le richieste crescono, spostare i dati in una tabella Supabase
|
||||
// riempita da un job cron (stesso pattern di promemoria-palloni).
|
||||
let cache: DatiCsi | undefined;
|
||||
let scadenza = 0;
|
||||
|
||||
async function scarica(url: string): Promise<string> {
|
||||
const res = await fetch(url, {
|
||||
headers: { "User-Agent": "CrAPP/1.0 (+https://crapvolley.it)" },
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`CSI ${res.status} su ${url}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
async function leggiCsi(): Promise<DatiCsi> {
|
||||
const [html, json] = await Promise.all([scarica(urlClassifica()), scarica(urlPartite())]);
|
||||
const classifica = parseClassifica(html);
|
||||
const partite = partiteDaEventi(JSON.parse(json));
|
||||
if (classifica.length === 0 && partite.length === 0) {
|
||||
throw new Error("CSI: risposta senza classifica né partite");
|
||||
}
|
||||
return { classifica, partite, girone: CSI_GIRONE, aggiornato: new Date().toISOString() };
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/api/public/csi")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async () => {
|
||||
if (cache && Date.now() < scadenza) return Response.json(cache);
|
||||
try {
|
||||
cache = await leggiCsi();
|
||||
scadenza = Date.now() + SCADENZA_MS;
|
||||
} catch (error) {
|
||||
console.error("csi", error);
|
||||
// Meglio un dato vecchio che nessun dato: il portale CSI cambia raramente.
|
||||
if (!cache) return new Response("CSI non raggiungibile", { status: 503 });
|
||||
}
|
||||
return Response.json(cache);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
+41
-16
@@ -4,6 +4,8 @@ import { cn } from "@/lib/utils";
|
||||
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
||||
import { storicoMatch } from "@/lib/crapp-data";
|
||||
import { classificaConScout, useScoutMatches } from "@/lib/scout-store";
|
||||
import { useCsi } from "@/lib/csi";
|
||||
import { isNostraSquadra, partiteGiocate } from "@/lib/csi-core";
|
||||
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
||||
|
||||
export const Route = createFileRoute("/classifica")({
|
||||
@@ -21,27 +23,50 @@ export const Route = createFileRoute("/classifica")({
|
||||
component: Classifica,
|
||||
});
|
||||
|
||||
function formatAggiornamento(iso: string) {
|
||||
const d = new Date(iso);
|
||||
const oggi = new Date().toDateString() === d.toDateString();
|
||||
const ora = d.toLocaleTimeString("it-IT", { hour: "2-digit", minute: "2-digit" });
|
||||
return oggi ? `oggi alle ${ora}` : `il ${d.toLocaleDateString("it-IT")} alle ${ora}`;
|
||||
}
|
||||
|
||||
function Classifica() {
|
||||
const scoutMatches = useScoutMatches();
|
||||
const classifica = classificaConScout(scoutMatches);
|
||||
const risultati = [
|
||||
...scoutMatches.map((m) => ({
|
||||
id: m.id,
|
||||
avversario: m.avversario,
|
||||
casa: m.casa,
|
||||
setNostri: m.setNostri,
|
||||
setLoro: m.setLoro,
|
||||
})),
|
||||
...storicoMatch,
|
||||
];
|
||||
const { data: csi } = useCsi();
|
||||
|
||||
const classifica = csi?.classifica.length ? csi.classifica : classificaConScout(scoutMatches);
|
||||
const risultati = csi?.partite.length
|
||||
? partiteGiocate(csi.partite).map((p) => ({
|
||||
id: p.id,
|
||||
avversario: p.avversario,
|
||||
casa: p.casa,
|
||||
setNostri: p.setNostri ?? 0,
|
||||
setLoro: p.setLoro ?? 0,
|
||||
}))
|
||||
: [
|
||||
...scoutMatches.map((m) => ({
|
||||
id: m.id,
|
||||
avversario: m.avversario,
|
||||
casa: m.casa,
|
||||
setNostri: m.setNostri,
|
||||
setLoro: m.setLoro,
|
||||
})),
|
||||
...storicoMatch,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader titolo="Campionato" sottotitolo="Girone C · CSI Milano" />
|
||||
<PageHeader
|
||||
titolo="Campionato"
|
||||
sottotitolo={csi ? `${csi.girone} · CSI Bologna` : "CSI Bologna"}
|
||||
/>
|
||||
|
||||
<div className="px-5 pt-4">
|
||||
<div className="flex items-center gap-2 rounded-2xl bg-secondary px-3 py-2 text-xs text-muted-foreground">
|
||||
<RefreshCw className="h-3.5 w-3.5 text-accent" />
|
||||
Dati CSI aggiornati oggi alle 08:40 (demo)
|
||||
{csi
|
||||
? `Dati CSI aggiornati ${formatAggiornamento(csi.aggiornato)}`
|
||||
: "Dati CSI in arrivo"}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<ScoutEntry variante="compatto" />
|
||||
@@ -58,7 +83,7 @@ function Classifica() {
|
||||
<span className="text-center">Pt</span>
|
||||
</div>
|
||||
{classifica.map((r) => {
|
||||
const noi = r.squadra === "CRAP Volley";
|
||||
const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley";
|
||||
return (
|
||||
<div
|
||||
key={r.pos}
|
||||
@@ -82,7 +107,7 @@ function Classifica() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section titolo="Ultimi risultati del girone">
|
||||
<Section titolo="Ultimi risultati ufficiali">
|
||||
<div className="space-y-2">
|
||||
{risultati.map((m) => (
|
||||
<div
|
||||
@@ -101,4 +126,4 @@ function Classifica() {
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user