Add comprehensive project documentation and Docker infrastructure for Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball simulation platform replacing the legacy Google Sheets system. Documentation Added: - Complete PRD (Product Requirements Document) - Project README with dual development workflows - Implementation guide with 5-phase roadmap - Architecture docs (backend, frontend, database, WebSocket) - CLAUDE.md context files for each major directory Infrastructure Added: - Root docker-compose.yml for full stack orchestration - Dockerfiles for backend and both frontends (multi-stage builds) - .dockerignore files for optimal build context - .env.example with all required configuration - Updated .gitignore for Python, Node, Nuxt, and Docker Project Structure: - backend/ - FastAPI + Socket.io game engine (Python 3.11+) - frontend-sba/ - SBA League Nuxt 3 frontend - frontend-pd/ - PD League Nuxt 3 frontend - .claude/implementation/ - Detailed implementation guides Supports two development workflows: 1. Local dev (recommended): Services run natively with hot-reload 2. Full Docker: One-command stack orchestration for testing/demos Next: Phase 1 implementation (backend/frontend foundations)
14 KiB
14 KiB
Frontend PD - Paper Dynasty Web App
Overview
Vue 3 + Nuxt 3 frontend for the PD (Paper Dynasty) league. Provides real-time game interface with WebSocket communication to the game backend.
Technology Stack
- Framework: Nuxt 3 (Vue 3 Composition API)
- Language: TypeScript (strict mode)
- Styling: Tailwind CSS
- State Management: Pinia
- WebSocket: Socket.io-client
- HTTP Client: Axios (or Nuxt's built-in fetch)
- Auth: Discord OAuth with JWT
League-Specific Characteristics
PD League
- Player Data: Detailed model with scouting data, ratings, probabilities
- Focus: Advanced analytics and detailed player evaluation
- Branding: Green primary color (#16a34a)
- API: PD-specific REST API for team/player data with analytics
Project Structure
frontend-pd/
├── assets/
│ ├── css/
│ │ └── tailwind.css # Tailwind imports
│ └── images/ # PD branding assets
│
├── components/
│ ├── Branding/ # PD-specific branding
│ │ ├── Header.vue
│ │ ├── Footer.vue
│ │ └── Logo.vue
│ └── League/ # PD-specific features
│ ├── PlayerCardDetailed.vue # Player cards with scouting
│ └── ScoutingPanel.vue # Scouting data display
│
├── composables/
│ ├── useAuth.ts # Authentication state
│ ├── useWebSocket.ts # WebSocket connection
│ ├── useGameState.ts # Game state management
│ └── useLeagueConfig.ts # PD-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 # PD player types (with scouting)
│ ├── 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
│
├── public/ # Static assets
├── app.vue # Root component
├── nuxt.config.ts # Nuxt configuration
├── tailwind.config.js # Tailwind configuration
├── tsconfig.json # TypeScript configuration
└── package.json
Shared Components
Many components are shared between SBA and PD frontends. These will be located in a shared component library:
Shared:
- Game board visualization
- Play-by-play feed
- Dice roll animations
- Decision input forms
- WebSocket connection status
PD-Specific:
- PD branding (header, footer, colors)
- Detailed player card display (with scouting data)
- Scouting data panels
- Advanced analytics views
Development Workflow
Daily Development
# Install dependencies (first time)
npm install
# Run dev server with hot-reload
npm run dev
# Frontend available at http://localhost:3001
Building
# Build for production
npm run build
# Preview production build
npm run preview
# Generate static site (if needed)
npm run generate
Code Quality
# Type checking
npm run type-check
# Linting
npm run lint
# Fix linting issues
npm run lint:fix
Coding Standards
Vue/TypeScript Style
- Composition API: Use
<script setup>syntax - TypeScript: Strict mode, explicit types for props/emits
- Component Names: PascalCase for components
- File Names: PascalCase for components, kebab-case for utilities
Component Structure
<template>
<div class="component-wrapper">
<!-- Template content -->
</div>
</template>
<script setup lang="ts">
// Imports
import { ref, computed } from 'vue'
// Props/Emits with TypeScript
interface Props {
gameId: string
showScouting: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
update: [value: string]
close: []
}>()
// Reactive state
const localState = ref('')
// Computed properties
const displayMode = computed(() => {
return props.showScouting ? 'detailed' : 'simple'
})
// Methods
const handleClick = () => {
emit('update', localState.value)
}
</script>
<style scoped>
/* Component-specific styles */
.component-wrapper {
@apply p-4 bg-white rounded-lg;
}
</style>
Composable Pattern
// composables/useGameActions.ts
export const useGameActions = (gameId: string) => {
const { socket } = useWebSocket()
const gameStore = useGameStore()
const setDefense = (positioning: string) => {
if (!socket.value?.connected) {
throw new Error('Not connected')
}
socket.value.emit('set_defense', { game_id: gameId, positioning })
}
return {
setDefense,
// ... other actions
}
}
Store Pattern (Pinia)
// store/game.ts
export const useGameStore = defineStore('game', () => {
// State
const gameState = ref<GameState | null>(null)
const loading = ref(false)
// Computed
const currentInning = computed(() => gameState.value?.inning ?? 1)
// Actions
const setGameState = (state: GameState) => {
gameState.value = state
}
return {
// State
gameState,
loading,
// Computed
currentInning,
// Actions
setGameState,
}
})
Configuration
Environment Variables
Create .env file with:
NUXT_PUBLIC_LEAGUE_ID=pd
NUXT_PUBLIC_LEAGUE_NAME=Paper Dynasty
NUXT_PUBLIC_API_URL=http://localhost:8000
NUXT_PUBLIC_WS_URL=http://localhost:8000
NUXT_PUBLIC_DISCORD_CLIENT_ID=your-client-id
NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3001/auth/callback
Nuxt Config
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
runtimeConfig: {
public: {
leagueId: 'pd',
leagueName: 'Paper Dynasty',
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
// ... other config
}
},
css: ['~/assets/css/tailwind.css'],
typescript: {
strict: true,
typeCheck: true
}
})
Tailwind Config (PD Theme)
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#16a34a', // PD Green
50: '#f0fdf4',
100: '#dcfce7',
// ... other shades
},
secondary: {
DEFAULT: '#ea580c', // PD Orange
// ... other shades
}
}
}
}
}
WebSocket Integration
Connection Management
// composables/useWebSocket.ts
const { $socket } = useNuxtApp()
const authStore = useAuthStore()
onMounted(() => {
if (authStore.token) {
$socket.connect(authStore.token)
}
})
onUnmounted(() => {
$socket.disconnect()
})
Event Handling
// composables/useGameEvents.ts
export const useGameEvents = () => {
const { socket } = useWebSocket()
const gameStore = useGameStore()
onMounted(() => {
socket.value?.on('game_state_update', (data: GameState) => {
gameStore.setGameState(data)
})
socket.value?.on('play_completed', (data: PlayOutcome) => {
gameStore.handlePlayCompleted(data)
})
})
onUnmounted(() => {
socket.value?.off('game_state_update')
socket.value?.off('play_completed')
})
}
Type Definitions
PD Player Type (with Scouting Data)
// types/player.ts
export interface PdPlayer {
id: number
name: string
image: string
scouting_data: {
power: number
contact: number
speed: number
fielding: number
// ... other scouting metrics
}
ratings: {
overall: number
batting: number
pitching?: number
// ... other ratings
}
probabilities: {
single: number
double: number
triple: number
homerun: number
walk: number
strikeout: number
// ... other probabilities
}
}
export interface Lineup {
id: number
game_id: string
card_id: number
position: string
batting_order?: number
is_starter: boolean
is_active: boolean
player: PdPlayer
}
Game State Type
// types/game.ts
export interface GameState {
game_id: string
status: 'pending' | 'active' | 'completed'
inning: number
half: 'top' | 'bottom'
outs: number
balls: number
strikes: number
home_score: number
away_score: number
runners: {
first: number | null
second: number | null
third: number | null
}
current_batter: PdPlayer | null
current_pitcher: PdPlayer | null
}
PD-Specific Features
Scouting Data Display
<template>
<div class="scouting-panel">
<h3 class="text-lg font-bold mb-2">Scouting Report</h3>
<div class="grid grid-cols-2 gap-2">
<div v-for="(value, key) in player.scouting_data" :key="key">
<span class="text-sm text-gray-600">{{ formatLabel(key) }}</span>
<div class="flex items-center gap-2">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div
class="bg-primary h-2 rounded-full"
:style="{ width: `${value}%` }"
/>
</div>
<span class="text-sm font-semibold">{{ value }}</span>
</div>
</div>
</div>
</div>
</template>
Advanced Player Card
<template>
<div class="player-card">
<!-- Basic Info -->
<img :src="player.image" :alt="player.name" />
<h3>{{ player.name }}</h3>
<!-- Ratings -->
<div class="ratings">
<span>Overall: {{ player.ratings.overall }}</span>
<span>Batting: {{ player.ratings.batting }}</span>
</div>
<!-- Probabilities (collapsible on mobile) -->
<details class="md:open">
<summary>Probabilities</summary>
<div class="grid grid-cols-2 gap-1 text-sm">
<div v-for="(prob, outcome) in player.probabilities" :key="outcome">
{{ outcome }}: {{ (prob * 100).toFixed(1) }}%
</div>
</div>
</details>
</div>
</template>
Mobile-First Design
Responsive Breakpoints
- xs: 375px (Small phones)
- sm: 640px (Large phones)
- md: 768px (Tablets)
- lg: 1024px (Desktop)
Mobile Layout Principles
- Single column layout on mobile
- Bottom sheet for decision inputs
- Sticky scoreboard at top
- Touch-friendly buttons (44x44px minimum)
- Swipe gestures for navigation
- Collapsible scouting data on mobile
Example Responsive Component
<template>
<div class="game-view">
<!-- Sticky scoreboard -->
<div class="sticky top-0 z-10 bg-white shadow">
<ScoreBoard :score="score" />
</div>
<!-- Main content -->
<div class="container mx-auto p-4">
<!-- Mobile: stacked, Desktop: grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="lg:col-span-2">
<GameBoard :state="gameState" />
</div>
<div>
<ScoutingPanel :player="currentBatter" />
<PlayByPlay :plays="plays" />
</div>
</div>
</div>
</div>
</template>
Common Tasks
Adding a New Page
- Create file in
pages/directory - Use
<script setup>with TypeScript - Add necessary composables (auth, websocket, etc.)
- Define route meta if needed
Adding a New Component
- Create in appropriate
components/subdirectory - Define Props/Emits interfaces
- Use Tailwind for styling
- Export for use in other components
Adding a New Store
- Create in
store/directory - Use Composition API syntax
- Define state, computed, and actions
- Export with
defineStore
Performance Considerations
- Code Splitting: Auto by Nuxt routes
- Lazy Loading: Use
defineAsyncComponentfor heavy components (especially scouting panels) - Image Optimization: Use Nuxt Image module
- State Management: Keep only necessary data in stores
- WebSocket: Throttle/debounce frequent updates
- Scouting Data: Lazy load detailed analytics
Troubleshooting
WebSocket Won't Connect
- Check backend is running at
NUXT_PUBLIC_WS_URL - Verify token is valid
- Check browser console for errors
- Ensure CORS is configured correctly on backend
Type Errors
- Run
npm run type-checkto see all errors - Ensure types are imported correctly
- Check for mismatched types in props/emits
- Verify PdPlayer type matches backend structure
Hot Reload Not Working
- Restart dev server
- Clear
.nuxtdirectory:rm -rf .nuxt - Check for syntax errors in components
References
- Implementation Guide:
../.claude/implementation/01-infrastructure.md - Frontend Architecture:
../.claude/implementation/frontend-architecture.md - WebSocket Protocol:
../.claude/implementation/websocket-protocol.md - Full PRD:
../prd-web-scorecard-1.1.md
League: PD (Paper Dynasty) Port: 3001 Current Phase: Phase 1 - Core Infrastructure