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

33 KiB
Raw Blame History

Frontend Pages Module

Overview

The Pages module implements all Next.js pages using the Pages Router pattern. It contains 99 TypeScript files providing entry points for routing, authentication flows, entity CRUD operations, and specialized features like the visual tour builder and runtime presentations.

Location: frontend/src/pages/

Total Files: 100 TypeScript/TSX files

Router: Next.js Pages Router (not App Router)

Rendering: Client-Side Rendering (CSR) - appropriate for admin/builder applications


Architecture Diagram

pages/
│
├── _app.tsx                    # App entry point (324 LOC)
│   ├── Redux Provider
│   ├── DownloadContext Provider
│   ├── Axios interceptors
│   ├── PWA service worker
│   ├── ErrorBoundary
│   └── IntroGuide tour system
│
├── index.tsx                   # Root redirect → /
├── login.tsx                   # Auth → /login
├── forgot.tsx                  # Auth → /forgot
├── verify-email.tsx            # Auth → /verify-email
├── password-reset.tsx          # Auth → /password-reset
│
├── dashboard.tsx               # Admin home → /dashboard
├── profile.tsx                 # User profile → /profile
├── search.tsx                  # Global search → /search
├── error.tsx                   # Error page → /error
│
├── constructor.tsx             # Visual builder → /constructor (1515 LOC)
├── element-type-defaults.tsx   # Global element defaults → /element-type-defaults
├── global-transition-defaults.tsx # Global transitions → /global-transition-defaults
├── global-ui-control-defaults.tsx # Global UI controls → /global-ui-control-defaults
├── global-ui-control-defaults/[controlType].tsx # Single global UI control
├── project-element-defaults.tsx# Project element defaults → /project-element-defaults
├── project-ui-control-settings.tsx # Project UI controls → /project-ui-control-settings
│
├── privacy-policy.tsx          # Legal → /privacy-policy
├── terms-of-use.tsx            # Legal → /terms-of-use
│
├── p/[projectSlug]/            # Public presentations
│   ├── index.tsx               # Production → /p/[slug]
│   └── stage.tsx               # Stage preview → /p/[slug]/stage
│
├── api/                        # API routes
│   ├── logError.ts             # Error logging endpoint
│   └── hello.js                # Health check
│
└── [entity]/                   # 13 entity CRUD directories
    ├── [entity]-list.tsx       # List with filters
    ├── [entity]-new.tsx        # Create form
    ├── [entity]-edit.tsx       # Edit form
    ├── [entity]-view.tsx       # Read-only view
    ├── [entity]-table.tsx      # Embedded table
    └── [entityId].tsx          # Dynamic route

Page Categories

1. App Entry (_app.tsx)

Location: pages/_app.tsx Size: 324 LOC

The application entry point that wraps all pages with providers and global functionality.

Responsibilities:

Feature Description
Redux Provider Wraps app with Redux store
DownloadContext PWA download progress tracking
Axios Setup Base URL, JWT interceptors, 401 handling
PWA Registration Service worker registration (production only)
ErrorBoundary Global error catching
IntroGuide Optional tour guide system
Head Meta SEO, OG tags, PWA manifest
DevModeBadge Development mode indicator

Key Code Patterns:

// Per-page layout pattern
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode;
};

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout || ((page) => page);

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

Axios Interceptors:

