L'altezza della barra era dedotta dai padding delle voci e poi ricopiata a occhio altrove: `pb-24` nel contenitore delle pagine, `pb-28` nella celebrazione badge. Su iPhone i conti tornavano per 2 px — barra ~60 px più 34 px di safe area contro 96 px riservati — quindi bastava un inset diverso (orizzontale, o un modello con home indicator più alta) perché l'ultimo elemento della pagina finisse sotto la barra. Solo su iOS: su Android avanzavano 36 px. Ora la geometria sta in due token, `--altezza-nav` e `--pad-sicura-fondo`, e le utility `spazio-nav` e `pad-sicura-fondo` la leggono da lì. La striscia toccabile ha altezza fissa, così misura uguale sui due sistemi e sotto varia solo l'inset: `max(env(safe-area-inset-bottom), 0.5rem)` dà ad Android un respiro minimo dove iOS mette la home indicator. Identiche al pixel non possono essere — quell'inset esiste per un motivo fisico che Android non ha — ma la differenza è ora un margine in più, non un allineamento diverso. `100vh` diventa `100dvh` ovunque: su iOS Safari `vh` misura il viewport con la toolbar collassata, cioè più alto di quello visibile, e le schermate a tutta altezza risultavano più lunghe dello schermo. `dvh` segue la toolbar. Aggiunto il ripiego per Chrome su Android, che disattiva `backdrop-filter` senza accelerazione hardware: senza, su quei device la barra restava semitrasparente e il contenuto si leggeva attraverso. Resta fuori portata una differenza sola: in Safari non installata la toolbar di sistema sta in basso e collassa allo scroll, quindi la barra si muove con essa. Su Chrome la toolbar è in alto. Si risolve installando la PWA. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
4.9 KiB
TypeScript
137 lines
4.9 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, useGiocatoreCorrente } from "@/lib/user-store";
|
|
|
|
export const Route = createFileRoute("/benvenuto")({
|
|
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 giocatore = useGiocatoreCorrente();
|
|
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: "/" });
|
|
}, [puoEntrare, navigate]);
|
|
|
|
// 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>
|
|
);
|
|
}
|