33 KiB
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 requiredstage- 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/detailsCREATE_[ENTITY]- Create new recordsUPDATE_[ENTITY]- Edit existing recordsDELETE_[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>
);
Related Documentation
| 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 |