import { createFileRoute, Link } from "@tanstack/react-router"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { Flame, Trash2, Bell, Mail, LogOut, ShieldCheck, Bug, Lightbulb } from "lucide-react"; import { cn } from "@/lib/utils"; import { Card, PageHeader, StatTile, TeamLogo } from "@/components/crapp/ui-bits"; import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni"; import { Avatar } from "@/components/crapp/Avatar"; import { caricaAvatar, rimuoviAvatar, useAvatarEsiste, useImpostaAvatarEsiste, } from "@/lib/avatar-store"; import { SerieGriglia } from "@/components/crapp/SerieCard"; import { CollezioneBadge } from "@/components/crapp/CollezioneBadge"; import { ProfiloAllenatore, ProfiloAmministrativo } from "@/components/crapp/ProfiloAmministrativo"; import { useVotiSocial } from "@/lib/badge-social"; import { inizialiDa } from "@/lib/crapp-data"; import { useIo } from "@/lib/rosa"; import { usePresenzeUltimoMese } from "@/lib/presenze-mese"; import { attivaNotifiche, disattivaNotifiche, pushSupportato, statoNotifiche, } from "@/lib/push-client"; import { resetGiocatore, useGiocatoreBase } from "@/lib/user-store"; import { isAllenatore, nomeCompleto, ruoloVisibile, type GiocatoreSquadra, } from "@/lib/giocatori-squadra"; import { esci } from "@/lib/auth"; import { useIsAdmin } from "@/lib/ruoli"; import { useEmailNotifiche } from "@/lib/preferenze-utente"; import { Reveal } from "@/components/motion/Reveal"; import { version as APP_VERSION } from "../../package.json"; const TAB_PROFILO = ["stagione", "badge", "documenti", "impostazioni"] as const; type TabProfilo = (typeof TAB_PROFILO)[number]; function isTabProfilo(v: unknown): v is TabProfilo { return typeof v === "string" && (TAB_PROFILO as readonly string[]).includes(v); } export const Route = createFileRoute("/profilo")({ validateSearch: (search: Record): { tab?: TabProfilo } => { const tab = isTabProfilo(search["tab"]) ? search["tab"] : undefined; return tab ? { tab } : {}; }, head: () => ({ meta: [ { title: "Profilo giocatore — CrAPP" }, { name: "description", 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.", }, ], }), component: Profilo, }); function Profilo() { // L'allenatore ha un profilo ridotto: solo Docs e Opzioni, niente stagione né badge (DD-034). const base = useGiocatoreBase(); if (base && isAllenatore(base)) return ; return ; } function ProfiloGiocatore() { const { tab } = Route.useSearch(); const tabIniziale = tab ?? "stagione"; const votiSocial = useVotiSocial(); const g = useIo(); const ultimoMese = usePresenzeUltimoMese(g?.id); if (!g) return null; const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0; return ( <> {/* Sul profilo il logo torna in home al posto del link al profilo. */} } />
{g.streak} } label="Presenze di fila" />
), }, { id: "badge", label: "Badge", contenuto: , }, { id: "documenti", label: "Docs", contenuto: , }, { id: "impostazioni", label: "Opzioni", contenuto: , }, ]} /> ); } function ProfiloAllenatorePagina({ g }: { g: GiocatoreSquadra }) { const { tab } = Route.useSearch(); // Stagione e Badge non esistono per l'allenatore: un link diretto apre Docs. const tabIniziale = tab === "impostazioni" ? "impostazioni" : "documenti"; const nome = nomeCompleto(g); return ( <> } /> }, { id: "impostazioni", label: "Opzioni", contenuto: , }, ]} /> ); } function LinkHome() { return ( ); } /** Foto profilo: stessa per giocatore e allenatore (bucket `avatar-giocatori`). */ function CardAvatar({ id, fallback }: { id: string; fallback: string }) { const inputRef = useRef(null); const fotoEsiste = useAvatarEsiste(id); const impostaAvatarEsiste = useImpostaAvatarEsiste(); const [bust, setBust] = useState(0); async function onFile(e: React.ChangeEvent) { const file = e.target.files?.[0]; e.target.value = ""; if (!file) return; try { await caricaAvatar(id, file); setBust(Date.now()); impostaAvatarEsiste(id, true); toast.success("Immagine profilo aggiornata"); } catch { toast.error("Non sono riuscito a caricare l'immagine"); } } return (
{fotoEsiste.data ? ( ) : null}
); } /** Tab Opzioni: notifiche, dashboard admin, segnalazioni, uscita. Uguale per tutti. */ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) { const admin = useIsAdmin(); const [notifiche, setNotifiche] = useState(false); const [inCorso, setInCorso] = useState(false); const [supportate, setSupportate] = useState(true); const email = useEmailNotifiche(); useEffect(() => { setSupportate(pushSupportato()); statoNotifiche() .then(setNotifiche) .catch(() => setNotifiche(false)); }, []); async function cambiaNotifiche() { if (inCorso) return; setInCorso(true); try { if (notifiche) { await disattivaNotifiche(); setNotifiche(false); toast.success("Notifiche disattivate"); } else { await attivaNotifiche(giocatoreId); setNotifiche(true); toast.success("Notifiche attive"); } } catch (error) { toast.error(error instanceof Error ? error.message : "Notifiche non disponibili"); } finally { setInCorso(false); } } async function cambiaEmail() { if (email.imposta.isPending) return; const attive = !email.attive; try { await email.imposta.mutateAsync(attive); toast.success(attive ? "Email attive" : "Email disattivate"); } catch (error) { toast.error(error instanceof Error ? error.message : "Impossibile salvare la preferenza"); } } async function logout() { try { await esci(); resetGiocatore(); } catch (error) { toast.error(error instanceof Error ? error.message : "Uscita non riuscita"); } } return (
{admin ? ( Dashboard amministratore ) : null} Segnala un bug Suggerisci una nuova funzionalità

CrAPP v{APP_VERSION}

); }