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

84 KiB

Frontend Hooks Reference

Complete documentation for all custom React hooks in the Tour Builder Platform frontend.


Navigation

Category Hooks
Redux Integration useAppDispatch & useAppSelector
Data Management useFormSync · useEditPageSync · useDashboardCounts · useEntityTable · useCSVHandling · useFilterItems · useElementSettingsForm
Offline & Caching useOfflineMode · useStorageQuota
Preloading usePreloadOrchestrator · useNeighborGraph · useNetworkAware · usePWAPreload · usePreloadProgress
Media Playback useReversePlayback · useTransitionPlayback · useBackgroundTransition · usePageDataLoader · useElementEffects
Navigation usePageSwitch · usePageNavigation
Asset Upload useAssetUploader
Constructor useConstructorElements · useConstructorPageActions · useTransitionPreview · useCanvasElapsedTime · useCanvasElementDrag · useIconPreload · useMediaDurationProbe
Utility useDevCompilationStatus · useOutsideClick · useDraggable

All Hooks (A-Z)

Hook Category Used By Description
useAppDispatch Redux All Type-safe Redux dispatch
useAppSelector Redux All Type-safe Redux state selection
useAssetUploader Upload Constructor Batch asset uploads with progress
useBackgroundTransition Media Runtime, Constructor Background fade transition effects
useCanvasElapsedTime Constructor Constructor Elapsed time for element visibility
useCanvasElementDrag Constructor Constructor Element drag with % positioning
useConstructorElements Constructor Constructor Element CRUD operations
useConstructorPageActions Constructor Constructor Page save/create/publish
useCSVHandling Data List Pages CSV import/export
useDashboardCounts Data Dashboard Dashboard entity counts
useDevCompilationStatus Utility _app Next.js dev compilation status
useDraggable Utility Constructor Draggable panel management
useEditPageSync Data Edit Pages Edit page data sync
useElementEffects Media Runtime Element hover/focus/active effects
useElementSettingsForm Data Constructor Element settings form state
useEntityTable Data Table Pages Complete table management
useFilterItems Data List Pages Filter state management
useFormSync Data Edit Pages Form sync with Redux entities
useIconPreload Constructor Constructor Icon image preloading
useMediaDurationProbe Constructor Constructor Media duration probing
useNeighborGraph Preload (internal) Navigation graph building
useNetworkAware Preload (internal) Network condition monitoring
useOfflineMode Offline OfflineToggle Project offline download
useOutsideClick Utility Constructor Click outside detection
usePageDataLoader Media Runtime, Constructor Project/page data loading
usePageNavigation Navigation Runtime, Constructor Page state with history
usePageSwitch Navigation Runtime, Constructor Smooth page switching
usePreloadOrchestrator Preload Runtime, Constructor Asset preload with blob URLs
usePreloadProgress Preload OfflineToggle Preload job tracking
usePWAPreload Preload OfflineToggle PWA asset caching
useReversePlayback Media (internal) Reverse video playback
useStorageQuota Offline OfflineToggle Storage quota monitoring
useTransitionPlayback Media Runtime, Constructor Transition video management
useTransitionPreview Constructor Constructor Transition video preview

Legend: Runtime = RuntimePresentation.tsx, Constructor = constructor.tsx, (internal) = used by other hooks

Presentation Components Hook Usage

RuntimePresentation.tsx (public tour viewer):

usePageDataLoader ──→ usePreloadOrchestrator ──→ usePageSwitch
                              │
                              ▼
                    useTransitionPlayback ──→ useBackgroundTransition

constructor.tsx (tour editor):

usePageDataLoader ──→ usePreloadOrchestrator ──→ usePageSwitch
        │                     │
        ▼                     ▼
useConstructorElements   useTransitionPlayback ──→ useBackgroundTransition
        │                     │
        ▼                     ▼
useConstructorPageActions useTransitionPreview
        │
        ▼
useCanvasElementDrag, useCanvasElapsedTime, useIconPreload, useMediaDurationProbe

Overview

The platform provides a comprehensive set of custom hooks organized into categories:

  • Redux Integration - Type-safe store access
  • Data Management - Form sync, table management, CSV handling
  • Offline & Caching - PWA offline mode, storage quota
  • Preloading - Asset preloading, neighbor graph, network awareness
  • Media Playback - Video transitions, reverse playback
  • Navigation - Page navigation with history
  • Asset Upload - Batch upload with progress tracking
  • Constructor - Element management, page actions, transitions, dragging
  • Utility - Outside click detection, draggable panels

Important: Blob URL Rendering

Preloading hooks (usePreloadOrchestrator, usePageSwitch) return blob URLs for cached assets. When rendering these URLs as images, use native <img> tags instead of Next.js <Image>:

  • Next.js Image re-fetches on every render - even with 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.

import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Usage:

import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch, update } from '../stores/users/usersSlice';

function UserList() {
  const dispatch = useAppDispatch();
  const users = useAppSelector((state) => state.users.users);
  const loading = useAppSelector((state) => state.users.loading);

  useEffect(() => {
    dispatch(fetch({ query: '?limit=100' }));
  }, [dispatch]);

  return loading ? <Spinner /> : <Table data={users} />;
}

Benefits:

  • Full TypeScript support for state shape
  • Autocomplete for state paths
  • Type-checked dispatch payloads

Data Management Hooks

useFormSync

Source: frontend/src/hooks/useFormSync.ts

Synchronizes form state with Redux entity data, eliminating boilerplate in edit pages.

Signature:

function useFormSync<T extends BaseEntity, TFormValues extends Record<string, unknown>>(
  options: UseFormSyncOptions<T, TFormValues>
): UseFormSyncReturn<TFormValues>

Parameters:

Option Type Description
entitySelector (state: RootState) => T | T[] | undefined Redux state selector
fetchAction AsyncThunk Thunk to fetch entity
initialValues TFormValues Default form values
transformEntity (entity: T) => TFormValues Optional field mapper

Returns:

Property Type Description
formValues TFormValues Current form state
setFormValues (values: TFormValues) => void Update form
isLoading boolean Fetch in progress
entityId string | undefined ID from router
resetForm () => void Reset to initial values

Example:

const { formValues, setFormValues, isLoading, entityId } = useFormSync({
  entitySelector: (state) => state.projects.projects,
  fetchAction: fetchProjects,
  initialValues: {
    name: '',
    slug: '',
    description: '',
  },
  transformEntity: (project) => ({
    name: project.name,
    slug: project.slug,
    description: project.description || '',
  }),
});

// In Formik
<Formik
  initialValues={formValues}
  enableReinitialize
  onSubmit={handleSubmit}
>
  {/* form fields */}
</Formik>

Key Features:

  • Extracts entity ID from Next.js router query
  • Handles both single entity and array state shapes
  • Auto-fetches when ID is available
  • Optional transform for field name mapping

useEditPageSync

Source: frontend/src/hooks/useEditPageSync.ts

Simplified hook for edit pages that syncs form state with Redux entity data. Reduces ~50 lines of boilerplate code per edit page.

Signature:

function useEditPageSync<T extends Record<string, unknown>>(
  options: UseEditPageSyncOptions<T>
): UseEditPageSyncReturn<T>

Parameters:

Option Type Description
entitySelector (state: RootState) => unknown Redux selector for entity
fetchAction AsyncThunk Thunk to fetch entity
initialValues T Default form values
postProcess (entity: T, initial: T) => T Optional post-processing
idOverride string Optional ID override (defaults to router.query.id)

Returns:

Property Type Description
values T Current form values
setValues React.Dispatch<SetStateAction<T>> Update form values
id string | null Entity ID from router
isLoading boolean Fetch in progress
isFound boolean Entity was found

Example:

const initVals = { name: '', permissions: [] };