// Request interceptor - attach JWT token
axios.interceptors.request.use((config) => {
  const token = sessionStorage.getItem('token') || localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor - handle 401, presigned URL failures
axios.interceptors.response.use(
  (response) => response,
  (error) => {
    // Handle presigned S3 URL failures (CORS issues)
    if (isPresignedS3Url(requestUrl) && isNetworkError) {
      disablePresignedUrls();
    }

    // Handle 401 - clear tokens, redirect to login
    if (status === 401 && !isLoginRequest) {
      sessionStorage.removeItem('token');
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

2. Authentication Pages

Page Route Layout Description
login.tsx /login Guest Email/password login with OAuth hints
forgot.tsx /forgot Guest Password reset request
verify-email.tsx /verify-email Guest Email verification callback
password-reset.tsx /password-reset Guest Password reset with token

Login Page Pattern:

export default function Login() {
  const dispatch = useAppDispatch();
  const { currentUser, isFetching, errorMessage, token } = useAppSelector(
    (state) => state.auth
  );

  // Fetch user data when token exists
  useEffect(() => {
    if (token) dispatch(findMe());
  }, [token]);

  // Redirect to dashboard if logged in
  useEffect(() => {
    if (currentUser?.id) router.push('/dashboard');
  }, [currentUser?.id]);

  const handleSubmit = async (values) => {
    await dispatch(loginUser(values));
  };

  return (
    <SectionFullScreen bg='violet'>
      <CardBox>
        <Formik initialValues={...} onSubmit={handleSubmit}>
          <Form>
            <FormField label='Login'>
              <Field name='email' />
            </FormField>
            <FormField label='Password'>
              <Field name='password' type='password' />
            </FormField>
            <BaseButton type='submit' label={isFetching ? 'Loading...' : 'Login'} />
          </Form>
        </Formik>
      </CardBox>
    </SectionFullScreen>
  );
}

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

3. Dashboard Page

Location: pages/dashboard.tsx Size: 203 LOC Route: /dashboard

Admin home page displaying entity counts and AI-generated widgets.

Features:

  • Entity count cards with permission-based visibility
  • Links to entity list pages
  • Redirects viewer-only users with private production grants to the first allowed private presentation

Key Implementation:

const Dashboard = () => {
  const { currentUser } = useAppSelector((state) => state.auth);
  const { getCount, getVisibleEntities } = useDashboardCounts(currentUser);

  const visibleEntities = getVisibleEntities();

  return (
    <SectionMain>
      <div className='grid grid-cols-1 lg:grid-cols-3 gap-6'>
        {visibleEntities.map((entity) => (
          <DashboardCard
            key={entity.key}
            href={entity.href}
            label={entity.label}
            count={getCount(entity.key)}
            iconKey={entity.icon}
          />
        ))}
      </div>
    </SectionMain>
  );
};

Dashboard.getLayout = (page) => <LayoutAuthenticated>{page}</LayoutAuthenticated>;

4. Entity CRUD Pages (78 pages)

Each of the 13 entities has 6 standardized pages following consistent patterns.

Entities:

  • users, roles, permissions
  • projects, project_memberships
  • assets, asset_variants, presigned_url_requests
  • tour_pages, project_audio_tracks
  • publish_events, pwa_caches, access_logs

4.1 List Page ([entity]-list.tsx)

Pattern: Uses createListPage factory for most entities.

Factory Usage:

// pages/users/users-list.tsx
import { createListPage } from '../../factories/createListPage';
import TableUsers from '../../components/Users/TableUsers';
import { uploadCsv, setRefetch } from '../../stores/users/usersSlice';

const filters = [
  { label: 'First Name', title: 'firstName' },
  { label: 'Last Name', title: 'lastName' },
  { label: 'E-Mail', title: 'email' },
];

export default createListPage({
  entityName: 'users',
  entityTitle: 'Users',
  TableComponent: TableUsers,
  filters,
  readPermission: 'READ_USERS',
  createPermission: 'CREATE_USERS',
  uploadCsvAction: uploadCsv,
  setRefetchAction: setRefetch,
  newItemLabel: 'Add/Invite User',
});

Generated Features:

  • Title and breadcrumbs
  • "New Item" button (permission-checked)
  • Filter button (adds filter rows)
  • Download CSV button
  • Upload CSV button with modal
  • Entity-specific table component

Custom List Page Example (assets-list.tsx):

const AssetsTablesPage = () => {
  const { selectedProjectId } = useProjectSelector({ currentUser });
  const { uploadingSections, runBatchUpload } = useAssetUploader({
    selectedProjectId,
    onUploadComplete: () => loadAssets(selectedProjectId),
  });

  return (
    <SectionMain>
      <div className='grid grid-cols-1 xl:grid-cols-2 gap-4'>
        {ASSET_SECTIONS.map((section) => (
          <AssetSectionCard
            key={section.key}
            section={section}
            assets={assetsBySection[section.key]}
            onUpload={(files) => runBatchUpload(section, files)}
            onDeleteAsset={handleDeleteAsset}
          />
        ))}
      </div>
    </SectionMain>
  );
};

AssetsTablesPage.getLayout = (page) => (
  <LayoutAuthenticated permission='READ_ASSETS'>{page}</LayoutAuthenticated>
);

4.2 New Page ([entity]-new.tsx)

Pattern: Formik form with initial empty values.

// pages/projects/projects-new.tsx
const initialValues = {
  name: '',
  slug: '',
  description: '',
  // ... all entity fields
};

const ProjectsNew = () => {
  const dispatch = useAppDispatch();
  const router = useRouter();

  const handleSubmit = async (data) => {
    await dispatch(create(data));
    await router.push('/projects/projects-list');
  };

  return (
    <SectionMain>
      <CardBox>
        <Formik initialValues={initialValues} onSubmit={handleSubmit}>
          <Form>
            <FormField label='Name'>
              <Field name='name' placeholder='Name' />
            </FormField>
            {/* More fields */}
            <BaseButtons>
              <BaseButton type='submit' label='Submit' />
              <BaseButton type='reset' label='Reset' />
              <BaseButton label='Cancel' onClick={() => router.push('...-list')} />
            </BaseButtons>
          </Form>
        </Formik>
      </CardBox>
    </SectionMain>
  );
};

ProjectsNew.getLayout = (page) => (
  <LayoutAuthenticated permission='CREATE_PROJECTS'>{page}</LayoutAuthenticated>
);

4.3 Edit Page ([entity]-edit.tsx)

Pattern: Uses useEditPageSync hook for data loading and form sync.

// pages/assets/assets-edit.tsx
const EditAssetsPage = () => {
  const router = useRouter();
  const dispatch = useAppDispatch();

  const { values: initialValues, setValues, id } = useEditPageSync({
    entitySelector: (state) => state.assets.assets,
    fetchAction: fetch,
    initialValues: initVals,
  });

  const handleSubmit = async (data) => {
    if (id) {
      await dispatch(update({ id, data }));
      await router.push('/assets/assets-list');
    }
  };

  return (
    <Formik enableReinitialize initialValues={initialValues} onSubmit={handleSubmit}>
      <Form>
        {/* Form fields */}
      </Form>
    </Formik>
  );
};

EditAssetsPage.getLayout = (page) => (
  <LayoutAuthenticated permission='UPDATE_ASSETS'>{page}</LayoutAuthenticated>
);

4.4 View Page ([entity]-view.tsx)

Pattern: Read-only display with related entity tables.

// pages/projects/projects-view.tsx
const ProjectsView = () => {
  const dispatch = useAppDispatch();
  const project = useAppSelector((state) => state.projects.projects);
  const { id } = router.query;

  useEffect(() => {
    dispatch(fetch({ id }));
  }, [id]);

  return (
    <SectionMain>
      <CardBox>
        {/* Main entity fields */}
        <div className='mb-4'>
          <p className='block font-bold mb-2'>Name</p>
          <p>{project?.name}</p>
        </div>

        {/* Related entities tables */}
        <p className='block font-bold mb-2'>Assets</p>
        <CardBox hasTable>
          <table>
            <thead>...</thead>
            <tbody>
              {project?.assets_project?.map((item) => (
                <tr key={item.id} onClick={() => router.push(`/assets/assets-view/?id=${item.id}`)}>
                  <td>{item.name}</td>
                  {/* More columns */}
                </tr>
              ))}
            </tbody>
          </table>
        </CardBox>
      </CardBox>
    </SectionMain>
  );
};

ProjectsView.getLayout = (page) => (
  <LayoutAuthenticated permission='READ_PROJECTS'>{page}</LayoutAuthenticated>
);

4.5 Table Page ([entity]-table.tsx)

Pattern: Embedded table for use within other pages.

// Minimal wrapper around the table component
// Usually just renders the table with pagination

4.6 Dynamic Route ([entityId].tsx)

Pattern: Project workspace or redirect to view page.

Project Workspace Example:

// pages/projects/[projectsId].tsx
const ProjectWorkspacePage = () => {
  const { projectsId } = router.query;
  const [project, setProject] = useState(null);

  // Load project data
  useEffect(() => {
    if (!projectId) return;
    axios.get(`/projects/${projectId}`).then((res) => setProject(res.data));
  }, [projectId]);

  return (
    <SectionMain>
      {/* Project info card */}
      <CardBox>
        <h2>{project?.name}</h2>
        <p>Slug: {project?.slug}</p>
      </CardBox>

      {/* Action cards grid */}
      <div className='grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4'>
        <CardBox>
          <h3>Constructor</h3>
          <BaseButton href={`/constructor?projectId=${projectId}`} label='Open Constructor' />
        </CardBox>
        <CardBox>
          <h3>Assets</h3>
          <BaseButton href={`/assets/assets-list?projectId=${projectId}`} label='Open Assets' />
        </CardBox>
        {/* More action cards */}
      </div>

      {/* Publish section */}
      <CardBox>
        <h3>Publish</h3>
        <BaseButtons>
          <BaseButton label='Publish to Production' onClick={() => setIsPublishModalActive(true)} />
          <BaseButton label='To Production Presentation' onClick={() => window.open(presentationLinks.production)} />
          <BaseButton label='To Stage Presentation' onClick={() => window.open(presentationLinks.stage)} />
        </BaseButtons>
      </CardBox>
    </SectionMain>
  );
};

5. Constructor Page

Location: pages/constructor.tsx Size: 1515 LOC (largest page) Route: /constructor?projectId=X

Visual tour builder with canvas-based element editing.

Features:

  • Canvas with background image/video/audio
  • Draggable positioned elements
  • Element editor panel with General/CSS/Effects tabs
  • Main constructor toolbar with Edit/Interact mode, Page actions, Elements actions, Save, Stage, Exit, and Collapse sections
  • Save and Stage toolbar buttons keep a fixed compact width and reserve the timestamp row to avoid layout shifts between one-line and two-line states
  • Page selector, reorder, create, duplicate, and delete actions
  • Element copy/paste across pages in the same presentation
  • Transition preview
  • Asset preloading with blob URL caching
  • Gallery carousel overlay with draggable buttons

Key Hooks Used:

Hook Purpose
useConstructorElements Element CRUD, selection, nested item helpers, and constructor-local element clipboard
useConstructorPageActions Page save/create/duplicate and Save to Stage operations
useCanvasElementDrag Element positioning with percentage coordinates
useTransitionPreview Transition video preview state
usePreloadOrchestrator Asset preloading with S3 presigned URLs
usePageSwitch Page navigation with background transitions
useTransitionPlayback Transition video playback coordination
useBackgroundTransition Background overlay during page switch
useIconPreload Icon asset preloading
useMediaDurationProbe Media duration extraction with caching
useDraggable Draggable panel positioning
useOutsideClick Click-outside detection for deselection

Component Structure:

const ConstructorPage = () => {
  const router = useRouter();
  const canvasRef = useRef<HTMLDivElement>(null);

  const projectId = router.query.projectId;
  const pageIdFromRoute = router.query.pageId;

  // Element management
  const {
    elements,
    getElements,
    selectedElementId,
    selectedElement,
    addElement,
    updateElement,
    removeSelectedElement,
    selectElement,
    copySelectedElement,
    pasteCopiedElement,
    canPasteElement,
  } = useConstructorElements({
    initialElements,
    elementDefaultsByType,
    allowedNavigationTypes,
  });

  // Page persistence and page creation/duplication
  const {
    saveConstructor,
    saveToStage,
    createPage,
    duplicatePage,
  } = useConstructorPageActions({
    activePageId,
    elements,
    getElements,
    pageBackground,
    onReload: handleReload,
  });

  return (
    <div className='flex h-screen'>
      <ConstructorToolbar
        pages={pages}
        activePageId={activePageId}
        onPageChange={setActivePageId}
        onAddElement={addElement}
        onCopyElement={copySelectedElement}
        onPasteElement={pasteCopiedElement}
        canCopyElement={Boolean(selectedElement)}
        canPasteElement={canPasteElement}
        onSave={saveConstructor}
        onSaveToStage={saveToStage}
      />

      {/* Center: Canvas */}
      <div ref={canvasRef} className='flex-1 relative'>
        <CanvasBackground page={currentPage} />
        {elements.map((element) => (
          <CanvasElementComponent
            key={element.id}
            element={element}
            isSelected={element.id === selectedElementId}
            onSelect={() => selectElement(element.id)}
            onUpdate={updateElement}
          />
        ))}
      </div>

      {/* Right: Element Editor */}
      {selectedElementId && (
        <ElementEditorPanel
          element={selectedElement}
          onUpdate={updateElement}
          onDelete={removeSelectedElement}
        />
      )}

      {/* Overlays */}
      <TransitionPreviewOverlay />
    </div>
  );
};

ConstructorPage.getLayout = (page) => (
  <LayoutAuthenticated permission='UPDATE_TOUR_PAGES'>{page}</LayoutAuthenticated>
);

6. Runtime Presentation Pages

Public presentation pages for viewing published tours.

Production Presentation

Location: pages/p/[projectSlug]/index.tsx Route: /p/[slug] (e.g., /p/cardiff)

export default function ProductionPresentation() {
  const router = useRouter();
  const { projectSlug } = router.query;

  if (!projectSlug || typeof projectSlug !== 'string') {
    return <div>Loading...</div>;
  }

  return (
    <RuntimePresentation projectSlug={projectSlug} environment='production' />
  );
}

ProductionPresentation.getLayout = (page) => <LayoutGuest>{page}</LayoutGuest>;

Stage Presentation

Location: pages/p/[projectSlug]/stage.tsx Route: /p/[slug]/stage

export default function StagePresentation() {
  const router = useRouter();
  const { projectSlug } = router.query;

  return (
    <RuntimePresentation projectSlug={projectSlug} environment='stage' />
  );
}

StagePresentation.getLayout = (page) => <LayoutGuest>{page}</LayoutGuest>;

Environment Distinction:

  • production - Public, no auth required
  • stage - Preview, may require auth

7. Element Defaults Pages

Global Element Type Defaults

Location: pages/element-type-defaults.tsx Route: /element-type-defaults

Lists all 11 global element type defaults.

const ElementTypeDefaultsPage = () => {
  const [rows, setRows] = useState([]);

  useEffect(() => {
    axios.get('/element-type-defaults?limit=1000').then((res) => {
      setRows(res.data.rows);
    });
  }, []);

  return (
    <SectionMain>
      <CardBox>
        {rows.map((item) => (
          <Link key={item.id} href={`/element-type-defaults/${item.id}`}>
            <p>{item.name || toHumanLabel(item.element_type)}</p>
            <p>{item.element_type}  Order {item.sort_order}</p>
          </Link>
        ))}
      </CardBox>
    </SectionMain>
  );
};

ElementTypeDefaultsPage.getLayout = (page) => (
  <LayoutAuthenticated permission='READ_PAGE_ELEMENTS'>{page}</LayoutAuthenticated>
);

The page also links to /global-transition-defaults for transition defaults and lists global UI controls as separate items linking to per-control editors.

Global Transition Defaults

Location: pages/global-transition-defaults.tsx Route: /global-transition-defaults

Dedicated editor for platform-wide page transition defaults.

Global UI Control Defaults

Location: pages/global-ui-control-defaults.tsx Route: /global-ui-control-defaults

List page for platform-wide runtime controls. Each item opens a dedicated editor at /global-ui-control-defaults/offline, /global-ui-control-defaults/fullscreen, or /global-ui-control-defaults/sound. The editor uses UiControlsSettingsForm with General Settings, CSS Styles, and Effects tabs.

Project Element Defaults

Location: pages/project-element-defaults.tsx Route: /project-element-defaults?projectId=X

Project-specific element defaults that override global defaults.

Project UI Control Settings

Location: pages/project-ui-control-settings.tsx Route: /project-ui-control-settings?projectId=X&environment=dev

Dedicated editor for per-project fullscreen, sound, and offline control overrides. The form resolves global defaults as fallback, saves project environment overrides, and can remove the project override to return to global defaults.


8. Search Page

Location: pages/search.tsx Route: /search?query=X

Global search results page.

const SearchView = () => {
  const searchQuery = router.query.query;
  const [searchResults, setSearchResults] = useState([]);

  useEffect(() => {
    axios.post('/search', { searchQuery }).then((res) => {
      setSearchResults(res.data);
    });
  }, [searchQuery]);

  // Group results by table
  const groupedResults = searchResults.reduce((acc, item) => {
    acc[item.tableName] = acc[item.tableName] || [];
    acc[item.tableName].push(item);
    return acc;
  }, {});

  return (
    <SectionMain>
      <CardBox>
        <SearchResults searchResults={groupedResults} searchQuery={searchQuery} />
      </CardBox>
    </SectionMain>
  );
};

SearchView.getLayout = (page) => (
  <LayoutAuthenticated permission='CREATE_SEARCH'>{page}</LayoutAuthenticated>
);

9. API Routes

Next.js API routes for server-side functionality.

Error Logging (api/logError.ts)

Stores runtime errors to JSON file for debugging.

export default async function handler(req, res) {
  const dataFilePath = path.join(process.cwd(), 'json/runtimeError.json');

  if (req.method === 'GET') {
    const jsonData = await fsPromises.readFile(dataFilePath, 'utf-8');
    return res.status(200).json(JSON.parse(jsonData));
  }

  if (req.method === 'POST') {
    await fsPromises.writeFile(dataFilePath, JSON.stringify(req.body));
    return res.status(200).json({ message: 'Data stored successfully' });
  }

  if (req.method === 'DELETE') {
    await fsPromises.writeFile(dataFilePath, '{}');
    return res.status(200).json({ message: 'Data deleted successfully' });
  }
}

10. Static Pages

Page Route Description
index.tsx / Redirects guests to /login and authenticated users to /projects/projects-list
error.tsx /error Error display page
privacy-policy.tsx /privacy-policy Privacy policy text
terms-of-use.tsx /terms-of-use Terms of use text
profile.tsx /profile User profile edit

Layout System

getLayout Pattern

Pages define their layout via a static getLayout property:

// Authenticated page
MyPage.getLayout = (page: ReactElement) => (
  <LayoutAuthenticated permission='READ_USERS'>
    {page}
  </LayoutAuthenticated>
);

// Guest page (public)
LoginPage.getLayout = (page: ReactElement) => (
  <LayoutGuest>{page}</LayoutGuest>
);

// No layout (full-screen)
RuntimePage.getLayout = (page: ReactElement) => page;

Layout Components

Layout Purpose Features
LayoutAuthenticated Protected pages JWT validation, permission check, NavBar, Sidebar, Footer
LayoutGuest Public pages Minimal layout, no auth required

LayoutAuthenticated Features:

  • JWT token validation (client-side decode)
  • Permission checking per route
  • Project existence guard
  • NavBar with user menu
  • AsideMenu sidebar
  • FooterBar
  • IntroGuide tour integration

Permission System

Pages declare required permissions via layout:

// Single permission
<LayoutAuthenticated permission='READ_ASSETS'>{page}</LayoutAuthenticated>

// Multiple permissions (any)
<LayoutAuthenticated permissions={['READ_ASSETS', 'READ_PROJECTS']}>{page}</LayoutAuthenticated>

// No permission required (authenticated only)
<LayoutAuthenticated>{page}</LayoutAuthenticated>

Permission Naming Convention:

  • READ_[ENTITY] - View list/details
  • CREATE_[ENTITY] - Create new records
  • UPDATE_[ENTITY] - Edit existing records
  • DELETE_[ENTITY] - Delete records

Factory Pattern

createListPage Factory

Generates standardized list pages with common functionality.

Input:

interface ListPageConfig {
  entityName: string;          // API endpoint name
  entityTitle: string;         // Page title
  TableComponent: Component;   // Table component
  filters: Filter[];           // Available filters
  readPermission: string;      // Required permission
  createPermission: string;    // For "New" button
  uploadCsvAction: AsyncThunk; // CSV upload action
  setRefetchAction: Action;    // Trigger refetch
  newItemLabel?: string;       // "New Item" button label
  cardBoxId?: string;          // For tour targeting
}

Generated Output:

  • Page component with title
  • "New Item" button (permission-checked)
  • Filter button and filter rows
  • CSV download/upload buttons
  • Table component with pagination
  • Layout with permission check

Page File Reference

By Size (Largest First)

File Size Description
constructor.tsx 1515 LOC Visual tour builder
projects/projects-view.tsx 827 LOC Project view with all relations
_app.tsx 324 LOC App entry point
privacy-policy.tsx 292 LOC Legal page
login.tsx 230 LOC Authentication
terms-of-use.tsx 205 LOC Legal page
dashboard.tsx 203 LOC Admin home
project-element-defaults.tsx 199 LOC Project defaults
profile.tsx 182 LOC User profile
projects/[projectsId].tsx 354 LOC Project workspace
project-ui-control-settings.tsx 226 LOC Project UI controls
element-type-defaults.tsx 277 LOC Defaults hub
global-transition-defaults.tsx 221 LOC Global transition defaults
global-ui-control-defaults/[controlType].tsx 153 LOC Single global UI control
global-ui-control-defaults.tsx 103 LOC Global UI controls list
search.tsx 97 LOC Search results
index.tsx 25 LOC Root redirect
forgot.tsx 85 LOC Password reset

By Category

Category Files Total LOC
Entity CRUD 78 ~4000
Authentication 4 ~400
Special Pages 10 ~3100
Static/Legal 4 ~600
API Routes 2 ~100
Runtime 2 ~60
App Entry 1 ~324
Element Defaults 8 ~1100

Routing Reference

Static Routes

Route Page Layout Permission
/ index.tsx Guest Redirect entry
/login login.tsx Guest -
/forgot forgot.tsx Guest -
/verify-email verify-email.tsx Guest -
/password-reset password-reset.tsx Guest -
/dashboard dashboard.tsx Authenticated -
/profile profile.tsx Authenticated -
/search search.tsx Authenticated CREATE_SEARCH
/error error.tsx Guest -
/constructor constructor.tsx Authenticated UPDATE_TOUR_PAGES
/element-type-defaults element-type-defaults.tsx Authenticated READ_PAGE_ELEMENTS
/global-transition-defaults global-transition-defaults.tsx Authenticated UPDATE_PAGE_ELEMENTS
/global-ui-control-defaults global-ui-control-defaults.tsx Authenticated READ_PAGE_ELEMENTS
/project-element-defaults project-element-defaults.tsx Authenticated READ_PAGE_ELEMENTS
/project-ui-control-settings project-ui-control-settings.tsx Authenticated UPDATE_PAGE_ELEMENTS
/privacy-policy privacy-policy.tsx Guest -
/terms-of-use terms-of-use.tsx Guest -

Dynamic Routes

Route Pattern Page Description
/p/[projectSlug] p/[projectSlug]/index.tsx Production presentation
/p/[projectSlug]/stage p/[projectSlug]/stage.tsx Stage presentation
/global-ui-control-defaults/[controlType] global-ui-control-defaults/[controlType].tsx Single global UI control editor
/[entity]/[entityId] [entity]/[entityId].tsx Entity workspace/redirect

Entity Routes (×13)

Entity List New Edit View
users /users/users-list /users/users-new /users/users-edit?id=X /users/users-view?id=X
roles /roles/roles-list ... ... ...
permissions /permissions/permissions-list ... ... ...
projects /projects/projects-list ... ... ...
project_memberships /project_memberships/... ... ... ...
assets /assets/assets-list ... ... ...
asset_variants /asset_variants/... ... ... ...
presigned_url_requests /presigned_url_requests/... ... ... ...
tour_pages /tour_pages/tour_pages-list ... ... ...
project_audio_tracks /project_audio_tracks/... ... ... ...
publish_events /publish_events/... ... ... ...
pwa_caches /pwa_caches/pwa_caches-list ... ... ...
access_logs /access_logs/access_logs-list ... ... ...

Common Patterns

1. Page with Formik Form

import { Formik, Form, Field } from 'formik';

const MyPage = () => {
  const handleSubmit = async (values) => {
    await dispatch(create(values));
    router.push('/entity/entity-list');
  };

  return (
    <Formik initialValues={{...}} onSubmit={handleSubmit}>
      <Form>
        <FormField label='Name'>
          <Field name='name' />
        </FormField>
        <BaseButton type='submit' label='Submit' />
      </Form>
    </Formik>
  );
};

2. Page with Data Fetching

const MyPage = () => {
  const dispatch = useAppDispatch();
  const data = useAppSelector((state) => state.entity.items);
  const { id } = router.query;

  useEffect(() => {
    dispatch(fetch({ id }));
  }, [id]);

  return <div>{data?.name}</div>;
};

3. Page with useEditPageSync

const EditPage = () => {
  const { values, setValues, id } = useEditPageSync({
    entitySelector: (state) => state.entity.items,
    fetchAction: fetch,
    initialValues: { name: '', ... },
  });

  return (
    <Formik enableReinitialize initialValues={values} ...>
      ...
    </Formik>
  );
};

4. Page with Permission Check

MyPage.getLayout = (page) => (
  <LayoutAuthenticated permission='READ_ENTITY'>
    {page}
  </LayoutAuthenticated>
);

Document Description
frontend-architecture.md Overall frontend architecture
hooks-reference.md Custom hooks documentation
runtime-presentation.md Runtime presentation details
constructor-page-editor.md Constructor page details
ui-elements.md UI element types