39948-vm/frontend/docs/layouts-module.md
2026-07-03 16:11:24 +02:00

19 KiB

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(<Component {...pageProps} />)             │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Page Component                           │
│  Page.getLayout = (page) => <Layout>{page}</Layout>         │
└─────────────────────────────────────────────────────────────┘

Type Definition

// In _app.tsx
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode;
};

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout;
};

Usage in _app.tsx

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page) => page);

  return (
    <Provider store={store}>
      <DownloadProvider>
        {getLayout(
          <>
            <Head>...</Head>
            <ErrorBoundary>
              <Component {...pageProps} />
            </ErrorBoundary>
          </>
        )}
      </DownloadProvider>
    </Provider>
  );
}

Layout Files

1. LayoutAuthenticated (Authenticated.tsx)

Purpose: Wrapper for protected routes requiring authentication and optional permission checking.

Lines: 243 LOC

Props Interface

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

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:

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:

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:

if (minimal) {
  return <>{children}</>;
}

Loading State

Shows loading spinner while auth is being checked:

if (!isAuthChecked) {
  return (
    <div className={`${darkMode ? 'dark' : ''} ...`}>
      <div className='animate-spin rounded-full h-8 w-8 ...'></div>
      <p>Loading...</p>
    </div>
  );
}

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

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

export default function LayoutGuest({ children }: Props) {
  const darkMode = useAppSelector((state) => state.style.darkMode);
  const bgColor = useAppSelector((state) => state.style.bgLayoutColor);

  return (
    <div className={darkMode ? 'dark' : ''}>
      <div className={`${bgColor} dark:bg-slate-800 dark:text-slate-100`}>
        {children}
      </div>
    </div>
  );
}

Menu Configuration

Aside Menu (menuAside.ts)

Sidebar navigation items:

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:

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

// pages/users/users-list.tsx
import LayoutAuthenticated from '../../layouts/Authenticated';

function UsersListPage() {
  return (
    <SectionMain>
      <h1>Users</h1>
      {/* page content */}
    </SectionMain>
  );
}

UsersListPage.getLayout = function getLayout(page: ReactElement) {
  return (
    <LayoutAuthenticated permission='READ_USERS'>
      {page}
    </LayoutAuthenticated>
  );
};

export default UsersListPage;

Minimal Auth for Presentations

// pages/p/[projectSlug]/stage.tsx
import LayoutAuthenticated from '../../../layouts/Authenticated';

function StagePresentation() {
  return <RuntimePresentation mode="stage" />;
}

StagePresentation.getLayout = function getLayout(page: ReactElement) {
  return (
    <LayoutAuthenticated permission='READ_TOUR_PAGES' minimal>
      {page}
    </LayoutAuthenticated>
  );
};

Public Page with Guest Layout

// pages/login.tsx
import LayoutGuest from '../layouts/Guest';

function Login() {
  return (
    <SectionMain>
      <LoginForm />
    </SectionMain>
  );
}

Login.getLayout = function getLayout(page: ReactElement) {
  return <LayoutGuest>{page}</LayoutGuest>;
};

export default Login;

Public Page (Production Presentation)

// pages/p/[projectSlug]/index.tsx
import LayoutGuest from '../../../layouts/Guest';

function ProductionPresentation() {
  return <RuntimePresentation mode="production" />;
}

ProductionPresentation.getLayout = function getLayout(page: ReactElement) {
  return <LayoutGuest>{page}</LayoutGuest>;
};

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:

// 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);

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

// Every page should define its layout
Page.getLayout = function getLayout(page: ReactElement) {
  return <LayoutAuthenticated permission='...'>{page}</LayoutAuthenticated>;
};

2. Use Specific Permissions

// Good - specific permission
<LayoutAuthenticated permission='UPDATE_USERS'>

// Avoid - too broad
<LayoutAuthenticated>  // No permission check

3. Use Minimal Mode for Presentations

// Presentations need auth but no UI chrome
<LayoutAuthenticated permission='READ_TOUR_PAGES' minimal>

4. Guest Layout for Public Pages

// Public pages should use LayoutGuest
<LayoutGuest>{page}</LayoutGuest>

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