Replace demo stats with real data

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Cacciari
2026-09-01 22:34:07 +02:00
co-authored by Cursor
parent c314a08f6d
commit d63beae04c
6 changed files with 70 additions and 47 deletions
+8 -37
View File
@@ -82,40 +82,6 @@ function inizialiDa(nome: string) {
.toUpperCase();
}
type StatsDemo = Pick<Giocatore, "presenze" | "streak" | "mvp" | "mediaVoto">;
/** Statistiche demo: mix di badge bronzo / argento / oro già sbloccati. */
const statsDemo: Record<string, StatsDemo> = {
"Ivan Cacciari": { presenze: 31, streak: 8, mvp: 5, mediaVoto: 8.7 },
"Davide Grilli": { presenze: 27, streak: 5, mvp: 3, mediaVoto: 8.4 },
"Nicola Pezzoli": { presenze: 24, streak: 4, mvp: 2, mediaVoto: 8.2 },
"Laura Passabì": { presenze: 22, streak: 6, mvp: 3, mediaVoto: 8.1 },
"Francesca Tucci": { presenze: 19, streak: 3, mvp: 1, mediaVoto: 7.9 },
"Iacopo Ricci": { presenze: 21, streak: 2, mvp: 3, mediaVoto: 7.8 },
"Alessandra Brunacci": { presenze: 14, streak: 3, mvp: 1, mediaVoto: 7.5 },
"Mattias Bologna": { presenze: 12, streak: 2, mvp: 1, mediaVoto: 7.4 },
"Giada Valbonesi": { presenze: 13, streak: 4, mvp: 1, mediaVoto: 7.6 },
"Alessio Cocco": { presenze: 11, streak: 1, mvp: 0, mediaVoto: 7.2 },
"Mattia Catalano": { presenze: 9, streak: 2, mvp: 0, mediaVoto: 7.0 },
"Antonella Loverre": { presenze: 8, streak: 1, mvp: 0, mediaVoto: 6.9 },
"Carlo Di Castelnuovo": { presenze: 7, streak: 1, mvp: 0, mediaVoto: 6.8 },
"Camilla Esposito": { presenze: 6, streak: 2, mvp: 0, mediaVoto: 6.7 },
"Salvador Battistella": { presenze: 20, streak: 5, mvp: 1, mediaVoto: 7.7 },
"Silvia Chilese": { presenze: 16, streak: 3, mvp: 0, mediaVoto: 7.3 },
"Cristina Titone": { presenze: 15, streak: 2, mvp: 0, mediaVoto: 7.1 },
};
/** Serie demo derivate dalle statistiche: ogni tipo ha il suo contatore. */
function serieDa(s?: StatsDemo) {
const streak = s?.streak ?? 0;
const presenze = s?.presenze ?? 0;
return {
serieAllenamenti: streak,
seriePartite: Math.ceil(streak / 2),
serieConferme: presenze >= 20 ? 12 : presenze >= 14 ? 8 : presenze >= 8 ? 4 : 1,
};
}
export const giocatori: Giocatore[] = rosaCSI.map((r, i) => ({
id: `g${i + 1}`,
nome: r.nome,
@@ -123,14 +89,19 @@ export const giocatori: Giocatore[] = rosaCSI.map((r, i) => ({
ruolo: r.ruolo,
nascita: r.nascita,
iniziali: inizialiDa(r.nome),
totaliEventi: 32,
presenze: 0,
totaliEventi: 0,
streak: 0,
serieAllenamenti: 0,
seriePartite: 0,
serieConferme: 0,
mvp: 0,
mediaVoto: 0,
infortuni: 0,
ritardi: 0,
palloni: 0,
cacche: 0,
cacchePartita: 0,
...(statsDemo[r.nome] ?? { presenze: 0, streak: 0, mvp: 0, mediaVoto: 0 }),
...serieDa(statsDemo[r.nome]),
}));
export type RigaClassifica = {
+14
View File
@@ -84,3 +84,17 @@ export function vincitoriMvp(voti: VotoMvp[]): Record<string, string> {
export function mioVoto(voti: VotoMvp[], matchId: string, votanteId: string) {
return voti.find((v) => v.match_id === matchId && v.votante_id === votanteId) ?? null;
}
/** MVP vinti per giocatore, contando una vittoria per partita votata. */
export function mvpVintiPerGiocatore(voti: VotoMvp[]): Record<string, number> {
const out: Record<string, number> = {};
const matchIds = new Set(voti.map((v) => v.match_id));
for (const matchId of matchIds) {
const top = conteggioPartita(voti, matchId);
if (top.length > 0 && (top.length === 1 || top[0]!.voti > top[1]!.voti)) {
const id = top[0]!.id;
out[id] = (out[id] ?? 0) + 1;
}
}
return out;
}
+29
View File
@@ -1,12 +1,41 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import type { Stato } from "./crapp-data";
import type { Evento } from "./eventi";
export const PRESENZE_KEY = ["risposte-presenze"] as const;
/** eventoId -> giocatoreId -> stato */
export type MappaPresenze = Record<string, Record<string, Stato>>;
/** Allenamenti e partite CrAPP che contano per le statistiche di presenza. */
function eventiContanoPresenze(eventi: Evento[], giocatoreId?: string) {
return eventi.filter(
(e) =>
(e.tipo === "partita" || e.tipo === "allenamento") &&
(giocatoreId === undefined ||
e.convocati.length === 0 ||
e.convocati.includes(giocatoreId)),
);
}
/** Presenze effettive (presente o in ritardo) su eventi CrAPP. */
export function contaPresenzeGiocatore(
giocatoreId: string,
eventi: Evento[],
presenze: MappaPresenze,
): number {
return eventiContanoPresenze(eventi, giocatoreId).filter((e) => {
const stato = presenze[e.id]?.[giocatoreId];
return stato === "presente" || stato === "ritardo";
}).length;
}
/** Eventi CrAPP rilevanti per il denominatore presenze di un giocatore. */
export function totaliEventiGiocatore(giocatoreId: string, eventi: Evento[]): number {
return eventiContanoPresenze(eventi, giocatoreId).length;
}
async function fetchPresenze(): Promise<MappaPresenze> {
const { data, error } = await supabase
.from("risposte_presenze")
+11 -7
View File
@@ -1,6 +1,6 @@
import { useSyncExternalStore } from "react";
import { giocatori, type Giocatore } from "./crapp-data";
import { conInfortuni, useInfortuniERitardi } from "./infortuni";
import { useIo } from "./rosa";
const KEY = "crapp-user-v1";
const listeners = new Set<() => void>();
@@ -36,9 +36,9 @@ export function resetGiocatore() {
write(null);
}
/** Solo lettura locale: nessuna dipendenza da React Query (usabile fuori dal provider). */
export function useGiocatoreBase(): Giocatore | null {
const id = useSyncExternalStore(
/** Id del giocatore collegato al dispositivo (localStorage). */
export function useGiocatoreId(): string | null {
return useSyncExternalStore(
(cb) => {
listeners.add(cb);
return () => listeners.delete(cb);
@@ -46,11 +46,15 @@ export function useGiocatoreBase(): Giocatore | null {
() => read(),
() => null,
);
}
/** Anagrafica base dal localStorage: le statistiche restano a zero finché non passa da `useIo`. */
export function useGiocatoreBase(): Giocatore | null {
const id = useGiocatoreId();
return id ? (giocatori.find((x) => x.id === id) ?? null) : null;
}
/** Giocatore corrente con statistiche calcolate da dati reali (presenze, MVP, badge, …). */
export function useGiocatoreCorrente(): Giocatore | null {
const g = useGiocatoreBase();
const { infortuni, ritardi } = useInfortuniERitardi();
return g ? conInfortuni(g, infortuni, ritardi) : null;
return useIo();
}
+3 -1
View File
@@ -97,7 +97,9 @@ function Profilo() {
}
}
const percPresenze = Math.round((g.presenze / g.totaliEventi) * 100);
const percPresenze = g.totaliEventi
? Math.round((g.presenze / g.totaliEventi) * 100)
: 0;
async function onFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
+5 -2
View File
@@ -20,8 +20,11 @@ for (const g of giocatori) {
assert.ok(g.numero > 0, `${g.id}: numero di maglia positivo`);
assert.equal(g.iniziali.length, 2, `${g.id}: due iniziali`);
assert.equal(g.iniziali, g.iniziali.toUpperCase(), `${g.id}: iniziali maiuscole`);
assert.ok(g.mediaVoto >= 0 && g.mediaVoto <= 10, `${g.id}: media voto nel range 1-10`);
assert.ok(g.presenze <= g.totaliEventi, `${g.id}: presenze mai oltre gli eventi totali`);
assert.equal(g.mediaVoto, 0, `${g.id}: media voto iniziale a zero`);
assert.equal(g.presenze, 0, `${g.id}: presenze iniziali a zero`);
assert.equal(g.totaliEventi, 0, `${g.id}: totaliEventi iniziale a zero`);
assert.equal(g.mvp, 0, `${g.id}: mvp iniziale a zero`);
assert.equal(g.streak, 0, `${g.id}: streak iniziale a zero`);
}
// --- formatData --------------------------------------------------------------