Files
CRAPP/src/routes/calendario.tsx
T
davideandClaude Sonnet 5 953908388d Sposto rosa, classifica e storico partite dagli array hardcoded al CMS Strapi
Nuovi moduli src/lib/strapi-client.ts + rosa-base.ts/classifica-csi.ts/
storico-match.ts (hook TanStack Query, cache lunga, nessun polling). L'id
stabile gN diventa il campo `codice` su Strapi, per non rompere le FK verso
pagelle/cacche/mvp/palloni/presenze/azioni scout già chiavate su quella
stringa. isAdmin ora prende il Giocatore invece di un id, così non dipende
più dal roster. Rifattorizzati di conseguenza tutti i punti che importavano
`giocatori`/`classifica`/`storicoMatch` come array sincroni (componenti,
hook, due route server) per usare gli hook o un parametro esplicito.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 15:39:40 +02:00

321 lines
12 KiB
TypeScript

import { useEffect, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { CalendarPlus, ChevronLeft, ChevronRight, X } from "lucide-react";
import { Link } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
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,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
export const Route = createFileRoute("/calendario")({
head: () => ({
meta: [
{ title: "Calendario squadra — CrAPP" },
{
name: "description",
content: "Allenamenti, partite ed eventi extra del CRAP Volley con vista mensile e lista.",
},
{ property: "og:title", content: "Calendario squadra — CrAPP" },
{
property: "og:description",
content: "Vista mensile e lista eventi con promemoria per la squadra.",
},
],
}),
component: Calendario,
});
const giorniIT = ["L", "M", "M", "G", "V", "S", "D"];
const mesiIT = [
"Gennaio",
"Febbraio",
"Marzo",
"Aprile",
"Maggio",
"Giugno",
"Luglio",
"Agosto",
"Settembre",
"Ottobre",
"Novembre",
"Dicembre",
] as const;
function useMeseNav(initial = { anno: 2026, mese: 7 }) {
const [anno, setAnno] = useState(initial.anno);
const [mese, setMese] = useState(initial.mese);
const precedente = () => {
if (mese === 0) {
setMese(11);
setAnno((a) => a - 1);
} else {
setMese((m) => m - 1);
}
};
const successivo = () => {
if (mese === 11) {
setMese(0);
setAnno((a) => a + 1);
} else {
setMese((m) => m + 1);
}
};
return { anno, mese, precedente, successivo };
}
function giorniDelMese(anno: number, mese: number) {
const giorni = new Date(Date.UTC(anno, mese + 1, 0)).getUTCDate();
const primoGiorno = new Date(Date.UTC(anno, mese, 1)).getUTCDay();
const offsetLunedi = (primoGiorno + 6) % 7;
return { giorni, offsetLunedi };
}
function pad2(n: number) {
return String(n).padStart(2, "0");
}
function Calendario() {
const [vista, setVista] = useState<"mese" | "lista">("mese");
// SSR-safe: la data di oggi arriva solo dopo il mount.
const [oggi, setOggi] = useState<{ anno: number; mese: number; giorno: number } | null>(null);
useEffect(() => {
const d = new Date();
setOggi({ anno: d.getFullYear(), mese: d.getMonth(), giorno: d.getDate() });
}, []);
const [giornoSelezionato, setGiornoSelezionato] = useState<number | null>(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(giocatori, anno);
const compleanniMese = compleanni.filter((c) => c.data.startsWith(mesePrefix));
const eventiMese = eventi.filter((e) => e.data.startsWith(mesePrefix));
const eventiPerGiorno = new Map<number, Evento[]>();
for (const e of [...compleanniMese, ...eventiMese]) {
const g = Number(e.data.slice(8, 10));
const lista = eventiPerGiorno.get(g) ?? [];
lista.push(e);
eventiPerGiorno.set(g, lista);
}
function apriGiorno(giorno: number) {
if (!eventiPerGiorno.has(giorno)) return;
setGiornoSelezionato(giorno);
setDrawerAperto(true);
}
const eventiGiornoSelezionato = giornoSelezionato
? (eventiPerGiorno.get(giornoSelezionato) ?? [])
: [];
return (
<>
<PageHeader titolo="Calendario" sottotitolo={`${mesiIT[mese]} ${anno} · Stagione 2026/27`} />
<div className="px-5 pt-4">
<div className="flex rounded-full bg-secondary p-1">
{(["mese", "lista"] as const).map((v) => (
<button
key={v}
type="button"
onClick={() => setVista(v)}
className={cn(
"flex-1 rounded-full py-2 text-xs font-bold uppercase tracking-wide transition-colors",
vista === v ? "bg-card shadow-card text-foreground" : "text-muted-foreground",
)}
>
{v === "mese" ? "Vista mese" : "Lista eventi"}
</button>
))}
</div>
</div>
{vista === "mese" ? (
<Section titolo={mesiIT[mese]!}>
<div className="rounded-3xl bg-card p-4 shadow-card">
<div className="mb-3 flex items-center justify-between">
<button
type="button"
onClick={precedente}
className="grid h-9 w-9 place-items-center rounded-full bg-secondary text-foreground active:scale-95"
aria-label="Mese precedente"
>
<ChevronLeft className="h-5 w-5" />
</button>
<span className="font-display text-xl uppercase tracking-wide">
{mesiIT[mese]} {anno}
</span>
<button
type="button"
onClick={successivo}
className="grid h-9 w-9 place-items-center rounded-full bg-secondary text-foreground active:scale-95"
aria-label="Mese successivo"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
<div className="grid grid-cols-7 gap-1 text-center text-[11px] font-bold text-muted-foreground">
{giorniIT.map((g, i) => (
<span key={i}>{g}</span>
))}
</div>
<div className="mt-2 grid grid-cols-7 gap-1">
{Array.from({ length: offsetLunedi }).map((_, i) => (
<span key={`v${i}`} />
))}
{Array.from({ length: giorni }).map((_, i) => {
const giorno = i + 1;
const eventiGiorno = eventiPerGiorno.get(giorno) ?? [];
const haEventi = eventiGiorno.length > 0;
const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo)));
const coloreTipo: Record<Evento["tipo"], string> = {
partita: "var(--accent)",
allenamento: "var(--training)",
evento: "var(--warning)",
compleanno: "var(--success)",
};
const sfondo =
tipiGiorno.length > 1
? `linear-gradient(135deg, ${tipiGiorno
.map((t, idx) => {
const da = (idx / tipiGiorno.length) * 100;
const a = ((idx + 1) / tipiGiorno.length) * 100;
return `${coloreTipo[t]} ${da}%, ${coloreTipo[t]} ${a}%`;
})
.join(", ")})`
: undefined;
const tipo = tipiGiorno.length === 1 ? tipiGiorno[0] : undefined;
const isOggi =
!!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno;
const Cella = haEventi ? "button" : "div";
return (
<Cella
key={giorno}
type={haEventi ? "button" : undefined}
onClick={haEventi ? () => apriGiorno(giorno) : undefined}
style={sfondo ? { backgroundImage: sfondo } : undefined}
className={cn(
"relative grid aspect-square place-items-center rounded-xl text-sm font-semibold",
tipo === "partita" && "bg-accent text-accent-foreground",
tipo === "allenamento" && "bg-training text-training-foreground",
tipo === "evento" && "bg-warning text-warning-foreground",
tipo === "compleanno" && "bg-success text-success-foreground",
!tipo && !haEventi && "text-muted-foreground",
!tipo && haEventi && "text-foreground",
haEventi && "cursor-pointer transition-transform active:scale-90",
isOggi && "ring-2 ring-foreground ring-offset-1 ring-offset-card",
)}
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>
</Cella>
);
})}
</div>
<div className="mt-4 flex flex-wrap gap-3 text-[11px] font-semibold text-muted-foreground">
<span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita
</span>
<span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-training" /> Allenamento
</span>
<span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-warning" /> Eventi
</span>
<span className="inline-flex items-center gap-1">
<i className="h-2.5 w-2.5 rounded-full bg-success" /> Compleanni
</span>
</div>
</div>
</Section>
) : null}
{io && isAdmin(io) ? (
<div className="px-5 pt-4">
<Link
to="/eventi"
className="premi flex items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
>
<CalendarPlus className="h-4 w-4" /> Gestisci eventi
</Link>
</div>
) : null}
<Section titolo="Prossimi eventi">
<div className="space-y-3">
{eventiMese.length > 0 ? (
eventiMese.map((e) => {
const link = linkPerEvento(e);
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
})
) : (
<p className="rounded-3xl bg-card p-4 text-center text-sm text-muted-foreground shadow-card">
Nessun evento in {mesiIT[mese]!.toLowerCase()}
</p>
)}
</div>
</Section>
<Section titolo="Compleanni">
<div className="space-y-3">
{compleanniMese.length > 0 ? (
compleanniMese.map((c) => <EventoCard key={c.id} evento={c} />)
) : (
<p className="rounded-3xl bg-card p-4 text-center text-sm text-muted-foreground shadow-card">
Nessun compleanno in {mesiIT[mese]!.toLowerCase()}
</p>
)}
</div>
</Section>
<Drawer open={drawerAperto} onOpenChange={setDrawerAperto}>
<DrawerContent className="rounded-t-[24px] border-border bg-background px-4 pb-6 pt-2">
<DrawerHeader className="relative px-0 pb-2 text-left">
<DrawerTitle className="font-display text-2xl uppercase tracking-wide">
{giornoSelezionato ? `${giornoSelezionato} ${mesiIT[mese]}` : "Eventi"}
</DrawerTitle>
<DrawerClose className="absolute right-0 top-1 grid h-8 w-8 place-items-center rounded-full bg-secondary text-foreground transition-transform active:scale-90">
<X className="h-4 w-4" />
<span className="sr-only">Chiudi</span>
</DrawerClose>
</DrawerHeader>
<div className="max-h-[60vh] space-y-3 overflow-y-auto py-2">
{eventiGiornoSelezionato.length > 0 ? (
eventiGiornoSelezionato.map((e) => {
const link = linkPerEvento(e);
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
})
) : (
<p className="rounded-3xl bg-card p-4 text-center text-sm text-muted-foreground shadow-card">
Nessun evento in questa data
</p>
)}
</div>
</DrawerContent>
</Drawer>
</>
);
}