import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Outlet, Link, createRootRouteWithContext, useRouter, useNavigate, useLocation, HeadContent, Scripts, } from "@tanstack/react-router"; import { useEffect, useState, type ReactNode } from "react"; import appCss from "../styles.css?url"; import { reportLovableError } from "../lib/lovable-error-reporting"; import { BottomNav } from "../components/crapp/BottomNav"; import { CelebrazioneBadge } from "../components/crapp/CelebrazioneBadge"; import { Toaster } from "../components/ui/sonner"; import { TeamLogo } from "../components/crapp/ui-bits"; import { useGiocatoreBase } from "../lib/user-store"; import { useGiocatoriSquadra } from "../lib/giocatori-squadra"; import { useSessione } from "../lib/auth"; import { mantieniWorkerPushAggiornato } from "../lib/push-client"; function NotFoundComponent() { return (

404

Pagina non trovata

La pagina che cerchi non esiste o è stata spostata.

Torna alla home
); } function ErrorComponent({ error, reset }: { error: Error; reset: () => void }) { console.error(error); const router = useRouter(); useEffect(() => { reportLovableError(error, { boundary: "tanstack_root_error_component" }); }, [error]); return (

Questa pagina non si è caricata

Qualcosa è andato storto da parte nostra. Puoi riprovare o tornare alla home.

Torna alla home
); } export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ head: () => ({ meta: [ { charSet: "utf-8" }, // `viewport-fit=cover` è obbligatorio perché env(safe-area-inset-*) sia // diverso da 0 su iOS: senza, la BottomNav finisce sotto la home bar. { name: "viewport", content: "width=device-width, initial-scale=1, viewport-fit=cover", }, { title: "CrAPP — L'app del CRAP Volley" }, { name: "description", content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.", }, { name: "author", content: "CRAP Volley" }, // Deve combaciare con --background, altrimenti la barra di stato resta // nera sopra un'interfaccia chiara. { name: "theme-color", content: "#e4e8ed" }, { property: "og:title", content: "CrAPP — L'app del CRAP Volley" }, { property: "og:description", content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.", }, { property: "og:type", content: "website" }, // `summary` e non `summary_large_image`: l'unica immagine è l'icona // quadrata della squadra, non una copertina 2:1. { name: "twitter:card", content: "summary" }, { name: "twitter:title", content: "CrAPP — L'app del CRAP Volley" }, { name: "twitter:description", content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.", }, { property: "og:image", content: "/icon-512.png" }, { name: "twitter:image", content: "/icon-512.png" }, ], links: [ { rel: "stylesheet", href: appCss, }, { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:wght@400;500;600;700;800&display=swap", }, { rel: "icon", href: "/favicon.png", type: "image/png" }, { rel: "apple-touch-icon", href: "/icon-192.png", sizes: "192x192" }, { rel: "manifest", href: "/manifest.webmanifest" }, ], }), shellComponent: RootShell, component: RootComponent, notFoundComponent: NotFoundComponent, errorComponent: ErrorComponent, }); function RootShell({ children }: { children: ReactNode }) { return ( {children} ); } function RootComponent() { const { queryClient } = Route.useRouteContext(); // `AppShell` legge la rosa da `giocatori_squadra` (useGiocatoreBase → DD-015): serve stare // dentro il QueryClientProvider, quindi il provider avvolge tutto fin da qui. return ( ); } function AppShell() { const navigate = useNavigate(); const location = useLocation(); const giocatore = useGiocatoreBase(); const { isPending: squadraInCorso } = useGiocatoriSquadra(); const { pronta, utenteId } = useSessione(); const [mounted, setMounted] = useState(false); const isBenvenuto = location.pathname === "/benvenuto"; useEffect(mantieniWorkerPushAggiornato, []); // Senza sessione Google non si entra: l'identità la dà il login, non la scelta del nome // (DD-011). Si aspetta `pronta`, altrimenti il primo render sloggato rimbalzerebbe fuori // chi ha già la sessione in localStorage. Si aspetta anche `squadraInCorso`: finché la // rosa non è arrivata, `giocatore` risulta nullo anche per chi è già collegato (la rosa // di riserva usa id diversi da quelli veri), e rimbalzerebbe su /benvenuto chi stava solo // ricaricando una pagina profonda come /eventi. useEffect(() => { setMounted(true); if (pronta && !squadraInCorso && (!giocatore || !utenteId) && !isBenvenuto) { navigate({ to: "/benvenuto", search: { next: location.pathname } }); } }, [giocatore, utenteId, pronta, squadraInCorso, isBenvenuto, navigate, location.pathname]); if (!mounted || !pronta || squadraInCorso) { return (
); } return ( <>
{/* Required: nested routes render here. Removing breaks all child routes. */}
{!isBenvenuto && } {!isBenvenuto && } ); }