/** * App Layout * * Improved maintainability through: * - Separation of concerns via custom hooks * - Composable navigation filters * - Layout variant components * - Memoization for performance * */ import { type PropsWithChildren, useEffect } from "react"; import { useRouter } from "next/router"; import { signOut } from "next-auth/react"; import posthog from "posthog-js"; import { env } from "@/src/env.mjs"; import { useQueryProjectOrOrganization } from "@/src/features/projects/hooks"; import { ErrorPageWithSentry } from "@/src/components/error-page"; // Layout variants import { LoadingLayout } from "./variants/LoadingLayout"; import { UnauthenticatedLayout } from "./variants/UnauthenticatedLayout"; import { MinimalLayout } from "./variants/MinimalLayout"; import { AuthenticatedLayout } from "./variants/AuthenticatedLayout"; // Custom hooks import { useAuthSession } from "./hooks/useAuthSession"; import { useLayoutConfiguration } from "./hooks/useLayoutConfiguration"; import { useAuthGuard } from "./hooks/useAuthGuard"; import { useProjectAccess } from "./hooks/useProjectAccess"; import { useFilteredNavigation } from "./hooks/useFilteredNavigation"; import { useLayoutMetadata } from "./hooks/useLayoutMetadata"; /** * Main layout component * Determines which layout variant to render based on: * - Authentication state * - Current route * - Project access * - User permissions */ export function AppLayout(props: PropsWithChildren) { const router = useRouter(); const session = useAuthSession(); const { organization } = useQueryProjectOrOrganization(); // Determine layout configuration const { variant, hideNavigation, isPublishable } = useLayoutConfiguration( session.data ?? null, ); // Check authentication and redirects const authGuard = useAuthGuard(session, hideNavigation); // Check project access const projectAccess = useProjectAccess(session.data ?? null); // IMPORTANT: Call all hooks before any conditional returns // Load navigation and metadata (even if not used in all render paths) const navigation = useFilteredNavigation(session.data ?? null, organization); const activePathName = navigation.navigation.find( (item) => item.isActive, )?.title; const metadata = useLayoutMetadata(activePathName, navigation.navigation); // Handle auth guard actions (redirect or sign-out) useEffect(() => { if (authGuard.action === "redirect") { void router.replace(authGuard.url); } else if (authGuard.action === "sign-out") { void signOut({ redirect: false }); } }, [authGuard, router]); // Loading or redirecting state if ( authGuard.action === "loading" || authGuard.action === "redirect" || authGuard.action === "sign-out" ) { return ; } // Project access denied - handle based on path type if (session.status === "authenticated" && !projectAccess.hasAccess) { // For publishable paths (shared traces/sessions), render minimal layout without sidebar // This allows authenticated users to view shared content without seeing project navigation if (isPublishable) { return {props.children}; } // For non-publishable paths, show error page return ( ); } // Unauthenticated layout (sign-in, sign-up) // Must check variant BEFORE hideNavigation since auth pages set hideNavigation=true if (variant === "unauthenticated") { return {props.children}; } // Publishable paths (traces, sessions) when unauthenticated // Render minimal layout without navigation/sidebar if (isPublishable && session.status === "unauthenticated") { return {props.children}; } // Render minimal layout (onboarding, public routes) if (hideNavigation) { return {props.children}; } // Authenticated layout // At this point, all auth guards have passed and session.data is guaranteed to exist // The authGuard hook ensures we don't reach here without a valid session if (!session.data) { // This should never happen due to guards above, but TypeScript needs this return ; } const handleSignOut = async () => { sessionStorage.clear(); if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) { posthog.reset(); } await signOut({ callbackUrl: `/auth/sign-in` }); }; return ( {props.children} ); }