# Frontend Architecture ## Overview Two separate Nuxt 3 applications (one per league) with maximum code reuse through a shared component library. Mobile-first design with real-time WebSocket updates. ## Directory Structure ### Per-League Frontend (`frontend-sba/` and `frontend-pd/`) ``` frontend-{league}/ ├── assets/ # Static assets │ ├── css/ │ │ └── tailwind.css # Tailwind imports │ └── images/ │ ├── logo.png │ └── field-bg.svg │ ├── components/ # League-specific components │ ├── Branding/ │ │ ├── Header.vue │ │ ├── Footer.vue │ │ └── Logo.vue │ └── League/ │ └── SpecialFeatures.vue # League-specific UI elements │ ├── composables/ # Vue composables │ ├── useAuth.ts # Authentication state │ ├── useWebSocket.ts # WebSocket connection │ ├── useGameState.ts # Game state management │ └── useLeagueConfig.ts # League-specific config │ ├── layouts/ │ ├── default.vue # Standard layout │ ├── game.vue # Game view layout │ └── auth.vue # Auth pages layout │ ├── pages/ │ ├── index.vue # Home/dashboard │ ├── games/ │ │ ├── [id].vue # Game view │ │ ├── create.vue # Create new game │ │ └── history.vue # Completed games │ ├── auth/ │ │ ├── login.vue │ │ └── callback.vue # Discord OAuth callback │ └── spectate/ │ └── [id].vue # Spectator view │ ├── plugins/ │ ├── socket.client.ts # Socket.io plugin │ └── auth.ts # Auth plugin │ ├── store/ # Pinia stores │ ├── auth.ts # Authentication state │ ├── game.ts # Current game state │ ├── games.ts # Games list │ └── ui.ts # UI state (modals, toasts) │ ├── types/ │ ├── game.ts # Game-related types │ ├── player.ts # Player types │ ├── api.ts # API response types │ └── websocket.ts # WebSocket event types │ ├── utils/ │ ├── api.ts # API client │ ├── formatters.ts # Data formatting utilities │ └── validators.ts # Input validation │ ├── middleware/ │ ├── auth.ts # Auth guard │ └── game-access.ts # Game access validation │ ├── app.vue # Root component ├── nuxt.config.ts # Nuxt configuration ├── tailwind.config.js # Tailwind configuration ├── tsconfig.json # TypeScript configuration └── package.json ``` ### Shared Component Library (`shared-components/`) ``` shared-components/ ├── src/ │ ├── components/ │ │ ├── Game/ │ │ │ ├── GameBoard.vue # Baseball diamond visualization │ │ │ ├── ScoreBoard.vue # Score display │ │ │ ├── PlayByPlay.vue # Play history feed │ │ │ ├── CurrentSituation.vue # Current game context │ │ │ └── BaseRunners.vue # Runner indicators │ │ │ │ │ ├── Decisions/ │ │ │ ├── DefensivePositioning.vue │ │ │ ├── StolenBaseAttempt.vue │ │ │ ├── OffensiveApproach.vue │ │ │ └── DecisionTimer.vue │ │ │ │ │ ├── Actions/ │ │ │ ├── SubstitutionModal.vue │ │ │ ├── PitchingChange.vue │ │ │ └── ActionButton.vue │ │ │ │ │ ├── Display/ │ │ │ ├── PlayerCard.vue # Player card display │ │ │ ├── DiceRoll.vue # Dice animation │ │ │ ├── PlayOutcome.vue # Play result display │ │ │ └── ConnectionStatus.vue # WebSocket status │ │ │ │ │ └── Common/ │ │ ├── Button.vue │ │ ├── Modal.vue │ │ ├── Toast.vue │ │ └── Loading.vue │ │ │ ├── composables/ │ │ ├── useGameActions.ts # Shared game actions │ │ └── useGameDisplay.ts # Shared display logic │ │ │ └── types/ │ └── index.ts # Shared TypeScript types │ ├── package.json └── tsconfig.json ``` ## Key Components ### 1. WebSocket Composable (`composables/useWebSocket.ts`) **Responsibilities**: - Establish and maintain WebSocket connection - Handle reconnection logic - Emit events to server - Subscribe to server events - Connection status monitoring **Implementation**: ```typescript import { io, Socket } from 'socket.io-client' import { ref, onUnmounted } from 'vue' export const useWebSocket = () => { const socket = ref(null) const connected = ref(false) const reconnecting = ref(false) const connect = (token: string) => { const config = useRuntimeConfig() socket.value = io(config.public.wsUrl, { auth: { token }, reconnection: true, reconnectionDelay: 1000, reconnectionAttempts: 5 }) socket.value.on('connect', () => { connected.value = true reconnecting.value = false }) socket.value.on('disconnect', () => { connected.value = false }) socket.value.on('reconnecting', () => { reconnecting.value = true }) } const disconnect = () => { socket.value?.disconnect() socket.value = null connected.value = false } const emit = (event: string, data: any) => { if (!socket.value?.connected) { throw new Error('WebSocket not connected') } socket.value.emit(event, data) } const on = (event: string, handler: (...args: any[]) => void) => { socket.value?.on(event, handler) } const off = (event: string, handler?: (...args: any[]) => void) => { socket.value?.off(event, handler) } onUnmounted(() => { disconnect() }) return { socket, connected, reconnecting, connect, disconnect, emit, on, off } } ``` ### 2. Game State Store (`store/game.ts`) **Responsibilities**: - Hold current game state - Update state from WebSocket events - Provide computed properties for UI - Handle optimistic updates **Implementation**: ```typescript import { defineStore } from 'pinia' import type { GameState, PlayOutcome } from '~/types/game' export const useGameStore = defineStore('game', () => { // State const gameState = ref(null) const loading = ref(false) const error = ref(null) const pendingAction = ref(false) // Computed const inning = computed(() => gameState.value?.inning ?? 1) const half = computed(() => gameState.value?.half ?? 'top') const outs = computed(() => gameState.value?.outs ?? 0) const score = computed(() => ({ home: gameState.value?.home_score ?? 0, away: gameState.value?.away_score ?? 0 })) const runners = computed(() => gameState.value?.runners ?? {}) const currentBatter = computed(() => gameState.value?.current_batter) const currentPitcher = computed(() => gameState.value?.current_pitcher) const isMyTurn = computed(() => { // Logic to determine if it's user's turn return gameState.value?.decision_required?.user_id === useAuthStore().userId }) // Actions const setGameState = (state: GameState) => { gameState.value = state loading.value = false error.value = null } const updateState = (updates: Partial) => { if (gameState.value) { gameState.value = { ...gameState.value, ...updates } } } const handlePlayCompleted = (outcome: PlayOutcome) => { // Update state based on play outcome if (gameState.value) { updateState({ outs: outcome.outs_after, runners: outcome.runners_after, home_score: outcome.home_score, away_score: outcome.away_score }) } pendingAction.value = false } const setError = (message: string) => { error.value = message loading.value = false pendingAction.value = false } const reset = () => { gameState.value = null loading.value = false error.value = null pendingAction.value = false } return { // State gameState, loading, error, pendingAction, // Computed inning, half, outs, score, runners, currentBatter, currentPitcher, isMyTurn, // Actions setGameState, updateState, handlePlayCompleted, setError, reset } }) ``` ### 3. Game Board Component (`shared-components/Game/GameBoard.vue`) **Responsibilities**: - Visual baseball diamond - Show runners on base - Highlight active bases - Responsive scaling **Implementation**: ```vue ``` ### 4. Decision Flow Component (`shared-components/Decisions/DefensivePositioning.vue`) **Responsibilities**: - Present defensive positioning options - Validate selection - Submit decision via WebSocket - Show loading state **Implementation**: ```vue ``` ## State Management Architecture ### Pinia Stores **Auth Store**: - User authentication state - Discord profile data - Team ownership information - Token management **Game Store**: - Current game state - Real-time updates from WebSocket - Pending actions - Error handling **Games List Store**: - Active games list - Completed games history - Game filtering **UI Store**: - Modal states - Toast notifications - Loading indicators - Theme preferences ## Mobile-First Responsive Design ### Breakpoints ```javascript // tailwind.config.js module.exports = { theme: { screens: { 'xs': '375px', // Small phones 'sm': '640px', // Large phones 'md': '768px', // Tablets 'lg': '1024px', // Desktop 'xl': '1280px', // Large desktop } } } ``` ### Layout Strategy **Mobile (< 768px)**: - Single column layout - Bottom sheet for decisions - Sticky scoreboard at top - Collapsible play-by-play - Full-screen game board **Tablet (768px - 1024px)**: - Two column layout (game + sidebar) - Larger game board - Side panel for decisions - Expanded play history **Desktop (> 1024px)**: - Three column layout (optional) - Full game board center - Decision panel right - Stats panel left ## WebSocket Event Handling ### Event Listeners Setup ```typescript // composables/useGameActions.ts export const useGameActions = (gameId: string) => { const { socket, emit, on, off } = useWebSocket() const gameStore = useGameStore() onMounted(() => { // Join game room emit('join_game', { game_id: gameId, role: 'player' }) // Listen for state updates on('game_state_update', (data: GameState) => { gameStore.setGameState(data) }) on('play_completed', (data: PlayOutcome) => { gameStore.handlePlayCompleted(data) }) on('dice_rolled', (data: DiceRoll) => { // Trigger dice animation showDiceRoll(data.roll, data.animation_duration) }) on('decision_required', (data: DecisionPrompt) => { // Show decision UI gameStore.setDecisionRequired(data) }) on('invalid_action', (data: ErrorData) => { gameStore.setError(data.message) }) on('game_error', (data: ErrorData) => { gameStore.setError(data.message) }) }) onUnmounted(() => { // Clean up listeners off('game_state_update') off('play_completed') off('dice_rolled') off('decision_required') off('invalid_action') off('game_error') // Leave game room emit('leave_game', { game_id: gameId }) }) // Action methods const setDefense = (positioning: string) => { emit('set_defense', { game_id: gameId, positioning }) } const setStolenBase = (runners: string[]) => { emit('set_stolen_base', { game_id: gameId, runners }) } const setOffensiveApproach = (approach: string) => { emit('set_offensive_approach', { game_id: gameId, approach }) } return { setDefense, setStolenBase, setOffensiveApproach } } ``` ## League-Specific Customization ### Configuration ```typescript // frontend-sba/nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { public: { leagueId: 'sba', leagueName: 'Stratomatic Baseball Association', apiUrl: process.env.NUXT_PUBLIC_API_URL, wsUrl: process.env.NUXT_PUBLIC_WS_URL, discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID, primaryColor: '#1e40af', // Blue secondaryColor: '#dc2626' // Red } } }) // frontend-pd/nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { public: { leagueId: 'pd', leagueName: 'Paper Dynasty', apiUrl: process.env.NUXT_PUBLIC_API_URL, wsUrl: process.env.NUXT_PUBLIC_WS_URL, discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID, primaryColor: '#16a34a', // Green secondaryColor: '#ea580c' // Orange } } }) ``` ### Theming ```typescript // composables/useLeagueConfig.ts export const useLeagueConfig = () => { const config = useRuntimeConfig() return { leagueId: config.public.leagueId, leagueName: config.public.leagueName, colors: { primary: config.public.primaryColor, secondary: config.public.secondaryColor }, features: { showScoutingData: config.public.leagueId === 'pd', useSimplePlayerCards: config.public.leagueId === 'sba' } } } ``` ## Performance Optimizations ### Code Splitting - Lazy load game components - Route-based code splitting - Dynamic imports for heavy libraries ### Asset Optimization - Image lazy loading - SVG sprites for icons - Optimized font loading ### State Updates - Debounce non-critical updates - Optimistic UI updates - Efficient re-rendering with `v-memo` ## Error Handling ### Network Errors ```typescript const handleNetworkError = (error: Error) => { const uiStore = useUiStore() if (error.message.includes('WebSocket')) { uiStore.showToast({ type: 'error', message: 'Connection lost. Reconnecting...' }) } else { uiStore.showToast({ type: 'error', message: 'Network error. Please try again.' }) } } ``` ### Game Errors ```typescript const handleGameError = (error: GameError) => { const uiStore = useUiStore() uiStore.showModal({ title: 'Game Error', message: error.message, actions: [ { label: 'Retry', handler: () => retryLastAction() }, { label: 'Reload Game', handler: () => reloadGameState() } ] }) } ``` --- **Next Steps**: See [03-gameplay-features.md](./03-gameplay-features.md) for gameplay implementation details.