(null);
@@ -122,8 +123,7 @@ export function VotoSocial({ matchId }: { matchId: string }) {
- {vincitore.nome} · {vincitore.voti}{" "}
- {vincitore.voti === 1 ? "voto" : "voti"}
+ {vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"}
) : (
@@ -137,4 +137,4 @@ export function VotoSocial({ matchId }: { matchId: string }) {
})}
);
-}
\ No newline at end of file
+}
diff --git a/src/lib/classifica-csi.ts b/src/lib/classifica-csi.ts
new file mode 100644
index 0000000..2b29e9b
--- /dev/null
+++ b/src/lib/classifica-csi.ts
@@ -0,0 +1,44 @@
+import { useQuery } from "@tanstack/react-query";
+import { strapiFetch } from "./strapi-client";
+import type { RigaClassifica } from "./crapp-data";
+
+type RigaClassificaStrapi = {
+ pos: number;
+ squadra: string;
+ giocate: number;
+ vinte: number;
+ perse: number;
+ set_fatti: number;
+ set_subiti: number;
+ punti: number;
+};
+
+function daEntry(e: RigaClassificaStrapi): RigaClassifica {
+ return {
+ pos: e.pos,
+ squadra: e.squadra,
+ giocate: e.giocate,
+ vinte: e.vinte,
+ perse: e.perse,
+ setFatti: e.set_fatti,
+ setSubiti: e.set_subiti,
+ punti: e.punti,
+ };
+}
+
+export const CLASSIFICA_KEY = ["classifica-csi"] as const;
+
+async function fetchClassifica(): Promise {
+ const entries = await strapiFetch("riga-classificas?sort=pos");
+ return entries.map(daEntry);
+}
+
+/** Classifica campionato CSI dal CMS: cambia a giornata, non a sessione. */
+export function useClassificaCsi(): RigaClassifica[] {
+ const query = useQuery({
+ queryKey: CLASSIFICA_KEY,
+ queryFn: fetchClassifica,
+ staleTime: 60 * 60_000,
+ });
+ return query.data ?? [];
+}
diff --git a/src/lib/crapp-data.ts b/src/lib/crapp-data.ts
index e91ea33..4a57426 100644
--- a/src/lib/crapp-data.ts
+++ b/src/lib/crapp-data.ts
@@ -2,10 +2,18 @@ export type Stato = "presente" | "assente" | "forse" | "ritardo" | "infortunato"
export const statoMeta: Record = {
presente: { label: "Presente", emoji: "✅", className: "bg-success text-success-foreground" },
- assente: { label: "Assente", emoji: "❌", className: "bg-destructive text-destructive-foreground" },
+ assente: {
+ label: "Assente",
+ emoji: "❌",
+ className: "bg-destructive text-destructive-foreground",
+ },
forse: { label: "Forse", emoji: "🤔", className: "bg-warning text-warning-foreground" },
ritardo: { label: "In ritardo", emoji: "⏱️", className: "bg-info text-info-foreground" },
- infortunato: { label: "Infortunato", emoji: "🩹", className: "bg-primary text-primary-foreground" },
+ infortunato: {
+ label: "Infortunato",
+ emoji: "🩹",
+ className: "bg-primary text-primary-foreground",
+ },
};
/**
@@ -42,89 +50,6 @@ export type Giocatore = {
iniziali: string;
};
-type Rosa = { nome: string; nascita: string; ruolo: string; numero?: number };
-
-/** Rosa reale CRAP Volley (tesseramento CSI Bologna 25/26). */
-const rosaCSI: Rosa[] = [
- { nome: "Salvador Battistella", nascita: "1997-08-30", ruolo: "Libero", numero: 88 },
- { nome: "Mattias Bologna", nascita: "1996-12-07", ruolo: "Centrale", numero: 73 },
- { nome: "Alessandra Brunacci", nascita: "2000-04-28", ruolo: "Palleggiatore", numero: 8 },
- { nome: "Ivan Cacciari", nascita: "1995-05-01", ruolo: "Banda", numero: 23 },
- { nome: "Mattia Catalano", nascita: "1995-08-13", ruolo: "Palleggiatore", numero: 21 },
- { nome: "Silvia Chilese", nascita: "1996-02-01", ruolo: "Libero", numero: 11 },
- { nome: "Alessio Cocco", nascita: "1997-07-04", ruolo: "Centrale", numero: 77 },
- { nome: "Carlo Di Castelnuovo", nascita: "1994-05-28", ruolo: "Opposto", numero: 14 },
- { nome: "Camilla Esposito", nascita: "2003-04-04", ruolo: "Palleggiatore", numero: 7 },
- { nome: "Davide Grilli", nascita: "1998-11-30", ruolo: "Opposto", numero: 1 },
- { nome: "Antonella Loverre", nascita: "2000-02-02", ruolo: "Banda", numero: 22 },
- { nome: "Laura Passabì", nascita: "1999-10-03", ruolo: "Banda", numero: 5 },
- { nome: "Nicola Pezzoli", nascita: "2000-09-16", ruolo: "Centrale", numero: 4 },
- { nome: "Iacopo Ricci", nascita: "1996-12-13", ruolo: "Banda", numero: 2 },
- { nome: "Cristina Titone", nascita: "1993-03-24", ruolo: "Libero", numero: 3 },
- { nome: "Francesca Tucci", nascita: "2001-04-18", ruolo: "Centrale", numero: 18 },
- { nome: "Giada Valbonesi", nascita: "1994-05-20", ruolo: "Opposto", numero: 10 },
-];
-
-function inizialiDa(nome: string) {
- return nome
- .split(" ")
- .map((p) => p[0] ?? "")
- .join("")
- .slice(0, 2)
- .toUpperCase();
-}
-
-type StatsDemo = Pick;
-
-/** Statistiche demo: mix di badge bronzo / argento / oro già sbloccati. */
-const statsDemo: Record = {
- "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,
- numero: r.numero ?? 0,
- ruolo: r.ruolo,
- nascita: r.nascita,
- iniziali: inizialiDa(r.nome),
- totaliEventi: 32,
- 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 Match = {
id: string;
data: string;
@@ -136,13 +61,6 @@ export type Match = {
mvp: string;
};
-export const storicoMatch: Match[] = [
- { id: "m1", data: "2026-07-25", avversario: "Pallavolo Sesto", casa: true, setNostri: 3, setLoro: 1, parziali: [[25, 19], [23, 25], [25, 21], [25, 18]], mvp: "Davide Grilli" },
- { id: "m2", data: "2026-07-18", avversario: "ASD Rondinella", casa: false, setNostri: 2, setLoro: 3, parziali: [[25, 22], [19, 25], [25, 23], [20, 25], [12, 15]], mvp: "Ivan Cacciari" },
- { id: "m3", data: "2026-07-11", avversario: "Virtus Cinisello", casa: true, setNostri: 3, setLoro: 0, parziali: [[25, 15], [25, 20], [25, 17]], mvp: "Laura Passabì" },
- { id: "m4", data: "2026-07-04", avversario: "Nuova Bovisa", casa: false, setNostri: 3, setLoro: 2, parziali: [[21, 25], [25, 23], [18, 25], [25, 20], [15, 11]], mvp: "Nicola Pezzoli" },
-];
-
export type RigaClassifica = {
pos: number;
squadra: string;
@@ -154,16 +72,6 @@ export type RigaClassifica = {
punti: number;
};
-export const classifica: RigaClassifica[] = [
- { pos: 1, squadra: "ASD Rondinella", giocate: 12, vinte: 10, perse: 2, setFatti: 33, setSubiti: 12, punti: 29 },
- { pos: 2, squadra: "CRAP Volley", giocate: 12, vinte: 9, perse: 3, setFatti: 31, setSubiti: 16, punti: 26 },
- { pos: 3, squadra: "Pallavolo Sesto", giocate: 12, vinte: 8, perse: 4, setFatti: 29, setSubiti: 19, punti: 24 },
- { pos: 4, squadra: "Volley Bruzzano", giocate: 12, vinte: 7, perse: 5, setFatti: 26, setSubiti: 21, punti: 21 },
- { pos: 5, squadra: "Aurora Nera", giocate: 12, vinte: 5, perse: 7, setFatti: 22, setSubiti: 25, punti: 16 },
- { pos: 6, squadra: "Virtus Cinisello", giocate: 12, vinte: 3, perse: 9, setFatti: 15, setSubiti: 30, punti: 10 },
- { pos: 7, squadra: "Nuova Bovisa", giocate: 12, vinte: 2, perse: 10, setFatti: 13, setSubiti: 32, punti: 7 },
-];
-
export function formatData(iso: string) {
const d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("it-IT", { weekday: "short", day: "2-digit", month: "long" });
@@ -172,7 +80,6 @@ export function formatData(iso: string) {
/** Referenti che possono gestire eventi e sollecitare le risposte. */
export const adminNomi = ["Ivan Cacciari", "Iacopo Ricci", "Cristina Titone"];
-export function isAdmin(giocatoreId: string) {
- const g = giocatori.find((x) => x.id === giocatoreId);
- return Boolean(g && adminNomi.includes(g.nome));
+export function isAdmin(giocatore: Pick | null | undefined) {
+ return Boolean(giocatore && adminNomi.includes(giocatore.nome));
}
diff --git a/src/lib/eventi.ts b/src/lib/eventi.ts
index 78cce06..172580c 100644
--- a/src/lib/eventi.ts
+++ b/src/lib/eventi.ts
@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
-import { giocatori } from "./crapp-data";
+import type { Giocatore } from "./crapp-data";
export type EventoTipo = "partita" | "allenamento" | "evento" | "compleanno";
@@ -158,7 +158,10 @@ export function useEliminaEvento() {
}
/** Compleanni della rosa, come eventi di calendario dell'anno indicato. */
-export function compleanniEventi(anno = new Date().getFullYear()): Evento[] {
+export function compleanniEventi(
+ giocatori: Giocatore[],
+ anno = new Date().getFullYear(),
+): Evento[] {
return giocatori
.map((g) => {
const md = g.nascita.slice(5);
@@ -181,7 +184,7 @@ export function compleanniEventi(anno = new Date().getFullYear()): Evento[] {
}
/** Rosa convocata per un evento: se non specificata vale tutta la rosa. */
-export function convocatiEvento(evento: Evento | null) {
+export function convocatiEvento(giocatori: Giocatore[], evento: Evento | null) {
if (!evento || evento.convocati.length === 0) return giocatori;
return giocatori.filter((g) => evento.convocati.includes(g.id));
}
diff --git a/src/lib/obiettivi.ts b/src/lib/obiettivi.ts
index 7766581..ca4f3ed 100644
--- a/src/lib/obiettivi.ts
+++ b/src/lib/obiettivi.ts
@@ -1,4 +1,4 @@
-import { giocatori, storicoMatch, type Giocatore } from "./crapp-data";
+import type { Giocatore, Match } from "./crapp-data";
import type { Evento } from "./eventi";
import type { MappaPresenze } from "./presenze";
import { mediaSquadra, type VotoPagella } from "./pagelle";
@@ -26,12 +26,12 @@ export const contestoVuoto: ContestoObiettivi = { eventi: [], presenze: {}, page
const MESE = "2026-08";
-function percentualePresenzeMese(ctx: ContestoObiettivi) {
+function percentualePresenzeMese(ctx: ContestoObiettivi, numGiocatori: number) {
const delMese = ctx.eventi.filter(
(e) => e.data.startsWith(MESE) && (e.tipo === "partita" || e.tipo === "allenamento"),
);
- if (delMese.length === 0) return 0;
- const posti = delMese.length * giocatori.length;
+ if (delMese.length === 0 || numGiocatori === 0) return 0;
+ const posti = delMese.length * numGiocatori;
const presenti = delMese.reduce((s, e) => {
const risposte = ctx.presenze[e.id] ?? {};
return s + Object.values(risposte).filter((x) => x === "presente" || x === "ritardo").length;
@@ -39,10 +39,10 @@ function percentualePresenzeMese(ctx: ContestoObiettivi) {
return Math.round((presenti / posti) * 100);
}
-function percentualeRisposte(ctx: ContestoObiettivi) {
+function percentualeRisposte(ctx: ContestoObiettivi, numGiocatori: number) {
const daRispondere = ctx.eventi.filter((e) => e.tipo !== "compleanno");
- if (daRispondere.length === 0) return 0;
- const posti = daRispondere.length * giocatori.length;
+ if (daRispondere.length === 0 || numGiocatori === 0) return 0;
+ const posti = daRispondere.length * numGiocatori;
const risposte = daRispondere.reduce(
(s, e) => s + Object.keys(ctx.presenze[e.id] ?? {}).length,
0,
@@ -50,21 +50,21 @@ function percentualeRisposte(ctx: ContestoObiettivi) {
return Math.round((risposte / posti) * 100);
}
-const vittorie = storicoMatch.filter((m) => m.setNostri > m.setLoro).length;
-
/** Obiettivi collaborativi: si muovono con il contributo di tutta la rosa. */
export function obiettiviSquadra(
- rosa: Giocatore[] = giocatori,
+ rosa: Giocatore[] = [],
ctx: ContestoObiettivi = contestoVuoto,
+ storicoMatch: Match[] = [],
): ObiettivoSquadra[] {
const somma = (f: (g: Giocatore) => number) => rosa.reduce((s, g) => s + f(g), 0);
const continui = rosa.filter((g) => g.serieAllenamenti >= 3).length;
+ const vittorie = storicoMatch.filter((m) => m.setNostri > m.setLoro).length;
return [
{
id: "o1",
titolo: "90% di presenze ad agosto",
descrizione: "Media presenze su partite e allenamenti del mese",
- valore: percentualePresenzeMese(ctx),
+ valore: percentualePresenzeMese(ctx, rosa.length),
target: 90,
unita: "%",
scadenza: "2026-08-31",
@@ -75,7 +75,7 @@ export function obiettiviSquadra(
id: "o2",
titolo: "Tutti rispondono alle convocazioni",
descrizione: "Percentuale di risposte date sugli eventi in programma",
- valore: percentualeRisposte(ctx),
+ valore: percentualeRisposte(ctx, rosa.length),
target: 90,
unita: "%",
scadenza: "2026-09-30",
@@ -169,8 +169,12 @@ export function progressoObiettivo(o: ObiettivoSquadra) {
return Math.min(100, Math.round((o.valore / o.target) * 100));
}
-export function obiettiviOrdinati(rosa?: Giocatore[], ctx?: ContestoObiettivi) {
- return obiettiviSquadra(rosa, ctx).sort((a, b) => {
+export function obiettiviOrdinati(
+ rosa?: Giocatore[],
+ ctx?: ContestoObiettivi,
+ storicoMatch?: Match[],
+) {
+ return obiettiviSquadra(rosa, ctx, storicoMatch).sort((a, b) => {
const pa = progressoObiettivo(a);
const pb = progressoObiettivo(b);
const ca = pa >= 100 ? 1 : 0;
diff --git a/src/lib/palloni-core.ts b/src/lib/palloni-core.ts
index b0131c2..30a5ad7 100644
--- a/src/lib/palloni-core.ts
+++ b/src/lib/palloni-core.ts
@@ -1,4 +1,4 @@
-import { giocatori } from "./crapp-data";
+import type { Giocatore } from "./crapp-data";
import type { Evento } from "./eventi";
export type Turno = { evento_id: string; giocatore_id: string; aggiornato_da: string | null };
@@ -18,6 +18,7 @@ export function eventiPalloni(eventi: Evento[]): Evento[] {
export function completaTurni(
turni: Record,
eventi: Evento[],
+ giocatori: Giocatore[],
): Record {
const risultato: Record = { ...turni };
const conteggio = new Map(giocatori.map((g) => [g.id, 0]));
@@ -32,17 +33,15 @@ export function completaTurni(
}
if (assegnato) return;
- const scelto = giocatori
- .slice()
- .sort((a, b) => {
- const ca = conteggio.get(a.id) ?? 0;
- const cb = conteggio.get(b.id) ?? 0;
- if (ca !== cb) return ca - cb;
- const ua = ultimo.get(a.id) ?? -1;
- const ub = ultimo.get(b.id) ?? -1;
- if (ua !== ub) return ua - ub;
- return a.nome.localeCompare(b.nome);
- })[0];
+ const scelto = giocatori.slice().sort((a, b) => {
+ const ca = conteggio.get(a.id) ?? 0;
+ const cb = conteggio.get(b.id) ?? 0;
+ if (ca !== cb) return ca - cb;
+ const ua = ultimo.get(a.id) ?? -1;
+ const ub = ultimo.get(b.id) ?? -1;
+ if (ua !== ub) return ua - ub;
+ return a.nome.localeCompare(b.nome);
+ })[0];
if (!scelto) return;
risultato[evento.id] = scelto.id;
diff --git a/src/lib/palloni.ts b/src/lib/palloni.ts
index e55c107..aed02a3 100644
--- a/src/lib/palloni.ts
+++ b/src/lib/palloni.ts
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { completaTurni } from "./palloni-core";
import { useEventi } from "./eventi";
+import { useRosaBase } from "./rosa-base";
export const TURNI_KEY = ["turni-palloni"] as const;
@@ -26,8 +27,9 @@ export function useTurniPalloni() {
// Cambia raramente: una lettura per sessione è sufficiente.
const query = useQuery({ queryKey: TURNI_KEY, queryFn: fetchTurni, staleTime: 30 * 60_000 });
const { eventi } = useEventi();
+ const giocatori = useRosaBase();
const salvati = query.data ?? {};
- return { ...query, salvati, turni: completaTurni(salvati, eventi) };
+ return { ...query, salvati, turni: completaTurni(salvati, eventi, giocatori) };
}
export function useAssegnaTurno() {
diff --git a/src/lib/presenze-mese.ts b/src/lib/presenze-mese.ts
index 8716484..aa02591 100644
--- a/src/lib/presenze-mese.ts
+++ b/src/lib/presenze-mese.ts
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { useEventi } from "./eventi";
import { useRispostePresenze } from "./presenze";
-import { giocatori } from "./crapp-data";
+import { useRosaBase } from "./rosa-base";
/**
* Percentuale di presenze dell'ultimo mese (30 giorni), utile per le convocazioni.
@@ -46,6 +46,7 @@ export function usePresenzeUltimoMeseTutti(): Record<
> {
const { eventi } = useEventi();
const { presenze } = useRispostePresenze();
+ const giocatori = useRosaBase();
return useMemo(() => {
const oggi = new Date();
@@ -60,8 +61,7 @@ export function usePresenzeUltimoMeseTutti(): Record<
const out: Record = {};
for (const e of rilevanti) {
- const ids =
- e.convocati.length > 0 ? e.convocati : giocatori.map((g) => g.id);
+ const ids = e.convocati.length > 0 ? e.convocati : giocatori.map((g) => g.id);
for (const id of ids) {
const rec = (out[id] ??= { presenti: 0, totali: 0, percentuale: 0 });
rec.totali += 1;
@@ -73,5 +73,5 @@ export function usePresenzeUltimoMeseTutti(): Record<
rec.percentuale = rec.totali ? Math.round((rec.presenti / rec.totali) * 100) : 0;
}
return out;
- }, [eventi, presenze]);
-}
\ No newline at end of file
+ }, [eventi, presenze, giocatori]);
+}
diff --git a/src/lib/rosa-base.ts b/src/lib/rosa-base.ts
new file mode 100644
index 0000000..7366528
--- /dev/null
+++ b/src/lib/rosa-base.ts
@@ -0,0 +1,62 @@
+import { useQuery } from "@tanstack/react-query";
+import { strapiFetch } from "./strapi-client";
+import type { Giocatore } from "./crapp-data";
+
+function inizialiDa(nome: string) {
+ return nome
+ .split(" ")
+ .map((p) => p[0] ?? "")
+ .join("")
+ .slice(0, 2)
+ .toUpperCase();
+}
+
+type GiocatoreStrapi = {
+ codice: string;
+ nome: string;
+ numero: number;
+ ruolo: string;
+ nascita: string;
+};
+
+/** Anagrafica base (senza statistiche, calcolate a parte da useRosa/giocatoriConScout). */
+function daEntry(e: GiocatoreStrapi): Giocatore {
+ return {
+ id: e.codice,
+ nome: e.nome,
+ numero: e.numero,
+ ruolo: e.ruolo,
+ nascita: e.nascita,
+ iniziali: inizialiDa(e.nome),
+ totaliEventi: 0,
+ presenze: 0,
+ streak: 0,
+ serieAllenamenti: 0,
+ seriePartite: 0,
+ serieConferme: 0,
+ mvp: 0,
+ mediaVoto: 0,
+ palloni: 0,
+ cacche: 0,
+ cacchePartita: 0,
+ infortuni: 0,
+ ritardi: 0,
+ };
+}
+
+export const ROSA_BASE_KEY = ["rosa-base"] as const;
+
+/** Usabile anche fuori da React (route server, script): stessa fonte dati dell'hook. */
+export async function fetchRosaBase(): Promise {
+ const entries = await strapiFetch("giocatoris?sort=numero");
+ return entries.map((e) => daEntry(e));
+}
+
+/** Anagrafica rosa dal CMS: cambia a stagione, non a sessione — cache lunga, niente polling. */
+export function useRosaBaseQuery() {
+ return useQuery({ queryKey: ROSA_BASE_KEY, queryFn: fetchRosaBase, staleTime: 60 * 60_000 });
+}
+
+export function useRosaBase(): Giocatore[] {
+ return useRosaBaseQuery().data ?? [];
+}
diff --git a/src/lib/rosa.ts b/src/lib/rosa.ts
index d8b1ea4..d82f2a4 100644
--- a/src/lib/rosa.ts
+++ b/src/lib/rosa.ts
@@ -1,5 +1,6 @@
import { useMemo } from "react";
-import { giocatori, type Giocatore } from "./crapp-data";
+import type { Giocatore } from "./crapp-data";
+import { useRosaBase } from "./rosa-base";
import { giocatoriConScout, useScoutMatches } from "./scout-store";
import { useVotiMvp, vincitoriMvp } from "./mvp-voti";
import { mediePagelle, usePagelle } from "./pagelle";
@@ -11,6 +12,7 @@ import { useGiocatoreBase } from "./user-store";
import { useEventi } from "./eventi";
import { useRispostePresenze } from "./presenze";
import { obiettiviOrdinati } from "./obiettivi";
+import { useStoricoMatch } from "./storico-match";
/**
* Rosa completa con tutte le statistiche personali (presenze, MVP, media voto,
@@ -18,6 +20,7 @@ import { obiettiviOrdinati } from "./obiettivi";
* nessuna query aggiuntiva rispetto a quelle che l'app fa comunque.
*/
export function useRosa(): Giocatore[] {
+ const base = useRosaBase();
const scoutMatches = useScoutMatches();
const voti = useVotiMvp();
const { voti: pagelle } = usePagelle();
@@ -31,7 +34,7 @@ export function useRosa(): Giocatore[] {
const medie = mediePagelle(pagelle);
const statCacche = statisticheCacche(cacche);
const palloni = conteggioTurni(turni);
- return giocatoriConScout(scoutMatches, vincitoriMvp(votiMvp)).map((g) => ({
+ return giocatoriConScout(base, scoutMatches, vincitoriMvp(votiMvp)).map((g) => ({
...g,
mediaVoto: medie[g.id]?.media ?? g.mediaVoto,
palloni: palloni[g.id] ?? 0,
@@ -40,7 +43,7 @@ export function useRosa(): Giocatore[] {
infortuni: infortuni[g.id] ?? 0,
ritardi: ritardi[g.id] ?? 0,
}));
- }, [scoutMatches, votiMvp, pagelle, cacche, turni, infortuni, ritardi]);
+ }, [base, scoutMatches, votiMvp, pagelle, cacche, turni, infortuni, ritardi]);
}
/** Il giocatore selezionato sul dispositivo, con le statistiche complete. */
@@ -57,7 +60,6 @@ export function useObiettivi() {
const { eventi } = useEventi();
const { presenze } = useRispostePresenze();
const { voti: pagelle } = usePagelle();
- return obiettiviOrdinati(rosa, { eventi, presenze, pagelle });
+ const storicoMatch = useStoricoMatch();
+ return obiettiviOrdinati(rosa, { eventi, presenze, pagelle }, storicoMatch);
}
-
-export { giocatori };
diff --git a/src/lib/scout-export.ts b/src/lib/scout-export.ts
index 843d9ef..4123c07 100644
--- a/src/lib/scout-export.ts
+++ b/src/lib/scout-export.ts
@@ -1,4 +1,4 @@
-import { giocatori } from "./crapp-data";
+import type { Giocatore } from "./crapp-data";
import { azioniMeta, totaliPerGiocatore, type ScoutMatch } from "./scout-store";
function riga(campi: Array) {
@@ -11,9 +11,16 @@ function riga(campi: Array) {
}
/** Esporta la scoutizzazione di una partita in CSV (separatore ";" per Excel IT). */
-export function csvScoutMatch(match: ScoutMatch): string {
+export function csvScoutMatch(giocatori: Giocatore[], match: ScoutMatch): string {
const righe: string[] = [];
- righe.push(riga(["Partita", match.casa ? "CRAP Volley" : match.avversario, "vs", match.casa ? match.avversario : "CRAP Volley"]));
+ righe.push(
+ riga([
+ "Partita",
+ match.casa ? "CRAP Volley" : match.avversario,
+ "vs",
+ match.casa ? match.avversario : "CRAP Volley",
+ ]),
+ );
righe.push(riga(["Data", match.data, "Set", `${match.setNostri}-${match.setLoro}`]));
righe.push("");
righe.push(riga(["Set", "Parziale nostro", "Parziale loro"]));
diff --git a/src/lib/scout-store.ts b/src/lib/scout-store.ts
index 797198c..8816f2f 100644
--- a/src/lib/scout-store.ts
+++ b/src/lib/scout-store.ts
@@ -1,24 +1,54 @@
import { useSyncExternalStore } from "react";
-import { giocatori, classifica, type Giocatore, type RigaClassifica } from "./crapp-data";
+import type { Giocatore, RigaClassifica } from "./crapp-data";
-export type AzioneTipo =
- | "attacco"
- | "ace"
- | "muro"
- | "errore"
- | "punto_avv"
- | "errore_avv";
+export type AzioneTipo = "attacco" | "ace" | "muro" | "errore" | "punto_avv" | "errore_avv";
export const azioniMeta: Record<
AzioneTipo,
{ label: string; short: string; nostro: boolean; richiedeGiocatore: boolean; className: string }
> = {
- attacco: { label: "Punto attacco", short: "Punto", nostro: true, richiedeGiocatore: true, className: "bg-accent text-accent-foreground" },
- ace: { label: "Ace", short: "Ace", nostro: true, richiedeGiocatore: true, className: "bg-success text-success-foreground" },
- muro: { label: "Muro", short: "Muro", nostro: true, richiedeGiocatore: true, className: "bg-info text-info-foreground" },
- errore: { label: "Errore nostro", short: "Errore", nostro: false, richiedeGiocatore: true, className: "bg-destructive text-destructive-foreground" },
- punto_avv: { label: "Punto avversario", short: "Punto avv.", nostro: false, richiedeGiocatore: false, className: "bg-muted text-muted-foreground" },
- errore_avv: { label: "Errore avversario", short: "Err. avv.", nostro: true, richiedeGiocatore: false, className: "bg-secondary text-foreground" },
+ attacco: {
+ label: "Punto attacco",
+ short: "Punto",
+ nostro: true,
+ richiedeGiocatore: true,
+ className: "bg-accent text-accent-foreground",
+ },
+ ace: {
+ label: "Ace",
+ short: "Ace",
+ nostro: true,
+ richiedeGiocatore: true,
+ className: "bg-success text-success-foreground",
+ },
+ muro: {
+ label: "Muro",
+ short: "Muro",
+ nostro: true,
+ richiedeGiocatore: true,
+ className: "bg-info text-info-foreground",
+ },
+ errore: {
+ label: "Errore nostro",
+ short: "Errore",
+ nostro: false,
+ richiedeGiocatore: true,
+ className: "bg-destructive text-destructive-foreground",
+ },
+ punto_avv: {
+ label: "Punto avversario",
+ short: "Punto avv.",
+ nostro: false,
+ richiedeGiocatore: false,
+ className: "bg-muted text-muted-foreground",
+ },
+ errore_avv: {
+ label: "Errore avversario",
+ short: "Err. avv.",
+ nostro: true,
+ richiedeGiocatore: false,
+ className: "bg-secondary text-foreground",
+ },
};
export type Azione = {
@@ -125,10 +155,11 @@ export function totaliSquadra(matches: ScoutMatch[]) {
/** Presenze e MVP ricavati dalle partite scoutate: nessuna statistica individuale offensiva.
* `mvpPerMatch` mappa idPartita -> nome dell'MVP eletto dalla squadra. */
export function giocatoriConScout(
+ baseGiocatori: Giocatore[],
matches: ScoutMatch[],
mvpPerMatch: Record = {},
): Giocatore[] {
- return giocatori.map((g) => {
+ return baseGiocatori.map((g) => {
let presenze = 0;
let mvp = 0;
for (const m of matches) {
@@ -144,9 +175,12 @@ export function giocatoriConScout(
});
}
-/** Classifica demo aggiornata con i match scoutati (solo la nostra riga). */
-export function classificaConScout(matches: ScoutMatch[]): RigaClassifica[] {
- if (matches.length === 0) return classifica;
+/** Classifica CSI aggiornata con i match scoutati (solo la nostra riga). */
+export function classificaConScout(
+ classificaBase: RigaClassifica[],
+ matches: ScoutMatch[],
+): RigaClassifica[] {
+ if (matches.length === 0) return classificaBase;
const agg = matches.reduce(
(s, m) => {
const vinta = m.setNostri > m.setLoro;
@@ -160,7 +194,7 @@ export function classificaConScout(matches: ScoutMatch[]): RigaClassifica[] {
},
{ giocate: 0, vinte: 0, perse: 0, setFatti: 0, setSubiti: 0, punti: 0 },
);
- return classifica
+ return classificaBase
.map((r) =>
r.squadra === "CRAP Volley"
? {
@@ -176,4 +210,4 @@ export function classificaConScout(matches: ScoutMatch[]): RigaClassifica[] {
)
.sort((a, b) => b.punti - a.punti || b.setFatti - b.setSubiti - (a.setFatti - a.setSubiti))
.map((r, i) => ({ ...r, pos: i + 1 }));
-}
\ No newline at end of file
+}
diff --git a/src/lib/storico-match.ts b/src/lib/storico-match.ts
new file mode 100644
index 0000000..8661fd2
--- /dev/null
+++ b/src/lib/storico-match.ts
@@ -0,0 +1,43 @@
+import { useQuery } from "@tanstack/react-query";
+import { strapiFetch } from "./strapi-client";
+import type { Match } from "./crapp-data";
+
+type MatchStoricoStrapi = {
+ data: string;
+ avversario: string;
+ casa: boolean;
+ set_nostri: number;
+ set_loro: number;
+ parziali: Array<[number, number]> | null;
+ mvp: string | null;
+};
+
+function daEntry(e: MatchStoricoStrapi & { id: number }): Match {
+ return {
+ id: `m${e.id}`,
+ data: e.data,
+ avversario: e.avversario,
+ casa: e.casa,
+ setNostri: e.set_nostri,
+ setLoro: e.set_loro,
+ parziali: e.parziali ?? [],
+ mvp: e.mvp ?? "",
+ };
+}
+
+export const STORICO_MATCH_KEY = ["storico-match"] as const;
+
+async function fetchStoricoMatch(): Promise {
+ const entries = await strapiFetch("match-storicos?sort=data:desc");
+ return entries.map(daEntry);
+}
+
+/** Storico partite campionato CSI dal CMS: cambia a partita giocata, non a sessione. */
+export function useStoricoMatch(): Match[] {
+ const query = useQuery({
+ queryKey: STORICO_MATCH_KEY,
+ queryFn: fetchStoricoMatch,
+ staleTime: 60 * 60_000,
+ });
+ return query.data ?? [];
+}
diff --git a/src/lib/strapi-client.ts b/src/lib/strapi-client.ts
new file mode 100644
index 0000000..fff0919
--- /dev/null
+++ b/src/lib/strapi-client.ts
@@ -0,0 +1,22 @@
+// Client di sola lettura per il CMS Strapi (rosa, classifica, storico partite — vedi
+// infra/strapi/). Niente SDK: l'API REST di Strapi è semplice query/JSON, un wrapper fetch basta.
+// I content type letti qui hanno il ruolo "Public" abilitato in lettura (find/findOne), stesso
+// pattern delle tabelle Supabase "open RLS" già usate nel progetto — nessun token da gestire
+// lato client per queste letture.
+
+function strapiUrl(): string {
+ const url = import.meta.env["VITE_STRAPI_URL"] || process.env["VITE_STRAPI_URL"];
+ if (!url) throw new Error("Manca la variabile d'ambiente VITE_STRAPI_URL.");
+ return url;
+}
+
+type StrapiEntry = T & { id: number };
+type StrapiListResponse = { data: StrapiEntry[] };
+
+/** GET su un content type Strapi (path relativo, es. "giocatoris?sort=numero"). */
+export async function strapiFetch(path: string): Promise[]> {
+ const res = await fetch(`${strapiUrl()}/api/${path}`);
+ if (!res.ok) throw new Error(`Strapi ${path}: ${res.status}`);
+ const json = (await res.json()) as StrapiListResponse;
+ return json.data;
+}
diff --git a/src/lib/user-store.ts b/src/lib/user-store.ts
index 579afca..8b46d94 100644
--- a/src/lib/user-store.ts
+++ b/src/lib/user-store.ts
@@ -1,5 +1,6 @@
import { useSyncExternalStore } from "react";
-import { giocatori, type Giocatore } from "./crapp-data";
+import type { Giocatore } from "./crapp-data";
+import { useRosaBase } from "./rosa-base";
import { conInfortuni, useInfortuniERitardi } from "./infortuni";
const KEY = "crapp-user-v1";
@@ -36,7 +37,7 @@ export function resetGiocatore() {
write(null);
}
-/** Solo lettura locale: nessuna dipendenza da React Query (usabile fuori dal provider). */
+/** Richiede un ancestor QueryClientProvider (la rosa base arriva dal CMS Strapi). */
export function useGiocatoreBase(): Giocatore | null {
const id = useSyncExternalStore(
(cb) => {
@@ -46,7 +47,8 @@ export function useGiocatoreBase(): Giocatore | null {
() => read(),
() => null,
);
- return id ? (giocatori.find((x) => x.id === id) ?? null) : null;
+ const rosa = useRosaBase();
+ return id ? (rosa.find((x) => x.id === id) ?? null) : null;
}
export function useGiocatoreCorrente(): Giocatore | null {
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 85d0be3..289a1db 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -23,6 +23,7 @@ import { Route as ApiPublicPromemoriaPalloniRouteImport } from './routes/api/pub
import { Route as ApiPublicPushConfigRouteImport } from './routes/api/public/push-config'
import { Route as ApiPublicPushMessaggioRouteImport } from './routes/api/public/push-messaggio'
import { Route as ApiPublicPushSubscribeRouteImport } from './routes/api/public/push-subscribe'
+import { Route as ApiPublicScoutFinaleRouteImport } from './routes/api/public/scout-finale'
import { Route as ApiPublicSollecitaPresenzeRouteImport } from './routes/api/public/sollecita-presenze'
const IndexRoute = IndexRouteImport.update({
@@ -96,6 +97,11 @@ const ApiPublicPushSubscribeRoute = ApiPublicPushSubscribeRouteImport.update({
path: '/api/public/push-subscribe',
getParentRoute: () => rootRouteImport,
} as any)
+const ApiPublicScoutFinaleRoute = ApiPublicScoutFinaleRouteImport.update({
+ id: '/api/public/scout-finale',
+ path: '/api/public/scout-finale',
+ getParentRoute: () => rootRouteImport,
+} as any)
const ApiPublicSollecitaPresenzeRoute =
ApiPublicSollecitaPresenzeRouteImport.update({
id: '/api/public/sollecita-presenze',
@@ -118,6 +124,7 @@ export interface FileRoutesByFullPath {
'/api/public/push-config': typeof ApiPublicPushConfigRoute
'/api/public/push-messaggio': typeof ApiPublicPushMessaggioRoute
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
+ '/api/public/scout-finale': typeof ApiPublicScoutFinaleRoute
'/api/public/sollecita-presenze': typeof ApiPublicSollecitaPresenzeRoute
}
export interface FileRoutesByTo {
@@ -135,6 +142,7 @@ export interface FileRoutesByTo {
'/api/public/push-config': typeof ApiPublicPushConfigRoute
'/api/public/push-messaggio': typeof ApiPublicPushMessaggioRoute
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
+ '/api/public/scout-finale': typeof ApiPublicScoutFinaleRoute
'/api/public/sollecita-presenze': typeof ApiPublicSollecitaPresenzeRoute
}
export interface FileRoutesById {
@@ -153,6 +161,7 @@ export interface FileRoutesById {
'/api/public/push-config': typeof ApiPublicPushConfigRoute
'/api/public/push-messaggio': typeof ApiPublicPushMessaggioRoute
'/api/public/push-subscribe': typeof ApiPublicPushSubscribeRoute
+ '/api/public/scout-finale': typeof ApiPublicScoutFinaleRoute
'/api/public/sollecita-presenze': typeof ApiPublicSollecitaPresenzeRoute
}
export interface FileRouteTypes {
@@ -172,6 +181,7 @@ export interface FileRouteTypes {
| '/api/public/push-config'
| '/api/public/push-messaggio'
| '/api/public/push-subscribe'
+ | '/api/public/scout-finale'
| '/api/public/sollecita-presenze'
fileRoutesByTo: FileRoutesByTo
to:
@@ -189,6 +199,7 @@ export interface FileRouteTypes {
| '/api/public/push-config'
| '/api/public/push-messaggio'
| '/api/public/push-subscribe'
+ | '/api/public/scout-finale'
| '/api/public/sollecita-presenze'
id:
| '__root__'
@@ -206,6 +217,7 @@ export interface FileRouteTypes {
| '/api/public/push-config'
| '/api/public/push-messaggio'
| '/api/public/push-subscribe'
+ | '/api/public/scout-finale'
| '/api/public/sollecita-presenze'
fileRoutesById: FileRoutesById
}
@@ -224,6 +236,7 @@ export interface RootRouteChildren {
ApiPublicPushConfigRoute: typeof ApiPublicPushConfigRoute
ApiPublicPushMessaggioRoute: typeof ApiPublicPushMessaggioRoute
ApiPublicPushSubscribeRoute: typeof ApiPublicPushSubscribeRoute
+ ApiPublicScoutFinaleRoute: typeof ApiPublicScoutFinaleRoute
ApiPublicSollecitaPresenzeRoute: typeof ApiPublicSollecitaPresenzeRoute
}
@@ -327,6 +340,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiPublicPushSubscribeRouteImport
parentRoute: typeof rootRouteImport
}
+ '/api/public/scout-finale': {
+ id: '/api/public/scout-finale'
+ path: '/api/public/scout-finale'
+ fullPath: '/api/public/scout-finale'
+ preLoaderRoute: typeof ApiPublicScoutFinaleRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/api/public/sollecita-presenze': {
id: '/api/public/sollecita-presenze'
path: '/api/public/sollecita-presenze'
@@ -352,6 +372,7 @@ const rootRouteChildren: RootRouteChildren = {
ApiPublicPushConfigRoute: ApiPublicPushConfigRoute,
ApiPublicPushMessaggioRoute: ApiPublicPushMessaggioRoute,
ApiPublicPushSubscribeRoute: ApiPublicPushSubscribeRoute,
+ ApiPublicScoutFinaleRoute: ApiPublicScoutFinaleRoute,
ApiPublicSollecitaPresenzeRoute: ApiPublicSollecitaPresenzeRoute,
}
export const routeTree = rootRouteImport
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index ea6c32d..e1a645d 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -18,6 +18,7 @@ import { CelebrazioneBadge } from "../components/crapp/CelebrazioneBadge";
import { Toaster } from "../components/ui/sonner";
import { TeamLogo } from "../components/crapp/ui-bits";
import { useGiocatoreBase } from "../lib/user-store";
+import { useRosaBaseQuery } from "../lib/rosa-base";
function NotFoundComponent() {
return (
@@ -87,22 +88,36 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()(
{ title: "CrAPP — L'app del CRAP Volley" },
{
name: "description",
- content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
+ content:
+ "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
},
{ name: "author", content: "CRAP Volley" },
{ name: "theme-color", content: "#111111" },
{ property: "og:title", content: "CrAPP — L'app del CRAP Volley" },
{
property: "og:description",
- content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
+ content:
+ "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
},
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:site", content: "@Lovable" },
{ name: "twitter:title", content: "CrAPP — L'app del CRAP Volley" },
- { name: "twitter:description", content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile." },
- { property: "og:image", content: "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png" },
- { name: "twitter:image", content: "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png" },
+ {
+ name: "twitter:description",
+ content:
+ "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
+ },
+ {
+ property: "og:image",
+ content:
+ "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png",
+ },
+ {
+ name: "twitter:image",
+ content:
+ "https://pub-bb2e103a32db4e198524a2e9ed8f35b4.r2.dev/fd5ebd0b-6661-43fa-936c-35856d1068c4/id-preview-2942946e--8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c.lovable.app-1785491191791.png",
+ },
],
links: [
{
@@ -142,18 +157,32 @@ function RootShell({ children }: { children: ReactNode }) {
function RootComponent() {
const { queryClient } = Route.useRouteContext();
+ // La rosa base (nome/id giocatore) arriva dal CMS Strapi via TanStack Query: il provider deve
+ // avvolgere anche RootBody, che la legge tramite useGiocatoreBase.
+ return (
+
+
+
+ );
+}
+
+function RootBody() {
const navigate = useNavigate();
const location = useLocation();
const giocatore = useGiocatoreBase();
+ const rosaQuery = useRosaBaseQuery();
const [mounted, setMounted] = useState(false);
const isBenvenuto = location.pathname === "/benvenuto";
useEffect(() => {
setMounted(true);
+ // Aspetta il primo caricamento della rosa base: prima che arrivi, "nessun giocatore
+ // risolto" non significa "nessuna selezione salvata".
+ if (rosaQuery.isPending) return;
if (!giocatore && !isBenvenuto) {
navigate({ to: "/benvenuto" });
}
- }, [giocatore, isBenvenuto, navigate]);
+ }, [giocatore, isBenvenuto, navigate, rosaQuery.isPending]);
if (!mounted) {
return (
@@ -164,7 +193,7 @@ function RootComponent() {
}
return (
-
+ <>
{/* Required: nested routes render here. Removing
breaks all child routes. */}
@@ -172,6 +201,6 @@ function RootComponent() {
{!isBenvenuto &&
}
{!isBenvenuto &&
}
-
+ >
);
}
diff --git a/src/routes/allenamento.$id.tsx b/src/routes/allenamento.$id.tsx
index d645847..205bb29 100644
--- a/src/routes/allenamento.$id.tsx
+++ b/src/routes/allenamento.$id.tsx
@@ -1,11 +1,12 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowLeft, MapPin, Clock, Users, Dumbbell, CalendarDays } from "lucide-react";
import { PageHeader, Section } from "@/components/crapp/ui-bits";
-import { formatData, giocatori } from "@/lib/crapp-data";
+import { formatData } from "@/lib/crapp-data";
import { convocatiEvento, useEvento } from "@/lib/eventi";
import { TurnoPalloni } from "@/components/crapp/TurnoPalloni";
import { RosaPresenze } from "@/components/crapp/RosaPresenze";
import { usePresenzeEvento } from "@/lib/presenze";
+import { useRosaBase } from "@/lib/rosa-base";
export const Route = createFileRoute("/allenamento/$id")({
head: () => {
@@ -13,9 +14,15 @@ export const Route = createFileRoute("/allenamento/$id")({
return {
meta: [
{ title: `${titolo} — CrAPP` },
- { name: "description", content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley." },
+ {
+ name: "description",
+ content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley.",
+ },
{ property: "og:title", content: `${titolo} — CrAPP` },
- { property: "og:description", content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley." },
+ {
+ property: "og:description",
+ content: "Dettaglio allenamento, orario, luogo e presenze del CRAP Volley.",
+ },
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" },
],
@@ -28,6 +35,7 @@ function AllenamentoDetail() {
const { id } = Route.useParams();
const { evento } = useEvento(id);
const { risposte } = usePresenzeEvento(id);
+ const giocatori = useRosaBase();
const presentiVeri = giocatori.filter(
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
).length;
@@ -84,7 +92,7 @@ function AllenamentoDetail() {
- Conferme: {presentiVeri}/{convocatiEvento(evento).length}
+ Conferme: {presentiVeri}/{convocatiEvento(giocatori, evento).length}
diff --git a/src/routes/api/public/promemoria-palloni.ts b/src/routes/api/public/promemoria-palloni.ts
index 187ef77..450517d 100644
--- a/src/routes/api/public/promemoria-palloni.ts
+++ b/src/routes/api/public/promemoria-palloni.ts
@@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { completaTurni, eventiPalloni, eventoPrecedente, oggiISO } from "@/lib/palloni-core";
import { inviaPush } from "@/lib/webpush.server";
import { leggiEventi } from "@/lib/eventi.server";
+import { fetchRosaBase } from "@/lib/rosa-base";
export const Route = createFileRoute("/api/public/promemoria-palloni")({
server: {
@@ -15,7 +16,8 @@ export const Route = createFileRoute("/api/public/promemoria-palloni")({
const salvati: Record
= {};
for (const riga of righe ?? []) salvati[riga.evento_id] = riga.giocatore_id;
const eventi = await leggiEventi();
- const turni = completaTurni(salvati, eventi);
+ const giocatori = await fetchRosaBase();
+ const turni = completaTurni(salvati, eventi, giocatori);
const oggi = oggiISO();
const destinatari = new Set();
@@ -56,4 +58,4 @@ export const Route = createFileRoute("/api/public/promemoria-palloni")({
},
},
},
-});
\ No newline at end of file
+});
diff --git a/src/routes/api/public/push-messaggio.ts b/src/routes/api/public/push-messaggio.ts
index 2d66658..b275c8c 100644
--- a/src/routes/api/public/push-messaggio.ts
+++ b/src/routes/api/public/push-messaggio.ts
@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
-import { formatData, giocatori } from "@/lib/crapp-data";
+import { formatData } from "@/lib/crapp-data";
import {
completaTurni,
eventiPalloni,
@@ -9,6 +9,7 @@ import {
oggiISO,
} from "@/lib/palloni-core";
import { leggiEventi } from "@/lib/eventi.server";
+import { fetchRosaBase } from "@/lib/rosa-base";
const schema = z.object({ endpoint: z.string().url().max(1000) });
@@ -40,7 +41,8 @@ export const Route = createFileRoute("/api/public/push-messaggio")({
.eq("endpoint", parsed.data.endpoint)
.maybeSingle();
- if (!iscrizione) return Response.json({ title: "CrAPP", body: "Controlla il turno palloni." });
+ if (!iscrizione)
+ return Response.json({ title: "CrAPP", body: "Controlla il turno palloni." });
const { data: righe } = await supabaseAdmin
.from("turni_palloni")
@@ -48,7 +50,8 @@ export const Route = createFileRoute("/api/public/push-messaggio")({
const salvati: Record = {};
for (const riga of righe ?? []) salvati[riga.evento_id] = riga.giocatore_id;
const eventi = await leggiEventi();
- const turni = completaTurni(salvati, eventi);
+ const giocatori = await fetchRosaBase();
+ const turni = completaTurni(salvati, eventi, giocatori);
const oggi = oggiISO();
const mioId = iscrizione.giocatore_id;
@@ -76,9 +79,11 @@ export const Route = createFileRoute("/api/public/push-messaggio")({
return Response.json({
title: "CrAPP · Turno palloni",
- body: nome ? `${nome}, controlla il turno palloni nel calendario.` : "Controlla il calendario.",
+ body: nome
+ ? `${nome}, controlla il turno palloni nel calendario.`
+ : "Controlla il calendario.",
});
},
},
},
-});
\ No newline at end of file
+});
diff --git a/src/routes/api/public/sollecita-presenze.ts b/src/routes/api/public/sollecita-presenze.ts
index 57d96ce..bd4c9db 100644
--- a/src/routes/api/public/sollecita-presenze.ts
+++ b/src/routes/api/public/sollecita-presenze.ts
@@ -1,8 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
-import { formatData, giocatori } from "@/lib/crapp-data";
+import { formatData } from "@/lib/crapp-data";
import { leggiEventi } from "@/lib/eventi.server";
import { inviaPush } from "@/lib/webpush.server";
+import { fetchRosaBase } from "@/lib/rosa-base";
const schema = z.object({
eventoId: z.string().min(1).max(50),
@@ -28,6 +29,7 @@ export const Route = createFileRoute("/api/public/sollecita-presenze")({
.eq("evento_id", evento.id);
const stati = new Map((righe ?? []).map((r) => [r.giocatore_id, r.stato]));
+ const giocatori = await fetchRosaBase();
const destinatari = giocatori
.filter((g) => {
const stato = stati.get(g.id);
diff --git a/src/routes/benvenuto.tsx b/src/routes/benvenuto.tsx
index 1140b9b..8ea34aa 100644
--- a/src/routes/benvenuto.tsx
+++ b/src/routes/benvenuto.tsx
@@ -1,7 +1,7 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { TeamLogo } from "@/components/crapp/ui-bits";
-import { giocatori } from "@/lib/crapp-data";
+import { useRosaBase } from "@/lib/rosa-base";
import { impostaGiocatore, useGiocatoreCorrente } from "@/lib/user-store";
export const Route = createFileRoute("/benvenuto")({
@@ -25,6 +25,7 @@ export const Route = createFileRoute("/benvenuto")({
function Benvenuto() {
const navigate = useNavigate();
const giocatore = useGiocatoreCorrente();
+ const giocatori = useRosaBase();
useEffect(() => {
if (giocatore) {
diff --git a/src/routes/calendario.tsx b/src/routes/calendario.tsx
index e25c52f..791e1bf 100644
--- a/src/routes/calendario.tsx
+++ b/src/routes/calendario.tsx
@@ -8,6 +8,7 @@ import { PageHeader, Section } from "@/components/crapp/ui-bits";
import { isAdmin } from "@/lib/crapp-data";
import { compleanniEventi, useEventi, type Evento } from "@/lib/eventi";
import { useGiocatoreCorrente } from "@/lib/user-store";
+import { useRosaBase } from "@/lib/rosa-base";
import {
Drawer,
DrawerClose,
@@ -97,12 +98,13 @@ function Calendario() {
const [giornoSelezionato, setGiornoSelezionato] = useState(null);
const [drawerAperto, setDrawerAperto] = useState(false);
const io = useGiocatoreCorrente();
+ const giocatori = useRosaBase();
const { eventi } = useEventi();
const { anno, mese, precedente, successivo } = useMeseNav();
const { giorni, offsetLunedi } = giorniDelMese(anno, mese);
const mesePrefix = `${anno}-${pad2(mese + 1)}`;
- const compleanni = compleanniEventi(anno);
+ const compleanni = compleanniEventi(giocatori, anno);
const compleanniMese = compleanni.filter((c) => c.data.startsWith(mesePrefix));
const eventiMese = eventi.filter((e) => e.data.startsWith(mesePrefix));
@@ -120,7 +122,9 @@ function Calendario() {
setDrawerAperto(true);
}
- const eventiGiornoSelezionato = giornoSelezionato ? eventiPerGiorno.get(giornoSelezionato) ?? [] : [];
+ const eventiGiornoSelezionato = giornoSelezionato
+ ? (eventiPerGiorno.get(giornoSelezionato) ?? [])
+ : [];
return (
<>
@@ -200,7 +204,8 @@ function Calendario() {
.join(", ")})`
: undefined;
const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
- const isOggi = !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
+ const isOggi =
+ !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
const Cella = haEventi ? "button" : "div";
return (
- {giorno}
+
+ {giorno}
+
);
})}
@@ -245,7 +252,7 @@ function Calendario() {
) : null}
- {io && isAdmin(io.id) ? (
+ {io && isAdmin(io) ? (
({
id: m.id,
@@ -41,7 +44,7 @@ function Classifica() {
- Dati CSI aggiornati oggi alle 08:40 (demo)
+ Dati CSI gestiti da chi cura la classifica
@@ -101,4 +104,4 @@ function Classifica() {
>
);
-}
\ No newline at end of file
+}
diff --git a/src/routes/eventi.tsx b/src/routes/eventi.tsx
index d85e406..9a12827 100644
--- a/src/routes/eventi.tsx
+++ b/src/routes/eventi.tsx
@@ -4,7 +4,8 @@ import { ArrowLeft, CalendarPlus, Loader2, Pencil, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { PageHeader, Section } from "@/components/crapp/ui-bits";
-import { formatData, giocatori, isAdmin } from "@/lib/crapp-data";
+import { formatData, isAdmin } from "@/lib/crapp-data";
+import { useRosaBase } from "@/lib/rosa-base";
import {
categoriaEvento,
daCategoria,
@@ -47,12 +48,13 @@ const tipi: Array<{ id: CategoriaEvento; label: string }> = [
function GestioneEventi() {
const io = useGiocatoreCorrente();
+ const giocatori = useRosaBase();
const { eventi, isPending } = useEventi();
const salva = useSalvaEvento();
const elimina = useEliminaEvento();
const [bozza, setBozza] = useState
(null);
- if (!io || !isAdmin(io.id)) {
+ if (!io || !isAdmin(io)) {
return (
<>
@@ -118,7 +120,9 @@ function GestioneEventi() {
{bozza ? (
-
e.id === bozza.id) ? "Modifica evento" : "Nuovo evento"}>
+ e.id === bozza.id) ? "Modifica evento" : "Nuovo evento"}
+ >
{tipi.map((t) => (
@@ -239,7 +243,9 @@ function GestioneEventi() {
}
className={cn(
"truncate rounded-xl px-2.5 py-2 text-left text-xs font-semibold transition-colors",
- scelto ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground",
+ scelto
+ ? "bg-accent text-accent-foreground"
+ : "bg-secondary text-muted-foreground",
)}
>
{g.nome}
@@ -276,7 +282,10 @@ function GestioneEventi() {
) : (
{eventi.map((e) => (
-
+
{e.titolo}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index ec8a27c..08b2d2d 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -7,7 +7,8 @@ import { Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
import { Reveal } from "@/components/motion/Reveal";
import { Barra } from "@/components/motion/Barra";
import { Numero } from "@/components/motion/Numero";
-import { classifica, storicoMatch } from "@/lib/crapp-data";
+import { useClassificaCsi } from "@/lib/classifica-csi";
+import { useStoricoMatch } from "@/lib/storico-match";
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
import { useEventi, type Evento } from "@/lib/eventi";
import { useIo, useObiettivi } from "@/lib/rosa";
@@ -24,7 +25,8 @@ export const Route = createFileRoute("/")({
{ property: "og:title", content: "CrAPP — L'app del CRAP Volley" },
{
property: "og:description",
- content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
+ content:
+ "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
},
],
}),
@@ -33,13 +35,15 @@ export const Route = createFileRoute("/")({
function Index() {
const giocatore = useIo();
+ const classifica = useClassificaCsi();
+ const storicoMatch = useStoricoMatch();
const { eventi } = useEventi();
const oggi = new Date().toISOString().slice(0, 10);
const prossimi: Evento[] = eventi.filter((e) => e.data >= oggi).slice(0, 3);
const prossimo = prossimi[0] ?? null;
const linkProssimo = prossimo ? linkPerEvento(prossimo) : null;
- const noi = classifica.find((r) => r.squadra === "CRAP Volley")!;
- const ultima = storicoMatch[0]!;
+ const noi = classifica.find((r) => r.squadra === "CRAP Volley") ?? null;
+ const ultima = storicoMatch[0] ?? null;
const obiettivi = useObiettivi();
const obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null;
@@ -58,25 +62,27 @@ function Index() {
-
-
-
{noi.pos}º
-
In classifica
+ {noi ? (
+
+
+
{noi.pos}º
+
In classifica
+
+
+
+ {noi.vinte}-{noi.perse}
+
+
Bilancio
+
+
+
+
+ {giocatore.streak}
+
+
Streak
+
-
-
- {noi.vinte}-{noi.perse}
-
-
Bilancio
-
-
-
-
- {giocatore.streak}
-
-
Streak
-
-
+ ) : null}
@@ -114,66 +120,72 @@ function Index() {
-
- Storico
-
- }
- >
-
-
-
-
CRAP Volley vs {ultima.avversario}
-
- {ultima.casa ? "In casa" : "Trasferta"} · MVP {ultima.mvp}
+ {ultima ? (
+
+ Storico
+
+ }
+ >
+
+
+
+
CRAP Volley vs {ultima.avversario}
+
+ {ultima.casa ? "In casa" : "Trasferta"} · MVP {ultima.mvp}
+
+
+
+ {ultima.setNostri}-{ultima.setLoro}
-
- {ultima.setNostri}-{ultima.setLoro}
-
+
+ {ultima.parziali.map((p, i) => (
+
+ {p[0]}-{p[1]}
+
+ ))}
+
-
- {ultima.parziali.map((p, i) => (
-
- {p[0]}-{p[1]}
-
- ))}
-
-
-
+
+ ) : null}
+
Tutti
}
>
{obiettivo ? (
-
-
-
{obiettivo.emoji} {obiettivo.titolo}
+
+
+ {obiettivo.emoji} {obiettivo.titolo}
+
+
+
+ Siamo al —{" "}
+ {obiettivo.valore}/{obiettivo.target} {obiettivo.unita}.
+
+
+ {microcopyObiettivo(obiettivo)}
+
+
{obiettivo.impatto}
-
-
- Siamo al —{" "}
- {obiettivo.valore}/{obiettivo.target}{" "}
- {obiettivo.unita}.
-
-
{microcopyObiettivo(obiettivo)}
-
{obiettivo.impatto}
-
) : null}
diff --git a/src/routes/partita.$id.tsx b/src/routes/partita.$id.tsx
index 57b258d..c8694f6 100644
--- a/src/routes/partita.$id.tsx
+++ b/src/routes/partita.$id.tsx
@@ -2,13 +2,15 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowLeft, MapPin, Clock, Users, Trophy, Swords, Download } from "lucide-react";
import { cn } from "@/lib/utils";
import { PageHeader, Section } from "@/components/crapp/ui-bits";
-import { storicoMatch, formatData, giocatori, isAdmin } from "@/lib/crapp-data";
+import { formatData, isAdmin } from "@/lib/crapp-data";
import { convocatiEvento, useEvento } from "@/lib/eventi";
import { Pagelle } from "@/components/crapp/Pagelle";
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
import { useGiocatoreCorrente } from "@/lib/user-store";
+import { useRosaBase } from "@/lib/rosa-base";
+import { useStoricoMatch } from "@/lib/storico-match";
import { VotazioneMvp } from "@/components/crapp/VotazioneMvp";
import { VotoSocial } from "@/components/crapp/VotoSocial";
import { TurnoPalloni } from "@/components/crapp/TurnoPalloni";
@@ -21,9 +23,15 @@ export const Route = createFileRoute("/partita/$id")({
return {
meta: [
{ title: `${titolo} — CrAPP` },
- { name: "description", content: "Dettaglio partita, formazione e risultati del CRAP Volley." },
+ {
+ name: "description",
+ content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
+ },
{ property: "og:title", content: `${titolo} — CrAPP` },
- { property: "og:description", content: "Dettaglio partita, formazione e risultati del CRAP Volley." },
+ {
+ property: "og:description",
+ content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
+ },
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" },
],
@@ -36,6 +44,8 @@ function PartitaDetail() {
const { id } = Route.useParams();
const { evento } = useEvento(id);
const io = useGiocatoreCorrente();
+ const giocatori = useRosaBase();
+ const storicoMatch = useStoricoMatch();
const scoutMatches = useScoutMatches();
const { risposte } = usePresenzeEvento(id);
const presentiVeri = giocatori.filter(
@@ -56,7 +66,7 @@ function PartitaDetail() {
);
}
- const convocati = convocatiEvento(evento);
+ const convocati = convocatiEvento(giocatori, evento);
const scout = scoutMatches.find((m) => m.id === evento.id || m.data === evento.data) ?? null;
const match = scout
? {
@@ -71,7 +81,7 @@ function PartitaDetail() {
: storicoMatch.find((m) => m.data === evento.data);
const totaliTeam = scout ? totaliSquadra([scout]) : null;
const avversario = evento.titolo.includes(" vs ")
- ? evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo
+ ? (evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo)
: evento.titolo;
const casa = evento.casa;
const vinta = match && match.setNostri > match.setLoro;
@@ -138,7 +148,9 @@ function PartitaDetail() {
{match.setNostri} - {match.setLoro}
@@ -147,7 +159,10 @@ function PartitaDetail() {
{match.parziali.map(([noi, loro], i) => (
-
+
{noi}
Set {i + 1}
{loro}
@@ -169,7 +184,8 @@ function PartitaDetail() {
) : (
- La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il risultato.
+ La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il
+ risultato.
)}
@@ -224,10 +240,15 @@ function PartitaDetail() {
);
})}
- {io && isAdmin(io.id) ? (
+ {io && isAdmin(io) ? (