const EditRolesPage = () => {
  const { values, id, isLoading } = useEditPageSync({
    entitySelector: (state) => state.roles.roles,
    fetchAction: fetch,
    initialValues: initVals,
  });

  const handleSubmit = async (data) => {
    await dispatch(update({ id, data }));
    router.push('/roles/roles-list');
  };

  return (
    <Formik enableReinitialize initialValues={values} onSubmit={handleSubmit}>
      ...
    </Formik>
  );
};

Simplified Variant:

// Returns [values, setValues] tuple for drop-in replacement
const [initialValues, setInitialValues] = useEditPageSyncSimple({
  entitySelector: (state) => state.roles.roles,
  fetchAction: fetch,
  initialValues: initVals,
});

useDashboardCounts

Source: frontend/src/hooks/useDashboardCounts.ts

Fetches entity counts for the dashboard with permission-based filtering. Uses Promise.allSettled for resilience.

Signature:

function useDashboardCounts(currentUser: User | null): UseDashboardCountsReturn

Returns:

Property Type Description
counts Record<string, number | string | null> Entity counts
loading boolean Fetch in progress
error Error | null Error if failed
refetch () => Promise<void> Manual refetch
getCount (key: string) => EntityCountValue Get count for entity
getVisibleEntities () => EntityConfig[] Get permitted entities

Example:

const { counts, loading, getVisibleEntities, getCount } = useDashboardCounts(currentUser);

const entities = getVisibleEntities();
const userCount = getCount('users'); // number | 'Loading...' | null

return (
  <DashboardCards>
    {entities.map((entity) => (
      <Card key={entity.key} count={getCount(entity.key)} />
    ))}
  </DashboardCards>
);

Entity Configuration: Uses DASHBOARD_ENTITIES constant with 13 entities (users, roles, permissions, projects, etc.), each with permission requirements.


useEntityTable

Source: frontend/src/hooks/useEntityTable.ts

Complete table state management including pagination, sorting, filtering, selection, and CRUD.

Signature:

function useEntityTable<T extends BaseEntity>(
  options: UseEntityTableOptions<T>
): UseEntityTableReturn<T>

Parameters:

Option Type Description
entityName string Entity identifier
sliceSelector (state: RootState) => EntitySliceState<T> Redux slice selector
fetchAction AsyncThunk Fetch thunk
updateAction AsyncThunk Update thunk (optional)
deleteAction AsyncThunk Delete single item thunk
deleteByIdsAction AsyncThunk Batch delete thunk (optional)
setRefetchAction (value: boolean) => Action Trigger refetch
loadColumnsFunction Function Column config loader
filters Filter[] Available filters
perPage number Items per page (default: 10)

Returns:

interface UseEntityTableReturn<T> {
  // Data
  data: T[];
  columns: GridColDef[];
  loading: boolean;
  count: number;

  // Pagination
  currentPage: number;
  setCurrentPage: (page: number) => void;
  numPages: number;

  // Sorting
  sortModel: GridSortModel;
  setSortModel: (model: GridSortModel) => void;

  // Selection
  selectedRows: string[];
  setSelectedRows: (ids: string[]) => void;

  // Filters
  filterItems: FilterItem[];
  setFilterItems: (items: FilterItem[]) => void;
  filterRequest: string;
  handleFilterSubmit: () => void;
  handleFilterReset: () => void;

  // Delete operations
  isDeleteModalActive: boolean;
  deleteTargetId: string | null;
  handleDeleteClick: (id: string) => void;
  handleDeleteConfirm: () => Promise<void>;
  handleDeleteCancel: () => void;
  handleDeleteSelected: () => Promise<void>;

  // Table operations
  handleRowUpdate: (id: string, data: Partial<T>) => Promise<void>;
  loadData: (page?: number, request?: string) => void;
}

Example:

const {
  data,
  columns,
  loading,
  currentPage,
  setCurrentPage,
  numPages,
  sortModel,
  setSortModel,
  filterItems,
  setFilterItems,
  handleFilterSubmit,
  handleDeleteConfirm,
  isDeleteModalActive,
} = useEntityTable({
  entityName: 'assets',
  sliceSelector: (state) => state.assets,
  fetchAction: fetchAssets,
  deleteAction: deleteAsset,
  deleteByIdsAction: deleteAssetsByIds,
  setRefetchAction: setRefetch,
  loadColumnsFunction: configureAssetsCols,
  filters: assetFilters,
  perPage: 25,
});

return (
  <>
    <FilterBar
      filters={filters}
      filterItems={filterItems}
      setFilterItems={setFilterItems}
      onSubmit={handleFilterSubmit}
    />
    <DataGrid
      rows={data}
      columns={columns}
      loading={loading}
      sortModel={sortModel}
      onSortModelChange={setSortModel}
      paginationModel={{ page: currentPage, pageSize: 25 }}
      onPaginationModelChange={({ page }) => setCurrentPage(page)}
      rowCount={count}
    />
    <DeleteModal
      isActive={isDeleteModalActive}
      onConfirm={handleDeleteConfirm}
      onCancel={handleDeleteCancel}
    />
  </>
);

useCSVHandling

Source: frontend/src/hooks/useCSVHandling.ts

Manages CSV import and export operations for bulk data handling.

Signature:

function useCSVHandling(options: UseCSVHandlingOptions): UseCSVHandlingReturn

Parameters:

Option Type Description
endpoint string API endpoint (e.g., 'users')
uploadAction AsyncThunk CSV upload thunk
setRefetchAction (value: boolean) => Action Trigger data reload
fileName string Export filename (optional)

Returns:

Property Type Description
csvFile File | null Selected file
setCsvFile (file: File | null) => void Set file
isModalActive boolean Import modal visible
setIsModalActive (active: boolean) => void Toggle modal
isUploading boolean Upload in progress
isDownloading boolean Download in progress
downloadCSV () => Promise<void> Trigger export
uploadCSV () => Promise<void> Trigger import
error string | null Error message

Example:

const {
  csvFile,
  setCsvFile,
  isUploading,
  isDownloading,
  downloadCSV,
  uploadCSV,
  error,
} = useCSVHandling({
  endpoint: 'users',
  uploadAction: uploadUsersCsv,
  setRefetchAction: setRefetch,
  fileName: 'users-export.csv',
});

return (
  <>
    <Button onClick={downloadCSV} disabled={isDownloading}>
      Export CSV
    </Button>
    <input
      type="file"
      accept=".csv"
      onChange={(e) => setCsvFile(e.target.files?.[0] || null)}
    />
    <Button onClick={uploadCSV} disabled={isUploading || !csvFile}>
      Import CSV
    </Button>
    {error && <Alert severity="error">{error}</Alert>}
  </>
);

useFilterItems

Source: frontend/src/hooks/useFilterItems.ts

Manages filter state for data tables with support for range and enum filters.

Signature:

function useFilterItems(filters: Filter[]): UseFilterItemsReturn

Filter Definition:

interface Filter {
  title: string;      // Field name
  label: string;      // Display label
  number?: boolean;   // Range filter (numbers)
  date?: boolean;     // Range filter (dates)
  type?: 'enum';      // Enum filter
  options?: string[]; // Enum options
}

Returns:

Property Type Description
filterItems FilterItem[] Active filters
setFilterItems (items: FilterItem[]) => void Replace filters
addFilter () => void Add new filter row
removeFilter (id: string) => void Remove filter
updateFilter (id: string, fields: Partial<FilterFields>) => void Update filter
resetFilters () => void Clear all filters
generateFilterRequest () => string Build query string
filterRequest FilterRequest Filter object

Example:

const filters: Filter[] = [
  { title: 'name', label: 'Name' },
  { title: 'created_at', label: 'Created', date: true },
  { title: 'environment', label: 'Environment', type: 'enum', options: ['dev', 'stage', 'production'] },
];

