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