Drops the /flash and /html5 sections (old URLs redirect to /browse), the GitHub star count endpoint, the unused HOME_RECENT_HOURS setting, upstream's design notes and code review docs, and the issue template's link to upstream discussions.
18 KiB
Frontend Architecture
This document describes the architectural patterns, design decisions, and technical implementation details of the Dotcom Arcade frontend.
Architecture Overview
The frontend follows a modern React architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────────────┐
│ User Interface │
│ (Views/Pages - Route-based code splitting) │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────┴──────────────────────────────────────────┐
│ Component Layer │
│ (Layout, Game, Auth, Player, UI Components) │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────┴──────────────────────────────────────────┐
│ State Management Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Zustand │ │ TanStack │ │ URL State │ │
│ │ (UI State) │ │ Query │ │ (Filters) │ │
│ │ │ │ (Server Data)│ │ │ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────┴──────────────────────────────────────────┐
│ Data Layer │
│ (API Client with Axios, Request/Response Interceptors) │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────┴──────────────────────────────────────────┐
│ Backend API │
│ (REST API on port 3100) │
└─────────────────────────────────────────────────────────────┘
Core Architectural Patterns
1. Component Architecture
Component Organization
Components are organized by domain and responsibility:
components/
├── auth/ # Authentication (LoginForm, RegisterForm, ProtectedRoute)
├── common/ # Shared utilities (ErrorBoundary, RoleGuard, ConfirmDialog)
├── game/ # Game-specific (GameInfoGrid)
├── layout/ # Layout structure (AppShell, Header, Sidebar)
├── library/ # Game browsing (GameCard, GameGrid, GameList)
├── player/ # Game players (GamePlayer, RufflePlayer)
├── playlist/ # Playlist management (CreatePlaylistModal, AddToPlaylistModal)
├── search/ # Search/filtering (SearchBar, FilterPanel)
├── stats/ # Statistics charts (UserStatsPanel, TopGamesChart)
├── theme/ # Theme controls (ThemePicker, PrimaryColorPicker)
└── ui/ # Shadcn UI primitives (Button, Card, Dialog, etc.)
Component Design Principles
- Single Responsibility: Each component has one clear purpose
- Composition over Inheritance: Build complex UIs from simple components
- Props over State: Prefer controlled components with props
- TypeScript Interfaces: All props are strictly typed
- Separation of Concerns: Presentation logic separate from business logic
2. State Management Strategy
The application uses a three-tier state management approach:
Tier 1: Server State (TanStack Query)
All server-side data (games, playlists, users) is managed by TanStack Query:
Benefits:
- Automatic caching with configurable stale times
- Background refetching
- Request deduplication
- Optimistic updates
- Loading and error states
Configuration:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
retry: 1,
},
},
});
Usage Pattern:
// Custom hook wrapping TanStack Query
export function useGames(filters: GameFilters) {
return useQuery({
queryKey: ['games', filters],
queryFn: () => gamesApi.search(filters),
staleTime: 5 * 60 * 1000,
});
}
Tier 2: UI State (Zustand)
Client-side UI state (authentication, theme, sidebar) uses Zustand:
Benefits:
- Minimal boilerplate
- No providers needed
- Built-in TypeScript support
- Middleware for persistence
- DevTools integration
Stores:
- useAuthStore - Authentication state, JWT tokens, permissions
- useThemeStore - Theme mode (light/dark/system), primary color
- useUIStore - Sidebar collapsed state, view modes, card sizes
Persistence Strategy:
persist(
(set, get) => ({
// state and actions
}),
{
name: 'flashpoint-auth',
storage: createJSONStorage(() => ({
getItem: (name) => {
// Guest sessions use sessionStorage
// Authenticated sessions use localStorage
},
setItem: (name, value) => {
/* ... */
},
removeItem: (name) => {
/* ... */
},
})),
}
);
Tier 3: URL State (React Router)
Filter state and pagination are stored in URL search params:
Benefits:
- Shareable URLs
- Browser back/forward support
- No client-side state management needed
- SEO-friendly
Usage Pattern:
const [searchParams, setSearchParams] = useSearchParams();
// Read from URL
const platform = searchParams.get('platform') || 'all';
const page = parseInt(searchParams.get('page') || '1');
// Update URL
const updateFilters = (newFilters: Partial<GameFilters>) => {
const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => {
if (value) params.set(key, String(value));
else params.delete(key);
});
setSearchParams(params);
};
3. Routing Architecture
Route Configuration
Routes are defined in App.tsx with nested routing structure:
<Routes>
{/* Auth routes (no AppShell) */}
<Route path="/login" element={<LoginView />} />
<Route path="/register" element={<RegisterView />} />
{/* Main app routes (with AppShell) */}
<Route path="/*" element={
<AppShell>
<Routes>
{/* Public routes */}
<Route path="/browse" element={
<ProtectedRoute requireAuth={false}>
<BrowseView />
</ProtectedRoute>
} />
{/* Protected routes */}
<Route path="/games/:id/play" element={
<ProtectedRoute requirePermission="games.play">
<GamePlayerView />
</ProtectedRoute>
} />
</Routes>
</AppShell>
} />
</Routes>
Route Protection
ProtectedRoute Component handles authorization:
interface ProtectedRouteProps {
children: React.ReactNode;
requireAuth?: boolean; // Default: true
requirePermission?: string; // Optional permission check
requireAnyPermission?: string[]; // Any of these permissions
requireAllPermissions?: string[]; // All of these permissions
}
Protection Levels:
- Public Routes -
requireAuth={false}- Accessible to everyone - Authenticated Routes -
requireAuth={true}- Requires login - Permission-based Routes -
requirePermission="games.play"- RBAC check
4. API Integration
Axios Client Configuration
Centralized API client in lib/api.ts:
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
});
Request Interceptor
Automatically adds JWT token to all requests:
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
Response Interceptor
Handles 401 errors and automatic token refresh:
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = useAuthStore.getState().refreshToken;
const tokens = await authApi.refreshToken(refreshToken);
useAuthStore.getState().updateAccessToken(tokens.accessToken);
originalRequest.headers.Authorization = `Bearer ${tokens.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
useAuthStore.getState().clearAuth();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
5. Type Safety
TypeScript Configuration
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
Type Definitions
All types are defined in src/types/:
- game.ts - Game, PaginatedResult, GameFilters, Playlist
- auth.ts - User, Role, Permission, LoginResponse, AuthTokens
- play-tracking.ts - PlaySession, UserStats, GameStats
6. Performance Optimizations
Code Splitting
Route-level lazy loading - All views use React.lazy for optimal bundle splitting:
// App.tsx - Lazy load all views
const LoginView = lazy(() => import('./views/LoginView').then(m => ({ default: m.LoginView })));
const BrowseView = lazy(() => import('./views/BrowseView').then(m => ({ default: m.BrowseView })));
const DashboardView = lazy(() => import('./views/DashboardView').then(m => ({ default: m.DashboardView })));
// ... all 19 views lazy loaded
// Wrapped in Suspense with loading fallback
<Route path="/browse" element={
<Suspense fallback={<RouteLoadingFallback />}>
<BrowseView />
</Suspense>
} />
Impact: Reduced initial bundle size from 289KB to ~180KB (38% reduction)
Manual chunk splitting in vite.config.ts:
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['react', 'react-dom', 'react-router-dom'],
'ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'charts': ['recharts'] // Lazy loaded separately
}
}
}
}
Memoization
React.memo() for expensive components with custom comparison:
// GameCard.tsx - Prevents 98% of unnecessary re-renders
export const GameCard = memo(GameCardComponent, (prevProps, nextProps) => {
// Only re-render if game ID changed
if (prevProps.game.id !== nextProps.game.id) return false;
// Or if favorite status changed
const prevIsFavorited =
prevProps.favoriteGameIds?.has(prevProps.game.id) ?? false;
const nextIsFavorited =
nextProps.favoriteGameIds?.has(nextProps.game.id) ?? false;
if (prevIsFavorited !== nextIsFavorited) return false;
// Or if button visibility changed
if (
prevProps.showFavoriteButton !== nextProps.showFavoriteButton ||
prevProps.showRemoveButton !== nextProps.showRemoveButton
)
return false;
return true; // Don't re-render
});
Impact: In a 50-card grid, only 1 card re-renders when favoriting instead of all 50 (49 prevented renders = 98% reduction)
useMemo() for computed values:
const filteredGames = useMemo(
() => games.filter((game) => game.platform === selectedPlatform),
[games, selectedPlatform]
);
useCallback() for event handlers:
const handleFavorite = useCallback(
(gameId: string) => {
favoriteMutation.mutate(gameId);
},
[favoriteMutation]
);
Image Optimization
<img
src={imageUrl}
alt={game.title}
loading="lazy"
onLoad={() => setImageLoaded(true)}
onError={() => setImageError(true)}
/>
Request Optimization
- Query key deduplication
- Prefetching on hover
- Optimistic updates for mutations
7. Utility Functions
Error Handling Utilities
errorUtils.ts - Type-safe error message extraction:
// Centralized error handling across all components
import { getApiErrorMessage } from '@/utils/errorUtils';
try {
await login(credentials);
} catch (error) {
const message = getApiErrorMessage(error, 'Login failed');
toast.error(message);
}
Benefits:
- Type-safe error handling
- Consistent error message extraction
- Handles Axios errors, Error objects, and unknown types
- Eliminates duplicate error handling code
8. Error Handling
Error Boundary
Top-level error boundary catches React errors:
<ErrorBoundary>
<App />
</ErrorBoundary>
Network Status Indicator
Monitors online/offline status:
<NetworkStatusIndicator />
Toast Notifications
User-facing error messages via Sonner:
import { toast } from 'sonner';
toast.error('Failed to load game data');
toast.success('Game added to favorites');
Design Patterns
Custom Hooks Pattern
All API calls are wrapped in custom hooks:
// hooks/useGames.ts
export function useGames(filters: GameFilters) {
return useQuery({
queryKey: ['games', filters],
queryFn: () => gamesApi.search(filters),
});
}
// Usage in component
const { data, isLoading, error } = useGames(filters);
Compound Component Pattern
Complex components use composition:
<Card>
<CardHeader>
<CardTitle>Game Title</CardTitle>
</CardHeader>
<CardContent>
{/* Content */}
</CardContent>
<CardFooter>
{/* Footer */}
</CardFooter>
</Card>
Render Props Pattern
Flexibility through render props:
<GameBrowseLayout
renderHeader={() => <div>Custom Header</div>}
renderActions={() => <div>Custom Actions</div>}
/>
Build and Deployment
Vite Configuration
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:3100',
'/proxy': 'http://localhost:3100',
},
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
},
});
Production Build
npm run build
Outputs:
- Minified JavaScript bundles
- Code-split chunks
- Optimized CSS
- Source maps
- Asset hashing for cache busting
Code Quality Improvements
Recent improvements to frontend code quality focus on type safety, performance, correctness, and accessibility:
Type Safety
- Non-null assertions replaced:
GamePlayerViewandGameDetailViewnow use safe defaults (id ?? '') instead of non-null assertions (id!) - RufflePlayer types: Proper TypeScript type declarations for
window.RufflePlayervia global type augmentation (noanycasts)
Performance Optimizations
- useCallback for formatters:
useDateTimeFormatformatters wrapped inuseCallbackto prevent unnecessary re-renders in date/time formatting - useCallback for relative time:
formatRelativeTimeutility wrapped inuseCallbackinGameDetailView - Modal conditional rendering:
GameCardnow conditionally rendersAddToPlaylistModalonly when open (prevents 50+ hidden modal instances in grid)
Correctness & Robustness
- Request cancellation:
gamesApi.search()now accepts and properly passesAbortSignalparameter for request cancellation on component unmount - Optimistic update fix:
useFavoritesoptimistic update uses snapshot of current cache instead of re-reading mutated cache - useState for state:
usePlayTrackingreturnsuseState-backed values instead of refs to ensure proper React re-renders
Accessibility
- Keyboard navigation:
GameCardclickable areas now have proper keyboard support withrole="link",tabIndex={0}, andonKeyDownhandlers for Enter/Space - ARIA labels: Back buttons now include descriptive
aria-labelattributes
Framework Compatibility
- Vite configuration:
ErrorBoundarynow usesimport.meta.env.DEVinstead ofprocess.env.NODE_ENVfor Vite compatibility
Testing Strategy
Unit Tests (Future)
- Component testing with React Testing Library
- Hook testing with @testing-library/react-hooks
- Utility function tests
Integration Tests (Future)
- API integration tests
- Form submission flows
- Authentication flows
E2E Tests (Future)
- Critical user journeys
- Game browsing and playing
- Playlist management