528 lines
18 KiB
Markdown
528 lines
18 KiB
Markdown
# Tour Builder Platform - Frontend
|
|
|
|
Next.js 15 application with React 19, TypeScript, Redux Toolkit, and Tailwind CSS for the Tour Builder Platform.
|
|
|
|
## Tech Stack
|
|
|
|
- **Framework**: Next.js 15 with Turbopack
|
|
- **UI Library**: React 19
|
|
- **Language**: TypeScript 5.4
|
|
- **State Management**: Redux Toolkit
|
|
- **Styling**: Tailwind CSS 3.4 + MUI 6
|
|
- **Forms**: Formik
|
|
- **Data Grid**: MUI X Data Grid v7
|
|
- **Charts**: ApexCharts, Chart.js
|
|
- **Drag & Drop**: react-dnd
|
|
- **PWA**: Serwist (Service Worker)
|
|
- **i18n**: i18next
|
|
- **Offline Storage**: Dexie (IndexedDB)
|
|
|
|
## Prerequisites
|
|
|
|
- Node.js 18+
|
|
- npm or yarn
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Install dependencies
|
|
npm install
|
|
|
|
# Start development server (port 3000)
|
|
npm run dev
|
|
|
|
# Production build
|
|
npm run build
|
|
npm run start
|
|
```
|
|
|
|
The app runs on **port 3000** by default (configurable via `FRONT_PORT` env var).
|
|
|
|
## Available Commands
|
|
|
|
```bash
|
|
npm run dev # Start dev server with Turbopack
|
|
npm run build # Production build
|
|
npm run start # Start production server
|
|
npm run lint # ESLint check (.ts, .tsx files)
|
|
npm run format # Format code with Prettier
|
|
```
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
frontend/src/
|
|
├── pages/ # Next.js pages (file-based routing)
|
|
│ ├── _app.tsx # App wrapper (Redux, i18n, toast providers)
|
|
│ ├── index.tsx # Landing/redirect page
|
|
│ ├── login.tsx # Login page
|
|
│ ├── register.tsx # Registration page
|
|
│ ├── dashboard.tsx # Main dashboard
|
|
│ ├── constructor.tsx # Tour builder/editor (drag-drop, elements)
|
|
│ ├── search.tsx # Global search results
|
|
│ ├── p/[projectSlug]/ # Public tour pages (PWA-enabled)
|
|
│ │ ├── index.tsx # Production presentation
|
|
│ │ └── stage.tsx # Stage preview
|
|
│ ├── projects/ # Project CRUD pages
|
|
│ ├── tour_pages/ # Tour page management
|
|
│ ├── assets/ # Asset library
|
|
│ └── ... # Other entity pages
|
|
│
|
|
├── components/ # React components (PascalCase)
|
|
│ ├── Assets/ # Asset management components
|
|
│ ├── Constructor/ # Tour builder components (CanvasElement, etc.)
|
|
│ ├── ElementSettings/ # Shared element settings form components
|
|
│ ├── UiElements/ # Unified element rendering (WYSIWYG consistency)
|
|
│ │ ├── UiElementRenderer.tsx # Main entry point (used by both constructor & runtime)
|
|
│ │ ├── shared/ # Shared hooks (useElementWrapperStyle)
|
|
│ │ └── elements/ # Per-type components (NavigationElement, GalleryElement, etc.)
|
|
│ ├── RuntimeElement.tsx # Runtime element wrapper (position, effects)
|
|
│ ├── Generic/ # Generic CRUD components
|
|
│ ├── CardBox.tsx # Card container
|
|
│ ├── NavBar.tsx # Top navigation
|
|
│ ├── AsideMenu*.tsx # Sidebar menu components
|
|
│ ├── FormField.tsx # Form input wrapper
|
|
│ ├── DataGridMultiSelect.tsx # Enhanced data grid
|
|
│ └── ... # UI components
|
|
│
|
|
├── stores/ # Redux Toolkit
|
|
│ ├── store.ts # Store configuration
|
|
│ ├── hooks.ts # useAppSelector, useAppDispatch
|
|
│ ├── authSlice.ts # Authentication state
|
|
│ ├── mainSlice.ts # UI state (sidebar, modals)
|
|
│ ├── styleSlice.ts # Theme/styling state
|
|
│ ├── createEntitySlice.ts # Factory for entity slices
|
|
│ └── {entity}/ # Entity-specific slices
|
|
│ └── {entity}Slice.ts
|
|
│
|
|
├── hooks/ # Custom React hooks (32 hooks)
|
|
│ ├── usePreloadOrchestrator.ts # Asset preloading with S3 presigned URLs
|
|
│ ├── usePageSwitch.ts # Page navigation with preloaded blob URLs
|
|
│ ├── useNeighborGraph.ts # Page navigation graph (maxDepth: 1)
|
|
│ ├── useTransitionPlayback.ts # Video transitions
|
|
│ ├── usePageNavigation.ts # Runtime page history
|
|
│ ├── useReversePlayback.ts # Reverse video playback
|
|
│ ├── useConstructorElements.ts # Manage canvas elements
|
|
│ ├── useConstructorPageActions.ts # Save, publish, create pages
|
|
│ ├── useOfflineMode.ts # PWA offline detection
|
|
│ ├── useFormSync.ts # Form state synchronization
|
|
│ ├── useEntityTable.ts # Data grid with CRUD
|
|
│ └── ... # Other hooks (see Custom Hooks section)
|
|
│
|
|
├── types/ # TypeScript definitions
|
|
│ ├── constructor.ts # Tour builder types
|
|
│ ├── runtime.ts # Runtime playback types
|
|
│ ├── preload.ts # Asset preloading types
|
|
│ ├── entities.ts # Entity interfaces
|
|
│ ├── permissions.ts # RBAC types
|
|
│ └── ... # Other types
|
|
│
|
|
├── lib/ # Utility libraries
|
|
│ ├── assetUrl.ts # CDN URL resolution
|
|
│ ├── constructorHelpers.ts # Constructor page helpers
|
|
│ ├── elementDefaults.ts # Element default values
|
|
│ ├── elementEffects.ts # Element effect utilities
|
|
│ ├── elementStyles.ts # Element styling utilities
|
|
│ ├── extractPageLinks.ts # Extract navigation links from pages
|
|
│ ├── fonts.ts # Font configuration
|
|
│ ├── imagePreDecode.ts # Image pre-decoding
|
|
│ ├── logger.ts # Client-side logging
|
|
│ ├── mediaDuration.ts # Video/audio duration
|
|
│ ├── mediaHelpers.ts # Media utilities
|
|
│ ├── navigationHelpers.ts # Navigation utilities
|
|
│ ├── parseJson.ts # Safe JSON parsing
|
|
│ ├── slugHelpers.ts # Slug generation utilities
|
|
│ ├── tourFlowHelpers.ts # Tour flow utilities
|
|
│ ├── offline/ # Offline utilities
|
|
│ │ ├── DownloadEventBus.ts # Download event handling
|
|
│ │ ├── DownloadManager.ts # Download queue management
|
|
│ │ └── StorageManager.ts # Cache API storage for assets
|
|
│ └── offlineDb/ # IndexedDB (Dexie)
|
|
│ ├── schema.ts # Dexie database schema
|
|
│ └── OfflineDbManager.ts # Offline data management
|
|
│
|
|
├── layouts/ # Page layouts
|
|
│ ├── Authenticated.tsx # Logged-in users layout
|
|
│ └── Guest.tsx # Public pages layout
|
|
│
|
|
├── css/ # Stylesheets
|
|
│ └── _theme.css # Tailwind theme overrides
|
|
│
|
|
├── config/ # Configuration files
|
|
│ ├── offline.config.ts # Offline/PWA settings
|
|
│ └── preload.config.ts # Preload priorities and settings
|
|
├── schemas/ # Validation schemas (Zod)
|
|
├── factories/ # Component/hook factories
|
|
├── context/ # React contexts
|
|
├── sw.ts # Service Worker (Serwist)
|
|
├── menuAside.ts # Sidebar menu definition
|
|
├── menuNavBar.ts # Navbar menu definition
|
|
├── colors.ts # Color palette
|
|
└── styles.ts # Style constants
|
|
```
|
|
|
|
## Key Pages
|
|
|
|
### Constructor (`/constructor?projectId=`)
|
|
|
|
Visual tour builder with:
|
|
- Drag-and-drop element positioning
|
|
- Canvas-based page editing
|
|
- Element property panels
|
|
- Page thumbnail navigation
|
|
- Live preview
|
|
- **Always shows `dev` environment content**
|
|
- "Save to Stage" button copies dev → stage
|
|
|
|
### Runtime Presentation (`RuntimePresentation.tsx`)
|
|
|
|
The tour playback is handled by the `RuntimePresentation` component (used in public tour pages), featuring:
|
|
- Full-screen presentation mode
|
|
- Video transitions between pages
|
|
- Forward/reverse playback
|
|
- Background audio
|
|
- Keyboard/touch navigation
|
|
- Asset preloading with S3 presigned URLs
|
|
|
|
### Public Tours (`/p/[projectSlug]`)
|
|
|
|
PWA-enabled public tour access:
|
|
- **`/p/[projectSlug]`** - Shows `production` environment (published content)
|
|
- **`/p/[projectSlug]/stage`** - Shows `stage` environment (preview)
|
|
- Offline support via Service Worker
|
|
- Asset caching in Cache API and IndexedDB
|
|
- Direct S3 downloads via presigned URLs
|
|
|
|
## State Management
|
|
|
|
### Redux Slices
|
|
|
|
| Slice | Purpose |
|
|
|-------|---------|
|
|
| `authSlice` | User authentication, JWT tokens |
|
|
| `mainSlice` | UI state (sidebar, dark mode) |
|
|
| `styleSlice` | Theme configuration |
|
|
| `{entity}Slice` | Entity CRUD state |
|
|
|
|
### Usage Pattern
|
|
|
|
```typescript
|
|
import { useAppSelector, useAppDispatch } from '../stores/hooks';
|
|
import { fetch, create, update, deleteItem } from '../stores/projects/projectsSlice';
|
|
|
|
const dispatch = useAppDispatch();
|
|
const { rows, loading } = useAppSelector((state) => state.projects);
|
|
|
|
// Fetch with pagination
|
|
dispatch(fetch({ query: '?limit=100&offset=0' }));
|
|
|
|
// Create
|
|
dispatch(create({ data: newProject }));
|
|
```
|
|
|
|
## Custom Hooks
|
|
|
|
### Runtime & Preloading Hooks
|
|
|
|
| Hook | Purpose |
|
|
|------|---------|
|
|
| `usePreloadOrchestrator` | Asset preloading with S3 presigned URLs and ready blob URLs |
|
|
| `usePageSwitch` | Page navigation using preloaded blob URLs (O(1) instant lookup) |
|
|
| `useNeighborGraph` | Build page navigation graph (maxDepth: 1) |
|
|
| `useTransitionPlayback` | Video transition playback coordination |
|
|
| `usePageNavigation` | Runtime page history management |
|
|
| `useReversePlayback` | Reverse video playback |
|
|
| `usePageDataLoader` | Load pages with environment filtering |
|
|
| `usePreloadProgress` | Track preload progress |
|
|
| `useBackgroundTransition` | Background transition effects |
|
|
|
|
### PWA & Offline Hooks
|
|
|
|
| Hook | Purpose |
|
|
|------|---------|
|
|
| `useOfflineMode` | Detect offline/online status |
|
|
| `usePWAPreload` | Preload assets for offline |
|
|
| `useStorageQuota` | Monitor IndexedDB usage |
|
|
| `useNetworkAware` | Network-aware operations |
|
|
|
|
### Constructor Hooks
|
|
|
|
| Hook | Purpose |
|
|
|------|---------|
|
|
| `useConstructorElements` | Manage canvas elements |
|
|
| `useConstructorPageActions` | Save, publish, create pages |
|
|
| `useCanvasElementDrag` | Element drag-and-drop |
|
|
| `useCanvasElapsedTime` | Track canvas elapsed time |
|
|
| `useTransitionPreview` | Preview transitions |
|
|
| `useElementEffects` | Element visual effects |
|
|
| `useIconPreload` | Preload element icons |
|
|
| `useMediaDurationProbe` | Probe media duration |
|
|
|
|
### Form & Table Hooks
|
|
|
|
| Hook | Purpose |
|
|
|------|---------|
|
|
| `useFormSync` | Sync form state with Redux |
|
|
| `useEntityTable` | Data grid with sorting, filtering, pagination |
|
|
| `useEditPageSync` | Sync edit page state |
|
|
| `useFilterItems` | Filter items in lists |
|
|
| `useCSVHandling` | CSV import/export |
|
|
|
|
### Utility Hooks
|
|
|
|
| Hook | Purpose |
|
|
|------|---------|
|
|
| `useDraggable` | Generic draggable behavior |
|
|
| `useOutsideClick` | Detect clicks outside element |
|
|
| `useDashboardCounts` | Dashboard statistics |
|
|
| `useProjectAssets` | Project asset management |
|
|
| `useDevCompilationStatus` | Dev server compilation status |
|
|
|
|
**Component-specific hooks:**
|
|
- `useElementSettingsForm` (`components/ElementSettings/`) - Element settings form state
|
|
|
|
## Element Types
|
|
|
|
The tour builder supports these UI elements:
|
|
|
|
| Type | Description |
|
|
|------|-------------|
|
|
| `navigation_next` | Forward navigation button |
|
|
| `navigation_prev` | Back navigation button |
|
|
| `spot` | Hotspot/clickable area |
|
|
| `description` | Text description |
|
|
| `tooltip` | Hover tooltip |
|
|
| `gallery` | Image gallery |
|
|
| `carousel` | Image carousel |
|
|
| `logo` | Logo element |
|
|
| `video_player` | Video player |
|
|
| `audio_player` | Audio player |
|
|
| `popup` | Popup/modal |
|
|
|
|
### Element Defaults Hierarchy
|
|
|
|
Element types follow a three-tier defaults system:
|
|
|
|
1. **Global** (`element_type_defaults`) - Platform-wide default settings (11 predefined types)
|
|
2. **Project** (`project_element_defaults`) - Project-specific overrides (auto-snapshotted on project creation)
|
|
3. **Instance** (`tour_pages.ui_schema_json`) - Page-specific element instances with custom settings
|
|
|
|
The constructor fetches project defaults via `/api/project-element-defaults?projectId=xxx` and merges them when creating new elements. Existing element values in `ui_schema_json` take precedence over defaults.
|
|
|
|
**Admin Pages:**
|
|
- `/element-type-defaults` - Edit global defaults (platform-wide)
|
|
- `/project-element-defaults/[id]` - Edit project defaults (with Reset to Global, Diff indicator)
|
|
|
|
**Key files:**
|
|
- `components/ElementSettings/` - Shared form components (tabs, CSS styling, type-specific sections)
|
|
- `components/ElementSettings/useElementSettingsForm.ts` - Form state hook (~60 fields)
|
|
- `types/constructor.ts` - `normalizeElementDefault()`, `buildElementDefaultsMap()` utilities
|
|
- `pages/constructor.tsx` - Loads project defaults, creates elements with merged settings
|
|
|
|
## Content Environments
|
|
|
|
Tour pages have a three-tier environment model:
|
|
|
|
| Environment | Route | Description |
|
|
|-------------|-------|-------------|
|
|
| `dev` | `/constructor?projectId=` | Editing/draft content |
|
|
| `stage` | `/p/[projectSlug]/stage` | Pre-production review |
|
|
| `production` | `/p/[projectSlug]` | Published public content |
|
|
|
|
**Publishing flow:** `dev` → `stage` → `production`
|
|
|
|
Pages are filtered by environment before display:
|
|
```typescript
|
|
const filteredPages = pages.filter(p => p.environment === environment);
|
|
const { pageLinks, preloadElements } = extractPageLinksAndElements(filteredPages);
|
|
```
|
|
|
|
The `X-Runtime-Environment` header tells the backend which environment to query.
|
|
|
|
## Environment Variables
|
|
|
|
```env
|
|
# Backend API URL (defaults to localhost:8080)
|
|
NEXT_PUBLIC_BACK_API=http://localhost:8080
|
|
|
|
# Frontend port (optional, default 3000)
|
|
FRONT_PORT=3000
|
|
```
|
|
|
|
## API Integration
|
|
|
|
API calls are made via Axios to the backend:
|
|
|
|
```typescript
|
|
// src/pages/api/ contains API route handlers
|
|
// Most data fetching goes through Redux thunks
|
|
|
|
import axios from 'axios';
|
|
|
|
const response = await axios.get('/api/projects', {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
});
|
|
```
|
|
|
|
## Styling
|
|
|
|
### Tailwind CSS
|
|
|
|
Primary styling via Tailwind with custom theme in `css/_theme.css`:
|
|
|
|
```css
|
|
/* Theme overrides use @apply */
|
|
.btn-primary {
|
|
@apply bg-blue-500 hover:bg-blue-600 text-white;
|
|
}
|
|
```
|
|
|
|
### MUI Components
|
|
|
|
MUI is used for complex components (Data Grid, dialogs):
|
|
|
|
```typescript
|
|
import { DataGrid } from '@mui/x-data-grid';
|
|
```
|
|
|
|
### Sidebar Styling
|
|
|
|
Target `#asideMenu` for sidebar customization:
|
|
|
|
```css
|
|
#asideMenu {
|
|
/* Sidebar styles */
|
|
}
|
|
```
|
|
|
|
## PWA & Offline Support
|
|
|
|
### Service Worker
|
|
|
|
Generated by Serwist from `src/sw.ts`:
|
|
|
|
- Precaches static assets
|
|
- Runtime caching for API responses
|
|
- Background sync for offline changes
|
|
|
|
### Asset Preloading (S3 Direct Download)
|
|
|
|
Assets are preloaded directly from S3 for better performance:
|
|
|
|
```typescript
|
|
// 1. Request presigned URLs (max 50 per batch, 1-hour expiry)
|
|
POST /api/file/presign { urls: ["assets/img.jpg", ...] }
|
|
|
|
// 2. Download directly from S3 → Store in Cache API (dual key)
|
|
// 3. Create blob URL → Decode image → Store in readyBlobUrlsRef (dual key)
|
|
|
|
// 4. Instant lookup during navigation (O(1)) - use storage key for reliability
|
|
const blobUrl = preloadOrchestrator.getReadyBlobUrl(storageKey);
|
|
```
|
|
|
|
**Storage Key Mapping:** Assets are cached under both download URL and canonical storage key (e.g., `assets/project/video.mp4`). Lookups prioritize storage key because presigned URL signatures change on each resolution. This ensures cache hits even after URL regeneration or page refresh.
|
|
|
|
**Preload Priority:**
|
|
| Type | Priority | Notes |
|
|
|------|----------|-------|
|
|
| Transition video | +150 | Needed immediately on click |
|
|
| Image | +100 | Backgrounds |
|
|
| Audio | +50 | Background audio |
|
|
| Video | +30 | Can stream progressively |
|
|
|
|
### Storage Layers
|
|
|
|
| Layer | Size Limit | Purpose |
|
|
|-------|------------|---------|
|
|
| Cache API | < 5MB | Fast asset storage |
|
|
| IndexedDB (Dexie) | ≥ 5MB | Large assets, offline data |
|
|
|
|
```typescript
|
|
import { offlineDb } from '../lib/offlineDb';
|
|
|
|
// Store assets for offline access
|
|
await offlineDb.assets.put({ id, data });
|
|
```
|
|
|
|
## Internationalization
|
|
|
|
Uses i18next with browser language detection:
|
|
|
|
```typescript
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const { t } = useTranslation();
|
|
return <span>{t('common.save')}</span>;
|
|
```
|
|
|
|
Translation files in `public/locales/{lang}/`.
|
|
|
|
## MUI X Data Grid v7
|
|
|
|
Use the new `valueGetter` signature:
|
|
|
|
```typescript
|
|
// Value transformation
|
|
valueGetter: (value) => value?.id ?? value
|
|
|
|
// Row access (for computed values)
|
|
valueGetter: (_value, row) => new Date(row.created_at)
|
|
```
|
|
|
|
## Docker
|
|
|
|
The project includes Docker support:
|
|
|
|
```bash
|
|
cd docker
|
|
chmod +x start-backend.sh wait-for-it.sh
|
|
docker-compose up
|
|
```
|
|
|
|
Access at `http://localhost:3000`
|
|
|
|
### Docker Files
|
|
|
|
- `Dockerfile` - Production build for cloud deployment
|
|
- `docker/docker-compose.yml` - Full stack (frontend, backend, db)
|
|
- `docker/start-backend.sh` - Backend startup script
|
|
- `docker/wait-for-it.sh` - Service dependency waiter
|
|
|
|
## Development Tips
|
|
|
|
### Adding a New Entity Page
|
|
|
|
1. Create Redux slice in `stores/{entity}/{entity}Slice.ts`
|
|
2. Create pages: `{entity}.tsx`, `{entity}/[id].tsx`
|
|
3. Add to sidebar menu in `menuAside.ts`
|
|
4. Add permissions check in page wrapper
|
|
|
|
### Creating Custom Hooks
|
|
|
|
Place in `src/hooks/` with `use` prefix:
|
|
|
|
```typescript
|
|
// src/hooks/useMyHook.ts
|
|
export function useMyHook() {
|
|
// Hook logic
|
|
}
|
|
|
|
// Export from index
|
|
// src/hooks/index.ts
|
|
export * from './useMyHook';
|
|
```
|
|
|
|
### Type Safety
|
|
|
|
All entities should have TypeScript interfaces in `src/types/`:
|
|
|
|
```typescript
|
|
// src/types/entities.ts
|
|
export interface Project {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
// ...
|
|
}
|
|
```
|