- Architecture Overview
- Design System & Theme
- Component Architecture
- State Management
- Data Management
- Development Workflow
- Code Organization
- Performance Considerations
- Deployment
- Troubleshooting
Presidential Punch Peru is built as a modern Single Page Application (SPA) using React 18 with TypeScript. The application follows a component-based architecture with clear separation of concerns.
| Technology | Purpose | Why Chosen |
|---|---|---|
| React 18 | UI Framework | Modern hooks, concurrent features, excellent ecosystem |
| TypeScript | Type Safety | Prevents runtime errors, better DX, self-documenting code |
| Vite | Build Tool | Fast HMR, modern bundling, optimized for React |
| React Router | Client-side routing | Standard React routing solution, code splitting support |
| Zustand | State Management | Lightweight, minimal boilerplate compared to Redux |
| TanStack Query | Server State | Caching, synchronization, background updates |
| shadcn/ui | Component Library | Customizable, accessible, Tailwind-based |
| Tailwind CSS | Styling | Utility-first, consistent design tokens, rapid development |
- Component-First Approach: All UI is built as reusable components with clear interfaces
- Domain-Driven Organization: Components are organized by feature domain (candidate, compare, political-compass)
- Separation of Concerns: Clear boundaries between UI, state, data, and business logic
- Performance-First: Code splitting, lazy loading, optimized bundling
- Accessibility: Built-in a11y support through shadcn/ui components
The application uses a distinctive retro gaming theme inspired by 90s fighting games:
/* Primary Theme Colors */
--background: 222 47% 11%; /* Deep space blue */
--foreground: 210 40% 98%; /* High contrast white */
--primary: 320 90% 65%; /* Vibrant magenta */
--secondary: 180 100% 50%; /* Neon cyan */
--accent: 45 100% 50%; /* Electric yellow */
/* Team Colors for Political Spectrum */
--team-left: 0 84% 60%; /* P1 Red (left/progressive) */
--team-right: 217 89% 61%; /* P2 Blue (right/conservative) */- Headings: 'Press Start 2P' (pixel font for retro gaming feel)
- Body Text: 'Inter' (modern, readable font for content)
- Contrast: High contrast ratios for accessibility
- Chunky Borders: Sharp corners, defined edges
- Neon Effects: Glowing borders and shadows
- Pixelated Elements: Retro-style buttons and components
- Fighting Game HUD: Status bars, character selection interfaces
The design system uses CSS custom properties for consistent theming:
/* Example usage in components */
.candidate-panel-left {
box-shadow: inset 4px 0 0 hsl(var(--team-left));
}
.section-title {
color: hsl(var(--accent));
text-shadow: 2px 2px hsl(var(--background));
clip-path: polygon(0 0, 100% 0, 100% 100%, 8px 100%, 0 calc(100% - 8px));
}/
├── api/ # Vercel Serverless Functions (Backend)
├── src/
│ ├── components/ # React components
│ │ ├── ui/ # Generic UI (shadcn)
│ │ ├── game/ # Game-specific components
│ │ └── ...
│ ├── hooks/ # Custom logic & API integration
│ ├── store/ # Zustand global state
│ ├── data/ # Static candidate data
│ └── pages/ # Route components// Good: Composable components
<Card>
<CardHeader>
<CardTitle>Candidate Name</CardTitle>
</CardHeader>
<CardContent>
<CandidateDetails />
</CardContent>
</Card>interface CandidateAvatarProps {
src: string;
alt: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string; // Allow style overrides
}Components are built mobile-first with responsive behavior:
// Example: Different layouts for mobile/desktop
const CompareViewLayout = () => {
const isMobile = useIsMobile();
return isMobile ? (
<MobileComparisonGrid />
) : (
<DesktopFightingArena />
);
};The comparison functionality is split across multiple components:
- ComparePage.tsx: Top-level page container and routing
- CompareViewLayout.tsx: Layout coordinator, handles responsive switching
- CandidateFullBody.tsx: Character display for desktop "fighting" view
- ComparePanelDesktop.tsx: Detailed information panels with accordions
- ComparePanelMobile.tsx: Mobile-optimized comparison grid
- CandidatePicker.tsx: Character selection interface
- Dynamic SVG-based visualization
- Interactive candidate positioning
- Responsive scaling and touch support
We encapsulate business logic and "Game Feel" mechanics in specialized hooks found in src/hooks/:
useGameAPI: The primary "Service Layer". It manages:sessionIdgeneration and persistence.useNextPair: Fetches the next two candidates to compare.
useOptimisticVote: Core UX Hook. Implements "The Punch" philosophy.- Updates the vote count and UI state immediately (optimistically).
- Sends the API request in the background.
- Handles rollback if the server request fails.
useGameKeyboard: Manages keyboard event listeners (Arrow keys) for desktop play, ensuring accessibility and triggering sound effects.useGameCompletion: Monitors session progress and triggers the "Game Over" / Results modal when the target vote count is reached.
useItemListSEO: Dynamically injects JSON-LD structured data for Search Engine Optimization.
We use Zustand for global UI state that needs to be accessed across components but doesn't require complex reducer logic:
useGameUIStore: Manages ephemeral UI state for the active game session:isInfoOpen: Controls the candidate details overlay.reducedMotion: Syncs with system accessibility settings.
useCompareStore: Manages the pairing state in the "Compare" mode (who is Player 1 vs Player 2).
// Example: Game UI Store
export const useGameUIStore = create<GameUIState>((set) => ({
isInfoOpen: false,
setInfoOpen: (isOpen) => set({ isInfoOpen: isOpen }),
reducedMotion: false,
// ...
}));- Single Source of Truth: Global state for candidate selection
- Immutable Updates: All state changes create new objects
- Smart Selectors: Logic for candidate slot assignment
- Minimal State: Only store what can't be derived
Used for any future server-side data fetching:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
},
},
});The application uses Vercel Serverless Functions located in the /api directory. This acts as our backend layer, handling business logic and storage interactions.
POST /api/game/vote:- Optimized for high-throughput write speed ("Fire-and-forget").
- Validates session and candidate IDs.
- Writes vote data to storage.
GET /api/ranking/personal:- Replays the user's specific vote history.
- Calculates dynamic ELO ratings on-the-fly.
- Returns a personalized leaderboard.
We implement a dual-mode storage strategy to facilitate easy development:
- Development: In-Memory
Map<>.- Fast, zero setup required.
- Data resets on server restart.
- Prevents polluting production data with test votes.
- Production: Vercel Blob Storage.
- Stores individual JSON files per vote for high concurrency.
- Durable and auditable.
The candidate model is comprehensive and type-safe:
interface Candidate {
id: string;
nombre: string;
ideologia: string;
headshot: string; // Profile image
fullBody: string; // Full character image
proyectoPolitico: { // Political platform
titulo: string;
resumen: string;
detalles?: DetailSection[];
};
creenciasClave: Belief[]; // Key beliefs with sources
trayectoria: Experience[]; // Political trajectory
presenciaDigital: SocialMedia;
mapaDePoder: PowerMapping; // Political alliances
}- Static Data: Stored in
/src/data/as TypeScript modules - Type Safety: All data structures are typed
- Source Attribution: Links to evidence and sources
- Internationalization Ready: Structure supports multiple languages
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Lint code
npm run lint- URL: http://localhost:8080
- Hot Module Replacement: Instant updates during development
- Error Overlay: Clear error messages and stack traces
// eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
)- Feature Branches: Create branches for each feature
- Commit Messages: Use conventional commit format
- Pull Requests: All changes go through PR review
- Continuous Integration: Automated linting and building
- Components: PascalCase (
CandidateAvatar.tsx) - Hooks: camelCase with
useprefix (useCompareStore.ts) - Utils: camelCase (
utils.ts) - Constants: UPPER_SNAKE_CASE in files, camelCase files (
constants.ts)
// External libraries
import React from 'react';
import { useQuery } from '@tanstack/react-query';
// Internal utilities
import { cn } from '@/lib/utils';
import { UI_CLASSES } from '@/lib/constants';
// Components
import { Button } from '@/components/ui/button';
import { CandidateAvatar } from '@/components/candidate/CandidateAvatar';
// Types and data
import { Candidate } from '@/data/candidates';We use absolute imports with the @/ alias:
// ✅ Good
import { Button } from '@/components/ui/button';
// ❌ Avoid
import { Button } from '../../components/ui/button';// Lazy load page components
const HomePage = lazy(() => import('./pages/HomePage'));
const ComparePage = lazy(() => import('./pages/ComparePage'));// vite.config.ts
rollupOptions: {
output: {
manualChunks: {
'vendor-ui': ['@radix-ui/react-accordion', '@radix-ui/react-dialog'],
'vendor-icons': ['react-icons', 'lucide-react'],
'political-compass': ['src/components/political-compass/PoliticalCompass.tsx'],
}
}
}- Lazy Loading: All images load lazily by default
- Responsive Images: Different sizes for different viewports
- Format Optimization: WebP with fallbacks
# Analyze bundle size
npm run build
npx vite-bundle-analyzer dist/We treat performance as a core product feature. The application must adhere to strict "Game Feel" contracts to ensure the voting experience remains visceral and rhythmic.
| Experience | Definition | Metric (p95) | Rationale |
|---|---|---|---|
| The Punch | Instant feedback | < 100ms | Voting must feel visceral. Optimistic UI is required. |
| The Flow | Sustained rhythm | < 1000ms | Transitions must not break the user's "voting trance". |
| The Reach | Digital inclusion | < 3000ms | Functional on 3G Rural Peru networks (10Mbps/60ms). |
The project uses k6 for load testing.
brew install k6 # macOS
# or
npm install -g k6We have defined npm scripts for common testing scenarios:
| Script | Scenario | Users | Duration | Purpose |
|---|---|---|---|---|
npm run loadtest:smoke |
Smoke Test | 5 | 1m | Quick sanity check |
npm run loadtest:baseline |
Baseline | 50 | 5m | Normal operating metrics |
npm run loadtest:peak |
Peak Load | 1k | 15m | Simulate election events |
npm run loadtest:stress |
Stress Test | 3k+ | 30m | Find breaking points |
npm run loadtest:peru |
Peru Specific | 50 | 5m | Test with Peru network profiles |
To ensure "The Reach", use the Peru-specific test which simulates local network conditions (latency, bandwidth, packet loss).
# Test specific network profiles
NETWORK_PROFILE=urban npm run loadtest:peru # Lima (4G Good)
NETWORK_PROFILE=rural npm run loadtest:peru # Remote (3G Limited)
NETWORK_PROFILE=congested npm run loadtest:peru # Peak Hours (7-10 PM)See load-tests/config.js for the exact definitions of these network profiles.
- Use TypeScript strict mode
- Follow React hooks rules
- Prefer composition over inheritance
- Write self-documenting code
- Add JSDoc for complex functions
When adding tests, consider:
- Unit tests for utilities and hooks
- Component tests for UI components
- Integration tests for user flows
- E2E tests for critical paths
This documentation is a living document. Please update it as the codebase evolves.