# 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
```bash
# Install dependencies (first time)
npm install

# Run dev server with hot-reload
npm run dev

# Frontend available at http://localhost:3001
```

### Building
```bash
# Build for production
npm run build

# Preview production build
npm run preview

# Generate static site (if needed)
npm run generate
```

### Code Quality
```bash
# 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
```vue
<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
```typescript
// 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)
```typescript
// 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:

```bash
# 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
```typescript
// 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)
```javascript
// 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
```typescript
// composables/useWebSocket.ts
const { $socket } = useNuxtApp()
const authStore = useAuthStore()

onMounted(() => {
  if (authStore.token) {
    $socket.connect(authStore.token)
  }
})

onUnmounted(() => {
  $socket.disconnect()
})
```

### Event Handling
```typescript
// 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)
```typescript
// 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
```typescript
// 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
```vue
<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
```vue
<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
```vue
<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