# 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:**
```typescript
// Per-page layout pattern
export type NextPageWithLayout
, IP = P> = NextPage
& {
getLayout?: (page: ReactElement) => ReactNode;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page) => page);
return (
{getLayout(
<>
...
{isDev && }
>
)}
);
}
```
**Axios Interceptors:**
```typescript
// 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:**
```typescript
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 (
);
}
Login.getLayout = (page: ReactElement) => {page};
```
---
### 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:**
```typescript
const Dashboard = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const { getCount, getVisibleEntities } = useDashboardCounts(currentUser);
const visibleEntities = getVisibleEntities();
return (
{visibleEntities.map((entity) => (
))}
);
};
Dashboard.getLayout = (page) => {page};
```
---
### 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:**
```typescript
// 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):**
```typescript
const AssetsTablesPage = () => {
const { selectedProjectId } = useProjectSelector({ currentUser });
const { uploadingSections, runBatchUpload } = useAssetUploader({
selectedProjectId,
onUploadComplete: () => loadAssets(selectedProjectId),
});
return (
{ASSET_SECTIONS.map((section) => (
runBatchUpload(section, files)}
onDeleteAsset={handleDeleteAsset}
/>
))}
);
};
AssetsTablesPage.getLayout = (page) => (
{page}
);
```
#### 4.2 New Page (`[entity]-new.tsx`)
**Pattern:** Formik form with initial empty values.
```typescript
// 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 (
);
};
ProjectsNew.getLayout = (page) => (
{page}
);
```
#### 4.3 Edit Page (`[entity]-edit.tsx`)
**Pattern:** Uses `useEditPageSync` hook for data loading and form sync.
```typescript
// 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 (
);
};
EditAssetsPage.getLayout = (page) => (
{page}
);
```
#### 4.4 View Page (`[entity]-view.tsx`)
**Pattern:** Read-only display with related entity tables.
```typescript
// 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 (
{/* Main entity fields */}
{/* Related entities tables */}
Assets
...
{project?.assets_project?.map((item) => (
router.push(`/assets/assets-view/?id=${item.id}`)}>
| {item.name} |
{/* More columns */}
))}
);
};
ProjectsView.getLayout = (page) => (
{page}
);
```
#### 4.5 Table Page (`[entity]-table.tsx`)
**Pattern:** Embedded table for use within other pages.
```typescript
// 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:**
```typescript
// 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 (
{/* Project info card */}
{project?.name}
Slug: {project?.slug}
{/* Action cards grid */}
Constructor
Assets
{/* More action cards */}
{/* Publish section */}
Publish
setIsPublishModalActive(true)} />
window.open(presentationLinks.production)} />
window.open(presentationLinks.stage)} />
);
};
```
---
### 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:**
```typescript
const ConstructorPage = () => {
const router = useRouter();
const canvasRef = useRef(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 (
{/* Center: Canvas */}
{elements.map((element) => (
selectElement(element.id)}
onUpdate={updateElement}
/>
))}
{/* Right: Element Editor */}
{selectedElementId && (
)}
{/* Overlays */}
);
};
ConstructorPage.getLayout = (page) => (
{page}
);
```
---
### 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`)
```typescript
export default function ProductionPresentation() {
const router = useRouter();
const { projectSlug } = router.query;
if (!projectSlug || typeof projectSlug !== 'string') {
return Loading...
;
}
return (
);
}
ProductionPresentation.getLayout = (page) => {page};
```
#### Stage Presentation
**Location:** `pages/p/[projectSlug]/stage.tsx`
**Route:** `/p/[slug]/stage`
```typescript
export default function StagePresentation() {
const router = useRouter();
const { projectSlug } = router.query;
return (
);
}
StagePresentation.getLayout = (page) => {page};
```
**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.
```typescript
const ElementTypeDefaultsPage = () => {
const [rows, setRows] = useState([]);
useEffect(() => {
axios.get('/element-type-defaults?limit=1000').then((res) => {
setRows(res.data.rows);
});
}, []);
return (
{rows.map((item) => (
{item.name || toHumanLabel(item.element_type)}
{item.element_type} • Order {item.sort_order}
))}
);
};
ElementTypeDefaultsPage.getLayout = (page) => (
{page}
);
```
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.
```typescript
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 (
);
};
SearchView.getLayout = (page) => (
{page}
);
```
---
### 9. API Routes
Next.js API routes for server-side functionality.
#### Error Logging (`api/logError.ts`)
Stores runtime errors to JSON file for debugging.
```typescript
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:
```typescript
// Authenticated page
MyPage.getLayout = (page: ReactElement) => (
{page}
);
// Guest page (public)
LoginPage.getLayout = (page: ReactElement) => (
{page}
);
// 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:
```typescript
// Single permission
{page}
// Multiple permissions (any)
{page}
// No permission required (authenticated only)
{page}
```
**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:**
```typescript
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
```typescript
import { Formik, Form, Field } from 'formik';
const MyPage = () => {
const handleSubmit = async (values) => {
await dispatch(create(values));
router.push('/entity/entity-list');
};
return (
);
};
```
### 2. Page with Data Fetching
```typescript
const MyPage = () => {
const dispatch = useAppDispatch();
const data = useAppSelector((state) => state.entity.items);
const { id } = router.query;
useEffect(() => {
dispatch(fetch({ id }));
}, [id]);
return {data?.name}
;
};
```
### 3. Page with useEditPageSync
```typescript
const EditPage = () => {
const { values, setValues, id } = useEditPageSync({
entitySelector: (state) => state.entity.items,
fetchAction: fetch,
initialValues: { name: '', ... },
});
return (
...
);
};
```
### 4. Page with Permission Check
```typescript
MyPage.getLayout = (page) => (
{page}
);
```
---
## Related Documentation
| Document | Description |
|----------|-------------|
| [frontend-architecture.md](./frontend-architecture.md) | Overall frontend architecture |
| [hooks-reference.md](./hooks-reference.md) | Custom hooks documentation |
| [runtime-presentation.md](./runtime-presentation.md) | Runtime presentation details |
| [constructor-page-editor.md](./constructor-page-editor.md) | Constructor page details |
| [ui-elements.md](../../documentation/ui-elements.md) | UI element types |