const {
  filterItems,
  addFilter,
  removeFilter,
  updateFilter,
  generateFilterRequest,
  resetFilters,
} = useFilterItems(filters);

// Generate query: "&name=test&created_atRange=2024-01-01&created_atRange=2024-12-31"
const queryString = generateFilterRequest();

useElementSettingsForm

Source: frontend/src/components/ElementSettings/useElementSettingsForm.ts

Form state management hook for element settings across global defaults, project defaults, and constructor pages.

Signature:

function useElementSettingsForm(
  options: UseElementSettingsFormOptions
): UseElementSettingsFormReturn

interface UseElementSettingsFormOptions {
  elementType: CanvasElementType | string;
}

Returns:

Property Type Description
state FormState Current form state (all settings)
setField (field, value) => void Update single field
setFields (updates) => void Update multiple fields
applySettings (json) => void Load settings from JSON
getStyleValues () => ElementStyleProperties Get CSS style values
buildSettingsJson () => Record<string, unknown> Serialize to JSON for saving
isNavigationType boolean Element is navigation_next/prev
isTooltipType boolean Element is tooltip
isDescriptionType boolean Element is description
isGalleryType boolean Element is gallery
isCarouselType boolean Element is carousel
isMediaType boolean Element is video/audio player
addGalleryCard () => void Add gallery card
removeGalleryCard (id) => void Remove gallery card
updateGalleryCard (id, field, value) => void Update gallery card
addCarouselSlide () => void Add carousel slide
removeCarouselSlide (id) => void Remove carousel slide
updateCarouselSlide (id, field, value) => void Update carousel slide

FormState includes:

  • Common: label, xPercent, yPercent, appearDelaySec, appearDurationSec
  • CSS: width, height, margin, padding, fontSize, border, borderRadius, opacity, etc.
  • Navigation: iconUrl, navLabel, navType, targetPageSlug, transitionVideoUrl, etc.
  • Tooltip: tooltipTitle, tooltipText
  • Description: descriptionTitle, descriptionText, typography settings
  • Media: mediaUrl, mediaAutoplay, mediaLoop, mediaMuted
  • Gallery: galleryCards array
  • Carousel: carouselSlides array, prev/next icons

Example:

const form = useElementSettingsForm({ elementType: 'navigation_next' });

// Load existing settings
useEffect(() => {
  if (item?.settings_json) {
    form.applySettings(item.settings_json);
  }
}, [item]);

// Update field
form.setField('navLabel', 'Next Page');

// Save
const handleSave = async () => {
  const settings = form.buildSettingsJson();
  await axios.put(`/project-element-defaults/${id}`, {
    data: { settings_json: settings },
  });
};

// Render type-specific sections
{form.isNavigationType && (
  <NavigationSettingsSection
    navLabel={form.state.navLabel}
    onChange={(field, value) => form.setField(field, value)}
  />
)}

Key Features:

  • Type-aware: Shows/hides fields based on element type
  • Bidirectional: applySettings() loads JSON, buildSettingsJson() serializes
  • Validation: Clamps position to 0-100, normalizes CSS units
  • Array management: Gallery cards and carousel slides with add/remove/update

Offline & Caching Hooks

useOfflineMode

Source: frontend/src/hooks/useOfflineMode.ts

Manages offline mode state and project download functionality with progress tracking.

Signature:

function useOfflineMode(options: UseOfflineModeOptions): UseOfflineModeResult

Parameters:

Option Type Description
projectId string | null Project to download
projectSlug string Project slug (optional)
projectName string Display name (optional)
enabled boolean Enable hook (default: true)

Returns:

interface UseOfflineModeResult {
  // Status
  isOfflineCapable: boolean;    // Browser supports offline
  isDownloaded: boolean;        // Project fully cached
  isDownloading: boolean;       // Download in progress
  status: ProjectOfflineStatus; // 'not_downloaded' | 'downloading' | 'downloaded' | 'error' | 'outdated'
  progress: number;             // 0-100
  downloadedAssets: number;
  totalAssets: number;
  downloadedBytes: number;
  totalBytes: number;
  error: string | null;

  // Actions
  startDownload: () => Promise<void>;
  pauseDownload: () => void;
  resumeDownload: () => void;
  cancelDownload: () => void;
  deleteOfflineData: () => Promise<void>;
  checkForUpdates: () => Promise<boolean>;

  // Info
  projectInfo: OfflineProject | null;
  estimatedSize: number;
  formatSize: (bytes: number) => string;
}

Example:

const {
  isOfflineCapable,
  status,
  progress,
  downloadedAssets,
  totalAssets,
  startDownload,
  deleteOfflineData,
  formatSize,
  downloadedBytes,
} = useOfflineMode({
  projectId: project?.id,
  projectSlug: project?.slug,
  enabled: true,
});

return (
  <div>
    {status === 'not_downloaded' && (
      <Button onClick={startDownload}>
        Download for Offline ({formatSize(estimatedSize)})
      </Button>
    )}
    {status === 'downloading' && (
      <ProgressBar value={progress}>
        {downloadedAssets}/{totalAssets} assets
      </ProgressBar>
    )}
    {status === 'downloaded' && (
      <Button onClick={deleteOfflineData}>Remove Offline Data</Button>
    )}
  </div>
);

Key Features:

  • Checks Service Worker and Cache API support
  • Uses frontend asset discovery (same as online preload) - no backend manifest
  • Requires 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:

function useStorageQuota(): UseStorageQuotaResult

Returns:

Property Type Description
usage number Bytes used
quota number | Infinity Total available
percentUsed number Usage percentage
available number Bytes remaining
isLoading boolean Fetching quota
error string | null Error message
refresh () => Promise<void> Manual refresh
requestPersistence () => Promise<boolean> Request persistent storage
isPersisted boolean Storage is persistent
isWarning boolean >= 80% used
isCritical boolean >= 95% used
formatSize (bytes: number) => string Format bytes
canStore (bytes: number) => boolean Check if fits

Example:

const {
  usage,
  quota,
  percentUsed,
  isWarning,
  isCritical,
  formatSize,
  requestPersistence,
  isPersisted,
} = useStorageQuota();

return (
  <StorageIndicator>
    <span>{formatSize(usage)} / {formatSize(quota)}</span>
    {isWarning && <WarningIcon />}
    {isCritical && <CriticalIcon />}
    {!isPersisted && (
      <Button onClick={requestPersistence}>
        Enable Persistent Storage
      </Button>
    )}
  </StorageIndicator>
);

Preloading Hooks

usePreloadOrchestrator

Source: frontend/src/hooks/usePreloadOrchestrator.ts

Used by: RuntimePresentation.tsx, constructor.tsx

Main coordinator for online mode asset preloading with priority queue management. Downloads assets from S3 presigned URLs, stores in Cache API/IndexedDB, and provides instant blob URL lookup for smooth page transitions.

Signature:

function usePreloadOrchestrator(
  options: UsePreloadOrchestratorOptions
): UsePreloadOrchestratorResult

Parameters:

Option Type Description
pages PreloadPage[] All pages (filtered by environment)
pageLinks PreloadPageLink[] Navigation links (from extractPageLinksAndElements)
elements PreloadElement[] Page elements
currentPageId string | null Active page
pageHistory string[] Navigation history
enabled boolean Enable preloading (default: true)
maxNeighborDepth number BFS depth limit (default: 1, immediate neighbors only)

Environment Isolation: Pages, pageLinks, and elements should be filtered by environment (dev, stage, production) before passing to this hook. This prevents preloading assets from other environments.

Returns:

Property Type Description
isPreloading boolean Downloads active
preloadedUrls Set<string> Cached URLs
queueLength number Pending downloads
preloadAsset (url: string, priority?: number) => void Manual preload
clearQueue () => void Cancel pending
getCachedBlobUrl (url: string) => Promise<string | null> Get cached blob (creates new blob URL)
isUrlPreloaded (url: string) => Promise<boolean> Check cache
getReadyBlobUrl (url: string) => string | null Instant lookup - decoded blob URL (O(1))

