mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-26 15:27:10 -05:00
Add documentation to files
This commit is contained in:
658
docs/FRONTEND_ARCHITECTURE.md
Normal file
658
docs/FRONTEND_ARCHITECTURE.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# Frontend Architecture
|
||||
|
||||
Complete documentation of ThrillWiki's React frontend architecture, routing, state management, and UI patterns.
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework**: React 18.3 with TypeScript
|
||||
- **Build Tool**: Vite
|
||||
- **Styling**: Tailwind CSS with custom design system
|
||||
- **UI Components**: shadcn/ui (Radix UI primitives)
|
||||
- **Routing**: React Router v6
|
||||
- **Data Fetching**: TanStack Query (React Query)
|
||||
- **Forms**: React Hook Form with Zod validation
|
||||
- **State Machines**: Custom TypeScript state machines
|
||||
- **Icons**: lucide-react
|
||||
|
||||
---
|
||||
|
||||
## Routing Structure
|
||||
|
||||
### URL Pattern Standards
|
||||
|
||||
**Parks:**
|
||||
```
|
||||
/parks/ → Global park list
|
||||
/parks/{parkSlug}/ → Individual park detail
|
||||
/parks/{parkSlug}/rides/ → Park's ride list
|
||||
/operators/{operatorSlug}/parks/ → Operator's parks
|
||||
/owners/{ownerSlug}/parks/ → Owner's parks
|
||||
```
|
||||
|
||||
**Rides:**
|
||||
```
|
||||
/rides/ → Global ride list
|
||||
/parks/{parkSlug}/rides/{rideSlug}/ → Ride detail (nested under park)
|
||||
/manufacturers/{manufacturerSlug}/rides/ → Manufacturer's rides
|
||||
/manufacturers/{manufacturerSlug}/models/ → Manufacturer's models
|
||||
/designers/{designerSlug}/rides/ → Designer's rides
|
||||
```
|
||||
|
||||
**Admin:**
|
||||
```
|
||||
/admin/ → Admin dashboard
|
||||
/admin/moderation/ → Moderation queue
|
||||
/admin/reports/ → Reports queue
|
||||
/admin/users/ → User management
|
||||
/admin/system-log/ → System activity log
|
||||
/admin/blog/ → Blog management
|
||||
/admin/settings/ → Admin settings
|
||||
```
|
||||
|
||||
### Route Configuration
|
||||
|
||||
```typescript
|
||||
// src/App.tsx
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Public routes */}
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/parks" element={<Parks />} />
|
||||
<Route path="/parks/:slug" element={<ParkDetail />} />
|
||||
<Route path="/parks/:parkSlug/rides" element={<ParkRides />} />
|
||||
<Route path="/parks/:parkSlug/rides/:rideSlug" element={<RideDetail />} />
|
||||
|
||||
{/* Admin routes - auth guard applied in page component */}
|
||||
<Route path="/admin" element={<AdminDashboard />} />
|
||||
<Route path="/admin/moderation" element={<AdminModeration />} />
|
||||
|
||||
{/* Auth routes */}
|
||||
<Route path="/auth" element={<Auth />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
|
||||
{/* 404 */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### Component Organization
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── admin/ # Admin forms and management tools
|
||||
│ ├── ParkForm.tsx
|
||||
│ ├── RideForm.tsx
|
||||
│ ├── UserManagement.tsx
|
||||
│ └── ...
|
||||
├── auth/ # Authentication components
|
||||
│ ├── AuthModal.tsx
|
||||
│ ├── MFAChallenge.tsx
|
||||
│ ├── TOTPSetup.tsx
|
||||
│ └── ...
|
||||
├── moderation/ # Moderation queue components
|
||||
│ ├── ModerationQueue.tsx
|
||||
│ ├── SubmissionReviewManager.tsx
|
||||
│ ├── QueueFilters.tsx
|
||||
│ └── ...
|
||||
├── parks/ # Park display components
|
||||
│ ├── ParkCard.tsx
|
||||
│ ├── ParkGrid.tsx
|
||||
│ ├── ParkFilters.tsx
|
||||
│ └── ...
|
||||
├── rides/ # Ride display components
|
||||
├── upload/ # Image upload components
|
||||
├── versioning/ # Version history components
|
||||
├── layout/ # Layout components
|
||||
│ ├── Header.tsx
|
||||
│ ├── Footer.tsx
|
||||
│ └── AdminLayout.tsx
|
||||
├── common/ # Shared components
|
||||
│ ├── LoadingGate.tsx
|
||||
│ ├── ProfileBadge.tsx
|
||||
│ └── SortControls.tsx
|
||||
└── ui/ # shadcn/ui base components
|
||||
├── button.tsx
|
||||
├── dialog.tsx
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Page Components
|
||||
|
||||
Top-level route components that:
|
||||
- Handle auth guards (admin pages use `useAdminGuard`)
|
||||
- Fetch initial data with React Query
|
||||
- Implement layout structure
|
||||
- Pass data to feature components
|
||||
|
||||
```typescript
|
||||
// Example: AdminModeration.tsx
|
||||
export function AdminModeration() {
|
||||
const { loading } = useAdminGuard('moderator');
|
||||
|
||||
if (loading) return <LoadingGate isLoading />;
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AdminPageLayout
|
||||
title="Moderation Queue"
|
||||
description="Review and approve submissions"
|
||||
>
|
||||
<ModerationQueue />
|
||||
</AdminPageLayout>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Feature Components
|
||||
|
||||
Domain-specific components with business logic.
|
||||
|
||||
```typescript
|
||||
// Example: ModerationQueue.tsx
|
||||
export function ModerationQueue() {
|
||||
const {
|
||||
items,
|
||||
isLoading,
|
||||
filters,
|
||||
pagination,
|
||||
handleAction,
|
||||
} = useModerationQueueManager();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<QueueFilters {...filters} />
|
||||
<QueueStats items={items} />
|
||||
{items.map(item => (
|
||||
<QueueItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
onAction={handleAction}
|
||||
/>
|
||||
))}
|
||||
<QueuePagination {...pagination} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### React Query (TanStack Query)
|
||||
|
||||
Used for ALL server state (data fetching, caching, mutations).
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```typescript
|
||||
// App.tsx
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
retry: 1,
|
||||
staleTime: 30000, // 30s fresh
|
||||
gcTime: 5 * 60 * 1000, // 5min cache
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
```
|
||||
|
||||
**Custom Hook Pattern:**
|
||||
|
||||
```typescript
|
||||
// src/hooks/useEntityVersions.ts
|
||||
export function useEntityVersions(entityType: EntityType, entityId: string) {
|
||||
const query = useQuery({
|
||||
queryKey: ['versions', entityType, entityId],
|
||||
queryFn: () => fetchVersions(entityType, entityId),
|
||||
enabled: !!entityId,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: rollbackVersion,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['versions', entityType, entityId]);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
versions: query.data,
|
||||
isLoading: query.isLoading,
|
||||
rollback: mutation.mutate
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### State Machines
|
||||
|
||||
Used for complex workflows with strict transitions.
|
||||
|
||||
**1. moderationStateMachine.ts** - Moderation workflow
|
||||
|
||||
```typescript
|
||||
type ModerationState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'claiming'; itemId: string }
|
||||
| { status: 'locked'; itemId: string; lockExpires: string }
|
||||
| { status: 'loading_data'; itemId: string; lockExpires: string }
|
||||
| { status: 'reviewing'; itemId: string; lockExpires: string; reviewData: SubmissionItemWithDeps[] }
|
||||
| { status: 'approving'; itemId: string }
|
||||
| { status: 'rejecting'; itemId: string }
|
||||
| { status: 'complete'; itemId: string; result: 'approved' | 'rejected' }
|
||||
| { status: 'error'; itemId: string; error: string }
|
||||
| { status: 'lock_expired'; itemId: string };
|
||||
|
||||
type ModerationAction =
|
||||
| { type: 'CLAIM_ITEM'; payload: { itemId: string } }
|
||||
| { type: 'LOCK_ACQUIRED'; payload: { lockExpires: string } }
|
||||
| { type: 'LOAD_DATA' }
|
||||
| { type: 'DATA_LOADED'; payload: { reviewData: SubmissionItemWithDeps[] } }
|
||||
| { type: 'START_APPROVAL' }
|
||||
| { type: 'START_REJECTION' }
|
||||
| { type: 'COMPLETE'; payload: { result: 'approved' | 'rejected' } }
|
||||
| { type: 'ERROR'; payload: { error: string } }
|
||||
| { type: 'LOCK_EXPIRED' }
|
||||
| { type: 'RESET' };
|
||||
|
||||
function moderationReducer(
|
||||
state: ModerationState,
|
||||
action: ModerationAction
|
||||
): ModerationState {
|
||||
// ... transition logic with guards
|
||||
}
|
||||
|
||||
// Usage in SubmissionReviewManager.tsx
|
||||
const [state, dispatch] = useReducer(moderationReducer, { status: 'idle' });
|
||||
```
|
||||
|
||||
**2. deletionDialogMachine.ts** - Account deletion wizard
|
||||
|
||||
```typescript
|
||||
type DeletionStep = 'warning' | 'confirm' | 'code';
|
||||
|
||||
type DeletionDialogState = {
|
||||
step: DeletionStep;
|
||||
confirmationCode: string;
|
||||
codeReceived: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
// Usage in AccountDeletionDialog.tsx
|
||||
```
|
||||
|
||||
### React Hook Form
|
||||
|
||||
Used for ALL forms with Zod validation.
|
||||
|
||||
```typescript
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const parkFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name required').max(255),
|
||||
slug: z.string().regex(/^[a-z0-9-]+$/),
|
||||
park_type: z.enum(['theme_park', 'amusement_park', 'water_park']),
|
||||
opening_date: z.string().optional(),
|
||||
// ... all fields validated
|
||||
});
|
||||
|
||||
export function ParkForm() {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(parkFormSchema),
|
||||
defaultValues: initialData,
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ParkFormData) => {
|
||||
await submitParkCreation(data, user.id);
|
||||
// Goes to moderation queue, NOT direct DB write
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{/* form fields */}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Context Providers
|
||||
|
||||
**1. AuthProvider** - Global auth state
|
||||
|
||||
```typescript
|
||||
// src/hooks/useAuth.tsx
|
||||
export function AuthProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [session, setSession] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(_event, session) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
}
|
||||
);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, session, loading }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**2. ThemeProvider** - Light/dark mode
|
||||
|
||||
```typescript
|
||||
// src/components/theme/ThemeProvider.tsx
|
||||
export function ThemeProvider({ children }) {
|
||||
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
root.classList.add(systemTheme);
|
||||
} else {
|
||||
root.classList.add(theme);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**3. LocationAutoDetectProvider** - Auto-detect measurement system
|
||||
|
||||
```typescript
|
||||
// src/components/providers/LocationAutoDetectProvider.tsx
|
||||
export function LocationAutoDetectProvider() {
|
||||
useEffect(() => {
|
||||
const detectLocation = async () => {
|
||||
const { data } = await supabase.functions.invoke('detect-location');
|
||||
if (data?.country) {
|
||||
const system = getMeasurementSystemFromCountry(data.country);
|
||||
// Update user preferences
|
||||
}
|
||||
};
|
||||
|
||||
detectLocation();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI/UX Patterns
|
||||
|
||||
### Design System
|
||||
|
||||
- **Colors**: HSL-based semantic tokens in `index.css`
|
||||
- **Typography**: Custom font scale with Tailwind classes
|
||||
- **Spacing**: Consistent spacing scale (4px, 8px, 16px, etc.)
|
||||
- **Components**: shadcn/ui with custom variants
|
||||
- **Responsive**: Mobile-first, breakpoints at sm, md, lg, xl
|
||||
|
||||
**Color Tokens (index.css):**
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
/* ... dark mode tokens */
|
||||
}
|
||||
```
|
||||
|
||||
### Reusable UI Components
|
||||
|
||||
**AdminPageLayout** - Wraps admin pages
|
||||
|
||||
```typescript
|
||||
<AdminPageLayout
|
||||
title="User Management"
|
||||
description="Manage users"
|
||||
onRefresh={handleRefresh}
|
||||
refreshMode="auto"
|
||||
pollInterval={30000}
|
||||
>
|
||||
{children}
|
||||
</AdminPageLayout>
|
||||
```
|
||||
|
||||
**LoadingGate** - Handles loading/error states
|
||||
|
||||
```typescript
|
||||
<LoadingGate
|
||||
isLoading={loading}
|
||||
error={error}
|
||||
variant="skeleton"
|
||||
>
|
||||
<Content />
|
||||
</LoadingGate>
|
||||
```
|
||||
|
||||
**ProfileBadge** - User display with role badges
|
||||
|
||||
```typescript
|
||||
<ProfileBadge
|
||||
username="john"
|
||||
displayName="John Doe"
|
||||
role="moderator"
|
||||
showRole
|
||||
size="md"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form Patterns
|
||||
|
||||
### Entity Forms
|
||||
|
||||
All entity forms follow this structure:
|
||||
|
||||
1. **Schema Definition** (Zod)
|
||||
2. **Form Setup** (React Hook Form)
|
||||
3. **Image Upload** (EntityMultiImageUploader)
|
||||
4. **Submit Handler** (entitySubmissionHelpers)
|
||||
|
||||
```typescript
|
||||
// 1. Schema
|
||||
const parkFormSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
slug: z.string().regex(/^[a-z0-9-]+$/),
|
||||
// ... all fields
|
||||
});
|
||||
|
||||
// 2. Form
|
||||
const form = useForm({
|
||||
resolver: zodResolver(parkFormSchema),
|
||||
defaultValues: initialData || defaultValues,
|
||||
});
|
||||
|
||||
// 3. Submit
|
||||
const onSubmit = async (data: ParkFormData) => {
|
||||
if (isEditing) {
|
||||
await submitParkUpdate(parkId, data, user.id);
|
||||
} else {
|
||||
await submitParkCreation(data, user.id);
|
||||
}
|
||||
toast({ title: "Submitted for review" });
|
||||
};
|
||||
```
|
||||
|
||||
### Image Upload
|
||||
|
||||
```typescript
|
||||
const [imageAssignments, setImageAssignments] = useState<ImageAssignments>({
|
||||
banner: null,
|
||||
card: null,
|
||||
uploaded: []
|
||||
});
|
||||
|
||||
<EntityMultiImageUploader
|
||||
value={imageAssignments}
|
||||
onChange={setImageAssignments}
|
||||
entityType="park"
|
||||
maxImages={20}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
### Data Fetching Hooks
|
||||
|
||||
```typescript
|
||||
// src/hooks/useEntityVersions.ts
|
||||
export function useEntityVersions(entityType, entityId)
|
||||
|
||||
// src/hooks/useModerationQueue.ts
|
||||
export function useModerationQueue(filters, pagination)
|
||||
|
||||
// src/hooks/useProfile.tsx
|
||||
export function useProfile(userId)
|
||||
|
||||
// src/hooks/useUserRole.ts
|
||||
export function useUserRole()
|
||||
```
|
||||
|
||||
### Utility Hooks
|
||||
|
||||
```typescript
|
||||
// src/hooks/useDebounce.ts
|
||||
export function useDebounce(value, delay)
|
||||
|
||||
// src/hooks/useMobile.tsx
|
||||
export function useMobile()
|
||||
|
||||
// src/hooks/useUnitPreferences.ts
|
||||
export function useUnitPreferences()
|
||||
```
|
||||
|
||||
### Guard Hooks
|
||||
|
||||
```typescript
|
||||
// src/hooks/useAdminGuard.ts
|
||||
export function useAdminGuard(requiredRole: AppRole = 'moderator')
|
||||
|
||||
// src/hooks/useRequireMFA.ts
|
||||
export function useRequireMFA()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### React Query Caching
|
||||
|
||||
```typescript
|
||||
// Aggressive caching for static data
|
||||
const { data: parks } = useQuery({
|
||||
queryKey: ['parks'],
|
||||
queryFn: fetchParks,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
|
||||
// Shorter caching for dynamic data
|
||||
const { data: queue } = useQuery({
|
||||
queryKey: ['moderation', 'queue', filters],
|
||||
queryFn: fetchQueue,
|
||||
staleTime: 10000, // 10 seconds
|
||||
gcTime: 60000, // 1 minute
|
||||
refetchInterval: 30000, // Auto-refresh every 30s
|
||||
});
|
||||
```
|
||||
|
||||
### Code Splitting
|
||||
|
||||
```typescript
|
||||
// Lazy load admin pages
|
||||
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
|
||||
const AdminModeration = lazy(() => import('./pages/AdminModeration'));
|
||||
|
||||
<Suspense fallback={<LoadingGate isLoading />}>
|
||||
<Route path="/admin" element={<AdminDashboard />} />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### Memoization
|
||||
|
||||
```typescript
|
||||
// Expensive computations
|
||||
const filteredItems = useMemo(() => {
|
||||
return items.filter(item =>
|
||||
item.status === statusFilter &&
|
||||
item.entity_type === typeFilter
|
||||
);
|
||||
}, [items, statusFilter, typeFilter]);
|
||||
|
||||
// Callback stability
|
||||
const handleAction = useCallback((itemId: string, action: string) => {
|
||||
performAction(itemId, action);
|
||||
}, [performAction]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**See Also:**
|
||||
- [DATABASE_ARCHITECTURE.md](./DATABASE_ARCHITECTURE.md) - Database schema
|
||||
- [SUBMISSION_FLOW.md](./SUBMISSION_FLOW.md) - Submission workflow
|
||||
- [AUTHENTICATION.md](./AUTHENTICATION.md) - Auth implementation
|
||||
Reference in New Issue
Block a user