strat-gameplay-webapp/frontend-sba/CLAUDE.md
2025-10-22 11:22:15 -05:00

12 KiB

Frontend SBA - Strat-O-Matic Baseball Association Web App

Overview

Vue 3 + Nuxt 3 frontend for the SBa (Strat-O-Matic Baseball Association) 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

SBA League

  • Player Data: Simple model (id, name, image)
  • Focus: Straightforward card-based gameplay
  • Branding: Blue primary color (#1e40af)
  • API: SBA-specific REST API for team/player data

Project Structure

frontend-sba/
├── assets/
│   ├── css/
│   │   └── tailwind.css        # Tailwind imports
│   └── images/                  # SBA branding assets
│
├── components/
│   ├── Branding/                # SBA-specific branding
│   │   ├── Header.vue
│   │   ├── Footer.vue
│   │   └── Logo.vue
│   └── League/                  # SBA-specific features
│       └── PlayerCardSimple.vue # Simple player cards
│
├── composables/
│   ├── useAuth.ts               # Authentication state
│   ├── useWebSocket.ts          # WebSocket connection
│   ├── useGameState.ts          # Game state management
│   └── useLeagueConfig.ts       # SBA-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                # SBA 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
│
├── 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

SBA-Specific:

  • SBA branding (header, footer, colors)
  • Simple player card display (no scouting data)
  • League-specific theming

Development Workflow

Daily Development

# Install dependencies (first time)
npm install

# Run dev server with hot-reload
npm run dev

# Frontend available at http://localhost:3000

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
  isActive: boolean
}

const props = defineProps<Props>()

const emit = defineEmits<{
  update: [value: string]
  close: []
}>()

// Reactive state
const localState = ref('')

// Computed properties
const displayValue = computed(() => {
  return props.isActive ? 'Active' : 'Inactive'
})

// 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=sba
NUXT_PUBLIC_LEAGUE_NAME=Stratomatic Baseball Association
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:3000/auth/callback

Nuxt Config

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],

  runtimeConfig: {
    public: {
      leagueId: 'sba',
      leagueName: 'Stratomatic Baseball Association',
      apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
      // ... other config
    }
  },

  css: ['~/assets/css/tailwind.css'],

  typescript: {
    strict: true,
    typeCheck: true
  }
})

Tailwind Config (SBA Theme)

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: '#1e40af', // SBA Blue
          50: '#eff6ff',
          100: '#dbeafe',
          // ... other shades
        },
        secondary: {
          DEFAULT: '#dc2626', // SBA Red
          // ... 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

SBA Player Type

// types/player.ts
export interface SbaPlayer {
  id: number
  name: string
  image: string
  team?: string
  manager?: string
}

export interface Lineup {
  id: number
  game_id: string
  card_id: number
  position: string
  batting_order?: number
  is_starter: boolean
  is_active: boolean
  player: SbaPlayer
}

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: SbaPlayer | null
  current_pitcher: SbaPlayer | null
}

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

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-2 gap-4">
        <GameBoard :state="gameState" />
        <PlayByPlay :plays="plays" />
      </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
  • Image Optimization: Use Nuxt Image module
  • State Management: Keep only necessary data in stores
  • WebSocket: Throttle/debounce frequent updates

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

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: SBA (Stratomatic Baseball Association) Port: 3000 Current Phase: Phase 1 - Core Infrastructure