Example:

const {
  isPreloading,
  preloadedUrls,
  getCachedBlobUrl,
  getReadyBlobUrl,
  preloadAsset,
} = usePreloadOrchestrator({
  pages,
  pageLinks,
  elements,
  currentPageId,
  enabled: !isOffline,
});

// Instant blob URL lookup (O(1), already decoded, ready to display)
const readyUrl = getReadyBlobUrl(videoUrl);
if (readyUrl) {
  videoRef.current.src = readyUrl;
}

// Fallback: async cached blob (creates new blob URL, may need decode)
const blobUrl = await getCachedBlobUrl(videoUrl);
videoRef.current.src = blobUrl || videoUrl;

// Manual high-priority preload
preloadAsset('https://example.com/important.mp4', 200);

S3 Presigned URL Flow with Storage Key Mapping:

1. POST /api/file/presign { urls: [asset1, asset2, ...] }  // Max 50 per batch
2. Download from presigned URLs (1-hour expiry)
3. Store in Cache API (keyed by both download URL and storage key)
4. Create blob URL: URL.createObjectURL(blob)
5. Decode if image
6. Store in readyBlobUrlsRef Map:
   - keyed by download URL (presigned URL)
   - keyed by storage key (canonical path, e.g., "assets/project-123/video.mp4")
   - keyed by proxy URL (for fallback compatibility)

Storage Key Mapping: The orchestrator maps downloaded blobs to multiple keys for reliable cache hits:

Key Type Example Purpose
Download URL https://s3...?X-Amz-Signature=ABC Original presigned URL
Storage Key assets/project-123/video.mp4 Canonical path (most reliable)
Proxy URL /api/file/download?privateUrl=... Fallback compatibility

This ensures lookups succeed regardless of which URL is used, since presigned URL signatures change on each resolution but the storage key remains constant.


useNeighborGraph

Source: frontend/src/hooks/useNeighborGraph.ts

Builds navigation graph from page links to determine which pages are neighbors and should have their assets preloaded.

Signature:

function useNeighborGraph(options: UseNeighborGraphOptions): NeighborGraphResult

Parameters:

Option Type Description
pages PreloadPage[] All pages (should be filtered by environment first)
pageLinks PreloadPageLink[] Navigation links with from_pageId and to_pageId
elements PreloadElement[] Page elements (for asset extraction)
maxDepth number BFS depth (default: 1, immediate neighbors only)

Note: Page links are extracted from ui_schema_json navigation elements using extractPageLinksAndElements() and passed as PreloadPageLink[] with resolved page IDs.

Environment Filtering: Pages and elements should be filtered by environment (dev, stage, or production) before passing to this hook to ensure preloading respects environment isolation.

Returns:

Method Signature Description
getNeighbors (pageId: string, depth?: number) => PreloadNeighborInfo[] Get neighbor pages
getAssetsForPages (pageIds: string[]) => PreloadAssetInfo[] Get page assets
getPrioritizedAssets (pageId: string, depth?: number) => PreloadAssetInfo[] Prioritized assets
adjacencyList Map<string, string[]> Raw graph (debugging)

Example:

const { getNeighbors, getPrioritizedAssets } = useNeighborGraph({
  pages,
  pageLinks,  // Links with from_pageId, to_pageId
  elements,   // Elements with content_json for asset extraction
  maxDepth: 1,
});

// Get neighbors within 1 hop
const neighbors = getNeighbors(currentPageId);
// Returns: [{pageId: 'page-2', distance: 1}]

// Get prioritized assets for preloading
const assets = getPrioritizedAssets(currentPageId);
// Sorted by priority: current page (1000) > neighbor (100/distance) + type bonus

useNetworkAware

Source: frontend/src/hooks/useNetworkAware.ts

Monitors network conditions and adapts preloading strategy.

Signature:

function useNetworkAware(): UseNetworkAwareResult

Returns:

Property Type Description
networkInfo NetworkInfo Connection details
shouldPreloadAggressively boolean Good connection
preferLowQuality boolean Slow connection
recommendedConcurrency number Parallel downloads
suggestOfflineMode boolean Very slow connection

NetworkInfo:

interface NetworkInfo {
  isOnline: boolean;
  effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | undefined;
  downlink: number | undefined;     // Mbps
  rtt: number | undefined;          // Round-trip time ms
  saveData: boolean;
}

Example:

const {
  networkInfo,
  shouldPreloadAggressively,
  recommendedConcurrency,
  preferLowQuality,
  suggestOfflineMode,
} = useNetworkAware();

// Adapt preloading
const concurrency = recommendedConcurrency; // 1 for 2g, 2 for 3g, 3 for 4g

// Show offline suggestion
if (suggestOfflineMode) {
  showToast('Connection is slow. Consider downloading for offline use.');
}

// Select asset quality
const videoUrl = preferLowQuality ? video.lowRes : video.highRes;

usePWAPreload

Source: frontend/src/hooks/usePWAPreload.ts

PWA asset preloading with progress tracking.

Signature:

function usePWAPreload(options: UsePWAPreloadOptions): PreloadState

Parameters:

Option Type Description
assets AssetToPreload[] Assets to preload
onComplete () => void Completion callback
onError (errors: string[]) => void Error callback
skipIfCached boolean Skip if already cached (default: true)

Returns:

Property Type Description
isPreloading boolean In progress
progress number 0-100
loadedCount number Completed assets
totalCount number Total assets
errors string[] Failed URLs
startPreload () => Promise<void> Manual trigger
isCached boolean All cached

Example:

const { isPreloading, progress, loadedCount, totalCount, startPreload } = usePWAPreload({
  assets: [
    { url: '/images/bg.jpg', type: 'image' },
    { url: '/videos/intro.mp4', type: 'video' },
  ],
  onComplete: () => setShowOverlay(false),
  skipIfCached: true,
});

return isPreloading ? (
  <LoadingOverlay>
    Loading {loadedCount}/{totalCount} ({Math.round(progress)}%)
  </LoadingOverlay>
) : null;

usePreloadProgress

Source: frontend/src/hooks/usePreloadProgress.ts

Tracks preload job progress via DownloadEventBus with auto-cleanup.

Signature:

function usePreloadProgress(): UsePreloadProgressResult

Returns:

Property Type Description
jobs PreloadJob[] All tracked jobs
activeCount number Downloading/queued jobs
completedCount number Completed jobs
errorCount number Failed jobs
totalProgress number Overall progress 0-100
isActive boolean Any active downloads
clearJob (id: string) => void Remove specific job
clearAllCompleted () => void Clear completed jobs
clearAllErrors () => void Clear errored jobs

PreloadJob:

interface PreloadJob {
  id: string;
  assetId: string;
  url: string;
  filename: string;
  progress: number;
  status: 'queued' | 'downloading' | 'completed' | 'error';
  bytesLoaded: number;
  totalBytes: number;
  addedAt: number;
  startedAt?: number;
  completedAt?: number;
  error?: string;
}

Example:

const {
  jobs,
  activeCount,
  totalProgress,
  isActive,
  clearAllCompleted,
} = usePreloadProgress();

return isActive ? (
  <PreloadIndicator>
    <span>{activeCount} downloads in progress ({totalProgress}%)</span>
    {jobs.map((job) => (
      <JobProgress key={job.id} job={job} />
    ))}
  </PreloadIndicator>
) : null;

Key Features:

  • Listens to DownloadEventBus for preload events
  • Auto-removes completed jobs after configurable delay
  • Auto-removes error jobs after longer delay
  • Byte-based progress calculation when available

Media Playback Hooks

useReversePlayback

