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 { AnimatePresence, motion } from "motion/react"; import { cn } from "@/lib/utils"; import { molla, proietta } from "@/lib/molla"; import { useMotoRidotto } from "@/lib/motion"; import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard"; import { Card, PageHeader, Section } from "@/components/crapp/ui-bits"; import { compleanniEventi, useEventi, type Evento } from "@/lib/eventi"; import { useAnagraficaRosa } from "@/lib/rosa"; import { usePuoGestireEventi } from "@/lib/ruoli"; import { giorniDelMese, giorniIT, mesiIT, pad2, useMeseNav } from "@/lib/calendario"; 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.", }, { property: "og:title", content: "Calendario squadra — CrAPP" }, { property: "og:description", content: "Vista mensile e prossimi eventi con promemoria per la squadra.", }, ], }), component: Calendario, }); /** Colore per tipo di evento, usato per dividere le celle con più tipi. */ const coloreTipo: Record = { partita: "var(--accent)", allenamento: "var(--training)", evento: "var(--warning)", compleanno: "var(--success)", }; function Calendario() { // 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(null); const [drawerAperto, setDrawerAperto] = useState(false); const puoGestire = usePuoGestireEventi(); const { eventi } = useEventi(); const rosa = useAnagraficaRosa({ conAllenatori: true }); // `useMotoRidotto` copre anche i device deboli (RAM bassa), non solo // `prefers-reduced-motion`: disattiva anche lo swipe orizzontale tra mesi. const ridotto = useMotoRidotto(); const { anno, mese, direzione, precedente, successivo } = useMeseNav(); const { giorni, offsetLunedi } = giorniDelMese(anno, mese); const mesePrefix = `${anno}-${pad2(mese + 1)}`; const compleanni = compleanniEventi(rosa, anno); const compleanniMese = compleanni.filter((c) => c.data.startsWith(mesePrefix)); const eventiMese = eventi.filter((e) => e.data.startsWith(mesePrefix)); const eventiPerGiorno = new Map(); 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) ?? []) : []; // Prossimi 3 eventi da oggi in avanti (indipendenti dal mese selezionato nella griglia). const oggiIso = oggi ? `${oggi.anno}-${pad2(oggi.mese + 1)}-${pad2(oggi.giorno)}` : null; const prossimiEventi = oggiIso ? eventi.filter((e) => e.data >= oggiIso).slice(0, 3) : []; return ( <>
{mesiIT[mese]} {anno}
{giorniIT.map((g, i) => ( {g} ))}
{/* Il mese si cambia anche con lo swipe: il punto d'arrivo si decide proiettando la velocità di rilascio (come la decelerazione dello scroll iOS), non dalla posizione del dito. `dragElastic` dà la resistenza progressiva al bordo invece di uno stop netto. `p-1` con `-mx-1` compensato: l'anello del giorno corrente (`ring-2 ring-offset-1`) sporge 3 px fuori dalla cella, e senza questo margine interno `overflow-hidden` lo taglia sulla prima riga e sulle colonne di bordo. Il padding sta dentro il riquadro di ritaglio, i margini negativi rimettono la griglia dov'era. */}
{ const arrivo = info.offset.x + proietta(info.velocity.x); if (arrivo < -60) successivo(); else if (arrivo > 60) precedente(); }} initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }} animate={{ opacity: 1, x: 0 }} exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }} transition={ridotto ? { duration: 0.2 } : molla.foglio} className="grid touch-pan-y grid-cols-7 gap-1" > {Array.from({ length: offsetLunedi }).map((_, i) => ( ))} {Array.from({ length: giorni }).map((_, i) => { const giorno = i + 1; const eventiGiorno = eventiPerGiorno.get(giorno) ?? []; const haEventi = eventiGiorno.length > 0; // Attenzione: si conta per **tipo**, non per numero di // eventi. Due partite nello stesso giorno restano una cella // rossa piena; si divide solo se i tipi sono diversi. const tipiGiorno = Array.from(new Set(eventiGiorno.map((e) => e.tipo))); // Più tipi: la cella si divide in bande a taglio netto (gli // stop sono duplicati apposta, non è una sfumatura), una per // tipo, in diagonale. 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 ( 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 ? `${giorno} ${mesiIT[mese]}: ${eventiGiorno.length} ${eventiGiorno.length === 1 ? "evento" : "eventi"}` : undefined } aria-current={isOggi ? "date" : undefined} > {/* Sulle celle divise il numero sta direttamente sulle bande, staccato dal fondo da un alone bianco: è la resa scelta, il colore deve restare pieno e visibile fino al bordo. */} {giorno} ); })}

Scorri a destra o sinistra per cambiare mese.

Partita Allenamento Eventi Compleanni
{puoGestire ? (
Gestisci eventi
) : null}
{prossimiEventi.length > 0 ? ( prossimiEventi.map((e) => { const link = linkPerEvento(e); return ; }) ) : (

Nessun evento in programma

)}
{compleanniMese.length > 0 ? ( compleanniMese.map((c) => ) ) : (

Nessun compleanno in {mesiIT[mese]!.toLowerCase()}

)}
{giornoSelezionato ? `${giornoSelezionato} ${mesiIT[mese]}` : "Eventi"} Chiudi
{eventiGiornoSelezionato.length > 0 ? ( eventiGiornoSelezionato.map((e) => { const link = linkPerEvento(e); return ; }) ) : (

Nessun evento in questa data

)}
); }