# 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 `` tags instead of Next.js ``: - **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 `` 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 = 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 ? : ; } ``` **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>( options: UseFormSyncOptions ): UseFormSyncReturn ``` **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 {/* form fields */} ``` **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>( options: UseEditPageSyncOptions ): UseEditPageSyncReturn ``` **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>` | 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 ( ... ); }; ``` **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` | Entity counts | | loading | `boolean` | Fetch in progress | | error | `Error \| null` | Error if failed | | refetch | `() => Promise` | 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 ( {entities.map((entity) => ( ))} ); ``` **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( options: UseEntityTableOptions ): UseEntityTableReturn ``` **Parameters:** | Option | Type | Description | |--------|------|-------------| | entityName | `string` | Entity identifier | | sliceSelector | `(state: RootState) => EntitySliceState` | 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 { // 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; handleDeleteCancel: () => void; handleDeleteSelected: () => Promise; // Table operations handleRowUpdate: (id: string, data: Partial) => Promise; 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 ( <> setCurrentPage(page)} rowCount={count} /> ); ``` --- ### 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` | Trigger export | | uploadCSV | `() => Promise` | 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 ( <> setCsvFile(e.target.files?.[0] || null)} /> {error && {error}} ); ``` --- ### 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) => 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` | 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 && ( 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; pauseDownload: () => void; resumeDownload: () => void; cancelDownload: () => void; deleteOfflineData: () => Promise; checkForUpdates: () => Promise; // 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 (
{status === 'not_downloaded' && ( )} {status === 'downloading' && ( {downloadedAssets}/{totalAssets} assets )} {status === 'downloaded' && ( )}
); ``` **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` | Manual refresh | | requestPersistence | `() => Promise` | 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 ( {formatSize(usage)} / {formatSize(quota)} {isWarning && } {isCritical && } {!isPersisted && ( )} ); ``` --- ## 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` | Cached URLs | | queueLength | `number` | Pending downloads | | preloadAsset | `(url: string, priority?: number) => void` | Manual preload | | clearQueue | `() => void` | Cancel pending | | getCachedBlobUrl | `(url: string) => Promise` | Get cached blob (creates new blob URL) | | isUrlPreloaded | `(url: string) => Promise` | 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` | 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` | 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 ? ( Loading {loadedCount}/{totalCount} ({Math.round(progress)}%) ) : 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 ? ( {activeCount} downloads in progress ({totalProgress}%) {jobs.map((job) => ( ))} ) : 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` | Video element ref | | onComplete | `() => void` | Completion callback | | preloadedUrls | `Set` | Cached URLs (optional) | | videoUrl | `string` | Current video URL (optional) | | getCachedBlobUrl | `(url: string) => Promise` | Blob URL getter (optional) | **Returns:** | Property | Type | Description | |----------|------|-------------| | startReverse | `() => Promise` | 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(null); const { startReverse, stopReverse, isReversing, isBuffering } = useReversePlayback({ videoRef, onComplete: () => navigateBack(), getCachedBlobUrl, }); // Trigger reverse when back button clicked const handleBack = async () => { await startReverse(); }; return (
); ``` **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; 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; getCachedBlobUrl?: (url: string) => Promise; 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(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 ( ); ``` **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; 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` | 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` | 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 ): 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 ( ); ``` **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; preloadedUrls?: Set; } ``` **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` | 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 && (
)} // Current background with onLoad handler ``` **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 `` tags instead of Next.js ``: ```tsx // Background image example {backgroundImageUrl.startsWith('blob:') ? ( pageSwitch.markBackgroundReady()} /> ) : ( 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 `` 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( options: UsePageNavigationOptions ): UsePageNavigationResult ``` **Parameters:** ```typescript interface NavigablePage { id: string; sort_order?: number; slug?: string; } interface UsePageNavigationOptions { 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` | Per-section queues | | runBatchUpload | `(section: AssetSection, files: File[]) => Promise` | 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 (
{uploadQueues['images']?.map((item) => ( ))}
); ``` **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>` | 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` | 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` | 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` | Save current state | | saveToStage | `() => Promise` | Save dev → stage | | createPage | `() => Promise` | Create new page | | createTransition | `(params) => Promise` | 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` | 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 ( ); ``` --- ### 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` | 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` | 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

{getDurationNote(backgroundVideoUrl)}

; // "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 (
{status === 'compiling' && } {status === 'error' && }
); ``` **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` | Element whose outside clicks to detect | | ignoreRefs | `RefObject[]` | 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 ``` --- ### 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 (
Drag Handle
Content
); ``` **With Button Ignore:** ```typescript // Prevent drag when clicking buttons inside the drag handle
Title {/* Clicking this won't start drag */}
``` --- ## 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 |