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

2684 lines
84 KiB
Markdown

# Frontend Hooks Reference
Complete documentation for all custom React hooks in the Tour Builder Platform frontend.
---
## Navigation
### Quick Links
| Category | Hooks |
|----------|-------|
| [Redux Integration](#redux-integration-hooks) | [useAppDispatch & useAppSelector](#useappdispatch--useappselector) |
| [Data Management](#data-management-hooks) | [useFormSync](#useformsync) · [useEditPageSync](#useeditpagesync) · [useDashboardCounts](#usedashboardcounts) · [useEntityTable](#useentitytable) · [useCSVHandling](#usecsvhandling) · [useFilterItems](#usefilteritems) · [useElementSettingsForm](#useelementsettingsform) |
| [Offline & Caching](#offline--caching-hooks) | [useOfflineMode](#useofflinemode) · [useStorageQuota](#usestoragequota) |
| [Preloading](#preloading-hooks) | [usePreloadOrchestrator](#usepreloadorchestrator) · [useNeighborGraph](#useneighborgraph) · [useNetworkAware](#usenetworkaware) · [usePWAPreload](#usepwapreload) · [usePreloadProgress](#usepreloadprogress) |
| [Media Playback](#media-playback-hooks) | [useReversePlayback](#usereverseplayback) · [useTransitionPlayback](#usetransitionplayback) · [useBackgroundTransition](#usebackgroundtransition) · [usePageDataLoader](#usepagedataloader) · [useElementEffects](#useelementeffects) |
| [Navigation](#navigation-hooks) | [usePageSwitch](#usepageswitch) · [usePageNavigation](#usepagenavigation) |
| [Asset Upload](#asset-upload-hooks) | [useAssetUploader](#useassetuploader) |
| [Constructor](#constructor-hooks) | [useConstructorElements](#useconstructorelements) · [useConstructorPageActions](#useconstructorpageactions) · [useTransitionPreview](#usetransitionpreview) · [useCanvasElapsedTime](#usecanvaselapsedtime) · [useCanvasElementDrag](#usecanvaselementdrag) · [useIconPreload](#useiconpreload) · [useMediaDurationProbe](#usemediadurationprobe) |
| [Utility](#utility-hooks) | [useDevCompilationStatus](#usedevcompilationstatus) · [useOutsideClick](#useoutsideclick) · [useDraggable](#usedraggable) |
### All Hooks (A-Z)
| Hook | Category | Used By | Description |
|------|----------|---------|-------------|
| [useAppDispatch](#useappdispatch--useappselector) | Redux | All | Type-safe Redux dispatch |
| [useAppSelector](#useappdispatch--useappselector) | Redux | All | Type-safe Redux state selection |
| [useAssetUploader](#useassetuploader) | Upload | Constructor | Batch asset uploads with progress |
| [useBackgroundTransition](#usebackgroundtransition) | Media | Runtime, Constructor | Background fade transition effects |
| [useCanvasElapsedTime](#usecanvaselapsedtime) | Constructor | Constructor | Elapsed time for element visibility |
| [useCanvasElementDrag](#usecanvaselementdrag) | Constructor | Constructor | Element drag with % positioning |
| [useConstructorElements](#useconstructorelements) | Constructor | Constructor | Element CRUD operations |
| [useConstructorPageActions](#useconstructorpageactions) | Constructor | Constructor | Page save/create/publish |
| [useCSVHandling](#usecsvhandling) | Data | List Pages | CSV import/export |
| [useDashboardCounts](#usedashboardcounts) | Data | Dashboard | Dashboard entity counts |
| [useDevCompilationStatus](#usedevcompilationstatus) | Utility | _app | Next.js dev compilation status |
| [useDraggable](#usedraggable) | Utility | Constructor | Draggable panel management |
| [useEditPageSync](#useeditpagesync) | Data | Edit Pages | Edit page data sync |
| [useElementEffects](#useelementeffects) | Media | Runtime | Element hover/focus/active effects |
| [useElementSettingsForm](#useelementsettingsform) | Data | Constructor | Element settings form state |
| [useEntityTable](#useentitytable) | Data | Table Pages | Complete table management |
| [useFilterItems](#usefilteritems) | Data | List Pages | Filter state management |
| [useFormSync](#useformsync) | Data | Edit Pages | Form sync with Redux entities |
| [useIconPreload](#useiconpreload) | Constructor | Constructor | Icon image preloading |
| [useMediaDurationProbe](#usemediadurationprobe) | Constructor | Constructor | Media duration probing |
| [useNeighborGraph](#useneighborgraph) | Preload | (internal) | Navigation graph building |
| [useNetworkAware](#usenetworkaware) | Preload | (internal) | Network condition monitoring |
| [useOfflineMode](#useofflinemode) | Offline | OfflineToggle | Project offline download |
| [useOutsideClick](#useoutsideclick) | Utility | Constructor | Click outside detection |
| [usePageDataLoader](#usepagedataloader) | Media | Runtime, Constructor | Project/page data loading |
| [usePageNavigation](#usepagenavigation) | Navigation | Runtime, Constructor | Page state with history |
| [usePageSwitch](#usepageswitch) | Navigation | Runtime, Constructor | Smooth page switching |
| [usePreloadOrchestrator](#usepreloadorchestrator) | Preload | Runtime, Constructor | Asset preload with blob URLs |
| [usePreloadProgress](#usepreloadprogress) | Preload | OfflineToggle | Preload job tracking |
| [usePWAPreload](#usepwapreload) | Preload | OfflineToggle | PWA asset caching |
| [useReversePlayback](#usereverseplayback) | Media | (internal) | Reverse video playback |
| [useStorageQuota](#usestoragequota) | Offline | OfflineToggle | Storage quota monitoring |
| [useTransitionPlayback](#usetransitionplayback) | Media | Runtime, Constructor | Transition video management |
| [useTransitionPreview](#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 `unoptimized` prop
- **Blob URLs don't benefit from optimization** - already in-memory
- **Use conditional rendering**: native `<img>` for `blob:` 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.
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
function useFilterItems(filters: Filter[]): UseFilterItemsReturn
```
**Filter Definition:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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 `pages` prop 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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
1. **Native:** `playbackRate = -1` (modern browsers)
2. **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:**
```typescript
function useTransitionPlayback(
options: UseTransitionPlaybackOptions
): UseTransitionPlaybackResult
```
**Parameters:**
```typescript
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:**
```typescript
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 only
- `reverse`: Play forward, then reverse same video
- `separate`: Play forward video, then separate reverse video
**Storage Key Lookup Priority:**
The hook uses `storageKey` for reliable cache hits across presigned URL regeneration:
1. `getReadyBlobUrl(storageKey)` - instant O(1) in-memory lookup
2. `getCachedBlobUrl(storageKey)` - Cache API lookup (post-refresh)
3. `getReadyBlobUrl(videoUrl)` - fallback by resolved URL
4. `getCachedBlobUrl(videoUrl)` - fallback by resolved URL
5. 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:**
```typescript
function useBackgroundTransition(
options: UseBackgroundTransitionOptions
): UseBackgroundTransitionResult
```
**Parameters:**
| Option | Type | Description |
|--------|------|-------------|
| pageSwitch | `PageSwitchInterface` | Page switch hook instance |
| fadeOut | `FadeOutConfig` | Optional fade-out configuration |
**FadeOutConfig (for RuntimePresentation):**
```typescript
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:**
```typescript
// 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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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 effects
- `onFocus`, `onBlur` - Focus effects
- `onMouseDown`, `onMouseUp` - Active/pressed effects
**Example:**
```typescript
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:**
```typescript
function usePageSwitch(options?: UsePageSwitchOptions): UsePageSwitchResult
```
**Parameters:**
| Option | Type | Description |
|--------|------|-------------|
| preloadCache | `PreloadCacheProvider` | Optional preload cache provider |
**PreloadCacheProvider Interface:**
```typescript
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:**
```typescript
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:**
1. Before switching, save current background as "previous"
2. Try `getReadyBlobUrl(storagePath)` - instant O(1) lookup by storage key
3. Try `getCachedBlobUrl(storagePath)` - Cache API lookup by storage key
4. Resolve to playback URL and try `getReadyBlobUrl(resolvedUrl)`
5. Try `getCachedBlobUrl(resolvedUrl)` by resolved URL
6. Fallback: load with presigned URL (retries with proxy on CORS failure)
7. Set new background and wait for Image onLoad
8. 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>`:
```tsx
// 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 `src` on 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:**
```typescript
function usePageNavigation<TPage extends NavigablePage>(
options: UsePageNavigationOptions<TPage>
): UsePageNavigationResult<TPage>
```
**Parameters:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
interface UploadQueueItem {
id: string;
fileName: string;
progress: number; // 0-100
status: 'queued' | 'uploading' | 'saving' | 'success' | 'error';
error?: string;
}
```
**Example:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
interface TransitionPreviewState {
videoUrl: string;
storageKey: string;
reverseMode: 'none' | 'reverse' | 'separate';
reverseVideoUrl?: string;
reverseStorageKey?: string;
durationSec?: number;
title: string;
}
```
**Example:**
```typescript
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:**
```typescript
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:**
```typescript
const { elapsedSec } = useCanvasElapsedTime({
pageId: activePageId,
enabled: !isLoading,
});
// Check if element should be visible
const isVisible = elapsedSec >= element.appearDelaySec;
```
**Helper Function:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
function useMediaDurationProbe(
options: UseMediaDurationProbeOptions
): UseMediaDurationProbeResult
```
**Parameters:**
| Option | Type | Description |
|--------|------|-------------|
| targets | `DurationProbeTarget[]` | Media sources to probe |
**DurationProbeTarget:**
```typescript
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:**
```typescript
const { getDurationNote, getDuration } = useMediaDurationProbe({
targets: [
{ source: backgroundVideoUrl, mediaType: 'video' },
{ source: backgroundAudioUrl, mediaType: 'audio' },
],
});
return <p>{getDurationNote(backgroundVideoUrl)}</p>; // "1:30" or ""
```
**Helper Function:**
```typescript
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:**
```typescript
function useDevCompilationStatus(): CompilationStatus
```
**Returns:** `'ready' | 'compiling' | 'error' | 'initial'`
**Example:**
```typescript
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:**
```typescript
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:**
```typescript
useOutsideClick({
containerRef: panelRef,
ignoreRefs: [buttonRef],
onOutsideClick: () => setSelectedId(''),
enabled: !!selectedId,
});
```
**With Data Attribute:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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](./runtime-presentation.md) | RuntimePresentation component - public tour viewer with transitions and offline support |
| [constructor-page-editor.md](./constructor-page-editor.md) | Constructor page - visual tour builder with element editing |
| [assets-preloading.md](../../documentation/assets-preloading.md) | Asset preloading architecture and S3 direct download flow |
| [page-transitions.md](../../documentation/page-transitions.md) | Video transitions stored on navigation elements |
| [offline-pwa-mode.md](../../documentation/offline-pwa-mode.md) | PWA offline capabilities and caching |