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,113 @@
|
||||
/**
|
||||
* Check del parsing CSI: `bun src/lib/csi-core.test.ts`.
|
||||
* Con CSI_LIVE=1 verifica anche gli endpoint reali del portale.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
isNostraSquadra,
|
||||
parseClassifica,
|
||||
partiteDaEventi,
|
||||
partiteGiocate,
|
||||
urlClassifica,
|
||||
urlPartite,
|
||||
} from "./csi-core";
|
||||
|
||||
const htmlClassifica = `
|
||||
<table class="ranking">
|
||||
<tr><th>Pos</th><th>Squadra</th></tr>
|
||||
<tr><td>1</td><td><a href="team_details.php?team_id=1">Altra Squadra Girone A</a></td>
|
||||
<td><b>40</b></td><td>16</td><td>13</td><td>3</td><td>2</td><td>1</td><td>42</td><td>18</td><td>24</td></tr>
|
||||
</table>
|
||||
<table class="ranking">
|
||||
<tr><th>Pos</th><th>Squadra</th></tr>
|
||||
<tr><td>1</td><td><a href="team_details.php?team_id=9">Paolo Poggi Volley & C.</a></td>
|
||||
<td><b>40</b></td><td>16</td><td>13</td><td>3</td><td>2</td><td>1</td><td>43</td><td>17</td><td>26</td></tr>
|
||||
<tr class="align-middle">
|
||||
<!-- Posizione --><td>4</td>
|
||||
<td class="d-flex"><img src="images/teams/logos/CRAP_Volley_3359.jpg" alt="">
|
||||
<a href="team_details.php?team_id=3359">C.R.A.P. Volley </a></td>
|
||||
<td><b>30</b></td><td>16</td><td>10</td><td>6</td><td>3</td><td>3</td><td>36</td><td>28</td><td>8</td>
|
||||
<td>1427</td><td>1329</td><td>98</td><td>0.56</td><td>1.07</td>
|
||||
<td><svg viewBox="0 0 16 16"><path d="M16 8A8"/></svg></td>
|
||||
</tr>
|
||||
</table>`;
|
||||
|
||||
const righe = parseClassifica(htmlClassifica);
|
||||
assert.equal(righe.length, 2, "prende solo il girone che contiene la nostra squadra");
|
||||
assert.deepEqual(righe[1], {
|
||||
pos: 4,
|
||||
squadra: "C.R.A.P. Volley",
|
||||
punti: 30,
|
||||
giocate: 16,
|
||||
vinte: 10,
|
||||
perse: 6,
|
||||
setFatti: 36,
|
||||
setSubiti: 28,
|
||||
});
|
||||
assert.equal(righe[0]!.squadra, "Paolo Poggi Volley & C.", "decodifica le entità HTML");
|
||||
assert.deepEqual(parseClassifica("<p>pagina cambiata</p>"), [], "portale cambiato: nessun crash");
|
||||
|
||||
assert.ok(isNostraSquadra("CRAP Volley") && isNostraSquadra("C.R.A.P. Volley "));
|
||||
assert.ok(!isNostraSquadra("CRAP Volley B"));
|
||||
|
||||
const eventi = [
|
||||
{
|
||||
id: 1,
|
||||
start: "2025-11-12T22:00:00",
|
||||
team1: "C.R.A.P. Volley",
|
||||
team2: "AMCM",
|
||||
result: "3 - 1",
|
||||
partials: "25 - 23</br>23 - 25</br>25 - 18</br>25 - 22</br>",
|
||||
field: "Sabin Piccolo",
|
||||
project: "PVM - Campionato Open Misto Eccellenza",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start: "2025-11-19T22:00:00",
|
||||
team1: "Gnu Tim SdC",
|
||||
team2: "C.R.A.P. Volley",
|
||||
result: "3 - 2",
|
||||
partials: "25 - 21</br>18 - 25</br>",
|
||||
field: "BVI",
|
||||
project: "PVM - Campionato Open Misto Eccellenza",
|
||||
},
|
||||
{ id: 3, start: "2026-09-10T21:00:00", team1: "C.R.A.P. Volley", team2: "Esselle", result: "" },
|
||||
{ id: 4, start: "2025-10-01T21:00:00", team1: "Esselle", team2: "AMCM", result: "3 - 0" },
|
||||
];
|
||||
|
||||
const partite = partiteDaEventi(eventi);
|
||||
assert.equal(partite.length, 3, "scarta le gare di altre squadre");
|
||||
assert.equal(partite[0]!.data, "2026-09-10", "ordinate dalla più recente");
|
||||
assert.equal(partite[0]!.setNostri, null, "gara futura senza risultato");
|
||||
|
||||
const casa = partite.find((p) => p.id === "1")!;
|
||||
assert.deepEqual([casa.casa, casa.avversario, casa.setNostri, casa.setLoro], [true, "AMCM", 3, 1]);
|
||||
assert.deepEqual(casa.parziali[1], [23, 25]);
|
||||
assert.equal(casa.ora, "22:00");
|
||||
|
||||
const trasferta = partite.find((p) => p.id === "2")!;
|
||||
assert.deepEqual(
|
||||
[trasferta.casa, trasferta.avversario, trasferta.setNostri, trasferta.setLoro],
|
||||
[false, "Gnu Tim SdC", 2, 3],
|
||||
"in trasferta il punteggio va ribaltato",
|
||||
);
|
||||
assert.deepEqual(trasferta.parziali[0], [21, 25], "anche i parziali");
|
||||
|
||||
assert.equal(partiteGiocate(partite).length, 2);
|
||||
assert.deepEqual(partiteDaEventi("non è un array"), [], "risposta inattesa: nessun crash");
|
||||
|
||||
if (process.env["CSI_LIVE"]) {
|
||||
const [html, json] = await Promise.all([
|
||||
fetch(urlClassifica()).then((r) => r.text()),
|
||||
fetch(urlPartite()).then((r) => r.json()),
|
||||
]);
|
||||
const live = parseClassifica(html);
|
||||
const nostra = live.find((r) => isNostraSquadra(r.squadra));
|
||||
assert.ok(nostra, "la nostra squadra è in classifica");
|
||||
assert.ok(live.length >= 8 && nostra.giocate > 0);
|
||||
const livePartite = partiteDaEventi(json);
|
||||
assert.ok(livePartite.length > 0 && partiteGiocate(livePartite).length > 0);
|
||||
console.log(`live: ${live.length} squadre, ${livePartite.length} partite, noi ${nostra.pos}°`);
|
||||
}
|
||||
|
||||
console.log("csi-core: ok");
|
||||
@@ -0,0 +1,163 @@
|
||||
import type { RigaClassifica } from "./crapp-data";
|
||||
|
||||
/**
|
||||
* Lettura dei dati ufficiali dal portale Livescore CSI Bologna.
|
||||
* Non esiste un'API documentata: usiamo gli stessi endpoint che il sito chiama
|
||||
* via ajax. Nessuna autenticazione, ma nessuna garanzia di stabilità: ogni
|
||||
* funzione qui deve fallire in modo pulito (array vuoto), mai lanciare.
|
||||
*/
|
||||
|
||||
export const CSI_BASE = "https://livescore.csibologna.it";
|
||||
/** Campionato Open Misto Eccellenza 2025/26. Cambia a ogni stagione. */
|
||||
export const CSI_PROJECT_ID = 767;
|
||||
/** C.R.A.P. Volley sul portale CSI. */
|
||||
export const CSI_TEAM_ID = 3359;
|
||||
export const CSI_GIRONE = "Girone B";
|
||||
export const CSI_NOME_SQUADRA = "C.R.A.P. Volley";
|
||||
|
||||
export const urlClassifica = (projectId = CSI_PROJECT_ID) =>
|
||||
`${CSI_BASE}/components/project-sheets.php?project_id=${projectId}`;
|
||||
export const urlPartite = (teamId = CSI_TEAM_ID) =>
|
||||
`${CSI_BASE}/assets/json/getEventsByTeamId.php?team_id=${teamId}`;
|
||||
|
||||
export type PartitaCsi = {
|
||||
id: string;
|
||||
data: string;
|
||||
ora: string;
|
||||
avversario: string;
|
||||
casa: boolean;
|
||||
/** null finché la gara non è stata giocata. */
|
||||
setNostri: number | null;
|
||||
setLoro: number | null;
|
||||
parziali: Array<[number, number]>;
|
||||
campo: string;
|
||||
competizione: string;
|
||||
};
|
||||
|
||||
export type DatiCsi = {
|
||||
classifica: RigaClassifica[];
|
||||
partite: PartitaCsi[];
|
||||
girone: string;
|
||||
aggiornato: string;
|
||||
};
|
||||
|
||||
/** "C.R.A.P. Volley" e "CRAP Volley" devono confrontarsi uguali. */
|
||||
function normalizza(nome: string): string {
|
||||
return nome.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
}
|
||||
|
||||
export function isNostraSquadra(nome: string): boolean {
|
||||
return normalizza(nome) === normalizza(CSI_NOME_SQUADRA);
|
||||
}
|
||||
|
||||
const entita: Record<string, string> = {
|
||||
amp: "&",
|
||||
lt: "<",
|
||||
gt: ">",
|
||||
quot: '"',
|
||||
nbsp: " ",
|
||||
deg: "°",
|
||||
apos: "'",
|
||||
};
|
||||
|
||||
function testo(html: string): string {
|
||||
return html
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/&(#\d+|[a-z]+);/gi, (intero, codice: string) =>
|
||||
codice.startsWith("#")
|
||||
? String.fromCharCode(Number(codice.slice(1)))
|
||||
: (entita[codice.toLowerCase()] ?? intero),
|
||||
)
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Estrae la classifica dal frammento HTML di `project-sheets.php`.
|
||||
* Il campionato ha due gironi: prendiamo la tabella che contiene la nostra
|
||||
* squadra. Colonne (indice del `<td>`): 0 Pos · 1 Squadra · 2 Punti ·
|
||||
* 3 Giocate · 4 Vinte · 5 Perse · 8 Set fatti · 9 Set subiti.
|
||||
*/
|
||||
export function parseClassifica(html: string): RigaClassifica[] {
|
||||
const tabelle = html.match(/<table[\s\S]*?<\/table>/gi) ?? [];
|
||||
const nostra = tabelle.find((t) => normalizza(testo(t)).includes(normalizza(CSI_NOME_SQUADRA)));
|
||||
if (!nostra) return [];
|
||||
|
||||
const righe: RigaClassifica[] = [];
|
||||
for (const riga of nostra.match(/<tr[\s\S]*?<\/tr>/gi) ?? []) {
|
||||
const celle = (riga.match(/<td[\s\S]*?<\/td>/gi) ?? []).map(testo);
|
||||
if (celle.length < 10) continue;
|
||||
const pos = Number(celle[0]);
|
||||
if (!Number.isFinite(pos) || pos === 0 || !celle[1]) continue;
|
||||
righe.push({
|
||||
pos,
|
||||
squadra: celle[1],
|
||||
punti: Number(celle[2]) || 0,
|
||||
giocate: Number(celle[3]) || 0,
|
||||
vinte: Number(celle[4]) || 0,
|
||||
perse: Number(celle[5]) || 0,
|
||||
setFatti: Number(celle[8]) || 0,
|
||||
setSubiti: Number(celle[9]) || 0,
|
||||
});
|
||||
}
|
||||
return righe;
|
||||
}
|
||||
|
||||
type EventoCsi = {
|
||||
id?: number | string;
|
||||
start?: string;
|
||||
team1?: string;
|
||||
team2?: string;
|
||||
result?: string;
|
||||
partials?: string;
|
||||
field?: string;
|
||||
project?: string;
|
||||
};
|
||||
|
||||
function punteggio(result: string | undefined): [number, number] | null {
|
||||
const m = /(\d+)\s*-\s*(\d+)/.exec(result ?? "");
|
||||
return m ? [Number(m[1]), Number(m[2])] : null;
|
||||
}
|
||||
|
||||
function parziali(partials: string | undefined): Array<[number, number]> {
|
||||
return [...(partials ?? "").matchAll(/(\d+)\s*-\s*(\d+)/g)].map((m) => [
|
||||
Number(m[1]),
|
||||
Number(m[2]),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Converte gli eventi di `getEventsByTeamId.php` nel formato usato dall'app. */
|
||||
export function partiteDaEventi(eventi: unknown): PartitaCsi[] {
|
||||
if (!Array.isArray(eventi)) return [];
|
||||
const partite: PartitaCsi[] = [];
|
||||
for (const evento of eventi as EventoCsi[]) {
|
||||
const team1 = testo(evento.team1 ?? "");
|
||||
const team2 = testo(evento.team2 ?? "");
|
||||
const casa = isNostraSquadra(team1);
|
||||
if (!casa && !isNostraSquadra(team2)) continue;
|
||||
|
||||
const [dataIso, oraIso] = (evento.start ?? "").split("T");
|
||||
if (!dataIso) continue;
|
||||
|
||||
const set = punteggio(evento.result);
|
||||
const tutti = parziali(evento.partials);
|
||||
partite.push({
|
||||
id: String(evento.id ?? `${dataIso}-${team1}-${team2}`),
|
||||
data: dataIso,
|
||||
ora: (oraIso ?? "").slice(0, 5),
|
||||
avversario: casa ? team2 : team1,
|
||||
casa,
|
||||
setNostri: set ? (casa ? set[0] : set[1]) : null,
|
||||
setLoro: set ? (casa ? set[1] : set[0]) : null,
|
||||
parziali: casa ? tutti : tutti.map(([a, b]) => [b, a] as [number, number]),
|
||||
campo: testo(evento.field ?? ""),
|
||||
competizione: testo(evento.project ?? ""),
|
||||
});
|
||||
}
|
||||
return partite.sort((a, b) => b.data.localeCompare(a.data));
|
||||
}
|
||||
|
||||
/** Solo le gare già giocate, dalla più recente. */
|
||||
export function partiteGiocate(partite: PartitaCsi[]): PartitaCsi[] {
|
||||
return partite.filter((p) => p.setNostri !== null && p.setLoro !== null);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { DatiCsi } from "./csi-core";
|
||||
|
||||
export const CSI_KEY = ["csi"] as const;
|
||||
|
||||
/**
|
||||
* Classifica e risultati ufficiali CSI. Il server tiene una cache di 6 ore:
|
||||
* qui basta una lettura per sessione.
|
||||
*/
|
||||
export function useCsi() {
|
||||
return useQuery<DatiCsi>({
|
||||
queryKey: CSI_KEY,
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/public/csi");
|
||||
if (!res.ok) throw new Error("CSI non disponibile");
|
||||
return res.json();
|
||||
},
|
||||
staleTime: 6 * 60 * 60_000,
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { Route as ScoutRouteImport } from './routes/scout'
|
||||
import { Route as SquadraRouteImport } from './routes/squadra'
|
||||
import { Route as AllenamentoIdRouteImport } from './routes/allenamento.$id'
|
||||
import { Route as PartitaIdRouteImport } from './routes/partita.$id'
|
||||
import { Route as ApiPublicCsiRouteImport } from './routes/api/public/csi'
|
||||
import { Route as ApiPublicPromemoriaPalloniRouteImport } from './routes/api/public/promemoria-palloni'
|
||||
import { Route as ApiPublicPushConfigRouteImport } from './routes/api/public/push-config'
|
||||
import { Route as ApiPublicPushMessaggioRouteImport } from './routes/api/public/push-messaggio'
|
||||
@@ -75,6 +76,11 @@ const PartitaIdRoute = PartitaIdRouteImport.update({
|
||||
path: '/partita/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ApiPublicCsiRoute = ApiPublicCsiRouteImport.update({
|
||||
id: '/api/public/csi',
|
||||
path: '/api/public/csi',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ApiPublicPromemoriaPalloniRoute =
|
||||
ApiPublicPromemoriaPalloniRouteImport.update({
|
||||
id: '/api/public/promemoria-palloni',
|
||||
@@ -114,6 +120,7 @@ export interface FileRoutesByFullPath {
|
||||
'/squadra': typeof SquadraRoute
|
||||
'/allenamento/$id': typeof AllenamentoIdRoute
|
||||
'/partita/$id': typeof PartitaIdRoute
|
||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||
'/api/public/push-messaggio': typeof ApiPublicPushMessaggioRoute
|
||||
@@ -131,6 +138,7 @@ export interface FileRoutesByTo {
|
||||
'/squadra': typeof SquadraRoute
|
||||
'/allenamento/$id': typeof AllenamentoIdRoute
|
||||
'/partita/$id': typeof PartitaIdRoute
|
||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||
'/api/public/push-messaggio': typeof ApiPublicPushMessaggioRoute
|
||||
@@ -149,6 +157,7 @@ export interface FileRoutesById {
|
||||
'/squadra': typeof SquadraRoute
|
||||
'/allenamento/$id': typeof AllenamentoIdRoute
|
||||
'/partita/$id': typeof PartitaIdRoute
|
||||
'/api/public/csi': typeof ApiPublicCsiRoute
|
||||
'/api/public/promemoria-palloni': typeof ApiPublicPromemoriaPalloniRoute
|
||||
'/api/public/push-config': typeof ApiPublicPushConfigRoute
|
||||
'/api/public/push-messaggio': typeof ApiPublicPushMessaggioRoute
|
||||
@@ -168,6 +177,7 @@ export interface FileRouteTypes {
|
||||
| '/squadra'
|
||||
| '/allenamento/$id'
|
||||
| '/partita/$id'
|
||||
| '/api/public/csi'
|
||||
| '/api/public/promemoria-palloni'
|
||||
| '/api/public/push-config'
|
||||
| '/api/public/push-messaggio'
|
||||
@@ -185,6 +195,7 @@ export interface FileRouteTypes {
|
||||
| '/squadra'
|
||||
| '/allenamento/$id'
|
||||
| '/partita/$id'
|
||||
| '/api/public/csi'
|
||||
| '/api/public/promemoria-palloni'
|
||||
| '/api/public/push-config'
|
||||
| '/api/public/push-messaggio'
|
||||
@@ -202,6 +213,7 @@ export interface FileRouteTypes {
|
||||
| '/squadra'
|
||||
| '/allenamento/$id'
|
||||
| '/partita/$id'
|
||||
| '/api/public/csi'
|
||||
| '/api/public/promemoria-palloni'
|
||||
| '/api/public/push-config'
|
||||
| '/api/public/push-messaggio'
|
||||
@@ -220,6 +232,7 @@ export interface RootRouteChildren {
|
||||
SquadraRoute: typeof SquadraRoute
|
||||
AllenamentoIdRoute: typeof AllenamentoIdRoute
|
||||
PartitaIdRoute: typeof PartitaIdRoute
|
||||
ApiPublicCsiRoute: typeof ApiPublicCsiRoute
|
||||
ApiPublicPromemoriaPalloniRoute: typeof ApiPublicPromemoriaPalloniRoute
|
||||
ApiPublicPushConfigRoute: typeof ApiPublicPushConfigRoute
|
||||
ApiPublicPushMessaggioRoute: typeof ApiPublicPushMessaggioRoute
|
||||
@@ -299,6 +312,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PartitaIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/api/public/csi': {
|
||||
id: '/api/public/csi'
|
||||
path: '/api/public/csi'
|
||||
fullPath: '/api/public/csi'
|
||||
preLoaderRoute: typeof ApiPublicCsiRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/api/public/promemoria-palloni': {
|
||||
id: '/api/public/promemoria-palloni'
|
||||
path: '/api/public/promemoria-palloni'
|
||||
@@ -348,6 +368,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
SquadraRoute: SquadraRoute,
|
||||
AllenamentoIdRoute: AllenamentoIdRoute,
|
||||
PartitaIdRoute: PartitaIdRoute,
|
||||
ApiPublicCsiRoute: ApiPublicCsiRoute,
|
||||
ApiPublicPromemoriaPalloniRoute: ApiPublicPromemoriaPalloniRoute,
|
||||
ApiPublicPushConfigRoute: ApiPublicPushConfigRoute,
|
||||
ApiPublicPushMessaggioRoute: ApiPublicPushMessaggioRoute,
|
||||
|
||||
@@ -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