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, type RigaClassifica } from "@/lib/crapp-data"; 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 { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti"; import { useEventi } from "@/lib/eventi"; import { LogoSquadra } from "@/components/crapp/DettaglioCsi"; const LOGO_NOI = "/logo-nerorosso.svg"; const NOME_NOI = "CRAP Volley"; /** Logo CRAP o avversario; cerchio con iniziali se il CSI non ha l'immagine. */ function LogoPartita({ nostro, logoAvversario, avversario, }: { nostro: boolean; logoAvversario: string; avversario: string; }) { if (nostro) { return ; } if (logoAvversario) { return ; } const iniziali = avversario .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((p) => p[0]?.toUpperCase() ?? "") .join(""); return ( {iniziali || "?"} ); } const TAB_CLASSIFICA = ["classifica", "storico"] as const; type TabClassifica = (typeof TAB_CLASSIFICA)[number]; function isTabClassifica(v: unknown): v is TabClassifica { return typeof v === "string" && (TAB_CLASSIFICA as readonly string[]).includes(v); } export const Route = createFileRoute("/classifica")({ validateSearch: (search: Record): { tab?: TabClassifica } => { const tab = isTabClassifica(search["tab"]) ? search["tab"] : undefined; return tab ? { tab } : {}; }, head: () => ({ meta: [ { title: "Classifica campionato — CrAPP" }, { name: "description", content: "Classifica e risultati del girone CSI seguiti in tempo reale dal CRAP Volley.", }, { property: "og:title", content: "Classifica campionato — CrAPP" }, { property: "og:description", content: "Posizioni, set e risultati aggiornati del girone." }, ], }), component: Classifica, }); function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: string }) { return (
# Squadra G Set Pt
{righe.length === 0 ? (

{vuoto}

) : ( righe.map((r) => { const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley"; return (
{r.pos} {r.squadra} {r.giocate} {r.setFatti}:{r.setSubiti} {r.punti}
); }) )}
); } 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 { tab } = Route.useSearch(); const scoutMatches = useScoutMatches(); const { data: csi } = useCsi(); const votiMvp = useVotiMvp(); 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( () => new Map(eventi.filter((e) => e.tipo === "partita").map((e) => [e.data, e.id])), [eventi], ); // I voti MVP sono legati all'evento CrAPP, non al referto CSI né allo scout. const tuttiMatch = useMemo(() => { const mvpPerData = (data: string) => mvpPerMatch[eventoIdPerData.get(data) ?? ""] ?? ""; const csiGiocate = csi ? partiteGiocate(csi.partite) : []; return csiGiocate.length ? csiGiocate.map((p) => { const m = matchDaPartitaCsi(p); return { ...m, mvp: mvpPerData(m.data), scout: false }; }) : scoutMatches.map((m) => ({ id: m.id, data: m.data, avversario: m.avversario, logoAvversario: "", casa: m.casa, setNostri: m.setNostri, setLoro: m.setLoro, parziali: m.parziali, mvp: mvpPerData(m.data), scout: true, })); }, [csi, scoutMatches, mvpPerMatch, eventoIdPerData]); // Ogni tab è memoizzata: un refetch in background di uno solo dei dati (CSI, // scout, eventi, voti MVP) non deve ricostruire il JSX dell'altra tab. const contenutoClassifica = useMemo( () => ( <>
{csi?.formatoSospetto ? ( ) : ( )} {csi?.formatoSospetto ? "Il portale CSI potrebbe aver cambiato formato: dati da verificare." : csi ? `Dati CSI aggiornati ${formatAggiornamento(csi.aggiornato)}` : "Dati CSI in arrivo"}
{classificaCoppa.length > 0 ? ( <>

Coppa

Girone

) : null} ), [csi, classifica, classificaCoppa], ); const contenutoStorico = useMemo( () => tuttiMatch.length === 0 ? (

Nessuna partita disponibile.

) : (
{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 = ( <>

{casa.nome}

{trasferta.nome}

{formatData(m.data)} · MVP {m.mvp || "da votare"} {m.scout ? " · scoutata" : ""}

{setCasa}-{setOspite} {cliccabile ? ( ) : null}
{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 ( {puntiCasa}-{puntiOspite} ); })} {eventoId && !m.mvp ? ( Vota MVP ) : null}
); const aria = `${casa.nome} vs ${trasferta.nome}: dettaglio partita`; if (eventoId) { return ( {contenuto} ); } // 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 ? (
{contenuto}
) : ( {contenuto} ); })}
), [tuttiMatch, eventoIdPerData], ); return ( <> ); }