Source: frontend/src/hooks/useReversePlayback.ts

Handles reverse video playback with fallback strategies.

Signature:

function useReversePlayback(options: UseReversePlaybackOptions): UseReversePlaybackResult

Parameters:

Option Type Description
videoRef RefObject<HTMLVideoElement> Video element ref
onComplete () => void Completion callback
preloadedUrls Set<string> Cached URLs (optional)
videoUrl string Current video URL (optional)
getCachedBlobUrl (url: string) => Promise<string | null> Blob URL getter (optional)

Returns:

Property Type Description
startReverse () => Promise<void> Start reverse playback
stopReverse () => void Stop playback
isReversing boolean Playback active
isBuffering boolean Buffering video
canUseNativeReverse boolean Browser supports native reverse

Example:

const videoRef = useRef<HTMLVideoElement>(null);

const { startReverse, stopReverse, isReversing, isBuffering } = useReversePlayback({
  videoRef,
  onComplete: () => navigateBack(),
  getCachedBlobUrl,
});

// Trigger reverse when back button clicked
const handleBack = async () => {
  await startReverse();
};

return (
  <div>
    <video ref={videoRef} src={transitionUrl} />
    {isBuffering && <Spinner />}
  </div>
);

Playback Strategies:

  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:

function useTransitionPlayback(
  options: UseTransitionPlaybackOptions
): UseTransitionPlaybackResult

Parameters:

interface UseTransitionPlaybackOptions {
  videoRef: RefObject<HTMLVideoElement | null>;
  transition: TransitionConfig | null;
  onComplete: (targetPageId?: string) => void;
  onError?: (reason: string) => void;

  timeouts?: {
    playbackStartMs?: number;   // Default: 3000
    durationBufferMs?: number;  // Default: 200
    hardTimeoutMs?: number;     // Default: 45000
  };

  features?: {
    useBlobUrl?: boolean;
    preDecodeImages?: boolean;
    getTargetPageImages?: () => string[];
  };

  preload?: {
    preloadedUrls?: Set<string>;
    getCachedBlobUrl?: (url: string) => Promise<string | null>;
    getReadyBlobUrl?: (url: string) => string | null;  // Instant O(1) lookup
  };
}

interface TransitionConfig {
  videoUrl: string;           // Resolved URL (presigned or proxy) for playback
  storageKey?: string;        // Raw storage path for cache lookup (e.g., "assets/project-123/video.mp4")
  reverseMode: 'none' | 'reverse' | 'separate';
  reverseVideoUrl?: string;   // For 'separate' mode
  reverseStorageKey?: string; // Storage key for reverse video
  durationSec?: number;
  targetPageId?: string;      // Resolved from targetPageSlug at navigation time
  displayName?: string;
}

Note: Navigation elements store targetPageSlug in ui_schema_json. At navigation time, the slug is resolved to a page ID before being passed to this hook.

Returns:

Property Type Description
phase PlaybackPhase Current phase
isBuffering boolean Loading video
isReversing boolean Reverse playback
cancel () => void Cancel transition
forceComplete () => void Skip to end

PlaybackPhase: 'idle' | 'preparing' | 'playing' | 'reversing' | 'finishing' | 'completed'

Example:

const videoRef = useRef<HTMLVideoElement>(null);

const { phase, isBuffering, cancel } = useTransitionPlayback({
  videoRef,
  transition: {
    videoUrl: resolveAssetPlaybackUrl(transitionPath),  // Resolved URL
    storageKey: transitionPath,  // Raw storage path for cache lookup
    reverseMode: 'reverse',
    durationSec: 1.5,
    targetPageId: 'page-gallery',
  },
  onComplete: (targetPageId) => {
    setCurrentPage(targetPageId);
  },
  features: {
    preDecodeImages: true,
    getTargetPageImages: () => getPageImages(targetPageId),
  },
  preload: {
    preloadedUrls: preloadOrchestrator.preloadedUrls,
    getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
    getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,  // Instant lookup
  },
});

return (
  <TransitionOverlay visible={phase !== 'idle'}>
    <video ref={videoRef} muted autoPlay playsInline />
    {isBuffering && <LoadingIndicator />}
    <Button onClick={cancel}>Skip</Button>
  </TransitionOverlay>
);

Reverse Modes:

  • none: Forward playback 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:

function useBackgroundTransition(
  options: UseBackgroundTransitionOptions
): UseBackgroundTransitionResult

Parameters:

Option Type Description
pageSwitch PageSwitchInterface Page switch hook instance
fadeOut FadeOutConfig Optional fade-out configuration

FadeOutConfig (for RuntimePresentation):

interface FadeOutConfig {
  pendingTransitionComplete: boolean;  // Video finished, waiting for bg ready
  isBackgroundReady: boolean;          // New background loaded
  transitionVideoRef: RefObject<HTMLVideoElement>;
  onTransitionCleanup: () => void;
}

Returns:

Property Type Description
isOverlayFadingOut boolean Overlay currently fading
resetFadeOut () => void Reset fade state

Example:

// Full mode with fade-out (RuntimePresentation)
const { isOverlayFadingOut, resetFadeOut } = useBackgroundTransition({
  pageSwitch,
  fadeOut: {
    pendingTransitionComplete,
    isBackgroundReady,
    transitionVideoRef,
    onTransitionCleanup: () => {
      setTransitionPreview(null);
      setPendingTransitionComplete(false);
    },
  },
});

// Simple mode - direct navigation only (constructor)
useBackgroundTransition({ pageSwitch });

Modes:

  • Full mode (RuntimePresentation): Fade-out animation + direct navigation clearing
  • Simple mode (constructor): Direct navigation clearing only

usePageDataLoader

Source: frontend/src/hooks/usePageDataLoader.ts

Used by: RuntimePresentation.tsx, constructor.tsx

Unified hook for loading project and page data in presentation components. Handles both public access (by slug) and authenticated access (by ID).

Signature:

function usePageDataLoader(
  options: UsePageDataLoaderOptions
): UsePageDataLoaderResult

Parameters:

Option Type Description
projectId string Project ID (constructor mode)
projectSlug string Project slug (runtime mode)
environment 'dev' | 'stage' | 'production' Environment filter
enabled boolean Enable loading (default: true)
apiHeaders Record<string, string> Custom API headers
initialPageId string Initial page ID from route

Returns:

Property Type Description
project RuntimeProject | null Loaded project
pages RuntimePage[] Filtered and sorted pages
isLoading boolean Loading in progress
error string Error message
reload (preservePageId?) => Promise<void> Reload data
initialPageId string Selected initial page ID

Example:

// Runtime mode (public presentation)
const { project, pages, isLoading, error } = usePageDataLoader({
  projectSlug: 'my-project',
  environment: 'production',
});

// Constructor mode (authenticated)
const { project, pages, isLoading, error, reload } = usePageDataLoader({
  projectId: 'uuid-here',
  environment: 'dev',
  enabled: isAuthReady,
});

Features:

  • Loads by slug (runtime) or ID (constructor)
  • Filters pages by environment
  • Sorts pages by sort_order
  • Handles authentication errors
  • Supports page preservation on reload

useElementEffects

Source: frontend/src/hooks/useElementEffects.ts

Manages element interactive effects (hover, focus, active) at runtime. Since CSS pseudo-classes don't work with inline styles, this hook handles state-based style application via JavaScript events.

Signature:

function useElementEffects(
  effects: Partial<ElementEffectProperties>
): UseElementEffectsResult

Returns:

Property Type Description
effectStyle CSSProperties Style to merge with base element style
eventHandlers object Event handlers for the element

Event Handlers:

  • onMouseEnter, onMouseLeave - Hover effects
  • onFocus, onBlur - Focus effects
  • onMouseDown, onMouseUp - Active/pressed effects

Example:

