Files
dotcom-arcade/docs/04-frontend/components/component-overview.md
T
2026-02-15 11:50:39 +07:00

12 KiB

Component Overview

This document provides an overview of the component architecture and organization in the Flashpoint Web frontend.

Component Organization

Components are organized by domain and functionality:

components/
├── activities/        # Activity log components (2 components)
├── auth/              # Authentication components (8 components)
├── common/            # Shared/utility components (14 components)
├── dialogs/           # Modal dialogs (1 component)
├── error/             # Error handling components (8 components)
├── game/              # Game-specific components (1 component)
├── github/            # GitHub integration (1 component)
├── home/              # Home page components (1 component)
├── jobs/              # Job management (4 components)
├── layout/            # Layout structure (9 components)
├── library/           # Game browsing/display (10 components)
├── player/            # Game players (3 components)
├── playlist/          # Playlist management (10 components)
│   └── SharePlaylistDialog includes domain selector for admins
├── roles/             # Role management (3 components)
├── search/            # Search and filtering (5 components)
├── settings/          # Settings tabs (6 components)
├── stats/             # Statistics/charts (5 components)
├── theme/             # Theme controls (1 component)
├── ui/                # Shadcn UI primitives (36 components)
└── users/             # User management (3 components)

Component Categories

Authentication Components

Located in components/auth/

  • LoginForm - User login form with validation
  • RegisterForm - User registration form
  • ProtectedRoute - Route wrapper for authorization
  • AuthContext - Authentication context provider

Common Components

Located in components/common/

  • Breadcrumbs - Navigation bar with integrated back button and contextual breadcrumb trail
    • Now acts as a navigation bar instead of just breadcrumbs
    • Integrated back button with vertical divider separator
    • Props: items (breadcrumb trail), showBackButton (default: true)
    • Visual: rounded bar with bg-muted/50, pill-style breadcrumb items
    • Exports types: BreadcrumbContext, PlayerBreadcrumbContext, BreadcrumbItem
    • BreadcrumbContext moved from GameCard.tsx to Breadcrumbs.tsx for centralization
  • FormattedDate - Reusable date/time formatting component with user preferences
  • CardSizeControl - Control for adjusting card size in grid views
  • ConfirmDialog - Generic confirmation dialog for destructive actions
  • ErrorBoundary - Error boundary wrapper for graceful error handling
  • FavoriteButton - Button to add games to favorites
  • RemoveFavoriteButton - Button to remove games from favorites
  • MaintenanceGuard - Component to display maintenance mode message
  • MobileWarningDialog - Warning for mobile users about limited functionality
  • NetworkStatusIndicator - Shows online/offline network status
  • RoleGuard - Component wrapper for role-based access control
  • ViewOptions - Toggle between list and grid view modes

Playlist Components

Located in components/playlist/

  • SharePlaylistDialog - Dialog for sharing playlists via URL. Admin users see a domain selector dropdown to choose which domain to use for the share link. Non-admin users use the default domain from public settings. Share URLs are constructed client-side via the buildShareUrl() utility.
  • Other playlist management components...

Settings Components

Located in components/settings/

  • AppSettingsTab - Application settings including Site Name, Maintenance Mode, Theme, and Domain Settings. The Domain Settings card allows admins to add/delete domains and set a default via radio buttons, with hostname validation.
  • Other settings tabs...

Layout Components

Located in components/layout/

  • AppShell - Main application layout container
  • Header - Top navigation bar with search, theme picker, user menu
  • Sidebar - Collapsible side navigation with route-based highlighting

Game Components

Located in components/library/ and components/game/

  • GameCard - Card view for individual games
    • Keyboard accessible: role="link", tabIndex={0}, and onKeyDown handler for Enter/Space keys
    • AddToPlaylistModal now conditionally renders only when open (prevents 50+ hidden modals in grid)
  • GameGrid - Responsive grid layout for game cards
  • GameList - List view for games
  • GameListItem - Individual list item
  • GameBrowseLayout - Complete browse layout with filters and pagination
  • GameInfoGrid - Detailed game information display

Player Components

Located in components/player/

  • GamePlayer - Universal game player wrapper
    • Debug logging moved to useEffect to prevent re-renders on every log
    • Game launch only proceeds once player is mounted
  • RufflePlayer - Flash game emulator component
    • Proper TypeScript type declarations for window.RufflePlayer (no any casts)
    • Supports scale modes: exactfit, noborder, showall (default), noscale

UI Components

Located in components/ui/

The application uses Shadcn UI - a collection of re-usable components built with Radix UI and Tailwind CSS. These are copied into the project and can be customized.

40+ components including:

  • Button, Input, Textarea, Select, Checkbox, Switch
  • Dialog, Alert Dialog, Sheet, Popover, Dropdown Menu
  • Card, Badge, Avatar, Separator, Skeleton
  • Table, Data Table, Pagination
  • Toast, Sonner (notifications)
  • Form components with React Hook Form integration

Component Design Patterns

1. Composition Pattern

Components use composition for flexibility:

<Card>
  <CardHeader>
    <CardTitle>Game Title</CardTitle>
    <CardDescription>Description</CardDescription>
  </CardHeader>
  <CardContent>
    {/* Content */}
  </CardContent>
  <CardFooter>
    {/* Actions */}
  </CardFooter>
</Card>

2. Controlled Components

Components receive state via props and notify parent of changes:

interface FilterPanelProps {
  filters: GameFilters;
  onFiltersChange: (filters: GameFilters) => void;
}

export function FilterPanel({ filters, onFiltersChange }: FilterPanelProps) {
  // Component is controlled by parent
}

3. Render Props

Flexible rendering through render props:

interface GameBrowseLayoutProps {
  renderHeader?: () => React.ReactNode;
  renderActions?: () => React.ReactNode;
  renderFilters?: () => React.ReactNode;
}

4. TypeScript Props

All components are fully typed:

interface GameCardProps {
  game: Game;
  onAddToPlaylist?: (gameId: string) => void;
  onToggleFavorite?: (gameId: string) => void;
  isFavorited?: boolean;
  showFavoriteOnHoverOnly?: boolean;
}

5. Skeleton Loading States

All data components have corresponding skeleton loaders:

// GameCard.tsx
export function GameCard({ game }: GameCardProps) { /* ... */ }

// GameCardSkeleton.tsx
export function GameCardSkeleton() {
  return <Skeleton className="h-full w-full" />;
}

Component Naming Conventions

File Naming

  • PascalCase for component files: GameCard.tsx, UserTable.tsx
  • camelCase for utility files: api.ts, utils.ts
  • kebab-case for UI primitive files: dropdown-menu.tsx, alert-dialog.tsx

Component Exports

// Named export (preferred for main components)
export function GameCard({ game }: GameCardProps) {
  /* ... */
}

// Default export (used for views/pages)
export default function BrowseView() {
  /* ... */
}

Component Props Best Practices

Optional vs Required Props

interface GamePlayerProps {
  // Required props
  title: string;
  platform: string;
  canPlayInBrowser: boolean;

  // Optional props with defaults
  allowFullscreen?: boolean; // Default: true
  showControls?: boolean; // Default: true
  height?: string; // Default: 'calc(100vh - 220px)'

  // Optional callbacks
  onFullscreenChange?: (isFullscreen: boolean) => void;
}

Default Props Pattern

export function GamePlayer({
  title,
  platform,
  canPlayInBrowser,
  allowFullscreen = true,
  showControls = true,
  height = 'calc(100vh - 220px)',
  onFullscreenChange,
}: GamePlayerProps) {
  // Implementation
}

State Management in Components

Local Component State

Use useState for UI-only state:

const [isFullscreen, setIsFullscreen] = useState(false);
const [imageError, setImageError] = useState(false);
const [isLoading, setIsLoading] = useState(true);

Global UI State (Zustand)

Access global state via Zustand stores:

const sidebarCollapsed = useUIStore((state) => state.sidebarCollapsed);
const { isAuthenticated, user } = useAuthStore();
const { mode, primaryColor } = useThemeStore();

Server State (TanStack Query)

Use custom hooks wrapping TanStack Query:

const { data: games, isLoading, error } = useGames(filters);
const { data: playlists } = usePlaylists();

Component Lifecycle

Mount Effect Hook

Use custom useMountEffect for mount-only effects:

import { useMountEffect } from '@/hooks/useMountEffect';

function MyComponent() {
  useMountEffect(() => {
    // Runs once on mount
    console.log('Component mounted');
  });
}

Cleanup Pattern

Always clean up resources in useEffect:

useEffect(() => {
  const handleResize = () => {
    // Handle resize
  };

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

Error Boundaries

Top-level error boundary catches React errors:

<ErrorBoundary>
  <App />
</ErrorBoundary>

Component-level error handling:

if (error) {
  return (
    <div className="text-center py-8">
      <AlertCircle className="mx-auto text-red-500" />
      <p>Failed to load data</p>
    </div>
  );
}

Accessibility

ARIA Labels

<button
  aria-label="Toggle sidebar"
  title="Toggle sidebar width"
  onClick={toggleSidebar}
>
  <Menu size={20} />
</button>

Keyboard Navigation

useEffect(() => {
  const handleEscKey = (event: KeyboardEvent) => {
    if (event.key === 'Escape' && isFullscreen) {
      event.preventDefault();
      setIsFullscreen(false);
    }
  };

  window.addEventListener('keydown', handleEscKey);
  return () => window.removeEventListener('keydown', handleEscKey);
}, [isFullscreen]);

Focus Management

const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
  inputRef.current?.focus();
}, []);

Performance Optimizations

React.memo

Memoize expensive components:

export const GameCard = memo(function GameCard({ game }: GameCardProps) {
  // Component implementation
});

useMemo

Memoize expensive computations:

const sortedGames = useMemo(() => {
  return games.sort((a, b) => a.title.localeCompare(b.title));
}, [games]);

useCallback

Memoize callback functions:

const handleAddToPlaylist = useCallback(
  (gameId: string) => {
    // Add to playlist logic
  },
  [
    /* dependencies */
  ]
);

Lazy Loading

Load images lazily:

<img
  src={imageUrl}
  alt={game.title}
  loading="lazy"
  onLoad={() => setImageLoaded(true)}
  onError={() => setImageError(true)}
/>

Testing Components (Future)

Component Testing Structure

import { render, screen } from '@testing-library/react';
import { GameCard } from './GameCard';

describe('GameCard', () => {
  it('renders game title', () => {
    const game = { id: '1', title: 'Test Game', /* ... */ };
    render(<GameCard game={game} />);
    expect(screen.getByText('Test Game')).toBeInTheDocument();
  });
});

Further Reading