Rimuove i dati dummy della classifica e usa i risultati CSI reali
La classifica e lo storico partite mostravano dati inventati (squadre e risultati finti) come base, poi sovrascritti dai dati CSI quando disponibili. Ora classifica, ultimi risultati, storico match e obiettivi di squadra legati alle vittorie usano solo dati reali (CSI o scout live), con stati vuoti quando i dati CSI non sono ancora disponibili. Include anche la correzione di tutti gli errori di formattazione prettier segnalati da `npm run lint` sul resto del codice sorgente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+19
-5
@@ -88,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: [
|
||||
{
|
||||
|
||||
@@ -13,9 +13,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" },
|
||||
],
|
||||
|
||||
@@ -9,4 +9,4 @@ export const Route = createFileRoute("/api/public/push-config")({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,9 +38,12 @@ export const Route = createFileRoute("/api/public/push-subscribe")({
|
||||
if (!parsed.success) return new Response("Dati non validi", { status: 400 });
|
||||
|
||||
const { supabaseAdmin } = await import("@/integrations/supabase/client.server");
|
||||
await supabaseAdmin.from("push_subscriptions").delete().eq("endpoint", parsed.data.endpoint);
|
||||
await supabaseAdmin
|
||||
.from("push_subscriptions")
|
||||
.delete()
|
||||
.eq("endpoint", parsed.data.endpoint);
|
||||
return Response.json({ ok: true });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,7 +122,9 @@ function Calendario() {
|
||||
setDrawerAperto(true);
|
||||
}
|
||||
|
||||
const eventiGiornoSelezionato = giornoSelezionato ? eventiPerGiorno.get(giornoSelezionato) ?? [] : [];
|
||||
const eventiGiornoSelezionato = giornoSelezionato
|
||||
? (eventiPerGiorno.get(giornoSelezionato) ?? [])
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -202,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 (
|
||||
<Cella
|
||||
@@ -224,7 +227,9 @@ function Calendario() {
|
||||
aria-label={haEventi ? `Eventi del ${giorno}` : undefined}
|
||||
aria-current={isOggi ? "date" : undefined}
|
||||
>
|
||||
<span className="relative drop-shadow-[0_1px_1px_rgba(255,255,255,0.5)]">{giorno}</span>
|
||||
<span className="relative drop-shadow-[0_1px_1px_rgba(255,255,255,0.5)]">
|
||||
{giorno}
|
||||
</span>
|
||||
</Cella>
|
||||
);
|
||||
})}
|
||||
|
||||
+64
-58
@@ -2,10 +2,9 @@ import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
||||
import { storicoMatch } from "@/lib/crapp-data";
|
||||
import { classificaConScout, useScoutMatches } from "@/lib/scout-store";
|
||||
import { useScoutMatches } from "@/lib/scout-store";
|
||||
import { useCsi } from "@/lib/csi";
|
||||
import { isNostraSquadra, partiteGiocate } from "@/lib/csi-core";
|
||||
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
||||
|
||||
export const Route = createFileRoute("/classifica")({
|
||||
@@ -34,25 +33,18 @@ function Classifica() {
|
||||
const scoutMatches = useScoutMatches();
|
||||
const { data: csi } = useCsi();
|
||||
|
||||
const classifica = csi?.classifica.length ? csi.classifica : classificaConScout(scoutMatches);
|
||||
const classifica = csi?.classifica ?? [];
|
||||
const risultati = csi?.partite.length
|
||||
? partiteGiocate(csi.partite).map((p) => ({
|
||||
id: p.id,
|
||||
avversario: p.avversario,
|
||||
casa: p.casa,
|
||||
setNostri: p.setNostri ?? 0,
|
||||
setLoro: p.setLoro ?? 0,
|
||||
}))
|
||||
: [
|
||||
...scoutMatches.map((m) => ({
|
||||
id: m.id,
|
||||
avversario: m.avversario,
|
||||
casa: m.casa,
|
||||
setNostri: m.setNostri,
|
||||
setLoro: m.setLoro,
|
||||
})),
|
||||
...storicoMatch,
|
||||
];
|
||||
? partiteGiocate(csi.partite).map(matchDaPartitaCsi)
|
||||
: scoutMatches.map((m) => ({
|
||||
id: m.id,
|
||||
data: m.data,
|
||||
avversario: m.avversario,
|
||||
casa: m.casa,
|
||||
setNostri: m.setNostri,
|
||||
setLoro: m.setLoro,
|
||||
parziali: m.parziali,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,47 +74,61 @@ function Classifica() {
|
||||
<span className="text-center">Set</span>
|
||||
<span className="text-center">Pt</span>
|
||||
</div>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
</Section>
|
||||
|
||||
<Section titolo="Ultimi risultati ufficiali">
|
||||
<div className="space-y-2">
|
||||
{risultati.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-2xl bg-card p-3 shadow-card"
|
||||
>
|
||||
<p className="truncate text-sm">
|
||||
{m.casa ? "CRAP Volley" : m.avversario} — {m.casa ? m.avversario : "CRAP Volley"}
|
||||
</p>
|
||||
<span className="font-display text-lg tabular-nums">
|
||||
{m.casa ? `${m.setNostri}-${m.setLoro}` : `${m.setLoro}-${m.setNostri}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{risultati.length === 0 ? (
|
||||
<p className="rounded-3xl bg-card p-4 text-center text-xs text-muted-foreground shadow-card">
|
||||
Nessun risultato disponibile.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{risultati.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-2xl bg-card p-3 shadow-card"
|
||||
>
|
||||
<p className="truncate text-sm">
|
||||
{m.casa ? "CRAP Volley" : m.avversario} — {m.casa ? m.avversario : "CRAP Volley"}
|
||||
</p>
|
||||
<span className="font-display text-lg tabular-nums">
|
||||
{m.casa ? `${m.setNostri}-${m.setLoro}` : `${m.setLoro}-${m.setNostri}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
|
||||
+10
-3
@@ -120,7 +120,9 @@ function GestioneEventi() {
|
||||
</div>
|
||||
|
||||
{bozza ? (
|
||||
<Section titolo={eventi.some((e) => e.id === bozza.id) ? "Modifica evento" : "Nuovo evento"}>
|
||||
<Section
|
||||
titolo={eventi.some((e) => e.id === bozza.id) ? "Modifica evento" : "Nuovo evento"}
|
||||
>
|
||||
<div className="space-y-3 rounded-3xl bg-card p-4 shadow-card">
|
||||
<div className="grid grid-cols-4 gap-1 rounded-full bg-secondary p-1">
|
||||
{tipi.map((t) => (
|
||||
@@ -241,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}
|
||||
@@ -278,7 +282,10 @@ function GestioneEventi() {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{eventi.map((e) => (
|
||||
<div key={e.id} className="flex items-center gap-2 rounded-3xl bg-card p-3 shadow-card">
|
||||
<div
|
||||
key={e.id}
|
||||
className="flex items-center gap-2 rounded-3xl bg-card p-3 shadow-card"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-bold leading-tight">{e.titolo}</p>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
|
||||
+44
-25
@@ -8,10 +8,13 @@ import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
||||
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 { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
||||
import { useEventi, type Evento } from "@/lib/eventi";
|
||||
import { useIo, useObiettivi } from "@/lib/rosa";
|
||||
import { useCsi } from "@/lib/csi";
|
||||
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||
import { useScoutMatches } from "@/lib/scout-store";
|
||||
import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
head: () => ({
|
||||
@@ -40,8 +43,17 @@ function Index() {
|
||||
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 { data: csi } = useCsi();
|
||||
const noi = csi?.classifica.find((r) => isNostraSquadra(r.squadra));
|
||||
const scoutMatches = useScoutMatches();
|
||||
const votiMvp = useVotiMvp();
|
||||
const mvpPerMatch = vincitoriMvp(votiMvp.data ?? []);
|
||||
const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
|
||||
const ultima = csiGiocate[0]
|
||||
? { ...matchDaPartitaCsi(csiGiocate[0]), mvp: mvpPerMatch[csiGiocate[0].id] ?? "" }
|
||||
: scoutMatches[0]
|
||||
? { ...scoutMatches[0], mvp: mvpPerMatch[scoutMatches[0].id] ?? scoutMatches[0].mvp }
|
||||
: null;
|
||||
const obiettivi = useObiettivi();
|
||||
const obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null;
|
||||
|
||||
@@ -62,12 +74,12 @@ function Index() {
|
||||
|
||||
<div className="mt-6 grid grid-cols-3 gap-2 text-center">
|
||||
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
||||
<p className="font-display text-2xl leading-none">{noi.pos}º</p>
|
||||
<p className="font-display text-2xl leading-none">{noi ? `${noi.pos}º` : "—"}</p>
|
||||
<p className="text-[10px] uppercase text-primary-foreground/60">In classifica</p>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
||||
<p className="font-display text-2xl leading-none">
|
||||
{noi.vinte}-{noi.perse}
|
||||
{noi ? `${noi.vinte}-${noi.perse}` : "—"}
|
||||
</p>
|
||||
<p className="text-[10px] uppercase text-primary-foreground/60">Bilancio</p>
|
||||
</div>
|
||||
@@ -130,29 +142,36 @@ function Index() {
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<div className="premi rounded-3xl bg-card p-4 shadow-card">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-bold">CRAP Volley vs {ultima.avversario}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{ultima.casa ? "In casa" : "Trasferta"} · MVP {ultima.mvp}
|
||||
{ultima ? (
|
||||
<div className="premi rounded-3xl bg-card p-4 shadow-card">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-bold">CRAP Volley vs {ultima.avversario}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{ultima.casa ? "In casa" : "Trasferta"}
|
||||
{ultima.mvp ? ` · MVP ${ultima.mvp}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-display text-3xl leading-none text-accent">
|
||||
{ultima.setNostri}-{ultima.setLoro}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-display text-3xl leading-none text-accent">
|
||||
{ultima.setNostri}-{ultima.setLoro}
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||
{ultima.parziali.map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="rounded-lg bg-secondary px-2 py-1 text-[11px] font-semibold tabular-nums"
|
||||
>
|
||||
{p[0]}-{p[1]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||
{ultima.parziali.map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="rounded-lg bg-secondary px-2 py-1 text-[11px] font-semibold tabular-nums"
|
||||
>
|
||||
{p[0]}-{p[1]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-3xl bg-card p-4 text-xs text-muted-foreground shadow-card">
|
||||
Nessun risultato disponibile.
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
|
||||
@@ -2,8 +2,10 @@ 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 } from "@/lib/crapp-data";
|
||||
import { formatData, giocatori } from "@/lib/crapp-data";
|
||||
import { convocatiEvento, useEvento } from "@/lib/eventi";
|
||||
import { useCsi } from "@/lib/csi";
|
||||
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||
import { Pagelle } from "@/components/crapp/Pagelle";
|
||||
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
|
||||
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
|
||||
@@ -22,9 +24,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" },
|
||||
],
|
||||
@@ -39,6 +47,7 @@ function PartitaDetail() {
|
||||
const io = useGiocatoreCorrente();
|
||||
const admin = useIsAdmin();
|
||||
const scoutMatches = useScoutMatches();
|
||||
const { data: csi } = useCsi();
|
||||
const { risposte } = usePresenzeEvento(id);
|
||||
const presentiVeri = giocatori.filter(
|
||||
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
|
||||
@@ -60,6 +69,9 @@ function PartitaDetail() {
|
||||
|
||||
const convocati = convocatiEvento(evento);
|
||||
const scout = scoutMatches.find((m) => m.id === evento.id || m.data === evento.data) ?? null;
|
||||
const csiMatch = csi
|
||||
? partiteGiocate(csi.partite).find((p) => p.data === evento.data)
|
||||
: undefined;
|
||||
const match = scout
|
||||
? {
|
||||
id: scout.id,
|
||||
@@ -70,10 +82,12 @@ function PartitaDetail() {
|
||||
setLoro: scout.setLoro,
|
||||
parziali: scout.parziali,
|
||||
}
|
||||
: storicoMatch.find((m) => m.data === evento.data);
|
||||
: csiMatch
|
||||
? matchDaPartitaCsi(csiMatch)
|
||||
: undefined;
|
||||
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;
|
||||
@@ -140,7 +154,9 @@ function PartitaDetail() {
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-3 py-1 text-lg font-display font-bold",
|
||||
vinta ? "bg-success text-success-foreground" : "bg-destructive text-destructive-foreground",
|
||||
vinta
|
||||
? "bg-success text-success-foreground"
|
||||
: "bg-destructive text-destructive-foreground",
|
||||
)}
|
||||
>
|
||||
{match.setNostri} - {match.setLoro}
|
||||
@@ -149,7 +165,10 @@ function PartitaDetail() {
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
{match.parziali.map(([noi, loro], i) => (
|
||||
<div key={i} className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2">
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2"
|
||||
>
|
||||
<span className="font-display text-lg">{noi}</span>
|
||||
<span className="text-xs font-semibold text-muted-foreground">Set {i + 1}</span>
|
||||
<span className="font-display text-lg">{loro}</span>
|
||||
@@ -171,7 +190,8 @@ function PartitaDetail() {
|
||||
) : (
|
||||
<Section titolo="In programma">
|
||||
<p className="rounded-3xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
||||
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.
|
||||
</p>
|
||||
</Section>
|
||||
)}
|
||||
@@ -229,7 +249,9 @@ function PartitaDetail() {
|
||||
{admin ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => scaricaCsv(`scout-${scout.data}-${scout.avversario}.csv`, csvScoutMatch(scout))}
|
||||
onClick={() =>
|
||||
scaricaCsv(`scout-${scout.data}-${scout.avversario}.csv`, csvScoutMatch(scout))
|
||||
}
|
||||
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
||||
>
|
||||
<Download className="h-4 w-4" /> Esporta CSV
|
||||
|
||||
+22
-12
@@ -32,7 +32,10 @@ export const Route = createFileRoute("/profilo")({
|
||||
content: "Foto, ruolo, statistiche personali, badge e obiettivi del giocatore CRAP Volley.",
|
||||
},
|
||||
{ property: "og:title", content: "Profilo giocatore — CrAPP" },
|
||||
{ property: "og:description", content: "Statistiche personali, badge e presenze della stagione." },
|
||||
{
|
||||
property: "og:description",
|
||||
content: "Statistiche personali, badge e presenze della stagione.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: Profilo,
|
||||
@@ -51,7 +54,9 @@ function Profilo() {
|
||||
|
||||
useEffect(() => {
|
||||
setSupportate(pushSupportato());
|
||||
statoNotifiche().then(setNotifiche).catch(() => setNotifiche(false));
|
||||
statoNotifiche()
|
||||
.then(setNotifiche)
|
||||
.catch(() => setNotifiche(false));
|
||||
}, []);
|
||||
|
||||
if (!g) return null;
|
||||
@@ -204,20 +209,25 @@ function Profilo() {
|
||||
<span
|
||||
className={cn(
|
||||
"grid h-8 w-8 shrink-0 place-items-center rounded-xl",
|
||||
notifiche ? "bg-accent-grad text-accent-foreground" : "bg-secondary text-muted-foreground",
|
||||
notifiche
|
||||
? "bg-accent-grad text-accent-foreground"
|
||||
: "bg-secondary text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
</span>
|
||||
</button>
|
||||
{["Notifiche convocazioni", "Promemoria allenamenti", "Cambi orario", "Bacheca squadra"].map(
|
||||
(v) => (
|
||||
<label key={v} className="flex items-center justify-between gap-3 px-4 py-3 text-sm">
|
||||
<span className="min-w-0 truncate">{v}</span>
|
||||
<input type="checkbox" defaultChecked className="h-5 w-9 accent-[var(--accent)]" />
|
||||
</label>
|
||||
),
|
||||
)}
|
||||
{[
|
||||
"Notifiche convocazioni",
|
||||
"Promemoria allenamenti",
|
||||
"Cambi orario",
|
||||
"Bacheca squadra",
|
||||
].map((v) => (
|
||||
<label key={v} className="flex items-center justify-between gap-3 px-4 py-3 text-sm">
|
||||
<span className="min-w-0 truncate">{v}</span>
|
||||
<input type="checkbox" defaultChecked className="h-5 w-9 accent-[var(--accent)]" />
|
||||
</label>
|
||||
))}
|
||||
{admin ? (
|
||||
<Link
|
||||
to="/admin"
|
||||
@@ -239,4 +249,4 @@ function Profilo() {
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -53,7 +53,17 @@ export const Route = createFileRoute("/scout")({
|
||||
|
||||
const ordineAzioni: AzioneTipo[] = ["attacco", "ace", "muro", "errore"];
|
||||
|
||||
function Blocco({ icona, titolo, testo, children }: { icona: React.ReactNode; titolo: string; testo: string; children?: React.ReactNode }) {
|
||||
function Blocco({
|
||||
icona,
|
||||
titolo,
|
||||
testo,
|
||||
children,
|
||||
}: {
|
||||
icona: React.ReactNode;
|
||||
titolo: string;
|
||||
testo: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="px-5 py-10">
|
||||
<div className="rounded-3xl bg-card p-6 text-center shadow-card">
|
||||
@@ -101,7 +111,9 @@ function Scout() {
|
||||
}
|
||||
|
||||
if (!pronto || sessione.isLoading || statoSalvato.isLoading) {
|
||||
return <Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />;
|
||||
return (
|
||||
<Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />
|
||||
);
|
||||
}
|
||||
|
||||
if (!partita) {
|
||||
@@ -187,7 +199,10 @@ function ScoutBoard({
|
||||
const base =
|
||||
iniziale ??
|
||||
statoIniziale(
|
||||
partita.titolo.replace(/CRAP Volley/gi, "").replace(/\s*vs\s*/i, "").trim() || "Avversario",
|
||||
partita.titolo
|
||||
.replace(/CRAP Volley/gi, "")
|
||||
.replace(/\s*vs\s*/i, "")
|
||||
.trim() || "Avversario",
|
||||
partita.titolo.trim().toLowerCase().startsWith("crap"),
|
||||
);
|
||||
const [avversario, setAvversario] = useState(base.avversario);
|
||||
@@ -439,9 +454,7 @@ function ScoutBoard({
|
||||
i === 0 && "anim-riga",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{g ? `#${g.numero} ${g.nome}` : "Avversario"}
|
||||
</span>
|
||||
<span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase",
|
||||
@@ -505,4 +518,4 @@ function ScoutBoard({
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+59
-33
@@ -4,11 +4,13 @@ import { Cake, ChevronDown, Crown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PageHeader, Section } from "@/components/crapp/ui-bits";
|
||||
import { Avatar } from "@/components/crapp/Avatar";
|
||||
import { formatData, storicoMatch } from "@/lib/crapp-data";
|
||||
import { formatData } from "@/lib/crapp-data";
|
||||
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
||||
import { useRosa, useObiettivi } from "@/lib/rosa";
|
||||
import { usePresenzeUltimoMeseTutti } from "@/lib/presenze-mese";
|
||||
import { totaliSquadra } from "@/lib/scout-store";
|
||||
import { useCsi } from "@/lib/csi";
|
||||
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||
import { mediaSquadra, usePagelle } from "@/lib/pagelle";
|
||||
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
||||
import { StatTile } from "@/components/crapp/ui-bits";
|
||||
@@ -43,7 +45,8 @@ export const Route = createFileRoute("/squadra")({
|
||||
{ title: "Squadra CRAP Volley — CrAPP" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Rosa completa del CRAP Volley: giocatori, dati anagrafici, statistiche stagionali e badge sbloccati.",
|
||||
content:
|
||||
"Rosa completa del CRAP Volley: giocatori, dati anagrafici, statistiche stagionali e badge sbloccati.",
|
||||
},
|
||||
{ property: "og:title", content: "Squadra CRAP Volley — CrAPP" },
|
||||
{
|
||||
@@ -55,7 +58,6 @@ export const Route = createFileRoute("/squadra")({
|
||||
component: Squadra,
|
||||
});
|
||||
|
||||
|
||||
const criteri = [
|
||||
{ id: "presenze", label: "Presenze" },
|
||||
{ id: "mediaVoto", label: "Media voto" },
|
||||
@@ -85,24 +87,31 @@ function Squadra() {
|
||||
const obiettivi = useObiettivi();
|
||||
const team = totaliSquadra(scoutMatches);
|
||||
const mediaPresenze = rosa.length
|
||||
? Math.round((rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100)
|
||||
? Math.round(
|
||||
(rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100,
|
||||
)
|
||||
: 0;
|
||||
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
|
||||
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
|
||||
const tuttiMatch = [
|
||||
...scoutMatches.map((m) => ({
|
||||
id: m.id,
|
||||
data: m.data,
|
||||
avversario: m.avversario,
|
||||
casa: m.casa,
|
||||
setNostri: m.setNostri,
|
||||
setLoro: m.setLoro,
|
||||
parziali: m.parziali,
|
||||
mvp: mvpPerMatch[m.id] ?? "",
|
||||
scout: true,
|
||||
})),
|
||||
...storicoMatch.map((m) => ({ ...m, scout: false })),
|
||||
];
|
||||
const { data: csi } = useCsi();
|
||||
const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
|
||||
const tuttiMatch = csiGiocate.length
|
||||
? csiGiocate.map((p) => ({
|
||||
...matchDaPartitaCsi(p),
|
||||
mvp: mvpPerMatch[p.id] ?? "",
|
||||
scout: false,
|
||||
}))
|
||||
: scoutMatches.map((m) => ({
|
||||
id: m.id,
|
||||
data: m.data,
|
||||
avversario: m.avversario,
|
||||
casa: m.casa,
|
||||
setNostri: m.setNostri,
|
||||
setLoro: m.setLoro,
|
||||
parziali: m.parziali,
|
||||
mvp: mvpPerMatch[m.id] ?? "",
|
||||
scout: true,
|
||||
}));
|
||||
const completati = obiettivi.filter((o) => progressoObiettivo(o) >= 100).length;
|
||||
const mediaObiettivi = obiettivi.length
|
||||
? Math.round(obiettivi.reduce((s, o) => s + progressoObiettivo(o), 0) / obiettivi.length)
|
||||
@@ -116,7 +125,10 @@ function Squadra() {
|
||||
<div className="space-y-2">
|
||||
{rosa.map((g) => {
|
||||
const stati = badgeGiocatore(g);
|
||||
const sbloccati = [...stati.filter((b) => b.grado !== null), ...badgeSegretiSbloccati(g)];
|
||||
const sbloccati = [
|
||||
...stati.filter((b) => b.grado !== null),
|
||||
...badgeSegretiSbloccati(g),
|
||||
];
|
||||
const isOpen = aperto === g.id;
|
||||
return (
|
||||
<article key={g.id} className="overflow-hidden rounded-3xl bg-card shadow-card">
|
||||
@@ -126,7 +138,11 @@ function Squadra() {
|
||||
className="flex w-full items-center gap-3 p-3 text-left active:scale-[0.99]"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<Avatar id={g.id} fallback={g.numero ? String(g.numero) : g.iniziali} className="h-11 w-11 text-lg" />
|
||||
<Avatar
|
||||
id={g.id}
|
||||
fallback={g.numero ? String(g.numero) : g.iniziali}
|
||||
className="h-11 w-11 text-lg"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
|
||||
<span className="mt-1 flex items-center gap-2">
|
||||
@@ -160,19 +176,20 @@ function Squadra() {
|
||||
<div className="border-t border-border px-4 pb-4 pt-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)} {g.nascita.slice(0, 4)}
|
||||
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)}{" "}
|
||||
{g.nascita.slice(0, 4)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ l: "Presenze", v: `${g.presenze}/${g.totaliEventi}` },
|
||||
{ l: "Presenze 30gg", v: `${mese[g.id]?.percentuale ?? 0}%` },
|
||||
{ l: "Presenze di fila", v: g.streak },
|
||||
{ l: "Media voto", v: g.mediaVoto || "—" },
|
||||
{ l: "MVP", v: g.mvp },
|
||||
{ l: "Cacche/partita 💩", v: g.cacchePartita || "—" },
|
||||
].map((s) => (
|
||||
{[
|
||||
{ l: "Presenze", v: `${g.presenze}/${g.totaliEventi}` },
|
||||
{ l: "Presenze 30gg", v: `${mese[g.id]?.percentuale ?? 0}%` },
|
||||
{ l: "Presenze di fila", v: g.streak },
|
||||
{ l: "Media voto", v: g.mediaVoto || "—" },
|
||||
{ l: "MVP", v: g.mvp },
|
||||
{ l: "Cacche/partita 💩", v: g.cacchePartita || "—" },
|
||||
].map((s) => (
|
||||
<div key={s.l} className="rounded-2xl bg-secondary p-2.5 text-center">
|
||||
<p className="font-display text-xl leading-none">{s.v}</p>
|
||||
<p className="mt-1 text-[10px] font-semibold uppercase text-muted-foreground">
|
||||
@@ -201,9 +218,16 @@ function Squadra() {
|
||||
<p className="mt-1 text-xs font-bold leading-tight">{b.def.nome}</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{b.valore} {b.def.unita}
|
||||
{b.prossimaSoglia ? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}` : ""}
|
||||
{b.prossimaSoglia
|
||||
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
|
||||
: ""}
|
||||
</p>
|
||||
<p className={cn("mt-1.5 text-[10px] font-bold uppercase", meta.text)}>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1.5 text-[10px] font-bold uppercase",
|
||||
meta.text,
|
||||
)}
|
||||
>
|
||||
{meta.label}
|
||||
</p>
|
||||
</div>
|
||||
@@ -223,7 +247,7 @@ function Squadra() {
|
||||
<Section titolo="Statistiche di squadra">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<StatTile valore={`${mediaPresenze}%`} label="Media presenze" />
|
||||
<StatTile valore={storicoMatch.length + scoutMatches.length} label="Match giocati" />
|
||||
<StatTile valore={tuttiMatch.length} label="Match giocati" />
|
||||
<StatTile valore={mediaSquadra(pagelle) || "—"} label="Media pagelle" />
|
||||
<StatTile valore={team.punti} label="Punti squadra" />
|
||||
<StatTile valore={team.ace} label="Ace squadra" />
|
||||
@@ -411,7 +435,9 @@ function Squadra() {
|
||||
const meta = gradoMeta[grado];
|
||||
const quanti = rosa.filter((g) => {
|
||||
const raggiunto = gradoRaggiunto(b, b.valore(g));
|
||||
return raggiunto ? gradiOrdine.indexOf(raggiunto) >= gradiOrdine.indexOf(grado) : false;
|
||||
return raggiunto
|
||||
? gradiOrdine.indexOf(raggiunto) >= gradiOrdine.indexOf(grado)
|
||||
: false;
|
||||
}).length;
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user