const { effectStyle, eventHandlers } = useElementEffects({
  hoverScale: 1.1,
  hoverOpacity: 0.8,
  activeScale: 0.95,
  transitionDuration: '0.2s',
});

return (
  <button
    style={{ ...baseStyle, ...effectStyle }}
    {...eventHandlers}
  >
    {content}
  </button>
);

Effect Priority: active > focus > hover > base (highest wins)


Navigation Hooks

usePageSwitch

Source: frontend/src/hooks/usePageSwitch.ts

Used by: RuntimePresentation.tsx, constructor.tsx

Unified page switching hook that eliminates white flashes during page transitions by using preloaded blob URLs and keeping previous background visible until new one is ready.

Signature:

function usePageSwitch(options?: UsePageSwitchOptions): UsePageSwitchResult

Parameters:

Option Type Description
preloadCache PreloadCacheProvider Optional preload cache provider

PreloadCacheProvider Interface:

interface PreloadCacheProvider {
  /** Instant lookup - returns decoded blob URL ready to display (O(1)) */
  getReadyBlobUrl?: (url: string) => string | null;
  /** Fallback: async blob URL from cache (creates new blob URL) */
  getCachedBlobUrl?: (url: string) => Promise<string | null>;
  preloadedUrls?: Set<string>;
}

Returns:

Property Type Description
currentBgImageUrl string Current background image URL
currentBgVideoUrl string Current background video URL
currentBgAudioUrl string Current background audio URL
previousBgImageUrl string Previous background for overlay
isSwitching boolean Page switch in progress
isNewBgReady boolean New background ready to display
switchToPage (page, onSwitched?) => Promise<void> Switch to page with transition
setBackgroundsDirectly (img, vid, aud) => void Direct set without transition
markBackgroundReady () => void Mark background as loaded
clearPreviousBackground () => void Clear overlay after fade

Example:

const pageSwitch = usePageSwitch({
  preloadCache: {
    getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,  // Instant O(1) lookup
    getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,  // Fallback
    preloadedUrls: preloadOrchestrator.preloadedUrls,
  },
});

// Switch to a page with smooth transition
await pageSwitch.switchToPage(targetPage, () => {
  setActivePageId(targetPage.id);
});

// In render - previous background overlay fades out
{pageSwitch.previousBgImageUrl && (
  <div
    className='absolute inset-0 transition-opacity duration-200'
    style={{
      backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
      opacity: pageSwitch.isNewBgReady ? 0 : 1,
    }}
  />
)}

// Current background with onLoad handler
<NextImage
  src={pageSwitch.currentBgImageUrl}
  onLoad={pageSwitch.markBackgroundReady}
/>

Key Features:

  • Instant blob URL lookup via getReadyBlobUrl() (O(1), already decoded)
  • Fallback to getCachedBlobUrl() if ready URL not available
  • Presigned URL fallback with automatic retry via proxy on CORS failure
  • Keeps previous background visible until new one is painted
  • Double RAF (requestAnimationFrame) ensures browser has painted before fade
  • Automatic blob URL cleanup to prevent memory leaks

White Flash Prevention Strategy with Storage Key Priority:

  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>:

// 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:

function usePageNavigation<TPage extends NavigablePage>(
  options: UsePageNavigationOptions<TPage>
): UsePageNavigationResult<TPage>

Parameters:

interface NavigablePage {
  id: string;
  sort_order?: number;
  slug?: string;
}

interface UsePageNavigationOptions<TPage> {
  pages: TPage[];
  defaultPageId?: string;
  trackHistory?: boolean;  // Default: true
  onPageChange?: (pageId: string, isBack: boolean) => void;
}

Returns:

Property Type Description
currentPageId string | null Active page ID
currentPage TPage | null Active page object
pageHistory string[] Navigation history
previousPageId string | null Previous page ID
defaultPage TPage | null Initial page
setCurrentPageId (pageId: string) => void Navigate (no history)
applyPageSelection (pageId: string, isBack?: boolean) => void Navigate with history
isBackNavigation (pageId: string) => boolean Check if back nav
goBack () => boolean Go to previous page
resetHistory () => void Clear history

Example - Runtime with History:

const nav = usePageNavigation({
  pages,
  trackHistory: true,
  onPageChange: (pageId, isBack) => {
    analytics.track('page_view', { pageId, isBack });
  },
});

// Navigate forward
nav.applyPageSelection('page-gallery');

// Navigate back
if (nav.previousPageId) {
  nav.goBack();
}

// Check direction
const transitionDirection = nav.isBackNavigation(targetPageId)
  ? 'reverse'
  : 'forward';

Example - Editor without History:

const nav = usePageNavigation({
  pages,
  defaultPageId: firstPageId,
  trackHistory: false,
});

// Simple navigation
nav.setCurrentPageId('page-2');

Asset Upload Hooks

useAssetUploader

Source: frontend/src/components/Assets/useAssetUploader.ts

Batch asset uploads with concurrent workers and progress tracking.

Signature:

function useAssetUploader(options: UseAssetUploaderOptions): UseAssetUploaderReturn

Parameters:

Option Type Description
selectedProjectId string Target project
onUploadComplete () => void Completion callback

Returns:

Property Type Description
uploadingSections string[] Sections with active uploads
uploadQueues Record<string, UploadQueueItem[]> Per-section queues
runBatchUpload (section: AssetSection, files: File[]) => Promise<void> Start upload

UploadQueueItem:

interface UploadQueueItem {
  id: string;
  fileName: string;
  progress: number;  // 0-100
  status: 'queued' | 'uploading' | 'saving' | 'success' | 'error';
  error?: string;
}

Example:

const { uploadingSections, uploadQueues, runBatchUpload } = useAssetUploader({
  selectedProjectId: project?.id,
  onUploadComplete: () => {
    toast.success('Upload complete!');
    refreshAssets();
  },
});

const handleFileSelect = (files: File[]) => {
  runBatchUpload(
    { key: 'images', label: 'Images', assetFormat: 'image', assetCategory: 'general' },
    files
  );
};

return (
  <div>
    <FileDropzone onDrop={handleFileSelect} />
    {uploadQueues['images']?.map((item) => (
      <UploadProgress key={item.id} item={item} />
    ))}
  </div>
);

Features:

  • Max 2 concurrent uploads
  • 5 MB chunk size, 3 retries
  • Media duration probing (video/audio)
  • Automatic metadata submission

Constructor Hooks

Hooks specifically designed for the visual tour builder (constructor.tsx). These manage element CRUD, page actions, transitions, dragging, and timing.

useConstructorElements

Source: frontend/src/hooks/useConstructorElements.ts

Manages element CRUD operations with defaults merging. Core hook for element management in the constructor.

Signature:

function useConstructorElements(
  options: UseConstructorElementsOptions
): UseConstructorElementsResult

Parameters:

Option Type Description
initialElements CanvasElement[] Starting elements
elementDefaultsByType Record<CanvasElementType, Partial<CanvasElement>> Project defaults
allowedNavigationTypes NavigationElementType[] Allowed nav types
onElementsChange (elements) => void Change callback
initialSelectedElementId string Initial selection from route
onElementSelected (id) => void Selection callback
onSelectionCleared () => void Clear selection callback
onElementAdded (element) => void Add callback
onElementRemoved (id) => void Remove callback

Returns:

Property Type Description
elements CanvasElement[] Current elements
setElements Dispatch<SetStateAction> Set elements directly
selectedElementId string Selected element ID
selectedElement CanvasElement | null Selected element
selectElement (id) => void Select element
clearSelection () => void Clear selection
addElement (type) => void Add new element
updateSelectedElement (patch) => void Update selected
updateElement (id, patch) => void Update by ID
removeSelectedElement () => void Remove selected
removeElement (id) => void Remove by ID
galleryCards {add, update, remove} Gallery operations
carouselSlides {add, update, remove} Carousel operations
updateElementPosition (id, x, y) => void Update position
normalizeNavigationType (el, type) => CanvasElement Normalize nav type

