84 KiB
Frontend Hooks Reference
Complete documentation for all custom React hooks in the Tour Builder Platform frontend.
Navigation
Quick Links
All Hooks (A-Z)
| Hook | Category | Used By | Description |
|---|---|---|---|
| useAppDispatch | Redux | All | Type-safe Redux dispatch |
| useAppSelector | Redux | All | Type-safe Redux state selection |
| useAssetUploader | Upload | Constructor | Batch asset uploads with progress |
| useBackgroundTransition | Media | Runtime, Constructor | Background fade transition effects |
| useCanvasElapsedTime | Constructor | Constructor | Elapsed time for element visibility |
| useCanvasElementDrag | Constructor | Constructor | Element drag with % positioning |
| useConstructorElements | Constructor | Constructor | Element CRUD operations |
| useConstructorPageActions | Constructor | Constructor | Page save/create/publish |
| useCSVHandling | Data | List Pages | CSV import/export |
| useDashboardCounts | Data | Dashboard | Dashboard entity counts |
| useDevCompilationStatus | Utility | _app | Next.js dev compilation status |
| useDraggable | Utility | Constructor | Draggable panel management |
| useEditPageSync | Data | Edit Pages | Edit page data sync |
| useElementEffects | Media | Runtime | Element hover/focus/active effects |
| useElementSettingsForm | Data | Constructor | Element settings form state |
| useEntityTable | Data | Table Pages | Complete table management |
| useFilterItems | Data | List Pages | Filter state management |
| useFormSync | Data | Edit Pages | Form sync with Redux entities |
| useIconPreload | Constructor | Constructor | Icon image preloading |
| useMediaDurationProbe | Constructor | Constructor | Media duration probing |
| useNeighborGraph | Preload | (internal) | Navigation graph building |
| useNetworkAware | Preload | (internal) | Network condition monitoring |
| useOfflineMode | Offline | OfflineToggle | Project offline download |
| useOutsideClick | Utility | Constructor | Click outside detection |
| usePageDataLoader | Media | Runtime, Constructor | Project/page data loading |
| usePageNavigation | Navigation | Runtime, Constructor | Page state with history |
| usePageSwitch | Navigation | Runtime, Constructor | Smooth page switching |
| usePreloadOrchestrator | Preload | Runtime, Constructor | Asset preload with blob URLs |
| usePreloadProgress | Preload | OfflineToggle | Preload job tracking |
| usePWAPreload | Preload | OfflineToggle | PWA asset caching |
| useReversePlayback | Media | (internal) | Reverse video playback |
| useStorageQuota | Offline | OfflineToggle | Storage quota monitoring |
| useTransitionPlayback | Media | Runtime, Constructor | Transition video management |
| useTransitionPreview | Constructor | Constructor | Transition video preview |
Legend: Runtime = RuntimePresentation.tsx, Constructor = constructor.tsx, (internal) = used by other hooks
Presentation Components Hook Usage
RuntimePresentation.tsx (public tour viewer):
usePageDataLoader ──→ usePreloadOrchestrator ──→ usePageSwitch
│
▼
useTransitionPlayback ──→ useBackgroundTransition
constructor.tsx (tour editor):
usePageDataLoader ──→ usePreloadOrchestrator ──→ usePageSwitch
│ │
▼ ▼
useConstructorElements useTransitionPlayback ──→ useBackgroundTransition
│ │
▼ ▼
useConstructorPageActions useTransitionPreview
│
▼
useCanvasElementDrag, useCanvasElapsedTime, useIconPreload, useMediaDurationProbe
Overview
The platform provides a comprehensive set of custom hooks organized into categories:
- Redux Integration - Type-safe store access
- Data Management - Form sync, table management, CSV handling
- Offline & Caching - PWA offline mode, storage quota
- Preloading - Asset preloading, neighbor graph, network awareness
- Media Playback - Video transitions, reverse playback
- Navigation - Page navigation with history
- Asset Upload - Batch upload with progress tracking
- Constructor - Element management, page actions, transitions, dragging
- Utility - Outside click detection, draggable panels
Important: Blob URL Rendering
Preloading hooks (usePreloadOrchestrator, usePageSwitch) return blob URLs for cached assets. When rendering these URLs as images, use native <img> tags instead of Next.js <Image>:
- Next.js Image re-fetches on every render - even with
unoptimizedprop - Blob URLs don't benefit from optimization - already in-memory
- Use conditional rendering: native
<img>forblob:URLs, Next.js Image for regular URLs
See usePageSwitch documentation for implementation example.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Frontend Hooks Architecture │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Redux Layer │ │
│ │ │ │
│ │ useAppDispatch ←──────────────────────────────→ useAppSelector │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Entity │ │ Entity │ │ │
│ │ │ Slices │ │ State │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ useFormSync │ │ useEntityTable │ │ useCSVHandling │ │
│ │ │ │ │ │ │ │
│ │ Form ↔ Entity │ │ Table + CRUD │ │ Import/Export │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Offline & Preload Layer │ │
│ │ │ │
│ │ useOfflineMode ──→ useStorageQuota │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ usePreloadOrchestrator ──→ useNeighborGraph │ │
│ │ │ (getReadyBlobUrl) │ │ │
│ │ │ ▼ │ │
│ │ ├──→ useNetworkAware usePWAPreload │ │
│ │ ▼ │ │
│ │ usePageSwitch (blob URL resolution + transitions) │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Media Layer │ │
│ │ │ │
│ │ useTransitionPlayback ──→ useReversePlayback │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ useBackgroundTransition ──→ usePageDataLoader │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Constructor Layer │ │
│ │ │ │
│ │ useConstructorElements ──→ useConstructorPageActions │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ useCanvasElementDrag useTransitionPreview │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ useCanvasElapsedTime useIconPreload, useMediaDurationProbe │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Utility Layer │ │
│ │ │ │
│ │ useDraggable ──→ useOutsideClick ──→ useElementEffects │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Redux Integration Hooks
useAppDispatch & useAppSelector
Source: frontend/src/stores/hooks.ts
Type-safe Redux hooks for accessing store and dispatching actions.
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Usage:
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch, update } from '../stores/users/usersSlice';
function UserList() {
const dispatch = useAppDispatch();
const users = useAppSelector((state) => state.users.users);
const loading = useAppSelector((state) => state.users.loading);
useEffect(() => {
dispatch(fetch({ query: '?limit=100' }));
}, [dispatch]);
return loading ? <Spinner /> : <Table data={users} />;
}
Benefits:
- Full TypeScript support for state shape
- Autocomplete for state paths
- Type-checked dispatch payloads
Data Management Hooks
useFormSync
Source: frontend/src/hooks/useFormSync.ts
Synchronizes form state with Redux entity data, eliminating boilerplate in edit pages.
Signature:
function useFormSync<T extends BaseEntity, TFormValues extends Record<string, unknown>>(
options: UseFormSyncOptions<T, TFormValues>
): UseFormSyncReturn<TFormValues>
Parameters:
| Option | Type | Description |
|---|---|---|
| entitySelector | (state: RootState) => T | T[] | undefined |
Redux state selector |
| fetchAction | AsyncThunk |
Thunk to fetch entity |
| initialValues | TFormValues |
Default form values |
| transformEntity | (entity: T) => TFormValues |
Optional field mapper |
Returns:
| Property | Type | Description |
|---|---|---|
| formValues | TFormValues |
Current form state |
| setFormValues | (values: TFormValues) => void |
Update form |
| isLoading | boolean |
Fetch in progress |
| entityId | string | undefined |
ID from router |
| resetForm | () => void |
Reset to initial values |
Example:
const { formValues, setFormValues, isLoading, entityId } = useFormSync({
entitySelector: (state) => state.projects.projects,
fetchAction: fetchProjects,
initialValues: {
name: '',
slug: '',
description: '',
},
transformEntity: (project) => ({
name: project.name,
slug: project.slug,
description: project.description || '',
}),
});
// In Formik
<Formik
initialValues={formValues}
enableReinitialize
onSubmit={handleSubmit}
>
{/* form fields */}
</Formik>
Key Features:
- Extracts entity ID from Next.js router query
- Handles both single entity and array state shapes
- Auto-fetches when ID is available
- Optional transform for field name mapping
useEditPageSync
Source: frontend/src/hooks/useEditPageSync.ts
Simplified hook for edit pages that syncs form state with Redux entity data. Reduces ~50 lines of boilerplate code per edit page.
Signature:
function useEditPageSync<T extends Record<string, unknown>>(
options: UseEditPageSyncOptions<T>
): UseEditPageSyncReturn<T>
Parameters:
| Option | Type | Description |
|---|---|---|
| entitySelector | (state: RootState) => unknown |
Redux selector for entity |
| fetchAction | AsyncThunk |
Thunk to fetch entity |
| initialValues | T |
Default form values |
| postProcess | (entity: T, initial: T) => T |
Optional post-processing |
| idOverride | string |
Optional ID override (defaults to router.query.id) |
Returns:
| Property | Type | Description |
|---|---|---|
| values | T |
Current form values |
| setValues | React.Dispatch<SetStateAction<T>> |
Update form values |
| id | string | null |
Entity ID from router |
| isLoading | boolean |
Fetch in progress |
| isFound | boolean |
Entity was found |
Example:
const initVals = { name: '', permissions: [] };
const EditRolesPage = () => {
const { values, id, isLoading } = useEditPageSync({
entitySelector: (state) => state.roles.roles,
fetchAction: fetch,
initialValues: initVals,
});
const handleSubmit = async (data) => {
await dispatch(update({ id, data }));
router.push('/roles/roles-list');
};
return (
<Formik enableReinitialize initialValues={values} onSubmit={handleSubmit}>
...
</Formik>
);
};
Simplified Variant:
// Returns [values, setValues] tuple for drop-in replacement
const [initialValues, setInitialValues] = useEditPageSyncSimple({
entitySelector: (state) => state.roles.roles,
fetchAction: fetch,
initialValues: initVals,
});
useDashboardCounts
Source: frontend/src/hooks/useDashboardCounts.ts
Fetches entity counts for the dashboard with permission-based filtering. Uses Promise.allSettled for resilience.
Signature:
function useDashboardCounts(currentUser: User | null): UseDashboardCountsReturn
Returns:
| Property | Type | Description |
|---|---|---|
| counts | Record<string, number | string | null> |
Entity counts |
| loading | boolean |
Fetch in progress |
| error | Error | null |
Error if failed |
| refetch | () => Promise<void> |
Manual refetch |
| getCount | (key: string) => EntityCountValue |
Get count for entity |
| getVisibleEntities | () => EntityConfig[] |
Get permitted entities |
Example:
const { counts, loading, getVisibleEntities, getCount } = useDashboardCounts(currentUser);
const entities = getVisibleEntities();
const userCount = getCount('users'); // number | 'Loading...' | null
return (
<DashboardCards>
{entities.map((entity) => (
<Card key={entity.key} count={getCount(entity.key)} />
))}
</DashboardCards>
);
Entity Configuration: Uses DASHBOARD_ENTITIES constant with 13 entities (users, roles, permissions, projects, etc.), each with permission requirements.
useEntityTable
Source: frontend/src/hooks/useEntityTable.ts
Complete table state management including pagination, sorting, filtering, selection, and CRUD.
Signature:
function useEntityTable<T extends BaseEntity>(
options: UseEntityTableOptions<T>
): UseEntityTableReturn<T>
Parameters:
| Option | Type | Description |
|---|---|---|
| entityName | string |
Entity identifier |
| sliceSelector | (state: RootState) => EntitySliceState<T> |
Redux slice selector |
| fetchAction | AsyncThunk |
Fetch thunk |
| updateAction | AsyncThunk |
Update thunk (optional) |
| deleteAction | AsyncThunk |
Delete single item thunk |
| deleteByIdsAction | AsyncThunk |
Batch delete thunk (optional) |
| setRefetchAction | (value: boolean) => Action |
Trigger refetch |
| loadColumnsFunction | Function |
Column config loader |
| filters | Filter[] |
Available filters |
| perPage | number |
Items per page (default: 10) |
Returns:
interface UseEntityTableReturn<T> {
// Data
data: T[];
columns: GridColDef[];
loading: boolean;
count: number;
// Pagination
currentPage: number;
setCurrentPage: (page: number) => void;
numPages: number;
// Sorting
sortModel: GridSortModel;
setSortModel: (model: GridSortModel) => void;
// Selection
selectedRows: string[];
setSelectedRows: (ids: string[]) => void;
// Filters
filterItems: FilterItem[];
setFilterItems: (items: FilterItem[]) => void;
filterRequest: string;
handleFilterSubmit: () => void;
handleFilterReset: () => void;
// Delete operations
isDeleteModalActive: boolean;
deleteTargetId: string | null;
handleDeleteClick: (id: string) => void;
handleDeleteConfirm: () => Promise<void>;
handleDeleteCancel: () => void;
handleDeleteSelected: () => Promise<void>;
// Table operations
handleRowUpdate: (id: string, data: Partial<T>) => Promise<void>;
loadData: (page?: number, request?: string) => void;
}
Example:
const {
data,
columns,
loading,
currentPage,
setCurrentPage,
numPages,
sortModel,
setSortModel,
filterItems,
setFilterItems,
handleFilterSubmit,
handleDeleteConfirm,
isDeleteModalActive,
} = useEntityTable({
entityName: 'assets',
sliceSelector: (state) => state.assets,
fetchAction: fetchAssets,
deleteAction: deleteAsset,
deleteByIdsAction: deleteAssetsByIds,
setRefetchAction: setRefetch,
loadColumnsFunction: configureAssetsCols,
filters: assetFilters,
perPage: 25,
});
return (
<>
<FilterBar
filters={filters}
filterItems={filterItems}
setFilterItems={setFilterItems}
onSubmit={handleFilterSubmit}
/>
<DataGrid
rows={data}
columns={columns}
loading={loading}
sortModel={sortModel}
onSortModelChange={setSortModel}
paginationModel={{ page: currentPage, pageSize: 25 }}
onPaginationModelChange={({ page }) => setCurrentPage(page)}
rowCount={count}
/>
<DeleteModal
isActive={isDeleteModalActive}
onConfirm={handleDeleteConfirm}
onCancel={handleDeleteCancel}
/>
</>
);
useCSVHandling
Source: frontend/src/hooks/useCSVHandling.ts
Manages CSV import and export operations for bulk data handling.
Signature:
function useCSVHandling(options: UseCSVHandlingOptions): UseCSVHandlingReturn
Parameters:
| Option | Type | Description |
|---|---|---|
| endpoint | string |
API endpoint (e.g., 'users') |
| uploadAction | AsyncThunk |
CSV upload thunk |
| setRefetchAction | (value: boolean) => Action |
Trigger data reload |
| fileName | string |
Export filename (optional) |
Returns:
| Property | Type | Description |
|---|---|---|
| csvFile | File | null |
Selected file |
| setCsvFile | (file: File | null) => void |
Set file |
| isModalActive | boolean |
Import modal visible |
| setIsModalActive | (active: boolean) => void |
Toggle modal |
| isUploading | boolean |
Upload in progress |
| isDownloading | boolean |
Download in progress |
| downloadCSV | () => Promise<void> |
Trigger export |
| uploadCSV | () => Promise<void> |
Trigger import |
| error | string | null |
Error message |
Example:
const {
csvFile,
setCsvFile,
isUploading,
isDownloading,
downloadCSV,
uploadCSV,
error,
} = useCSVHandling({
endpoint: 'users',
uploadAction: uploadUsersCsv,
setRefetchAction: setRefetch,
fileName: 'users-export.csv',
});
return (
<>
<Button onClick={downloadCSV} disabled={isDownloading}>
Export CSV
</Button>
<input
type="file"
accept=".csv"
onChange={(e) => setCsvFile(e.target.files?.[0] || null)}
/>
<Button onClick={uploadCSV} disabled={isUploading || !csvFile}>
Import CSV
</Button>
{error && <Alert severity="error">{error}</Alert>}
</>
);
useFilterItems
Source: frontend/src/hooks/useFilterItems.ts
Manages filter state for data tables with support for range and enum filters.
Signature:
function useFilterItems(filters: Filter[]): UseFilterItemsReturn
Filter Definition:
interface Filter {
title: string; // Field name
label: string; // Display label
number?: boolean; // Range filter (numbers)
date?: boolean; // Range filter (dates)
type?: 'enum'; // Enum filter
options?: string[]; // Enum options
}
Returns:
| Property | Type | Description |
|---|---|---|
| filterItems | FilterItem[] |
Active filters |
| setFilterItems | (items: FilterItem[]) => void |
Replace filters |
| addFilter | () => void |
Add new filter row |
| removeFilter | (id: string) => void |
Remove filter |
| updateFilter | (id: string, fields: Partial<FilterFields>) => void |
Update filter |
| resetFilters | () => void |
Clear all filters |
| generateFilterRequest | () => string |
Build query string |
| filterRequest | FilterRequest |
Filter object |
Example:
const filters: Filter[] = [
{ title: 'name', label: 'Name' },
{ title: 'created_at', label: 'Created', date: true },
{ title: 'environment', label: 'Environment', type: 'enum', options: ['dev', 'stage', 'production'] },
];
const {
filterItems,
addFilter,
removeFilter,
updateFilter,
generateFilterRequest,
resetFilters,
} = useFilterItems(filters);
// Generate query: "&name=test&created_atRange=2024-01-01&created_atRange=2024-12-31"
const queryString = generateFilterRequest();
useElementSettingsForm
Source: frontend/src/components/ElementSettings/useElementSettingsForm.ts
Form state management hook for element settings across global defaults, project defaults, and constructor pages.
Signature:
function useElementSettingsForm(
options: UseElementSettingsFormOptions
): UseElementSettingsFormReturn
interface UseElementSettingsFormOptions {
elementType: CanvasElementType | string;
}
Returns:
| Property | Type | Description |
|---|---|---|
| state | FormState |
Current form state (all settings) |
| setField | (field, value) => void |
Update single field |
| setFields | (updates) => void |
Update multiple fields |
| applySettings | (json) => void |
Load settings from JSON |
| getStyleValues | () => ElementStyleProperties |
Get CSS style values |
| buildSettingsJson | () => Record<string, unknown> |
Serialize to JSON for saving |
| isNavigationType | boolean |
Element is navigation_next/prev |
| isTooltipType | boolean |
Element is tooltip |
| isDescriptionType | boolean |
Element is description |
| isGalleryType | boolean |
Element is gallery |
| isCarouselType | boolean |
Element is carousel |
| isMediaType | boolean |
Element is video/audio player |
| addGalleryCard | () => void |
Add gallery card |
| removeGalleryCard | (id) => void |
Remove gallery card |
| updateGalleryCard | (id, field, value) => void |
Update gallery card |
| addCarouselSlide | () => void |
Add carousel slide |
| removeCarouselSlide | (id) => void |
Remove carousel slide |
| updateCarouselSlide | (id, field, value) => void |
Update carousel slide |
FormState includes:
- Common: label, xPercent, yPercent, appearDelaySec, appearDurationSec
- CSS: width, height, margin, padding, fontSize, border, borderRadius, opacity, etc.
- Navigation: iconUrl, navLabel, navType, targetPageSlug, transitionVideoUrl, etc.
- Tooltip: tooltipTitle, tooltipText
- Description: descriptionTitle, descriptionText, typography settings
- Media: mediaUrl, mediaAutoplay, mediaLoop, mediaMuted
- Gallery: galleryCards array
- Carousel: carouselSlides array, prev/next icons
Example:
const form = useElementSettingsForm({ elementType: 'navigation_next' });
// Load existing settings
useEffect(() => {
if (item?.settings_json) {
form.applySettings(item.settings_json);
}
}, [item]);
// Update field
form.setField('navLabel', 'Next Page');
// Save
const handleSave = async () => {
const settings = form.buildSettingsJson();
await axios.put(`/project-element-defaults/${id}`, {
data: { settings_json: settings },
});
};
// Render type-specific sections
{form.isNavigationType && (
<NavigationSettingsSection
navLabel={form.state.navLabel}
onChange={(field, value) => form.setField(field, value)}
/>
)}
Key Features:
- Type-aware: Shows/hides fields based on element type
- Bidirectional:
applySettings()loads JSON,buildSettingsJson()serializes - Validation: Clamps position to 0-100, normalizes CSS units
- Array management: Gallery cards and carousel slides with add/remove/update
Offline & Caching Hooks
useOfflineMode
Source: frontend/src/hooks/useOfflineMode.ts
Manages offline mode state and project download functionality with progress tracking.
Signature:
function useOfflineMode(options: UseOfflineModeOptions): UseOfflineModeResult
Parameters:
| Option | Type | Description |
|---|---|---|
| projectId | string | null |
Project to download |
| projectSlug | string |
Project slug (optional) |
| projectName | string |
Display name (optional) |
| enabled | boolean |
Enable hook (default: true) |
Returns:
interface UseOfflineModeResult {
// Status
isOfflineCapable: boolean; // Browser supports offline
isDownloaded: boolean; // Project fully cached
isDownloading: boolean; // Download in progress
status: ProjectOfflineStatus; // 'not_downloaded' | 'downloading' | 'downloaded' | 'error' | 'outdated'
progress: number; // 0-100
downloadedAssets: number;
totalAssets: number;
downloadedBytes: number;
totalBytes: number;
error: string | null;
// Actions
startDownload: () => Promise<void>;
pauseDownload: () => void;
resumeDownload: () => void;
cancelDownload: () => void;
deleteOfflineData: () => Promise<void>;
checkForUpdates: () => Promise<boolean>;
// Info
projectInfo: OfflineProject | null;
estimatedSize: number;
formatSize: (bytes: number) => string;
}
Example:
const {
isOfflineCapable,
status,
progress,
downloadedAssets,
totalAssets,
startDownload,
deleteOfflineData,
formatSize,
downloadedBytes,
} = useOfflineMode({
projectId: project?.id,
projectSlug: project?.slug,
enabled: true,
});
return (
<div>
{status === 'not_downloaded' && (
<Button onClick={startDownload}>
Download for Offline ({formatSize(estimatedSize)})
</Button>
)}
{status === 'downloading' && (
<ProgressBar value={progress}>
{downloadedAssets}/{totalAssets} assets
</ProgressBar>
)}
{status === 'downloaded' && (
<Button onClick={deleteOfflineData}>Remove Offline Data</Button>
)}
</div>
);
Key Features:
- Checks Service Worker and Cache API support
- Uses frontend asset discovery (same as online preload) - no backend manifest
- Requires
pagesprop for asset discovery - Priority-based downloads (images: 100, videos: 50, audio: 75)
- Progress via IndexedDB and DownloadEventBus
- Storage quota validation before download
- Handles partial → full download upgrades for offline completeness
useStorageQuota
Source: frontend/src/hooks/useStorageQuota.ts
Monitors storage quota and usage for offline assets.
Signature:
function useStorageQuota(): UseStorageQuotaResult
Returns:
| Property | Type | Description |
|---|---|---|
| usage | number |
Bytes used |
| quota | number | Infinity |
Total available |
| percentUsed | number |
Usage percentage |
| available | number |
Bytes remaining |
| isLoading | boolean |
Fetching quota |
| error | string | null |
Error message |
| refresh | () => Promise<void> |
Manual refresh |
| requestPersistence | () => Promise<boolean> |
Request persistent storage |
| isPersisted | boolean |
Storage is persistent |
| isWarning | boolean |
>= 80% used |
| isCritical | boolean |
>= 95% used |
| formatSize | (bytes: number) => string |
Format bytes |
| canStore | (bytes: number) => boolean |
Check if fits |
Example:
const {
usage,
quota,
percentUsed,
isWarning,
isCritical,
formatSize,
requestPersistence,
isPersisted,
} = useStorageQuota();
return (
<StorageIndicator>
<span>{formatSize(usage)} / {formatSize(quota)}</span>
{isWarning && <WarningIcon />}
{isCritical && <CriticalIcon />}
{!isPersisted && (
<Button onClick={requestPersistence}>
Enable Persistent Storage
</Button>
)}
</StorageIndicator>
);
Preloading Hooks
usePreloadOrchestrator
Source: frontend/src/hooks/usePreloadOrchestrator.ts
Used by: RuntimePresentation.tsx, constructor.tsx
Main coordinator for online mode asset preloading with priority queue management. Downloads assets from S3 presigned URLs, stores in Cache API/IndexedDB, and provides instant blob URL lookup for smooth page transitions.
Signature:
function usePreloadOrchestrator(
options: UsePreloadOrchestratorOptions
): UsePreloadOrchestratorResult
Parameters:
| Option | Type | Description |
|---|---|---|
| pages | PreloadPage[] |
All pages (filtered by environment) |
| pageLinks | PreloadPageLink[] |
Navigation links (from extractPageLinksAndElements) |
| elements | PreloadElement[] |
Page elements |
| currentPageId | string | null |
Active page |
| pageHistory | string[] |
Navigation history |
| enabled | boolean |
Enable preloading (default: true) |
| maxNeighborDepth | number |
BFS depth limit (default: 1, immediate neighbors only) |
Environment Isolation: Pages, pageLinks, and elements should be filtered by environment (dev, stage, production) before passing to this hook. This prevents preloading assets from other environments.
Returns:
| Property | Type | Description |
|---|---|---|
| isPreloading | boolean |
Downloads active |
| preloadedUrls | Set<string> |
Cached URLs |
| queueLength | number |
Pending downloads |
| preloadAsset | (url: string, priority?: number) => void |
Manual preload |
| clearQueue | () => void |
Cancel pending |
| getCachedBlobUrl | (url: string) => Promise<string | null> |
Get cached blob (creates new blob URL) |
| isUrlPreloaded | (url: string) => Promise<boolean> |
Check cache |
| getReadyBlobUrl | (url: string) => string | null |
Instant lookup - decoded blob URL (O(1)) |
Example:
const {
isPreloading,
preloadedUrls,
getCachedBlobUrl,
getReadyBlobUrl,
preloadAsset,
} = usePreloadOrchestrator({
pages,
pageLinks,
elements,
currentPageId,
enabled: !isOffline,
});
// Instant blob URL lookup (O(1), already decoded, ready to display)
const readyUrl = getReadyBlobUrl(videoUrl);
if (readyUrl) {
videoRef.current.src = readyUrl;
}
// Fallback: async cached blob (creates new blob URL, may need decode)
const blobUrl = await getCachedBlobUrl(videoUrl);
videoRef.current.src = blobUrl || videoUrl;
// Manual high-priority preload
preloadAsset('https://example.com/important.mp4', 200);
S3 Presigned URL Flow with Storage Key Mapping:
1. POST /api/file/presign { urls: [asset1, asset2, ...] } // Max 50 per batch
2. Download from presigned URLs (1-hour expiry)
3. Store in Cache API (keyed by both download URL and storage key)
4. Create blob URL: URL.createObjectURL(blob)
5. Decode if image
6. Store in readyBlobUrlsRef Map:
- keyed by download URL (presigned URL)
- keyed by storage key (canonical path, e.g., "assets/project-123/video.mp4")
- keyed by proxy URL (for fallback compatibility)
Storage Key Mapping: The orchestrator maps downloaded blobs to multiple keys for reliable cache hits:
| Key Type | Example | Purpose |
|---|---|---|
| Download URL | https://s3...?X-Amz-Signature=ABC |
Original presigned URL |
| Storage Key | assets/project-123/video.mp4 |
Canonical path (most reliable) |
| Proxy URL | /api/file/download?privateUrl=... |
Fallback compatibility |
This ensures lookups succeed regardless of which URL is used, since presigned URL signatures change on each resolution but the storage key remains constant.
useNeighborGraph
Source: frontend/src/hooks/useNeighborGraph.ts
Builds navigation graph from page links to determine which pages are neighbors and should have their assets preloaded.
Signature:
function useNeighborGraph(options: UseNeighborGraphOptions): NeighborGraphResult
Parameters:
| Option | Type | Description |
|---|---|---|
| pages | PreloadPage[] |
All pages (should be filtered by environment first) |
| pageLinks | PreloadPageLink[] |
Navigation links with from_pageId and to_pageId |
| elements | PreloadElement[] |
Page elements (for asset extraction) |
| maxDepth | number |
BFS depth (default: 1, immediate neighbors only) |
Note: Page links are extracted from ui_schema_json navigation elements using extractPageLinksAndElements() and passed as PreloadPageLink[] with resolved page IDs.
Environment Filtering: Pages and elements should be filtered by environment (dev, stage, or production) before passing to this hook to ensure preloading respects environment isolation.
Returns:
| Method | Signature | Description |
|---|---|---|
| getNeighbors | (pageId: string, depth?: number) => PreloadNeighborInfo[] |
Get neighbor pages |
| getAssetsForPages | (pageIds: string[]) => PreloadAssetInfo[] |
Get page assets |
| getPrioritizedAssets | (pageId: string, depth?: number) => PreloadAssetInfo[] |
Prioritized assets |
| adjacencyList | Map<string, string[]> |
Raw graph (debugging) |
Example:
const { getNeighbors, getPrioritizedAssets } = useNeighborGraph({
pages,
pageLinks, // Links with from_pageId, to_pageId
elements, // Elements with content_json for asset extraction
maxDepth: 1,
});
// Get neighbors within 1 hop
const neighbors = getNeighbors(currentPageId);
// Returns: [{pageId: 'page-2', distance: 1}]
// Get prioritized assets for preloading
const assets = getPrioritizedAssets(currentPageId);
// Sorted by priority: current page (1000) > neighbor (100/distance) + type bonus
useNetworkAware
Source: frontend/src/hooks/useNetworkAware.ts
Monitors network conditions and adapts preloading strategy.
Signature:
function useNetworkAware(): UseNetworkAwareResult
Returns:
| Property | Type | Description |
|---|---|---|
| networkInfo | NetworkInfo |
Connection details |
| shouldPreloadAggressively | boolean |
Good connection |
| preferLowQuality | boolean |
Slow connection |
| recommendedConcurrency | number |
Parallel downloads |
| suggestOfflineMode | boolean |
Very slow connection |
NetworkInfo:
interface NetworkInfo {
isOnline: boolean;
effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | undefined;
downlink: number | undefined; // Mbps
rtt: number | undefined; // Round-trip time ms
saveData: boolean;
}
Example:
const {
networkInfo,
shouldPreloadAggressively,
recommendedConcurrency,
preferLowQuality,
suggestOfflineMode,
} = useNetworkAware();
// Adapt preloading
const concurrency = recommendedConcurrency; // 1 for 2g, 2 for 3g, 3 for 4g
// Show offline suggestion
if (suggestOfflineMode) {
showToast('Connection is slow. Consider downloading for offline use.');
}
// Select asset quality
const videoUrl = preferLowQuality ? video.lowRes : video.highRes;
usePWAPreload
Source: frontend/src/hooks/usePWAPreload.ts
PWA asset preloading with progress tracking.
Signature:
function usePWAPreload(options: UsePWAPreloadOptions): PreloadState
Parameters:
| Option | Type | Description |
|---|---|---|
| assets | AssetToPreload[] |
Assets to preload |
| onComplete | () => void |
Completion callback |
| onError | (errors: string[]) => void |
Error callback |
| skipIfCached | boolean |
Skip if already cached (default: true) |
Returns:
| Property | Type | Description |
|---|---|---|
| isPreloading | boolean |
In progress |
| progress | number |
0-100 |
| loadedCount | number |
Completed assets |
| totalCount | number |
Total assets |
| errors | string[] |
Failed URLs |
| startPreload | () => Promise<void> |
Manual trigger |
| isCached | boolean |
All cached |
Example:
const { isPreloading, progress, loadedCount, totalCount, startPreload } = usePWAPreload({
assets: [
{ url: '/images/bg.jpg', type: 'image' },
{ url: '/videos/intro.mp4', type: 'video' },
],
onComplete: () => setShowOverlay(false),
skipIfCached: true,
});
return isPreloading ? (
<LoadingOverlay>
Loading {loadedCount}/{totalCount} ({Math.round(progress)}%)
</LoadingOverlay>
) : null;
usePreloadProgress
Source: frontend/src/hooks/usePreloadProgress.ts
Tracks preload job progress via DownloadEventBus with auto-cleanup.
Signature:
function usePreloadProgress(): UsePreloadProgressResult
Returns:
| Property | Type | Description |
|---|---|---|
| jobs | PreloadJob[] |
All tracked jobs |
| activeCount | number |
Downloading/queued jobs |
| completedCount | number |
Completed jobs |
| errorCount | number |
Failed jobs |
| totalProgress | number |
Overall progress 0-100 |
| isActive | boolean |
Any active downloads |
| clearJob | (id: string) => void |
Remove specific job |
| clearAllCompleted | () => void |
Clear completed jobs |
| clearAllErrors | () => void |
Clear errored jobs |
PreloadJob:
interface PreloadJob {
id: string;
assetId: string;
url: string;
filename: string;
progress: number;
status: 'queued' | 'downloading' | 'completed' | 'error';
bytesLoaded: number;
totalBytes: number;
addedAt: number;
startedAt?: number;
completedAt?: number;
error?: string;
}
Example:
const {
jobs,
activeCount,
totalProgress,
isActive,
clearAllCompleted,
} = usePreloadProgress();
return isActive ? (
<PreloadIndicator>
<span>{activeCount} downloads in progress ({totalProgress}%)</span>
{jobs.map((job) => (
<JobProgress key={job.id} job={job} />
))}
</PreloadIndicator>
) : null;
Key Features:
- Listens to DownloadEventBus for preload events
- Auto-removes completed jobs after configurable delay
- Auto-removes error jobs after longer delay
- Byte-based progress calculation when available
Media Playback Hooks
useReversePlayback
Source: frontend/src/hooks/useReversePlayback.ts
Handles reverse video playback with fallback strategies.
Signature:
function useReversePlayback(options: UseReversePlaybackOptions): UseReversePlaybackResult
Parameters:
| Option | Type | Description |
|---|---|---|
| videoRef | RefObject<HTMLVideoElement> |
Video element ref |
| onComplete | () => void |
Completion callback |
| preloadedUrls | Set<string> |
Cached URLs (optional) |
| videoUrl | string |
Current video URL (optional) |
| getCachedBlobUrl | (url: string) => Promise<string | null> |
Blob URL getter (optional) |
Returns:
| Property | Type | Description |
|---|---|---|
| startReverse | () => Promise<void> |
Start reverse playback |
| stopReverse | () => void |
Stop playback |
| isReversing | boolean |
Playback active |
| isBuffering | boolean |
Buffering video |
| canUseNativeReverse | boolean |
Browser supports native reverse |
Example:
const videoRef = useRef<HTMLVideoElement>(null);
const { startReverse, stopReverse, isReversing, isBuffering } = useReversePlayback({
videoRef,
onComplete: () => navigateBack(),
getCachedBlobUrl,
});
// Trigger reverse when back button clicked
const handleBack = async () => {
await startReverse();
};
return (
<div>
<video ref={videoRef} src={transitionUrl} />
{isBuffering && <Spinner />}
</div>
);
Playback Strategies:
- Native:
playbackRate = -1(modern browsers) - Frame-stepping: Fallback seeking (15-30 FPS based on cache status)
useTransitionPlayback
Source: frontend/src/hooks/useTransitionPlayback.ts
Used by: RuntimePresentation.tsx, constructor.tsx
Manages complex transition video playback with forward/reverse support. Handles blob URL resolution from preload cache for smooth seeking during reverse playback.
Signature:
function useTransitionPlayback(
options: UseTransitionPlaybackOptions
): UseTransitionPlaybackResult
Parameters:
interface UseTransitionPlaybackOptions {
videoRef: RefObject<HTMLVideoElement | null>;
transition: TransitionConfig | null;
onComplete: (targetPageId?: string) => void;
onError?: (reason: string) => void;
timeouts?: {
playbackStartMs?: number; // Default: 3000
durationBufferMs?: number; // Default: 200
hardTimeoutMs?: number; // Default: 45000
};
features?: {
useBlobUrl?: boolean;
preDecodeImages?: boolean;
getTargetPageImages?: () => string[];
};
preload?: {
preloadedUrls?: Set<string>;
getCachedBlobUrl?: (url: string) => Promise<string | null>;
getReadyBlobUrl?: (url: string) => string | null; // Instant O(1) lookup
};
}
interface TransitionConfig {
videoUrl: string; // Resolved URL (presigned or proxy) for playback
storageKey?: string; // Raw storage path for cache lookup (e.g., "assets/project-123/video.mp4")
reverseMode: 'none' | 'reverse' | 'separate';
reverseVideoUrl?: string; // For 'separate' mode
reverseStorageKey?: string; // Storage key for reverse video
durationSec?: number;
targetPageId?: string; // Resolved from targetPageSlug at navigation time
displayName?: string;
}
Note: Navigation elements store targetPageSlug in ui_schema_json. At navigation time, the slug is resolved to a page ID before being passed to this hook.
Returns:
| Property | Type | Description |
|---|---|---|
| phase | PlaybackPhase |
Current phase |
| isBuffering | boolean |
Loading video |
| isReversing | boolean |
Reverse playback |
| cancel | () => void |
Cancel transition |
| forceComplete | () => void |
Skip to end |
PlaybackPhase: 'idle' | 'preparing' | 'playing' | 'reversing' | 'finishing' | 'completed'
Example:
const videoRef = useRef<HTMLVideoElement>(null);
const { phase, isBuffering, cancel } = useTransitionPlayback({
videoRef,
transition: {
videoUrl: resolveAssetPlaybackUrl(transitionPath), // Resolved URL
storageKey: transitionPath, // Raw storage path for cache lookup
reverseMode: 'reverse',
durationSec: 1.5,
targetPageId: 'page-gallery',
},
onComplete: (targetPageId) => {
setCurrentPage(targetPageId);
},
features: {
preDecodeImages: true,
getTargetPageImages: () => getPageImages(targetPageId),
},
preload: {
preloadedUrls: preloadOrchestrator.preloadedUrls,
getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl, // Instant lookup
},
});
return (
<TransitionOverlay visible={phase !== 'idle'}>
<video ref={videoRef} muted autoPlay playsInline />
{isBuffering && <LoadingIndicator />}
<Button onClick={cancel}>Skip</Button>
</TransitionOverlay>
);
Reverse Modes:
none: Forward playback onlyreverse: Play forward, then reverse same videoseparate: Play forward video, then separate reverse video
Storage Key Lookup Priority:
The hook uses storageKey for reliable cache hits across presigned URL regeneration:
getReadyBlobUrl(storageKey)- instant O(1) in-memory lookupgetCachedBlobUrl(storageKey)- Cache API lookup (post-refresh)getReadyBlobUrl(videoUrl)- fallback by resolved URLgetCachedBlobUrl(videoUrl)- fallback by resolved URL- Network fetch - final fallback
useBackgroundTransition
Source: frontend/src/hooks/useBackgroundTransition.ts
Used by: RuntimePresentation.tsx, constructor.tsx
Manages background transition effects when switching pages. Handles fade-out animation of transition video overlay and coordinates with page switch hook.
Signature:
function useBackgroundTransition(
options: UseBackgroundTransitionOptions
): UseBackgroundTransitionResult
Parameters:
| Option | Type | Description |
|---|---|---|
| pageSwitch | PageSwitchInterface |
Page switch hook instance |
| fadeOut | FadeOutConfig |
Optional fade-out configuration |
FadeOutConfig (for RuntimePresentation):
interface FadeOutConfig {
pendingTransitionComplete: boolean; // Video finished, waiting for bg ready
isBackgroundReady: boolean; // New background loaded
transitionVideoRef: RefObject<HTMLVideoElement>;
onTransitionCleanup: () => void;
}
Returns:
| Property | Type | Description |
|---|---|---|
| isOverlayFadingOut | boolean |
Overlay currently fading |
| resetFadeOut | () => void |
Reset fade state |
Example:
// Full mode with fade-out (RuntimePresentation)
const { isOverlayFadingOut, resetFadeOut } = useBackgroundTransition({
pageSwitch,
fadeOut: {
pendingTransitionComplete,
isBackgroundReady,
transitionVideoRef,
onTransitionCleanup: () => {
setTransitionPreview(null);
setPendingTransitionComplete(false);
},
},
});
// Simple mode - direct navigation only (constructor)
useBackgroundTransition({ pageSwitch });
Modes:
- Full mode (RuntimePresentation): Fade-out animation + direct navigation clearing
- Simple mode (constructor): Direct navigation clearing only
usePageDataLoader
Source: frontend/src/hooks/usePageDataLoader.ts
Used by: RuntimePresentation.tsx, constructor.tsx
Unified hook for loading project and page data in presentation components. Handles both public access (by slug) and authenticated access (by ID).
Signature:
function usePageDataLoader(
options: UsePageDataLoaderOptions
): UsePageDataLoaderResult
Parameters:
| Option | Type | Description |
|---|---|---|
| projectId | string |
Project ID (constructor mode) |
| projectSlug | string |
Project slug (runtime mode) |
| environment | 'dev' | 'stage' | 'production' |
Environment filter |
| enabled | boolean |
Enable loading (default: true) |
| apiHeaders | Record<string, string> |
Custom API headers |
| initialPageId | string |
Initial page ID from route |
Returns:
| Property | Type | Description |
|---|---|---|
| project | RuntimeProject | null |
Loaded project |
| pages | RuntimePage[] |
Filtered and sorted pages |
| isLoading | boolean |
Loading in progress |
| error | string |
Error message |
| reload | (preservePageId?) => Promise<void> |
Reload data |
| initialPageId | string |
Selected initial page ID |
Example:
// Runtime mode (public presentation)
const { project, pages, isLoading, error } = usePageDataLoader({
projectSlug: 'my-project',
environment: 'production',
});
// Constructor mode (authenticated)
const { project, pages, isLoading, error, reload } = usePageDataLoader({
projectId: 'uuid-here',
environment: 'dev',
enabled: isAuthReady,
});
Features:
- Loads by slug (runtime) or ID (constructor)
- Filters pages by environment
- Sorts pages by sort_order
- Handles authentication errors
- Supports page preservation on reload
useElementEffects
Source: frontend/src/hooks/useElementEffects.ts
Manages element interactive effects (hover, focus, active) at runtime. Since CSS pseudo-classes don't work with inline styles, this hook handles state-based style application via JavaScript events.
Signature:
function useElementEffects(
effects: Partial<ElementEffectProperties>
): UseElementEffectsResult
Returns:
| Property | Type | Description |
|---|---|---|
| effectStyle | CSSProperties |
Style to merge with base element style |
| eventHandlers | object |
Event handlers for the element |
Event Handlers:
onMouseEnter,onMouseLeave- Hover effectsonFocus,onBlur- Focus effectsonMouseDown,onMouseUp- Active/pressed effects
Example:
const { effectStyle, eventHandlers } = useElementEffects({
hoverScale: 1.1,
hoverOpacity: 0.8,
activeScale: 0.95,
transitionDuration: '0.2s',
});
return (
<button
style={{ ...baseStyle, ...effectStyle }}
{...eventHandlers}
>
{content}
</button>
);
Effect Priority: active > focus > hover > base (highest wins)
Navigation Hooks
usePageSwitch
Source: frontend/src/hooks/usePageSwitch.ts
Used by: RuntimePresentation.tsx, constructor.tsx
Unified page switching hook that eliminates white flashes during page transitions by using preloaded blob URLs and keeping previous background visible until new one is ready.
Signature:
function usePageSwitch(options?: UsePageSwitchOptions): UsePageSwitchResult
Parameters:
| Option | Type | Description |
|---|---|---|
| preloadCache | PreloadCacheProvider |
Optional preload cache provider |
PreloadCacheProvider Interface:
interface PreloadCacheProvider {
/** Instant lookup - returns decoded blob URL ready to display (O(1)) */
getReadyBlobUrl?: (url: string) => string | null;
/** Fallback: async blob URL from cache (creates new blob URL) */
getCachedBlobUrl?: (url: string) => Promise<string | null>;
preloadedUrls?: Set<string>;
}
Returns:
| Property | Type | Description |
|---|---|---|
| currentBgImageUrl | string |
Current background image URL |
| currentBgVideoUrl | string |
Current background video URL |
| currentBgAudioUrl | string |
Current background audio URL |
| previousBgImageUrl | string |
Previous background for overlay |
| isSwitching | boolean |
Page switch in progress |
| isNewBgReady | boolean |
New background ready to display |
| switchToPage | (page, onSwitched?) => Promise<void> |
Switch to page with transition |
| setBackgroundsDirectly | (img, vid, aud) => void |
Direct set without transition |
| markBackgroundReady | () => void |
Mark background as loaded |
| clearPreviousBackground | () => void |
Clear overlay after fade |
Example:
const pageSwitch = usePageSwitch({
preloadCache: {
getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl, // Instant O(1) lookup
getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl, // Fallback
preloadedUrls: preloadOrchestrator.preloadedUrls,
},
});
// Switch to a page with smooth transition
await pageSwitch.switchToPage(targetPage, () => {
setActivePageId(targetPage.id);
});
// In render - previous background overlay fades out
{pageSwitch.previousBgImageUrl && (
<div
className='absolute inset-0 transition-opacity duration-200'
style={{
backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
opacity: pageSwitch.isNewBgReady ? 0 : 1,
}}
/>
)}
// Current background with onLoad handler
<NextImage
src={pageSwitch.currentBgImageUrl}
onLoad={pageSwitch.markBackgroundReady}
/>
Key Features:
- Instant blob URL lookup via
getReadyBlobUrl()(O(1), already decoded) - Fallback to
getCachedBlobUrl()if ready URL not available - Presigned URL fallback with automatic retry via proxy on CORS failure
- Keeps previous background visible until new one is painted
- Double RAF (requestAnimationFrame) ensures browser has painted before fade
- Automatic blob URL cleanup to prevent memory leaks
White Flash Prevention Strategy with Storage Key Priority:
- Before switching, save current background as "previous"
- Try
getReadyBlobUrl(storagePath)- instant O(1) lookup by storage key - Try
getCachedBlobUrl(storagePath)- Cache API lookup by storage key - Resolve to playback URL and try
getReadyBlobUrl(resolvedUrl) - Try
getCachedBlobUrl(resolvedUrl)by resolved URL - Fallback: load with presigned URL (retries with proxy on CORS failure)
- Set new background and wait for Image onLoad
- Fade out previous background overlay after paint confirmed
Storage Key Lookup:
The hook prioritizes storage key lookups (e.g., assets/project-123/bg.jpg) over resolved URLs because:
- Storage keys are canonical and don't change
- Presigned URL signatures change on each resolution
- Storage key mapping ensures cache hits across URL regenerations
Image Rendering for Blob URLs:
When rendering images from blob URLs, use native <img> tags instead of Next.js <Image>:
// Background image example
{backgroundImageUrl.startsWith('blob:') ? (
<img
src={backgroundImageUrl}
alt=""
className="absolute inset-0 w-full h-full object-cover"
onLoad={() => pageSwitch.markBackgroundReady()}
/>
) : (
<NextImage
src={backgroundImageUrl}
fill
sizes="100vw"
className="object-cover"
onLoad={() => pageSwitch.markBackgroundReady()}
/>
)}
Why conditional rendering:
- Next.js Image re-fetches
srcon every component re-render - Pages with frequent state updates (e.g., 100ms timers) cause thousands of requests
- Blob URLs are already in-memory and don't benefit from Next.js optimization
- Native
<img>with blob URLs is cached by browser and doesn't re-fetch
usePageNavigation
Source: frontend/src/hooks/usePageNavigation.ts
Page navigation state with optional history tracking.
Signature:
function usePageNavigation<TPage extends NavigablePage>(
options: UsePageNavigationOptions<TPage>
): UsePageNavigationResult<TPage>
Parameters:
interface NavigablePage {
id: string;
sort_order?: number;
slug?: string;
}
interface UsePageNavigationOptions<TPage> {
pages: TPage[];
defaultPageId?: string;
trackHistory?: boolean; // Default: true
onPageChange?: (pageId: string, isBack: boolean) => void;
}
Returns:
| Property | Type | Description |
|---|---|---|
| currentPageId | string | null |
Active page ID |
| currentPage | TPage | null |
Active page object |
| pageHistory | string[] |
Navigation history |
| previousPageId | string | null |
Previous page ID |
| defaultPage | TPage | null |
Initial page |
| setCurrentPageId | (pageId: string) => void |
Navigate (no history) |
| applyPageSelection | (pageId: string, isBack?: boolean) => void |
Navigate with history |
| isBackNavigation | (pageId: string) => boolean |
Check if back nav |
| goBack | () => boolean |
Go to previous page |
| resetHistory | () => void |
Clear history |
Example - Runtime with History:
const nav = usePageNavigation({
pages,
trackHistory: true,
onPageChange: (pageId, isBack) => {
analytics.track('page_view', { pageId, isBack });
},
});
// Navigate forward
nav.applyPageSelection('page-gallery');
// Navigate back
if (nav.previousPageId) {
nav.goBack();
}
// Check direction
const transitionDirection = nav.isBackNavigation(targetPageId)
? 'reverse'
: 'forward';
Example - Editor without History:
const nav = usePageNavigation({
pages,
defaultPageId: firstPageId,
trackHistory: false,
});
// Simple navigation
nav.setCurrentPageId('page-2');
Asset Upload Hooks
useAssetUploader
Source: frontend/src/components/Assets/useAssetUploader.ts
Batch asset uploads with concurrent workers and progress tracking.
Signature:
function useAssetUploader(options: UseAssetUploaderOptions): UseAssetUploaderReturn
Parameters:
| Option | Type | Description |
|---|---|---|
| selectedProjectId | string |
Target project |
| onUploadComplete | () => void |
Completion callback |
Returns:
| Property | Type | Description |
|---|---|---|
| uploadingSections | string[] |
Sections with active uploads |
| uploadQueues | Record<string, UploadQueueItem[]> |
Per-section queues |
| runBatchUpload | (section: AssetSection, files: File[]) => Promise<void> |
Start upload |
UploadQueueItem:
interface UploadQueueItem {
id: string;
fileName: string;
progress: number; // 0-100
status: 'queued' | 'uploading' | 'saving' | 'success' | 'error';
error?: string;
}
Example:
const { uploadingSections, uploadQueues, runBatchUpload } = useAssetUploader({
selectedProjectId: project?.id,
onUploadComplete: () => {
toast.success('Upload complete!');
refreshAssets();
},
});
const handleFileSelect = (files: File[]) => {
runBatchUpload(
{ key: 'images', label: 'Images', assetFormat: 'image', assetCategory: 'general' },
files
);
};
return (
<div>
<FileDropzone onDrop={handleFileSelect} />
{uploadQueues['images']?.map((item) => (
<UploadProgress key={item.id} item={item} />
))}
</div>
);
Features:
- Max 2 concurrent uploads
- 5 MB chunk size, 3 retries
- Media duration probing (video/audio)
- Automatic metadata submission
Constructor Hooks
Hooks specifically designed for the visual tour builder (constructor.tsx). These manage element CRUD, page actions, transitions, dragging, and timing.
useConstructorElements
Source: frontend/src/hooks/useConstructorElements.ts
Manages element CRUD operations with defaults merging. Core hook for element management in the constructor.
Signature:
function useConstructorElements(
options: UseConstructorElementsOptions
): UseConstructorElementsResult
Parameters:
| Option | Type | Description |
|---|---|---|
| initialElements | CanvasElement[] |
Starting elements |
| elementDefaultsByType | Record<CanvasElementType, Partial<CanvasElement>> |
Project defaults |
| allowedNavigationTypes | NavigationElementType[] |
Allowed nav types |
| onElementsChange | (elements) => void |
Change callback |
| initialSelectedElementId | string |
Initial selection from route |
| onElementSelected | (id) => void |
Selection callback |
| onSelectionCleared | () => void |
Clear selection callback |
| onElementAdded | (element) => void |
Add callback |
| onElementRemoved | (id) => void |
Remove callback |
Returns:
| Property | Type | Description |
|---|---|---|
| elements | CanvasElement[] |
Current elements |
| setElements | Dispatch<SetStateAction> |
Set elements directly |
| selectedElementId | string |
Selected element ID |
| selectedElement | CanvasElement | null |
Selected element |
| selectElement | (id) => void |
Select element |
| clearSelection | () => void |
Clear selection |
| addElement | (type) => void |
Add new element |
| updateSelectedElement | (patch) => void |
Update selected |
| updateElement | (id, patch) => void |
Update by ID |
| removeSelectedElement | () => void |
Remove selected |
| removeElement | (id) => void |
Remove by ID |
| galleryCards | {add, update, remove} |
Gallery operations |
| carouselSlides | {add, update, remove} |
Carousel operations |
| updateElementPosition | (id, x, y) => void |
Update position |
| normalizeNavigationType | (el, type) => CanvasElement |
Normalize nav type |
Example:
const {
elements,
selectedElement,
selectElement,
addElement,
updateSelectedElement,
removeSelectedElement,
} = useConstructorElements({
initialElements: parsedElements,
elementDefaultsByType: projectDefaults,
allowedNavigationTypes: ['navigation_next', 'navigation_prev'],
});
// Add new element
addElement('hotspot');
// Update selected element
updateSelectedElement({ label: 'New Label', xPercent: 50 });
useConstructorPageActions
Source: frontend/src/hooks/useConstructorPageActions.ts
Handles page create/save/publish operations in the constructor.
Signature:
function useConstructorPageActions(
options: UseConstructorPageActionsOptions
): UseConstructorPageActionsResult
Parameters:
| Option | Type | Description |
|---|---|---|
| projectId | string |
Current project ID |
| pages | TourPage[] |
All pages |
| activePage | TourPage | null |
Current page |
| activePageId | string |
Current page ID |
| elements | CanvasElement[] |
Current elements |
| backgroundImageUrl | string |
Background image |
| backgroundVideoUrl | string |
Background video |
| backgroundAudioUrl | string |
Background audio |
| onReload | (preservePageId?) => Promise<void> |
Reload callback |
| onSetActivePageId | (id) => void |
Set active page |
| onSetMenuOpen | (open) => void |
Set menu open |
| onError | (message) => void |
Error callback |
| onSuccess | (message) => void |
Success callback |
Returns:
| Property | Type | Description |
|---|---|---|
| isSaving | boolean |
Save in progress |
| isSavingToStage | boolean |
Stage save in progress |
| isCreatingPage | boolean |
Page creation in progress |
| isCreatingTransition | boolean |
Transition creation in progress |
| saveConstructor | () => Promise<void> |
Save current state |
| saveToStage | () => Promise<void> |
Save dev → stage |
| createPage | () => Promise<void> |
Create new page |
| createTransition | (params) => Promise<void> |
Create transition (legacy) |
Example:
const {
isSaving,
saveConstructor,
createPage,
isCreatingPage,
} = useConstructorPageActions({
projectId,
pages,
activePage,
activePageId,
elements,
backgroundImageUrl,
backgroundVideoUrl,
backgroundAudioUrl,
onReload: loadData,
onSetActivePageId: setActivePageId,
onError: setErrorMessage,
onSuccess: setSuccessMessage,
});
// Save changes
await saveConstructor();
// Create new page
await createPage();
useTransitionPreview
Source: frontend/src/hooks/useTransitionPreview.ts
Manages transition video preview state in the constructor. Used to preview forward and reverse transitions before navigation.
Signature:
function useTransitionPreview(
options: UseTransitionPreviewOptions
): UseTransitionPreviewResult
Parameters:
| Option | Type | Description |
|---|---|---|
| isNavigationElementType | (type) => boolean |
Type checker |
| onError | (message) => void |
Error callback |
Returns:
| Property | Type | Description |
|---|---|---|
| preview | TransitionPreviewState | null |
Current preview |
| pendingPageId | string |
Target page ID |
| openPreview | (element, direction) => void |
Open preview |
| openPreviewWithTarget | (element, direction, pageId) => void |
Open with target |
| closePreview | () => void |
Close preview |
| isActive | boolean |
Preview active |
TransitionPreviewState:
interface TransitionPreviewState {
videoUrl: string;
storageKey: string;
reverseMode: 'none' | 'reverse' | 'separate';
reverseVideoUrl?: string;
reverseStorageKey?: string;
durationSec?: number;
title: string;
}
Example:
const {
preview,
pendingPageId,
openPreviewWithTarget,
closePreview,
isActive,
} = useTransitionPreview({
isNavigationElementType,
onError: setErrorMessage,
});
// Open preview when clicking element
const handleClick = () => {
openPreviewWithTarget(element, 'forward', targetPage.id);
};
// Use with useTransitionPlayback
useTransitionPlayback({
transition: preview,
onComplete: (targetId) => {
switchToPage(targetId);
closePreview();
},
});
useCanvasElapsedTime
Source: frontend/src/hooks/useCanvasElapsedTime.ts
Tracks elapsed time since page load for element visibility timing.
Signature:
function useCanvasElapsedTime(
options: UseCanvasElapsedTimeOptions
): UseCanvasElapsedTimeResult
Parameters:
| Option | Type | Description |
|---|---|---|
| pageId | string |
Current page ID (resets timer) |
| enabled | boolean |
Timer active (default: true) |
| intervalMs | number |
Update interval (default: 100ms) |
Returns:
| Property | Type | Description |
|---|---|---|
| elapsedSec | number |
Elapsed time in seconds |
| reset | () => void |
Reset to zero |
| startedAt | number |
Start timestamp |
Example:
const { elapsedSec } = useCanvasElapsedTime({
pageId: activePageId,
enabled: !isLoading,
});
// Check if element should be visible
const isVisible = elapsedSec >= element.appearDelaySec;
Helper Function:
import { isElementVisibleAtTime } from '../hooks/useCanvasElapsedTime';
const visible = isElementVisibleAtTime(
elapsedSec,
element.appearDelaySec, // delay before appearing
element.appearDurationSec, // duration visible (null = infinite)
);
useCanvasElementDrag
Source: frontend/src/hooks/useCanvasElementDrag.ts
Handles dragging of canvas elements with percentage-based positioning.
Signature:
function useCanvasElementDrag(
options: UseCanvasElementDragOptions
): UseCanvasElementDragResult
Parameters:
| Option | Type | Description |
|---|---|---|
| canvasRef | RefObject<HTMLElement> |
Canvas container ref |
| onPositionChange | (id, x, y) => void |
Position update callback |
| enabled | boolean |
Dragging enabled (default: true) |
Returns:
| Property | Type | Description |
|---|---|---|
| onElementDragStart | (event, id, x, y) => void |
Start drag handler |
| isDragging | boolean |
Currently dragging |
| draggedElementId | string | null |
Dragged element ID |
| cancelDrag | () => void |
Cancel drag |
Example:
const { onElementDragStart, isDragging } = useCanvasElementDrag({
canvasRef,
onPositionChange: (id, x, y) => {
elementsHook.updateElementPosition(id, x, y);
},
enabled: isEditMode,
});
return (
<button
onMouseDown={(e) => onElementDragStart(
e, element.id, element.xPercent, element.yPercent
)}
>
{element.label}
</button>
);
useIconPreload
Source: frontend/src/hooks/useIconPreload.ts
Preloads icon images for smooth rendering without flash.
Signature:
function useIconPreload(
options: UseIconPreloadOptions
): UseIconPreloadResult
Parameters:
| Option | Type | Description |
|---|---|---|
| iconUrls | string[] |
URLs to preload |
| enabled | boolean |
Preloading enabled (default: true) |
Returns:
| Property | Type | Description |
|---|---|---|
| preloadedUrlMap | Record<string, boolean> |
Preload status map |
| isPreloaded | (url) => boolean |
Check if URL preloaded |
| pendingCount | number |
Icons being preloaded |
| allPreloaded | boolean |
All icons ready |
Example:
const iconUrls = elements
.filter(el => el.iconUrl)
.map(el => resolveAssetPlaybackUrl(el.iconUrl));
const { isPreloaded } = useIconPreload({ iconUrls });
// Only render element if icon is ready
if (element.iconUrl && !isPreloaded(element.iconUrl)) return null;
Helper Function:
import { buildIconPreloadTargets } from '../hooks/useIconPreload';
const targets = buildIconPreloadTargets(elements, {
isNavigationElementType,
isTooltipElementType,
isDescriptionElementType,
});
useMediaDurationProbe
Source: frontend/src/hooks/useMediaDurationProbe.ts
Probes media durations with caching and deduplication.
Signature:
function useMediaDurationProbe(
options: UseMediaDurationProbeOptions
): UseMediaDurationProbeResult
Parameters:
| Option | Type | Description |
|---|---|---|
| targets | DurationProbeTarget[] |
Media sources to probe |
DurationProbeTarget:
interface DurationProbeTarget {
source: string;
mediaType: 'video' | 'audio';
}
Returns:
| Property | Type | Description |
|---|---|---|
| durationBySource | Record<string, number | null> |
Duration map |
| getDuration | (source) => number | null |
Get duration |
| getDurationNote | (source) => string |
Get formatted note |
| isProbing | boolean |
Probes in progress |
Example:
const { getDurationNote, getDuration } = useMediaDurationProbe({
targets: [
{ source: backgroundVideoUrl, mediaType: 'video' },
{ source: backgroundAudioUrl, mediaType: 'audio' },
],
});
return <p>{getDurationNote(backgroundVideoUrl)}</p>; // "1:30" or ""
Helper Function:
import { buildDurationProbeTargets } from '../hooks/useMediaDurationProbe';
const targets = buildDurationProbeTargets({
backgroundVideoUrl,
backgroundAudioUrl,
selectedElement,
elements,
isMediaElementType,
isVideoPlayerElementType,
isNavigationElementType,
});
Utility Hooks
useDevCompilationStatus
Source: frontend/src/hooks/useDevCompilationStatus.ts
Tracks Next.js compilation status in development mode.
Signature:
function useDevCompilationStatus(): CompilationStatus
Returns: 'ready' | 'compiling' | 'error' | 'initial'
Example:
const status = useDevCompilationStatus();
return (
<div>
{status === 'compiling' && <TopLoadingBar />}
{status === 'error' && <CompilationErrorBanner />}
<MainContent />
</div>
);
Note: Returns 'ready' in production mode.
useOutsideClick
Source: frontend/src/hooks/useOutsideClick.ts
Detects clicks outside specified elements to clear selection. Useful for closing panels, deselecting elements, etc.
Signature:
function useOutsideClick(options: UseOutsideClickOptions): void
Parameters:
| Option | Type | Description |
|---|---|---|
| containerRef | RefObject<HTMLElement> |
Element whose outside clicks to detect |
| ignoreRefs | RefObject<HTMLElement>[] |
Additional refs to ignore |
| ignoreDataAttribute | string |
Data attribute to check |
| selectedValue | string |
Current selected value |
| onOutsideClick | () => void |
Click outside callback |
| enabled | boolean |
Hook active (default: true) |
Example:
useOutsideClick({
containerRef: panelRef,
ignoreRefs: [buttonRef],
onOutsideClick: () => setSelectedId(''),
enabled: !!selectedId,
});
With Data Attribute:
useOutsideClick({
containerRef: canvasRef,
ignoreDataAttribute: 'data-element-id',
selectedValue: selectedElementId,
onOutsideClick: () => clearSelection(),
enabled: !!selectedElementId,
});
// In element render
<button data-element-id={element.id}>...</button>
useDraggable
Source: frontend/src/hooks/useDraggable.ts
Generic draggable panel management with pointer tracking. Used for draggable controls, menus, and editor panels.
Signature:
function useDraggable(options?: UseDraggableOptions): UseDraggableResult
Parameters:
| Option | Type | Description |
|---|---|---|
| initialPosition | Position |
Starting position |
| minX | number |
Minimum x (default: 0) |
| minY | number |
Minimum y (default: 0) |
| maxX | number |
Maximum x (auto-calculated from window) |
| maxY | number |
Maximum y (auto-calculated from window) |
| elementWidth | number |
Element width for bounds |
| elementHeight | number |
Element height for bounds |
Returns:
| Property | Type | Description |
|---|---|---|
| position | Position |
Current position {x, y} |
| setPosition | (pos) => void |
Set position directly |
| isDragging | boolean |
Currently dragging |
| onDragStart | (event) => void |
Drag start handler |
| onDragStartIgnoreButtons | (event) => void |
Drag start (ignores buttons) |
Example:
const { position, onDragStart, isDragging } = useDraggable({
initialPosition: { x: 20, y: 20 },
elementWidth: 400,
});
return (
<div
style={{
position: 'fixed',
left: position.x,
top: position.y,
}}
>
<div
className="drag-handle"
onMouseDown={onDragStart}
>
Drag Handle
</div>
<div>Content</div>
</div>
);
With Button Ignore:
// Prevent drag when clicking buttons inside the drag handle
<div onMouseDown={onDragStartIgnoreButtons}>
<span>Title</span>
<button>Close</button> {/* Clicking this won't start drag */}
</div>
Quick Reference Table
| Hook | Category | Primary Use Case |
|---|---|---|
useAppDispatch |
Redux | Type-safe dispatch |
useAppSelector |
Redux | Type-safe state selection |
useFormSync |
Data | Sync forms with entities |
useEditPageSync |
Data | Edit page data sync |
useDashboardCounts |
Data | Dashboard entity counts |
useEntityTable |
Data | Complete table management |
useCSVHandling |
Data | CSV import/export |
useFilterItems |
Data | Filter state management |
useElementSettingsForm |
Data | Element settings form state |
useOfflineMode |
Offline | Project offline download |
useStorageQuota |
Offline | Storage monitoring |
usePreloadOrchestrator |
Preload | Asset preload with ready blob URLs |
useNeighborGraph |
Preload | Navigation graph building |
useNetworkAware |
Preload | Network condition monitoring |
usePWAPreload |
Preload | PWA asset caching |
usePreloadProgress |
Preload | Preload job tracking |
useReversePlayback |
Media | Reverse video playback |
useTransitionPlayback |
Media | Transition video management |
useBackgroundTransition |
Media | Background fade transition |
usePageDataLoader |
Media | Project/page data loading |
useElementEffects |
Media | Element interactive effects |
usePageSwitch |
Navigation | Smooth page switching without flash |
usePageNavigation |
Navigation | Page state with history |
useAssetUploader |
Upload | Batch asset uploads |
useConstructorElements |
Constructor | Element CRUD operations |
useConstructorPageActions |
Constructor | Page save/create/publish |
useTransitionPreview |
Constructor | Transition video preview |
useCanvasElapsedTime |
Constructor | Elapsed time tracking |
useCanvasElementDrag |
Constructor | Element drag positioning |
useIconPreload |
Constructor | Icon image preloading |
useMediaDurationProbe |
Constructor | Media duration probing |
useDevCompilationStatus |
Utility | Dev mode compilation |
useOutsideClick |
Utility | Click outside detection |
useDraggable |
Utility | Draggable panel management |
File Structure
frontend/src/
├── hooks/
│ ├── index.ts # Central exports
│ │
│ │ # Data Management
│ ├── useFormSync.ts
│ ├── useEditPageSync.ts # Edit page data sync
│ ├── useDashboardCounts.ts # Dashboard entity counts
│ ├── useEntityTable.ts
│ ├── useCSVHandling.ts
│ ├── useFilterItems.ts
│ │
│ │ # Offline & Caching
│ ├── useOfflineMode.ts
│ ├── useStorageQuota.ts
│ │
│ │ # Preloading
│ ├── usePreloadOrchestrator.ts
│ ├── useNeighborGraph.ts
│ ├── useNetworkAware.ts
│ ├── usePWAPreload.ts
│ ├── usePreloadProgress.ts
│ │
│ │ # Media Playback
│ ├── useReversePlayback.ts
│ ├── useTransitionPlayback.ts
│ ├── useBackgroundTransition.ts # Background fade transition
│ ├── usePageDataLoader.ts # Project/page loading
│ ├── useElementEffects.ts # Interactive effects
│ │
│ │ # Navigation
│ ├── usePageSwitch.ts
│ ├── usePageNavigation.ts
│ │
│ │ # Constructor-Specific
│ ├── useConstructorElements.ts # Element CRUD
│ ├── useConstructorPageActions.ts # Page save/create/publish
│ ├── useTransitionPreview.ts # Transition video preview
│ ├── useCanvasElapsedTime.ts # Elapsed time tracking
│ ├── useCanvasElementDrag.ts # Element drag positioning
│ ├── useIconPreload.ts # Icon image preloading
│ ├── useMediaDurationProbe.ts # Media duration probing
│ │
│ │ # Utility
│ ├── useDevCompilationStatus.ts
│ ├── useOutsideClick.ts # Click outside detection
│ └── useDraggable.ts # Draggable panel management
│
├── stores/
│ └── hooks.ts (useAppDispatch, useAppSelector)
│
└── components/
├── Assets/
│ └── useAssetUploader.ts
└── ElementSettings/
└── useElementSettingsForm.ts
Related Documentation
| Document | Description |
|---|---|
| runtime-presentation.md | RuntimePresentation component - public tour viewer with transitions and offline support |
| constructor-page-editor.md | Constructor page - visual tour builder with element editing |
| assets-preloading.md | Asset preloading architecture and S3 direct download flow |
| page-transitions.md | Video transitions stored on navigation elements |
| offline-pwa-mode.md | PWA offline capabilities and caching |