"use client"; import { ReactNode, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { Hexagon } from "lucide-react"; import { useAuth } from "@/lib/auth/auth-context"; import { finishHandOvers, purgeStaleDraftPhotos } from "@/lib/photos/store"; import { LogoutConfirmDialog } from "@/components/auth/logout-confirm-dialog"; import { NotificationBell } from "@/components/notification-bell"; import { OfflineBand } from "@/components/offline/offline-band"; import { UserAvatar } from "@/components/user-avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Sidebar } from "./sidebar"; import { BottomNav } from "./bottom-nav"; import { hasQuickAddButton, QuickAddSheet } from "./quick-add-sheet"; import { PageTransition } from "./page-transition"; /** Id of the page content, the target of the skip link. */ const MAIN_CONTENT_ID = "main-content"; export default function AppLayout({ children }: { children: ReactNode }) { const router = useRouter(); const pathname = usePathname(); const { user, isLoading, sessionMode } = useAuth(); const [isLogoutOpen, setIsLogoutOpen] = useState(false); // Focus lands here when the offline queue sheet closes and nothing else is left. const pageContentRef = useRef(null); useEffect(() => { if (!isLoading && !user) { router.replace("/login"); } }, [isLoading, user, router]); // Photos of form drafts nobody can restore any more (older than 3 days) // leave the device. Housekeeping only: if it fails, the next start tries again. useEffect(() => { void purgeStaleDraftPhotos(Date.now()).catch(() => undefined); }, []); // Online saves whose photos could not be tied to their inspection at once: // finished for the signed-in user, even offline - the photos then wait as // entries of their own. Every sync run of that user tries again too. const userId = user?.id; useEffect(() => { if (userId) { void finishHandOvers(userId).catch(() => undefined); } }, [userId]); if (isLoading || !user) { return null; } // The phone's quick-add button floats 16 px above the bottom nav and is // 48 px high: without room under the page it would cover the last buttons. const contentPaddingClass = hasQuickAddButton(pathname, user.permissions) ? "pb-[calc(var(--bottom-nav-height)+--spacing(18))]" : "pb-(--bottom-nav-height)"; // The page scrolls on the window. No ancestor of the page content may be a // scroll container (overflow auto/scroll/hidden): it would capture // `position: sticky` and the StickySubmit save bar would never stick. return (
{ // The hash alone would not move the focus into a Next.js page reliably. event.preventDefault(); pageContentRef.current?.focus(); pageContentRef.current?.scrollIntoView?.({ block: "start" }); }} className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-lg focus:bg-card focus:px-4 focus:py-3 focus:text-sm focus:font-medium focus:shadow-[var(--shadow-elevated)] focus:ring-3 focus:ring-ring/50 focus:outline-none" > Zum Inhalt springen
ApiCore
{user.email} }> Profil }> Sprache & Datenschutz setIsLogoutOpen(true)} > Abmelden
{children}
router.push("/login")} />
); }