Example:

const {
  elements,
  selectedElement,
  selectElement,
  addElement,
  updateSelectedElement,
  removeSelectedElement,
} = useConstructorElements({
  initialElements: parsedElements,
  elementDefaultsByType: projectDefaults,
  allowedNavigationTypes: ['navigation_next', 'navigation_prev'],
});

// Add new element
addElement('hotspot');

// Update selected element
updateSelectedElement({ label: 'New Label', xPercent: 50 });

useConstructorPageActions

Source: frontend/src/hooks/useConstructorPageActions.ts

Handles page create/save/publish operations in the constructor.

Signature:

function useConstructorPageActions(
  options: UseConstructorPageActionsOptions
): UseConstructorPageActionsResult

Parameters:

Option Type Description
projectId string Current project ID
pages TourPage[] All pages
activePage TourPage | null Current page
activePageId string Current page ID
elements CanvasElement[] Current elements
backgroundImageUrl string Background image
backgroundVideoUrl string Background video
backgroundAudioUrl string Background audio
onReload (preservePageId?) => Promise<void> Reload callback
onSetActivePageId (id) => void Set active page
onSetMenuOpen (open) => void Set menu open
onError (message) => void Error callback
onSuccess (message) => void Success callback

Returns:

Property Type Description
isSaving boolean Save in progress
isSavingToStage boolean Stage save in progress
isCreatingPage boolean Page creation in progress
isCreatingTransition boolean Transition creation in progress
saveConstructor () => Promise<void> Save current state
saveToStage () => Promise<void> Save dev → stage
createPage () => Promise<void> Create new page
createTransition (params) => Promise<void> Create transition (legacy)

Example:

const {
  isSaving,
  saveConstructor,
  createPage,
  isCreatingPage,
} = useConstructorPageActions({
  projectId,
  pages,
  activePage,
  activePageId,
  elements,
  backgroundImageUrl,
  backgroundVideoUrl,
  backgroundAudioUrl,
  onReload: loadData,
  onSetActivePageId: setActivePageId,
  onError: setErrorMessage,
  onSuccess: setSuccessMessage,
});

// Save changes
await saveConstructor();

// Create new page
await createPage();

useTransitionPreview

Source: frontend/src/hooks/useTransitionPreview.ts

Manages transition video preview state in the constructor. Used to preview forward and reverse transitions before navigation.

Signature:

function useTransitionPreview(
  options: UseTransitionPreviewOptions
): UseTransitionPreviewResult

Parameters:

Option Type Description
isNavigationElementType (type) => boolean Type checker
onError (message) => void Error callback

Returns:

Property Type Description
preview TransitionPreviewState | null Current preview
pendingPageId string Target page ID
openPreview (element, direction) => void Open preview
openPreviewWithTarget (element, direction, pageId) => void Open with target
closePreview () => void Close preview
isActive boolean Preview active

TransitionPreviewState:

interface TransitionPreviewState {
  videoUrl: string;
  storageKey: string;
  reverseMode: 'none' | 'reverse' | 'separate';
  reverseVideoUrl?: string;
  reverseStorageKey?: string;
  durationSec?: number;
  title: string;
}

Example:

const {
  preview,
  pendingPageId,
  openPreviewWithTarget,
  closePreview,
  isActive,
} = useTransitionPreview({
  isNavigationElementType,
  onError: setErrorMessage,
});

// Open preview when clicking element
const handleClick = () => {
  openPreviewWithTarget(element, 'forward', targetPage.id);
};

// Use with useTransitionPlayback
useTransitionPlayback({
  transition: preview,
  onComplete: (targetId) => {
    switchToPage(targetId);
    closePreview();
  },
});

useCanvasElapsedTime

Source: frontend/src/hooks/useCanvasElapsedTime.ts

Tracks elapsed time since page load for element visibility timing.

Signature:

function useCanvasElapsedTime(
  options: UseCanvasElapsedTimeOptions
): UseCanvasElapsedTimeResult

Parameters:

Option Type Description
pageId string Current page ID (resets timer)
enabled boolean Timer active (default: true)
intervalMs number Update interval (default: 100ms)

Returns:

Property Type Description
elapsedSec number Elapsed time in seconds
reset () => void Reset to zero
startedAt number Start timestamp

Example:

const { elapsedSec } = useCanvasElapsedTime({
  pageId: activePageId,
  enabled: !isLoading,
});

// Check if element should be visible
const isVisible = elapsedSec >= element.appearDelaySec;

Helper Function:

import { isElementVisibleAtTime } from '../hooks/useCanvasElapsedTime';

const visible = isElementVisibleAtTime(
  elapsedSec,
  element.appearDelaySec,      // delay before appearing
  element.appearDurationSec,   // duration visible (null = infinite)
);

useCanvasElementDrag

Source: frontend/src/hooks/useCanvasElementDrag.ts

Handles dragging of canvas elements with percentage-based positioning.

Signature:

function useCanvasElementDrag(
  options: UseCanvasElementDragOptions
): UseCanvasElementDragResult

Parameters:

Option Type Description
canvasRef RefObject<HTMLElement> Canvas container ref
onPositionChange (id, x, y) => void Position update callback
enabled boolean Dragging enabled (default: true)

Returns:

Property Type Description
onElementDragStart (event, id, x, y) => void Start drag handler
isDragging boolean Currently dragging
draggedElementId string | null Dragged element ID
cancelDrag () => void Cancel drag

Example:

const { onElementDragStart, isDragging } = useCanvasElementDrag({
  canvasRef,
  onPositionChange: (id, x, y) => {
    elementsHook.updateElementPosition(id, x, y);
  },
  enabled: isEditMode,
});

return (
  <button
    onMouseDown={(e) => onElementDragStart(
      e, element.id, element.xPercent, element.yPercent
    )}
  >
    {element.label}
  </button>
);

useIconPreload

Source: frontend/src/hooks/useIconPreload.ts

Preloads icon images for smooth rendering without flash.

Signature:

function useIconPreload(
  options: UseIconPreloadOptions
): UseIconPreloadResult

Parameters:

Option Type Description
iconUrls string[] URLs to preload
enabled boolean Preloading enabled (default: true)

Returns:

Property Type Description
preloadedUrlMap Record<string, boolean> Preload status map
isPreloaded (url) => boolean Check if URL preloaded
pendingCount number Icons being preloaded
allPreloaded boolean All icons ready

Example:

const iconUrls = elements
  .filter(el => el.iconUrl)
  .map(el => resolveAssetPlaybackUrl(el.iconUrl));

const { isPreloaded } = useIconPreload({ iconUrls });

// Only render element if icon is ready
if (element.iconUrl && !isPreloaded(element.iconUrl)) return null;

Helper Function:

import { buildIconPreloadTargets } from '../hooks/useIconPreload';

const targets = buildIconPreloadTargets(elements, {
  isNavigationElementType,
  isTooltipElementType,
  isDescriptionElementType,
});

useMediaDurationProbe

Source: frontend/src/hooks/useMediaDurationProbe.ts

Probes media durations with caching and deduplication.

Signature:

function useMediaDurationProbe(
  options: UseMediaDurationProbeOptions
): UseMediaDurationProbeResult

Parameters:

Option Type Description
targets DurationProbeTarget[] Media sources to probe

DurationProbeTarget:

interface DurationProbeTarget {
  source: string;
  mediaType: 'video' | 'audio';
}

Returns:

Property Type Description
durationBySource Record<string, number | null> Duration map
getDuration (source) => number | null Get duration
getDurationNote (source) => string Get formatted note
isProbing boolean Probes in progress

Example:

