strat-gameplay-webapp/frontend-pd/CLAUDE.md
Cal Corum 7d28eebd24 CLAUDE: Add multi-domain environment configuration support
- Create frontend-sba/.env.example and frontend-pd/.env.example templates
- Fix hardcoded allowedHosts in nuxt.config.ts (now reads NUXT_ALLOWED_HOSTS)
- Add NUXT_ALLOWED_HOSTS support to frontend-pd/nuxt.config.ts
- Update docker-compose.yml with missing env vars:
  - FRONTEND_URL, DISCORD_SERVER_REDIRECT_URI
  - ALLOWED_DISCORD_IDS, WS_HEARTBEAT_INTERVAL, WS_CONNECTION_TIMEOUT
  - NUXT_ALLOWED_HOSTS for both frontends
- Create docker-compose.prod.yml for production overrides
- Update root .env.example with new variables
- Add "Multi-Domain Deployment" section to README.md with checklist
- Update all CLAUDE.md files with environment configuration docs
- Remove obsolete 'version' attribute from docker-compose files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 13:58:42 -06:00

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

Copy .env.example to .env and configure:

# API endpoints
NUXT_PUBLIC_API_URL=http://localhost:8000
NUXT_PUBLIC_WS_URL=http://localhost:8000

# Discord OAuth (public client ID - safe to expose)
NUXT_PUBLIC_DISCORD_CLIENT_ID=your-client-id
NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3001/auth/callback

# Vite dev server allowed hosts (comma-separated)
# Add your domain for external access
NUXT_ALLOWED_HOSTS=localhost,127.0.0.1

Note: NUXT_ALLOWED_HOSTS is read in nuxt.config.ts to configure Vite's allowedHosts. Required when accessing dev server through a reverse proxy or external domain.

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

  1. Create file in pages/ directory
  2. Use <script setup> with TypeScript
  3. Add necessary composables (auth, websocket, etc.)
  4. Define route meta if needed

Adding a New Component

  1. Create in appropriate components/ subdirectory
  2. Define Props/Emits interfaces
  3. Use Tailwind for styling
  4. Export for use in other components

Adding a New Store

  1. Create in store/ directory
  2. Use Composition API syntax
  3. Define state, computed, and actions
  4. Export with defineStore

Performance Considerations

  • Code Splitting: Auto by Nuxt routes
  • Lazy Loading: Use defineAsyncComponent for 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-check to 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 .nuxt directory: 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