# Frontend Layouts Module ## Overview The Layouts module provides **page wrapper components** that handle authentication, authorization, navigation chrome, and styling for the application. Using Next.js's `getLayout` pattern, layouts are applied per-page rather than globally. **Location:** `frontend/src/layouts/` **Total Files:** 2 files (~262 LOC) --- ## Architecture ``` frontend/src/layouts/ ├── Authenticated.tsx (243 LOC) # Protected routes with full UI chrome └── Guest.tsx (19 LOC) # Public routes (login, register, etc.) Related Files: ├── menuAside.ts (42 LOC) # Sidebar menu configuration ├── menuNavBar.ts (51 LOC) # Top navbar menu configuration └── pages/_app.tsx (324 LOC) # getLayout pattern implementation ``` --- ## Layout Pattern (Next.js getLayout) ### Pattern Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ _app.tsx │ │ const getLayout = Component.getLayout || ((page) => page) │ │ return getLayout() │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Page Component │ │ Page.getLayout = (page) => {page} │ └─────────────────────────────────────────────────────────────┘ ``` ### Type Definition ```typescript // In _app.tsx export type NextPageWithLayout

, IP = P> = NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; type AppPropsWithLayout = AppProps & { Component: NextPageWithLayout; }; ``` ### Usage in _app.tsx ```typescript function MyApp({ Component, pageProps }: AppPropsWithLayout) { // Use the layout defined at the page level, if available const getLayout = Component.getLayout || ((page) => page); return ( {getLayout( <> ... )} ); } ``` --- ## Layout Files ### 1. LayoutAuthenticated (`Authenticated.tsx`) **Purpose:** Wrapper for protected routes requiring authentication and optional permission checking. **Lines:** 243 LOC #### Props Interface ```typescript type Props = { children: ReactNode; permission?: string; // Required permission for this page minimal?: boolean; // If true, render only auth check (no UI chrome) }; ``` #### Features | Feature | Description | |---------|-------------| | JWT Validation | Client-side token decode and expiry check | | Auto-redirect | Redirects to `/login` if no valid token | | Permission Guard | Redirects to `/error` if permission missing | | Project Existence Check | Redirects to project creation for certain routes | | NavBar | Top navigation with search and user menu | | AsideMenu | Sidebar navigation menu | | FooterBar | Page footer | | Dark Mode | Applies dark mode styling | | Responsive | Mobile-friendly sidebar toggle | | Minimal Mode | Auth-only mode for presentations | #### Authentication Flow ``` ┌─────────────────────────────────────────────────────────────┐ │ LayoutAuthenticated │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────┐ │ Get Token from │ │ Session/Local │ │ Storage │ └────────┬────────┘ │ ┌─────────▼─────────┐ │ Token Valid? │ │ (JWT decode + │ │ expiry check) │ └────────┬──────────┘ │ ┌───────────────┴───────────────┐ │ No │ Yes ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ dispatch │ │ Set axios │ │ logoutUser() │ │ Authorization │ │ redirect to │ │ header │ │ /login │ └────────┬────────┘ └─────────────────┘ │ ┌─────────▼─────────┐ │ currentUser │ │ loaded? │ └────────┬──────────┘ │ ┌───────────────┴───────────────┐ │ No │ Yes ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ dispatch │ │ Check │ │ findMe() │ │ permissions │ └─────────────────┘ └────────┬────────┘ │ ┌─────────▼─────────┐ │ Permission OK? │ └────────┬──────────┘ │ ┌───────────────────┴────────────┐ │ No │ Yes ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ redirect to │ │ Render page │ │ /error │ │ with layout │ └─────────────────┘ └─────────────────┘ ``` #### Token Validation ```typescript const isTokenValid = (tokenToCheck?: string | null) => { if (!tokenToCheck) return false; try { const decoded = jwt.decode(tokenToCheck); if (!decoded || typeof decoded !== 'object') return false; if (typeof decoded.exp !== 'number') return true; // No expiry = valid return Date.now() / 1000 < decoded.exp; // Check expiry } catch (error) { logger.error('Failed to decode auth token:', error); return false; } }; ``` #### Routes Requiring Project Certain routes redirect to project creation if no projects exist: ```typescript const ROUTES_REQUIRING_PROJECT = [ '/access_logs', '/assets', '/asset_variants', '/presigned_url_requests', '/project_audio_tracks', '/project_memberships', '/publish_events', '/pwa_caches', '/tour_pages', ]; ``` #### Constructor Fullscreen Mode The constructor page (`/constructor`) hides navigation chrome: ```typescript const isConstructorFullscreen = router.pathname === '/constructor'; // When true: // - NavBar is hidden // - AsideMenu is hidden // - FooterBar is hidden // - No padding applied ``` #### Minimal Mode For presentations that need auth but no UI chrome: ```typescript if (minimal) { return <>{children}; } ``` #### Loading State Shows loading spinner while auth is being checked: ```typescript if (!isAuthChecked) { return (

Loading...

); } ``` #### Layout Structure ``` ┌─────────────────────────────────────────────────────────────┐ │ NavBar │ │ [☰ Mobile] [Desktop Menu] [Search] [User Menu] [Logout] │ └─────────────────────────────────────────────────────────────┘ ┌──────────────┬──────────────────────────────────────────────┐ │ │ │ │ AsideMenu │ │ │ │ Page Content │ │ - Dashboard │ (children) │ │ - Projects │ │ │ - Users │ │ │ - Profile │ │ │ - Swagger │ │ │ │ │ └──────────────┴──────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ FooterBar │ └─────────────────────────────────────────────────────────────┘ ``` --- ### 2. LayoutGuest (`Guest.tsx`) **Purpose:** Simple wrapper for public pages without authentication. **Lines:** 19 LOC #### Props Interface ```typescript type Props = { children: ReactNode; }; ``` #### Features | Feature | Description | |---------|-------------| | Dark Mode | Applies dark mode styling from Redux | | Background Color | Applies theme background color | | No Auth | No authentication required | #### Implementation ```typescript export default function LayoutGuest({ children }: Props) { const darkMode = useAppSelector((state) => state.style.darkMode); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); return (
{children}
); } ``` --- ## Menu Configuration ### Aside Menu (`menuAside.ts`) Sidebar navigation items: ```typescript const menuAside: MenuAsideItem[] = [ { href: '/dashboard', icon: icon.mdiViewDashboardOutline, label: 'Dashboard', }, { href: '/projects/projects-list', label: 'Projects', icon: icon.mdiFolder, permissions: 'READ_PROJECTS', // Permission-gated }, { href: '/element-type-defaults', label: 'Element Defaults', icon: icon.mdiPaletteSwatch, permissions: 'READ_PAGE_ELEMENTS', }, { href: '/users/users-list', label: 'Users', icon: icon.mdiAccountGroup, permissions: 'READ_USERS', }, { href: '/profile', label: 'Profile', icon: icon.mdiAccountCircle, }, { href: swaggerDocsUrl, target: '_blank', // Opens in new tab label: 'Swagger', icon: icon.mdiFileCode, permissions: 'READ_API_DOCS', }, ]; ``` ### NavBar Menu (`menuNavBar.ts`) Top navigation items: ```typescript const menuNavBar: MenuNavBarItem[] = [ { isCurrentUser: true, // User dropdown menu: [ { icon: mdiAccount, label: 'My Profile', href: '/profile' }, { isDivider: true }, { icon: mdiLogout, label: 'Log Out', isLogout: true }, ], }, { icon: mdiThemeLightDark, label: 'Light/Dark', isDesktopNoLabel: true, isToggleLightDark: true, // Dark mode toggle }, { icon: mdiLogout, label: 'Log out', isDesktopNoLabel: true, isLogout: true, }, ]; // Additional export for web pages (currently empty) export const webPagesNavBar = []; ``` --- ## Usage Examples ### Basic Authenticated Page ```typescript // pages/users/users-list.tsx import LayoutAuthenticated from '../../layouts/Authenticated'; function UsersListPage() { return (

Users

{/* page content */}
); } UsersListPage.getLayout = function getLayout(page: ReactElement) { return ( {page} ); }; export default UsersListPage; ``` ### Minimal Auth for Presentations ```typescript // pages/p/[projectSlug]/stage.tsx import LayoutAuthenticated from '../../../layouts/Authenticated'; function StagePresentation() { return ; } StagePresentation.getLayout = function getLayout(page: ReactElement) { return ( {page} ); }; ``` ### Public Page with Guest Layout ```typescript // pages/login.tsx import LayoutGuest from '../layouts/Guest'; function Login() { return ( ); } Login.getLayout = function getLayout(page: ReactElement) { return {page}; }; export default Login; ``` ### Public Page (Production Presentation) ```typescript // pages/p/[projectSlug]/index.tsx import LayoutGuest from '../../../layouts/Guest'; function ProductionPresentation() { return ; } ProductionPresentation.getLayout = function getLayout(page: ReactElement) { return {page}; }; ``` --- ## Page Layout Mapping | Route Pattern | Layout | Permission | Mode | |---------------|--------|------------|------| | `/login` | Guest | - | Public | | `/verify-email` | Guest | - | Public | | `/password-reset` | Guest | - | Public | | `/terms-of-use` | Guest | - | Public | | `/p/[slug]` | Guest | - | Public presentation | | `/p/[slug]/stage` | Authenticated | `READ_TOUR_PAGES` | Minimal (stage preview) | | `/dashboard` | Authenticated | - | Full chrome | | `/profile` | Authenticated | - | Full chrome | | `/constructor` | Authenticated | - | Fullscreen (no chrome) | | `/users/*` | Authenticated | `READ/CREATE/UPDATE_USERS` | Full chrome | | `/roles/*` | Authenticated | `READ/CREATE/UPDATE_ROLES` | Full chrome | | `/projects/*` | Authenticated | `READ/CREATE/UPDATE_PROJECTS` | Full chrome | | `/assets/*` | Authenticated | `READ/CREATE/UPDATE_ASSETS` | Full chrome | | `/tour_pages/*` | Authenticated | `READ/CREATE/UPDATE_TOUR_PAGES` | Full chrome | | `/element-type-defaults` | Authenticated | `READ_PAGE_ELEMENTS` | Full chrome | --- ## Redux State Dependencies Both layouts depend on Redux style state: ```typescript // In styleSlice.ts interface StyleState { darkMode: boolean; // Dark mode toggle bgLayoutColor: string; // Background color class // ... } // Usage in layouts const darkMode = useAppSelector((state) => state.style.darkMode); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); ``` --- ## Related Components ### NavBar Component Top navigation bar with: - Mobile hamburger menu toggle - Desktop menu button - Search component - User menu items ### AsideMenu Component Sidebar navigation with: - Permission-filtered menu items - Mobile/desktop responsive - Collapsible on route change ### FooterBar Component Page footer with copyright/credits. ### Search Component Global search functionality in NavBar. --- ## Best Practices ### 1. Always Define getLayout ```typescript // Every page should define its layout Page.getLayout = function getLayout(page: ReactElement) { return {page}; }; ``` ### 2. Use Specific Permissions ```typescript // Good - specific permission // Avoid - too broad // No permission check ``` ### 3. Use Minimal Mode for Presentations ```typescript // Presentations need auth but no UI chrome ``` ### 4. Guest Layout for Public Pages ```typescript // Public pages should use LayoutGuest {page} ``` --- ## File Inventory | File | LOC | Purpose | |------|-----|---------| | `Authenticated.tsx` | 243 | Protected route wrapper with auth, permissions, UI chrome | | `Guest.tsx` | 19 | Public route wrapper with dark mode support | | `menuAside.ts` | 42 | Sidebar menu configuration | | `menuNavBar.ts` | 51 | Top navbar menu configuration | | **Total** | **355** | | --- ## Related Documentation - [pages-module.md](./pages-module.md) - Page structure and getLayout usage - [helpers-module.md](./helpers-module.md) - hasPermission function - [stores-module.md](./stores-module.md) - authSlice and styleSlice - [types-module.md](./types-module.md) - Permission types