const { getDurationNote, getDuration } = useMediaDurationProbe({
  targets: [
    { source: backgroundVideoUrl, mediaType: 'video' },
    { source: backgroundAudioUrl, mediaType: 'audio' },
  ],
});

return <p>{getDurationNote(backgroundVideoUrl)}</p>; // "1:30" or ""

Helper Function:

import { buildDurationProbeTargets } from '../hooks/useMediaDurationProbe';

const targets = buildDurationProbeTargets({
  backgroundVideoUrl,
  backgroundAudioUrl,
  selectedElement,
  elements,
  isMediaElementType,
  isVideoPlayerElementType,
  isNavigationElementType,
});

Utility Hooks

useDevCompilationStatus

Source: frontend/src/hooks/useDevCompilationStatus.ts

Tracks Next.js compilation status in development mode.

Signature:

function useDevCompilationStatus(): CompilationStatus

Returns: 'ready' | 'compiling' | 'error' | 'initial'

Example:

const status = useDevCompilationStatus();

return (
  <div>
    {status === 'compiling' && <TopLoadingBar />}
    {status === 'error' && <CompilationErrorBanner />}
    <MainContent />
  </div>
);

Note: Returns 'ready' in production mode.


useOutsideClick

Source: frontend/src/hooks/useOutsideClick.ts

Detects clicks outside specified elements to clear selection. Useful for closing panels, deselecting elements, etc.

Signature:

function useOutsideClick(options: UseOutsideClickOptions): void

Parameters:

Option Type Description
containerRef RefObject<HTMLElement> Element whose outside clicks to detect
ignoreRefs RefObject<HTMLElement>[] Additional refs to ignore
ignoreDataAttribute string Data attribute to check
selectedValue string Current selected value
onOutsideClick () => void Click outside callback
enabled boolean Hook active (default: true)

Example:

useOutsideClick({
  containerRef: panelRef,
  ignoreRefs: [buttonRef],
  onOutsideClick: () => setSelectedId(''),
  enabled: !!selectedId,
});

With Data Attribute:

useOutsideClick({
  containerRef: canvasRef,
  ignoreDataAttribute: 'data-element-id',
  selectedValue: selectedElementId,
  onOutsideClick: () => clearSelection(),
  enabled: !!selectedElementId,
});

// In element render
<button data-element-id={element.id}>...</button>

useDraggable

Source: frontend/src/hooks/useDraggable.ts

Generic draggable panel management with pointer tracking. Used for draggable controls, menus, and editor panels.

Signature:

function useDraggable(options?: UseDraggableOptions): UseDraggableResult

Parameters:

Option Type Description
initialPosition Position Starting position
minX number Minimum x (default: 0)
minY number Minimum y (default: 0)
maxX number Maximum x (auto-calculated from window)
maxY number Maximum y (auto-calculated from window)
elementWidth number Element width for bounds
elementHeight number Element height for bounds

Returns:

Property Type Description
position Position Current position {x, y}
setPosition (pos) => void Set position directly
isDragging boolean Currently dragging
onDragStart (event) => void Drag start handler
onDragStartIgnoreButtons (event) => void Drag start (ignores buttons)

Example:

const { position, onDragStart, isDragging } = useDraggable({
  initialPosition: { x: 20, y: 20 },
  elementWidth: 400,
});

return (
  <div
    style={{
      position: 'fixed',
      left: position.x,
      top: position.y,
    }}
  >
    <div
      className="drag-handle"
      onMouseDown={onDragStart}
    >
      Drag Handle
    </div>
    <div>Content</div>
  </div>
);

With Button Ignore:

// Prevent drag when clicking buttons inside the drag handle
<div onMouseDown={onDragStartIgnoreButtons}>
  <span>Title</span>
  <button>Close</button>  {/* Clicking this won't start drag */}
</div>

Quick Reference Table

Hook Category Primary Use Case
useAppDispatch Redux Type-safe dispatch
useAppSelector Redux Type-safe state selection
useFormSync Data Sync forms with entities
useEditPageSync Data Edit page data sync
useDashboardCounts Data Dashboard entity counts
useEntityTable Data Complete table management
useCSVHandling Data CSV import/export
useFilterItems Data Filter state management
useElementSettingsForm Data Element settings form state
useOfflineMode Offline Project offline download
useStorageQuota Offline Storage monitoring
usePreloadOrchestrator Preload Asset preload with ready blob URLs
useNeighborGraph Preload Navigation graph building
useNetworkAware Preload Network condition monitoring
usePWAPreload Preload PWA asset caching
usePreloadProgress Preload Preload job tracking
useReversePlayback Media Reverse video playback
useTransitionPlayback Media Transition video management
useBackgroundTransition Media Background fade transition
usePageDataLoader Media Project/page data loading
useElementEffects Media Element interactive effects
usePageSwitch Navigation Smooth page switching without flash
usePageNavigation Navigation Page state with history
useAssetUploader Upload Batch asset uploads
useConstructorElements Constructor Element CRUD operations
useConstructorPageActions Constructor Page save/create/publish
useTransitionPreview Constructor Transition video preview
useCanvasElapsedTime Constructor Elapsed time tracking
useCanvasElementDrag Constructor Element drag positioning
useIconPreload Constructor Icon image preloading
useMediaDurationProbe Constructor Media duration probing
useDevCompilationStatus Utility Dev mode compilation
useOutsideClick Utility Click outside detection
useDraggable Utility Draggable panel management

File Structure

frontend/src/
├── hooks/
│   ├── index.ts                    # Central exports
│   │
│   │ # Data Management
│   ├── useFormSync.ts
│   ├── useEditPageSync.ts          # Edit page data sync
│   ├── useDashboardCounts.ts       # Dashboard entity counts
│   ├── useEntityTable.ts
│   ├── useCSVHandling.ts
│   ├── useFilterItems.ts
│   │
│   │ # Offline & Caching
│   ├── useOfflineMode.ts
│   ├── useStorageQuota.ts
│   │
│   │ # Preloading
│   ├── usePreloadOrchestrator.ts
│   ├── useNeighborGraph.ts
│   ├── useNetworkAware.ts
│   ├── usePWAPreload.ts
│   ├── usePreloadProgress.ts
│   │
│   │ # Media Playback
│   ├── useReversePlayback.ts
│   ├── useTransitionPlayback.ts
│   ├── useBackgroundTransition.ts  # Background fade transition
│   ├── usePageDataLoader.ts        # Project/page loading
│   ├── useElementEffects.ts        # Interactive effects
│   │
│   │ # Navigation
│   ├── usePageSwitch.ts
│   ├── usePageNavigation.ts
│   │
│   │ # Constructor-Specific
│   ├── useConstructorElements.ts   # Element CRUD
│   ├── useConstructorPageActions.ts # Page save/create/publish
│   ├── useTransitionPreview.ts     # Transition video preview
│   ├── useCanvasElapsedTime.ts     # Elapsed time tracking
│   ├── useCanvasElementDrag.ts     # Element drag positioning
│   ├── useIconPreload.ts           # Icon image preloading
│   ├── useMediaDurationProbe.ts    # Media duration probing
│   │
│   │ # Utility
│   ├── useDevCompilationStatus.ts
│   ├── useOutsideClick.ts          # Click outside detection
│   └── useDraggable.ts             # Draggable panel management
│
├── stores/
│   └── hooks.ts  (useAppDispatch, useAppSelector)
│
└── components/
    ├── Assets/
    │   └── useAssetUploader.ts
    └── ElementSettings/
        └── useElementSettingsForm.ts

Document Description
runtime-presentation.md RuntimePresentation component - public tour viewer with transitions and offline support
constructor-page-editor.md Constructor page - visual tour builder with element editing
assets-preloading.md Asset preloading architecture and S3 direct download flow
page-transitions.md Video transitions stored on navigation elements
offline-pwa-mode.md PWA offline capabilities and caching