14 KiB
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
# 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
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)
│ ├── runtime.tsx # Tour playback viewer
│ ├── search.tsx # Global search results
│ ├── p/[slug].tsx # Public tour pages (PWA-enabled)
│ ├── 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
│ ├── ElementSettings/ # Shared element settings form components
│ ├── UiElements/ # Element type components
│ ├── 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
│ ├── useFormSync.ts # Form state synchronization
│ ├── useEntityTable.ts # Data grid with CRUD
│ ├── 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
│ ├── useOfflineMode.ts # PWA offline detection
│ └── ... # Other hooks
│
├── 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
│ ├── elementStyles.ts # Element styling utilities
│ ├── imagePreDecode.ts # Image pre-decoding
│ ├── mediaDuration.ts # Video/audio duration
│ ├── assetUrl.ts # CDN URL resolution
│ ├── extractPageLinks.ts # Extract navigation links from pages
│ ├── StorageManager.ts # Cache API storage for assets
│ ├── parseJson.ts # Safe JSON parsing
│ ├── logger.ts # Client-side logging
│ ├── offline/ # Offline utilities
│ └── offlineDb/ # IndexedDB (Dexie) setup
│
├── layouts/ # Page layouts
│ ├── Authenticated.tsx # Logged-in users layout
│ └── Guest.tsx # Public pages layout
│
├── css/ # Stylesheets
│ └── _theme.css # Tailwind theme overrides
│
├── config/ # Configuration files
│ └── 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
devenvironment content - "Save to Stage" button copies dev → stage
Runtime (/runtime)
Tour playback viewer with:
- 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/[slug])
PWA-enabled public tour access:
/p/[slug]- Showsproductionenvironment (published content)/p/[slug]/stage- Showsstageenvironment (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
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
| Hook | Purpose |
|---|---|
useFormSync |
Sync form state with Redux |
useEntityTable |
Data grid with sorting, filtering, pagination |
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 |
useOfflineMode |
Detect offline/online status |
usePWAPreload |
Preload assets for offline |
useStorageQuota |
Monitor IndexedDB usage |
useElementSettingsForm |
Element settings form state (~60 fields) |
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:
- Global (
element_type_defaults) - Platform-wide default settings (11 predefined types) - Project (
project_element_defaults) - Project-specific overrides (auto-snapshotted on project creation) - 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()utilitiespages/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/[slug]/stage |
Pre-production review |
production |
/p/[slug] |
Published public content |
Publishing flow: dev → stage → production
Pages are filtered by environment before display:
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
# API URL (defaults to localhost:8080)
NEXT_PUBLIC_API_URL=http://localhost:8080
# Frontend port (optional, default 3000)
FRONT_PORT=3000
API Integration
API calls are made via Axios to the backend:
// 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:
/* 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):
import { DataGrid } from '@mui/x-data-grid';
Sidebar Styling
Target #asideMenu for sidebar customization:
#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:
// 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
// 3. Create blob URL → Decode image → Store in readyBlobUrlsRef
// 4. Instant lookup during navigation (O(1))
const blobUrl = preloadOrchestrator.getReadyBlobUrl(originalUrl);
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 |
import { offlineDb } from '../lib/offlineDb';
// Store assets for offline access
await offlineDb.assets.put({ id, data });
Internationalization
Uses i18next with browser language detection:
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:
// 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:
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 deploymentdocker/docker-compose.yml- Full stack (frontend, backend, db)docker/start-backend.sh- Backend startup scriptdocker/wait-for-it.sh- Service dependency waiter
Development Tips
Adding a New Entity Page
- Create Redux slice in
stores/{entity}/{entity}Slice.ts - Create pages:
{entity}.tsx,{entity}/[id].tsx - Add to sidebar menu in
menuAside.ts - Add permissions check in page wrapper
Creating Custom Hooks
Place in src/hooks/ with use prefix:
// 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/:
// src/types/entities.ts
export interface Project {
id: string;
name: string;
slug: string;
// ...
}