# 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 (
);
}
```
#### 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 (
);
}
```
---
## 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