Files
CRAPP/src/routes/benvenuto.tsx
T
davideandClaude Sonnet 5 3258a24f20 Estende il fix di useIo/useRosa a EventoCard e altri 13 usi non statistici
EventoCard monta un hook per ogni card mostrata: usava useGiocatoreCorrente
(= useIo = useRosa, le 6 statistiche pesanti) solo per leggere il proprio id.
Il Calendario ne rende diverse insieme (prossimi eventi, compleanni, drawer del
giorno), quindi ogni apertura moltiplicava il ricalcolo dell'intera rosa.

Audit di tutti gli altri usi di useGiocatoreCorrente: 12 su 13 leggevano solo
id/nome/verita', mai una statistica. Corretti allo stesso modo (-> useGiocatoreBase):
benvenuto.tsx, VotazioneMvp, SondaggioCacche, VotoSocial, eventi.tsx,
PromemoriaPalloni (Home), TurnoPalloni, ScoutEntry, Pagelle.

partita.$id.tsx: `io` era dichiarato e mai piu' usato, rimosso.

Stesso pattern trovato anche su useRosa (non solo useGiocatoreCorrente) in
scout.tsx e RosaPresenze.tsx (montata su partita e allenamento): entrambi
passati al nuovo useAnagraficaRosa, esteso con ruolo e numero.

Unica eccezione: CelebrazioneBadge.tsx usa davvero le statistiche complete
(badge, MVP, serie) ed e' montato globalmente in __root.tsx - non downgradabile,
resta il costo di base piu' alto rimasto in giro.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 10:00:52 +02:00

144 lines
5.3 KiB
TypeScript

import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { LogIn, LogOut } from "lucide-react";
import { toast } from "sonner";
import { TeamLogo } from "@/components/crapp/ui-bits";
import { accediConGoogle, esci, useSessione } from "@/lib/auth";
import {
slotDi,
slotPerEmail,
useCollegaGiocatore,
useGiocatoriSquadra,
} from "@/lib/giocatori-squadra";
import { impostaGiocatore, resetGiocatore, useGiocatoreBase } from "@/lib/user-store";
export const Route = createFileRoute("/benvenuto")({
validateSearch: (search: Record<string, unknown>): { next?: string } => {
const next = search["next"];
// Solo un percorso interno: mai un URL assoluto, per non aprire un redirect esterno.
return typeof next === "string" && next.startsWith("/") ? { next } : {};
},
head: () => ({
meta: [
{ title: "Benvenuto — CrAPP" },
{
name: "description",
content: "Accedi e collega il tuo profilo giocatore per iniziare.",
},
{ property: "og:title", content: "Benvenuto — CrAPP" },
{
property: "og:description",
content: "Accedi e collega il tuo profilo giocatore per iniziare.",
},
],
}),
component: Benvenuto,
});
function Benvenuto() {
const navigate = useNavigate();
const { next } = Route.useSearch();
// Solo verità (`!!giocatore`): `useGiocatoreBase` (anagrafica) basta, niente statistiche.
const giocatore = useGiocatoreBase();
const { pronta, utenteId, emailUtente } = useSessione();
const { righe, daDatabase } = useGiocatoriSquadra();
const collega = useCollegaGiocatore();
const [inCorso, setInCorso] = useState(false);
const [tentato, setTentato] = useState(false);
const mioSlot = slotDi(righe, utenteId);
const slotEmail = slotPerEmail(righe, emailUtente);
// Si entra solo da loggati e con uno slot collegato (DD-011).
const puoEntrare = !!giocatore && !!utenteId;
useEffect(() => {
if (puoEntrare) navigate({ to: next ?? "/" });
}, [puoEntrare, navigate, next]);
// Chi sei lo dice lo slot collegato all'account, non quello che c'è in localStorage:
// senza slot la scelta salvata dalla vecchia selezione libera va buttata.
useEffect(() => {
if (mioSlot) impostaGiocatore(mioSlot.id);
else if (utenteId && daDatabase) resetGiocatore();
}, [mioSlot, utenteId, daDatabase]);
// Collegamento automatico per email (DD-018): un solo tentativo, mai su dati di
// fallback. Se fallisce o non trova corrispondenza resta lo stato d'errore, senza
// scelta manuale di ripiego.
useEffect(() => {
if (!utenteId || !daDatabase || mioSlot || !slotEmail || tentato) return;
setTentato(true);
collega
.mutateAsync({ giocatoreId: slotEmail.id, utenteId })
.then(() => impostaGiocatore(slotEmail.id))
.catch(() => toast.error("Collegamento non riuscito. Riprova o contatta un amministratore."));
}, [utenteId, daDatabase, mioSlot, slotEmail, tentato, collega]);
async function accedi() {
setInCorso(true);
try {
await accediConGoogle();
} catch (error) {
toast.error(error instanceof Error ? error.message : "Accesso non riuscito");
setInCorso(false);
}
}
async function esciERiprova() {
try {
await esci();
} catch (error) {
toast.error(error instanceof Error ? error.message : "Uscita non riuscita");
}
}
// Errore: email senza corrispondenza, oppure trovata ma il tentativo di collegamento
// è fallito (slot nel frattempo reclamato da altri, o errore di rete).
const erroreCollegamento =
!!utenteId && daDatabase && !mioSlot && (!slotEmail || collega.isError);
const inAttesaCollegamento = !!utenteId && !mioSlot && !erroreCollegamento;
return (
<div className="flex min-h-dvh flex-col items-center justify-center px-6 py-12">
<TeamLogo className="h-20 w-20" />
<h1 className="mt-6 text-center font-display text-4xl uppercase leading-none">
Benvenuto in CrAPP
</h1>
{!pronta ? null : !utenteId ? (
<>
<p className="mt-2 text-center text-sm text-muted-foreground">
Accedi con il tuo account Google per collegare il profilo giocatore.
</p>
<button
type="button"
onClick={accedi}
disabled={inCorso}
className="premi mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3.5 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-60"
>
<LogIn className="h-4 w-4" /> Accedi con Google
</button>
</>
) : erroreCollegamento ? (
<>
<p className="mt-2 text-center text-sm text-muted-foreground">
Nessun profilo trovato per la tua email. Contatta un amministratore per collegare il tuo
account.
</p>
<button
type="button"
onClick={() => void esciERiprova()}
className="premi mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-secondary py-3.5 text-sm font-bold uppercase shadow-card"
>
<LogOut className="h-4 w-4" /> Esci
</button>
</>
) : inAttesaCollegamento ? (
<p className="mt-2 text-center text-sm text-muted-foreground">
Ti stiamo collegando al tuo profilo...
</p>
) : null}
</div>
);
}