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:
@@ -21,26 +21,66 @@ Il portale **non espone un'API pubblica documentata**. Vengono usati gli stessi
|
|||||||
che il sito chiama internamente via ajax: sono raggiungibili senza autenticazione e senza
|
che il sito chiama internamente via ajax: sono raggiungibili senza autenticazione e senza
|
||||||
API key, ma **non offrono alcuna garanzia di stabilità**.
|
API key, ma **non offrono alcuna garanzia di stabilità**.
|
||||||
|
|
||||||
| Endpoint | Formato | Uso |
|
Le pagine "umane" (`league_details.php`, `team_details.php`) sono gusci lato server: non
|
||||||
| ------------------------------------------------ | ------- | ------------------------------------------------------------------------------ |
|
contengono dati, li caricano dopo via JS dagli stessi endpoint `components/*.php` —
|
||||||
| `components/project-sheets.php?project_id=767` | HTML | Classifica completa dei due gironi |
|
verificato leggendo `assets/js/project.js` e `assets/js/team.js`, referenziati in fondo
|
||||||
| `assets/json/getEventsByTeamId.php?team_id=3359` | JSON | Tutte le gare della squadra: data, ora, avversario, campo, risultato, parziali |
|
alle due pagine. Servono solo per la consultazione manuale nel browser (es. per ritrovare
|
||||||
|
un `project_id`), nessun codice le chiama direttamente.
|
||||||
Altri endpoint disponibili ma non usati: `getEventsByProjectIdHierarchical.php` (tutte le
|
|
||||||
gare del campionato), `project-chart-rankings.php` (solo punti), `project-next_matches.php`,
|
|
||||||
`project-last_results.php`, `team-roster.php`, `team-results.php`.
|
|
||||||
|
|
||||||
### Identificativi (stagione 2025/26)
|
### Identificativi (stagione 2025/26)
|
||||||
|
|
||||||
| Cosa | Valore |
|
| Cosa | Valore |
|
||||||
| ------------------- | -------------------------------------- |
|
| ---------------------- | ---------------------------------------- |
|
||||||
| Campionato | PVM - Campionato Open Misto Eccellenza |
|
| Campionato | PVM - Campionato Open Misto Eccellenza |
|
||||||
| `project_id` | `767` |
|
| `project_id` (girone) | `767` |
|
||||||
|
| Coppa | PVM Coppa CSI Misto Silver |
|
||||||
|
| `project_id` (coppa) | `848` |
|
||||||
| Squadra sul portale | `C.R.A.P. Volley` (con i punti) |
|
| Squadra sul portale | `C.R.A.P. Volley` (con i punti) |
|
||||||
| `team_id` | `3359` |
|
| `team_id` | `3359` |
|
||||||
| Girone | B |
|
| Girone | B |
|
||||||
|
|
||||||
Gli identificativi sono costanti in `src/lib/csi-core.ts`.
|
`project_id` (767), `CSI_COPPA_PROJECT_ID` (848) e `team_id` (3359) sono costanti in
|
||||||
|
`src/lib/csi-core.ts`.
|
||||||
|
|
||||||
|
Per ritrovare questi id a ogni cambio stagione: `components/team-main.php?team_id=3359`
|
||||||
|
(dietro `team_details.php`) contiene una sezione "Campionati" con un link
|
||||||
|
`league_details.php?project_id=…` per ogni competizione a cui la squadra è iscritta —
|
||||||
|
verificato chiamando l'endpoint direttamente, che oggi restituisce sia
|
||||||
|
`project_id=848` (Coppa) sia `project_id=767` (Campionato). Non serve aprire
|
||||||
|
`team_details.php` nel browser, questo componente basta.
|
||||||
|
|
||||||
|
### Endpoint usati dall'app
|
||||||
|
|
||||||
|
| Endpoint | Formato | Uso | Pagina "umana" corrispondente |
|
||||||
|
| -------------------------------------------------- | ------- | ------------------------------------ | ------------------------------------ |
|
||||||
|
| `components/project-sheets.php?project_id=767` | HTML | Classifica completa dei due gironi di campionato | `league_details.php?project_id=767` (tab "Classifica") |
|
||||||
|
| `components/project-sheets.php?project_id=848` | HTML | Classifica del girone di Coppa (solo fase a gironi, vedi limite 4) | `league_details.php?project_id=848` (tab "Classifica") |
|
||||||
|
| `assets/json/getEventsByTeamId.php?team_id=3359` | JSON | Tutte le gare della squadra | `team_details.php?team_id=3359` (tab "Calendario", `team-calendar.php`) |
|
||||||
|
|
||||||
|
**Formato di `project-sheets.php`** — tabella HTML per girone (una per `<table>`,
|
||||||
|
`parseClassifica()` in `csi-core.ts` prende quella che contiene il nome della squadra).
|
||||||
|
Colonne per `<td>` (0-indicizzate): `0` Pos · `1` Squadra (nome + logo + link a
|
||||||
|
`team_details.php?team_id=…`) · `2` Punti · `3` Partite giocate · `4` Vinte · `5` Perse ·
|
||||||
|
`6`-`7` Tie-break vinti/persi (non lette) · `8` Set fatti · `9` Set subiti · poi punti
|
||||||
|
fatti/subiti, quoziente, ultime cinque (non lette). Stessa struttura per `project_id=767`
|
||||||
|
(campionato) e `project_id=848` (fase a gironi della Coppa): `parseClassifica()` è
|
||||||
|
condivisa, nessun parser dedicato per la Coppa.
|
||||||
|
|
||||||
|
**Formato di `getEventsByTeamId.php`** — array JSON, un oggetto per gara (girone **e**
|
||||||
|
Coppa insieme, vedi limite 4), con: `id`, `start` (`"2025-11-12T22:00:00"`, data+ora
|
||||||
|
locale), `team1`/`team2` (nomi squadre), `result` (`"3 - 1"`, stringa libera), `partials`
|
||||||
|
(`"25 - 23</br>23 - 25</br>..."`, HTML nei separatori), `field` (impianto), `project`
|
||||||
|
(nome campionato, es. `"PVM - Coppa CSI Misto Silver"` — usato per distinguere le
|
||||||
|
competizioni, vedi limite 4), `league`, `group` (es. `"Girone B"`), `match_number`. Letto
|
||||||
|
da `partiteDaEventi()` in `csi-core.ts`, che estrae punteggio/parziali con le regex
|
||||||
|
`punteggio()`/`parziali()` — vedi limite 5 sui rischi di questo parsing.
|
||||||
|
|
||||||
|
Altri endpoint disponibili ma non usati: `getEventsByProjectIdHierarchical.php` (tutte le
|
||||||
|
gare del campionato), `project-chart-rankings.php` (solo punti), `project-next_matches.php`,
|
||||||
|
`project-last_results.php`, `project-sheets-scorers.php`/`-results.php`/`-measures.php`
|
||||||
|
(sotto-tab di `project-sheets`: marcatori, risultati per giornata, provvedimenti
|
||||||
|
disciplinari), `team-roster.php` (rosa), `team-staff.php`, `team-results.php`,
|
||||||
|
`team-scorers.php`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,16 +90,20 @@ Gli identificativi sono costanti in `src/lib/csi-core.ts`.
|
|||||||
CSI (portale)
|
CSI (portale)
|
||||||
↓ fetch server-side, cache 6 ore
|
↓ fetch server-side, cache 6 ore
|
||||||
/api/public/csi → src/routes/api/public/csi.ts
|
/api/public/csi → src/routes/api/public/csi.ts
|
||||||
↓ JSON { classifica, partite, girone, aggiornato }
|
↓ JSON { classifica, classificaCoppa, partite, girone, aggiornato }
|
||||||
useCsi() → src/lib/csi.ts (React Query, staleTime 6h)
|
useCsi() → src/lib/csi.ts (React Query, staleTime 6h)
|
||||||
↓
|
↓
|
||||||
/classifica → src/routes/classifica.tsx
|
/classifica → src/routes/classifica.tsx (tab "Classifica": Coppa sopra, Girone sotto)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`src/lib/csi-core.ts`** — costanti, tipi e funzioni pure: `parseClassifica()` (HTML → righe),
|
- **`src/lib/csi-core.ts`** — costanti, tipi e funzioni pure: `parseClassifica()` (HTML → righe,
|
||||||
`partiteDaEventi()` (JSON → partite), `isNostraSquadra()`, `partiteGiocate()`.
|
usata sia per `classifica` sia per `classificaCoppa`), `partiteDaEventi()` (JSON → partite),
|
||||||
- **`src/routes/api/public/csi.ts`** — unica route che contatta il CSI. Cache in memoria di
|
`isNostraSquadra()`, `partiteGiocate()`.
|
||||||
6 ore; in caso di errore restituisce l'ultimo dato buono (`503` solo se non ne esiste uno).
|
- **`src/routes/api/public/csi.ts`** — unica route che contatta il CSI: tre fetch in parallelo
|
||||||
|
(classifica girone, classifica Coppa, partite). Cache in memoria di 6 ore; in caso di errore
|
||||||
|
restituisce l'ultimo dato buono (`503` solo se non ne esiste uno). Se solo la Coppa fallisce
|
||||||
|
(`scarica(...).catch(() => "")`) la risposta resta comunque `200` con `classificaCoppa: []`:
|
||||||
|
è un dato supplementare, non blocca la classifica del girone.
|
||||||
- **`src/lib/csi.ts`** — hook client, una lettura per sessione.
|
- **`src/lib/csi.ts`** — hook client, una lettura per sessione.
|
||||||
- **`test/unit/csi-core.test.ts`** — check del parsing: `bun test/unit/csi-core.test.ts`.
|
- **`test/unit/csi-core.test.ts`** — check del parsing: `bun test/unit/csi-core.test.ts`.
|
||||||
Con `CSI_LIVE=1` verifica anche gli endpoint reali.
|
Con `CSI_LIVE=1` verifica anche gli endpoint reali.
|
||||||
@@ -83,13 +127,32 @@ useCsi() → src/lib/csi.ts (React Query, staleTime 6h)
|
|||||||
ancora disponibile" (o l'ultimo dato buono in cache, se ce n'è uno) e i risultati ricadono
|
ancora disponibile" (o l'ultimo dato buono in cache, se ce n'è uno) e i risultati ricadono
|
||||||
sulle partite dello Scout Live locale, non su dati demo — non esistono più in `crapp-data.ts`.
|
sulle partite dello Scout Live locale, non su dati demo — non esistono più in `crapp-data.ts`.
|
||||||
Il check con `CSI_LIVE=1` serve a scoprire il problema di parsing.
|
Il check con `CSI_LIVE=1` serve a scoprire il problema di parsing.
|
||||||
2. **`project_id` è legato alla stagione.** Per il 2026/27 servirà un nuovo id, ricavabile da
|
2. **`project_id` è legato alla stagione.** Per il 2026/27 servirà un nuovo id (vedi
|
||||||
`team_details.php?team_id=3359`, che elenca i campionati della squadra. Oggi va aggiornato
|
"Sorgente dati" sopra per come ritrovarlo). Oggi va aggiornato a mano in `csi-core.ts`.
|
||||||
a mano in `csi-core.ts`.
|
|
||||||
3. **La cache vive nel processo del server.** Si perde a ogni cold start e non è condivisa tra
|
3. **La cache vive nel processo del server.** Si perde a ogni cold start e non è condivisa tra
|
||||||
istanze. Sufficiente per una squadra; se serve di più, spostare i dati in una tabella
|
istanze. Sufficiente per una squadra; se serve di più, spostare i dati in una tabella
|
||||||
Supabase riempita da un job cron (stesso pattern di `promemoria-palloni`).
|
Supabase riempita da un job cron (stesso pattern di `promemoria-palloni`).
|
||||||
4. **I risultati includono anche la Coppa**, non solo il girone di campionato.
|
4. **Le partite includono sia il girone di campionato sia la Coppa, mescolate.**
|
||||||
|
`getEventsByTeamId.php?team_id=3359` è per squadra, non per competizione (vedi tabella
|
||||||
|
endpoint sopra): risponde con tutte le gare di `C.R.A.P. Volley`. Il campo `project`
|
||||||
|
distingue le due nel JSON grezzo, ma `partiteDaEventi()` (`csi-core.ts`) oggi non lo usa
|
||||||
|
per filtrare: tutte le gare finiscono in `DatiCsi.partite` senza distinzione (`storico
|
||||||
|
partite` in `/classifica` le mostra tutte insieme). Se in futuro servisse separarle, il
|
||||||
|
filtro va aggiunto su `evento.project` in `partiteDaEventi()`.
|
||||||
|
**La classifica della Coppa, invece, è mostrata** (sopra quella del girone in
|
||||||
|
`/classifica`): `project-sheets.php?project_id=848` (`CSI_COPPA_PROJECT_ID`) ha la stessa
|
||||||
|
struttura a tabella-per-girone di `project_id=767`, quindi `parseClassifica()` funziona
|
||||||
|
invariata — nessun parser dedicato. Resta un limite: quella pagina copre **solo la fase a
|
||||||
|
gironi**. La Coppa (PVM Coppa CSI Misto Silver) prevede due gironi da 4 squadre sola
|
||||||
|
andata seguiti da una finale secca tra le due vincenti, disputata in un `project_id`
|
||||||
|
figlio separato generato a fine fase a gironi (verificato con `curl` diretto:
|
||||||
|
`project-main.php?project_id=848` descrive il regolamento — "due gironi sola andata, le
|
||||||
|
due vincenti in finale, gare 3 set su 5" — e `project-sheets.php?project_id=848` mostra
|
||||||
|
sia le due classifiche a girone sia, in un'altra sezione della stessa risposta, il
|
||||||
|
tabellone a eliminazione con quel `project_id` figlio). Se la squadra arrivasse in
|
||||||
|
finale, `/classifica` continuerebbe a mostrare la classifica (ormai chiusa) del proprio
|
||||||
|
girone di Coppa, non l'esito della finale: non c'è codice che segua quel `project_id`
|
||||||
|
figlio, che oltretutto cambia a ogni edizione della Coppa e non è noto in anticipo.
|
||||||
5. **Le partite si leggono da JSON, con parsing fragile su campi testuali.** `result` e
|
5. **Le partite si leggono da JSON, con parsing fragile su campi testuali.** `result` e
|
||||||
`partials` in `getEventsByTeamId.php` sono stringhe libere tipo `"3-1"`, lette con
|
`partials` in `getEventsByTeamId.php` sono stringhe libere tipo `"3-1"`, lette con
|
||||||
un'espressione regolare (`punteggio()`/`parziali()` in `csi-core.ts`). Se il portale CSI
|
un'espressione regolare (`punteggio()`/`parziali()` in `csi-core.ts`). Se il portale CSI
|
||||||
@@ -126,7 +189,7 @@ useCsi() → src/lib/csi.ts (React Query, staleTime 6h)
|
|||||||
verificato con `curl` diretto sul loro dominio). `leggiCsi()` (`src/routes/api/public/
|
verificato con `curl` diretto sul loro dominio). `leggiCsi()` (`src/routes/api/public/
|
||||||
csi.ts`) intercetta l'eccezione di `JSON.parse` nel `try/catch` della route e risponde
|
csi.ts`) intercetta l'eccezione di `JSON.parse` nel `try/catch` della route e risponde
|
||||||
`503 "CSI non raggiungibile"` (o serve la cache se ce n'è una) — nessun crash, ma nessun
|
`503 "CSI non raggiungibile"` (o serve la cache se ce n'è una) — nessun crash, ma nessun
|
||||||
dato nuovo finché il portale non torna. **Effetto sulla suite test**: i 5 test di
|
dato nuovo finché il portale non torna. **Effetto sulla suite test**: i test di
|
||||||
`test/integration/api.test.ts` che leggono il CSI reale sondano `/api/public/csi` una
|
`test/integration/api.test.ts` che leggono il CSI reale sondano `/api/public/csi` una
|
||||||
volta prima di partire; se risponde con errore li salta (`salta()`, non `prova()`) invece
|
volta prima di partire; se risponde con errore li salta (`salta()`, non `prova()`) invece
|
||||||
di farli fallire, loggando il motivo — la suite resta verde durante un'indisponibilità
|
di farli fallire, loggando il motivo — la suite resta verde durante un'indisponibilità
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import type { RigaClassifica } from "./crapp-data";
|
|||||||
export const CSI_BASE = "https://livescore.csibologna.it";
|
export const CSI_BASE = "https://livescore.csibologna.it";
|
||||||
/** Campionato Open Misto Eccellenza 2025/26. Cambia a ogni stagione. */
|
/** Campionato Open Misto Eccellenza 2025/26. Cambia a ogni stagione. */
|
||||||
export const CSI_PROJECT_ID = 767;
|
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. */
|
/** C.R.A.P. Volley sul portale CSI. */
|
||||||
export const CSI_TEAM_ID = 3359;
|
export const CSI_TEAM_ID = 3359;
|
||||||
export const CSI_GIRONE = "Girone B";
|
export const CSI_GIRONE = "Girone B";
|
||||||
@@ -36,6 +42,8 @@ export type PartitaCsi = {
|
|||||||
|
|
||||||
export type DatiCsi = {
|
export type DatiCsi = {
|
||||||
classifica: RigaClassifica[];
|
classifica: RigaClassifica[];
|
||||||
|
/** Classifica del girone di Coppa (project_id 848). Vuota se il parsing fallisce. */
|
||||||
|
classificaCoppa: RigaClassifica[];
|
||||||
partite: PartitaCsi[];
|
partite: PartitaCsi[];
|
||||||
girone: string;
|
girone: string;
|
||||||
aggiornato: string;
|
aggiornato: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
|
CSI_COPPA_PROJECT_ID,
|
||||||
CSI_GIRONE,
|
CSI_GIRONE,
|
||||||
parseClassifica,
|
parseClassifica,
|
||||||
partiteDaEventi,
|
partiteDaEventi,
|
||||||
@@ -28,8 +29,15 @@ async function scarica(url: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function leggiCsi(): Promise<DatiCsi> {
|
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 classifica = parseClassifica(html);
|
||||||
|
const classificaCoppa = parseClassifica(htmlCoppa);
|
||||||
const eventiGrezzi = JSON.parse(json);
|
const eventiGrezzi = JSON.parse(json);
|
||||||
const partite = partiteDaEventi(eventiGrezzi);
|
const partite = partiteDaEventi(eventiGrezzi);
|
||||||
if (classifica.length === 0 && partite.length === 0) {
|
if (classifica.length === 0 && partite.length === 0) {
|
||||||
@@ -48,6 +56,7 @@ async function leggiCsi(): Promise<DatiCsi> {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
classifica,
|
classifica,
|
||||||
|
classificaCoppa,
|
||||||
partite,
|
partite,
|
||||||
girone: CSI_GIRONE,
|
girone: CSI_GIRONE,
|
||||||
aggiornato: new Date().toISOString(),
|
aggiornato: new Date().toISOString(),
|
||||||
|
|||||||
+49
-41
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
|||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { AlertTriangle, ChevronRight, RefreshCw } from "lucide-react";
|
import { AlertTriangle, ChevronRight, RefreshCw } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
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 { PageHeader } from "@/components/crapp/ui-bits";
|
||||||
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
||||||
import { useScoutMatches } from "@/lib/scout-store";
|
import { useScoutMatches } from "@/lib/scout-store";
|
||||||
@@ -37,6 +37,44 @@ export const Route = createFileRoute("/classifica")({
|
|||||||
component: 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) {
|
function formatAggiornamento(iso: string) {
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
const oggi = new Date().toDateString() === d.toDateString();
|
const oggi = new Date().toDateString() === d.toDateString();
|
||||||
@@ -52,6 +90,7 @@ function Classifica() {
|
|||||||
const { eventi } = useEventi();
|
const { eventi } = useEventi();
|
||||||
|
|
||||||
const classifica = useMemo(() => csi?.classifica ?? [], [csi]);
|
const classifica = useMemo(() => csi?.classifica ?? [], [csi]);
|
||||||
|
const classificaCoppa = useMemo(() => csi?.classificaCoppa ?? [], [csi]);
|
||||||
|
|
||||||
const mvpPerMatch = useMemo(() => vincitoriMvp(votiMvp.data ?? []), [votiMvp.data]);
|
const mvpPerMatch = useMemo(() => vincitoriMvp(votiMvp.data ?? []), [votiMvp.data]);
|
||||||
const eventoIdPerData = useMemo(
|
const eventoIdPerData = useMemo(
|
||||||
@@ -104,48 +143,17 @@ function Classifica() {
|
|||||||
? `Dati CSI aggiornati ${formatAggiornamento(csi.aggiornato)}`
|
? `Dati CSI aggiornati ${formatAggiornamento(csi.aggiornato)}`
|
||||||
: "Dati CSI in arrivo"}
|
: "Dati CSI in arrivo"}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden rounded-3xl bg-card shadow-card">
|
{classificaCoppa.length > 0 ? (
|
||||||
<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>
|
<h3 className="mb-2 px-1 text-sm font-bold text-foreground">Coppa</h3>
|
||||||
<span>Squadra</span>
|
<TabellaClassifica righe={classificaCoppa} vuoto="Classifica non disponibile." />
|
||||||
<span className="text-center">G</span>
|
<h3 className="mb-2 mt-4 px-1 text-sm font-bold text-foreground">Girone</h3>
|
||||||
<span className="text-center">Set</span>
|
</>
|
||||||
<span className="text-center">Pt</span>
|
) : null}
|
||||||
</div>
|
<TabellaClassifica righe={classifica} vuoto="Classifica non ancora disponibile." />
|
||||||
{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>
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
[csi, classifica],
|
[csi, classifica, classificaCoppa],
|
||||||
);
|
);
|
||||||
|
|
||||||
const contenutoStorico = useMemo(
|
const contenutoStorico = useMemo(
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ try {
|
|||||||
assert.match(res.headers.get("content-type") ?? "", /application\/json/);
|
assert.match(res.headers.get("content-type") ?? "", /application\/json/);
|
||||||
csi = (await json(res)) as DatiCsi;
|
csi = (await json(res)) as DatiCsi;
|
||||||
assert.ok(Array.isArray(csi.classifica) && csi.classifica.length > 0);
|
assert.ok(Array.isArray(csi.classifica) && csi.classifica.length > 0);
|
||||||
|
assert.ok(Array.isArray(csi.classificaCoppa));
|
||||||
assert.ok(Array.isArray(csi.partite) && csi.partite.length > 0);
|
assert.ok(Array.isArray(csi.partite) && csi.partite.length > 0);
|
||||||
assert.equal(csi.girone, "Girone B");
|
assert.equal(csi.girone, "Girone B");
|
||||||
assert.ok(!Number.isNaN(Date.parse(csi.aggiornato)), "timestamp valido");
|
assert.ok(!Number.isNaN(Date.parse(csi.aggiornato)), "timestamp valido");
|
||||||
@@ -68,6 +69,12 @@ try {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await provaCsi("la classifica di Coppa, se presente, contiene la nostra squadra", () => {
|
||||||
|
if ((csi?.classificaCoppa.length ?? 0) === 0) return; // fase a gironi finita, pagina cambiata
|
||||||
|
const noi = csi?.classificaCoppa.find((r) => isNostraSquadra(r.squadra));
|
||||||
|
assert.ok(noi, "C.R.A.P. Volley presente nel girone di Coppa");
|
||||||
|
});
|
||||||
|
|
||||||
await provaCsi("ogni partita è coerente con il formato dell'app", () => {
|
await provaCsi("ogni partita è coerente con il formato dell'app", () => {
|
||||||
for (const p of csi?.partite ?? []) {
|
for (const p of csi?.partite ?? []) {
|
||||||
assert.match(p.data, /^\d{4}-\d{2}-\d{2}$/, `${p.id}: data ISO`);
|
assert.match(p.data, /^\d{4}-\d{2}-\d{2}$/, `${p.id}: data ISO`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import {
|
import {
|
||||||
|
CSI_COPPA_PROJECT_ID,
|
||||||
isNostraSquadra,
|
isNostraSquadra,
|
||||||
parseClassifica,
|
parseClassifica,
|
||||||
partiteDaEventi,
|
partiteDaEventi,
|
||||||
@@ -48,6 +49,20 @@ assert.deepEqual(righe[1], {
|
|||||||
assert.equal(righe[0]!.squadra, "Paolo Poggi Volley & C.", "decodifica le entità HTML");
|
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.deepEqual(parseClassifica("<p>pagina cambiata</p>"), [], "portale cambiato: nessun crash");
|
||||||
|
|
||||||
|
// La Coppa ha un girone più piccolo (poche squadre) ma la stessa struttura di tabella del
|
||||||
|
// campionato: `parseClassifica()` funziona invariata, non serve un parser dedicato.
|
||||||
|
const htmlCoppa = `
|
||||||
|
<table class="ranking">
|
||||||
|
<tr><th>Pos</th><th>Squadra</th></tr>
|
||||||
|
<tr><td>1</td><td><a href="team_details.php?team_id=2">Set in the City</a></td>
|
||||||
|
<td><b>7</b></td><td>3</td><td>2</td><td>1</td><td>0</td><td>0</td><td>8</td><td>4</td></tr>
|
||||||
|
<tr><td>2</td><td><a href="team_details.php?team_id=3359">C.R.A.P. Volley</a></td>
|
||||||
|
<td><b>6</b></td><td>3</td><td>2</td><td>1</td><td>0</td><td>0</td><td>8</td><td>5</td></tr>
|
||||||
|
</table>`;
|
||||||
|
const righeCoppa = parseClassifica(htmlCoppa);
|
||||||
|
assert.equal(righeCoppa.length, 2, "girone di Coppa più piccolo del campionato: ok comunque");
|
||||||
|
assert.equal(righeCoppa[1]!.squadra, "C.R.A.P. Volley");
|
||||||
|
|
||||||
assert.ok(isNostraSquadra("CRAP Volley") && isNostraSquadra("C.R.A.P. Volley "));
|
assert.ok(isNostraSquadra("CRAP Volley") && isNostraSquadra("C.R.A.P. Volley "));
|
||||||
assert.ok(!isNostraSquadra("CRAP Volley B"));
|
assert.ok(!isNostraSquadra("CRAP Volley B"));
|
||||||
|
|
||||||
@@ -126,14 +141,20 @@ assert.equal(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (process.env["CSI_LIVE"]) {
|
if (process.env["CSI_LIVE"]) {
|
||||||
const [html, json] = await Promise.all([
|
const [html, htmlCoppa, json] = await Promise.all([
|
||||||
fetch(urlClassifica()).then((r) => r.text()),
|
fetch(urlClassifica()).then((r) => r.text()),
|
||||||
|
fetch(urlClassifica(CSI_COPPA_PROJECT_ID)).then((r) => r.text()),
|
||||||
fetch(urlPartite()).then((r) => r.json()),
|
fetch(urlPartite()).then((r) => r.json()),
|
||||||
]);
|
]);
|
||||||
const live = parseClassifica(html);
|
const live = parseClassifica(html);
|
||||||
const nostra = live.find((r) => isNostraSquadra(r.squadra));
|
const nostra = live.find((r) => isNostraSquadra(r.squadra));
|
||||||
assert.ok(nostra, "la nostra squadra è in classifica");
|
assert.ok(nostra, "la nostra squadra è in classifica");
|
||||||
assert.ok(live.length >= 8 && nostra.giocate > 0);
|
assert.ok(live.length >= 8 && nostra.giocate > 0);
|
||||||
|
const liveCoppa = parseClassifica(htmlCoppa);
|
||||||
|
assert.ok(
|
||||||
|
liveCoppa.some((r) => isNostraSquadra(r.squadra)),
|
||||||
|
"la nostra squadra è nel girone di Coppa",
|
||||||
|
);
|
||||||
const livePartite = partiteDaEventi(json);
|
const livePartite = partiteDaEventi(json);
|
||||||
assert.ok(livePartite.length > 0 && partiteGiocate(livePartite).length > 0);
|
assert.ok(livePartite.length > 0 && partiteGiocate(livePartite).length > 0);
|
||||||
console.log(`live: ${live.length} squadre, ${livePartite.length} partite, noi ${nostra.pos}°`);
|
console.log(`live: ${live.length} squadre, ${livePartite.length} partite, noi ${nostra.pos}°`);
|
||||||
|
|||||||
Reference in New Issue
Block a user