strat-gameplay-webapp/.claude/plans/010-shared-components.md
Cal Corum e0c12467b0 CLAUDE: Improve UX with single-click OAuth, enhanced games list, and layout fix
Frontend UX improvements:
- Single-click Discord OAuth from home page (no intermediate /auth page)
- Auto-redirect authenticated users from home to /games
- Fixed Nuxt layout system - app.vue now wraps NuxtPage with NuxtLayout
- Games page now has proper card container with shadow/border styling
- Layout header includes working logout with API cookie clearing

Games list enhancements:
- Display team names (lname) instead of just team IDs
- Show current score for each team
- Show inning indicator (Top/Bot X) for active games
- Responsive header with wrapped buttons on mobile

Backend improvements:
- Added team caching to SbaApiClient (1-hour TTL)
- Enhanced GameListItem with team names, scores, inning data
- Games endpoint now enriches response with SBA API team data

Docker optimizations:
- Optimized Dockerfile using --chown flag on COPY (faster than chown -R)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 16:14:00 -06:00

10 KiB

Plan 010: Create Shared Component Library

Priority: MEDIUM Effort: 1-2 weeks Status: NOT STARTED Risk Level: LOW - Code organization


Problem Statement

SBA and PD frontends have duplicate component implementations. When a component is updated in one frontend, it must be manually synchronized to the other.

Current state:

  • frontend-sba/components/ - Full implementation (~20 components)
  • frontend-pd/components/ - Minimal (mostly stubs)

Impact

  • DRY Violation: Same code in two places
  • Maintenance: Updates require changes in both places
  • Consistency: Risk of drift between implementations
  • Testing: Need to test same component twice

Proposed Structure

strat-gameplay-webapp/
├── packages/
│   └── shared-ui/              # Shared component library
│       ├── package.json
│       ├── nuxt.config.ts      # Nuxt module config
│       ├── components/
│       │   ├── Game/
│       │   ├── Decisions/
│       │   ├── Gameplay/
│       │   ├── Substitutions/
│       │   └── UI/
│       ├── composables/
│       ├── types/
│       └── styles/
├── frontend-sba/               # SBA-specific
│   └── nuxt.config.ts          # Extends shared-ui
└── frontend-pd/                # PD-specific
    └── nuxt.config.ts          # Extends shared-ui

Implementation Steps

Step 1: Create Package Structure (1 hour)

cd /mnt/NV2/Development/strat-gameplay-webapp

# Create shared package
mkdir -p packages/shared-ui/{components,composables,types,styles}

# Initialize package
cd packages/shared-ui

Create packages/shared-ui/package.json:

{
  "name": "@strat-gameplay/shared-ui",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "import": "./index.ts"
    },
    "./components/*": {
      "import": "./components/*"
    },
    "./composables/*": {
      "import": "./composables/*"
    },
    "./types/*": {
      "import": "./types/*"
    }
  },
  "dependencies": {
    "vue": "^3.4.0"
  },
  "devDependencies": {
    "@nuxt/kit": "^3.14.0",
    "typescript": "^5.3.0"
  },
  "peerDependencies": {
    "nuxt": "^3.14.0"
  }
}

Step 2: Create Nuxt Module (2 hours)

Create packages/shared-ui/nuxt.config.ts:

import { defineNuxtModule, addComponentsDir, addImportsDir } from '@nuxt/kit'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'

export default defineNuxtModule({
  meta: {
    name: '@strat-gameplay/shared-ui',
    configKey: 'sharedUi',
  },
  defaults: {
    prefix: '',  // No prefix for components
  },
  setup(options, nuxt) {
    const runtimeDir = dirname(fileURLToPath(import.meta.url))

    // Add components
    addComponentsDir({
      path: join(runtimeDir, 'components'),
      prefix: options.prefix,
      pathPrefix: false,
    })

    // Add composables
    addImportsDir(join(runtimeDir, 'composables'))

    // Add types
    nuxt.hook('prepare:types', ({ tsConfig }) => {
      tsConfig.include = tsConfig.include || []
      tsConfig.include.push(join(runtimeDir, 'types/**/*.d.ts'))
    })
  },
})

Create packages/shared-ui/index.ts:

// Export module
export { default } from './nuxt.config'

// Export types
export * from './types/game'
export * from './types/player'
export * from './types/websocket'

// Export composables
export { useWebSocket } from './composables/useWebSocket'
export { useGameActions } from './composables/useGameActions'

Step 3: Move Shared Components (4 hours)

Identify components that are league-agnostic:

Fully Shared (move to packages/shared-ui):

  • components/UI/ - All UI primitives
  • components/Game/ScoreBoard.vue - League-agnostic
  • components/Game/GameBoard.vue - League-agnostic
  • components/Game/CurrentSituation.vue - League-agnostic
  • components/Gameplay/DiceRoller.vue - League-agnostic
  • components/Gameplay/PlayResult.vue - League-agnostic

League-Specific (keep in frontend-*):

  • components/Decisions/ - Different for SBA vs PD
  • Player cards - Different data structures
  • Scouting views - PD only

Move shared components:

# Move UI components
cp -r frontend-sba/components/UI packages/shared-ui/components/

# Move Game components
cp -r frontend-sba/components/Game packages/shared-ui/components/

# Move Gameplay components
cp -r frontend-sba/components/Gameplay packages/shared-ui/components/

# Move composables
cp frontend-sba/composables/useWebSocket.ts packages/shared-ui/composables/
cp frontend-sba/composables/useGameActions.ts packages/shared-ui/composables/

# Move types
cp frontend-sba/types/game.ts packages/shared-ui/types/
cp frontend-sba/types/player.ts packages/shared-ui/types/
cp frontend-sba/types/websocket.ts packages/shared-ui/types/

Step 4: Create Theme System (2 hours)

Create packages/shared-ui/styles/themes.ts:

export interface LeagueTheme {
  primary: string
  secondary: string
  accent: string
  background: string
  surface: string
  text: string
  textMuted: string
}

export const sbaTheme: LeagueTheme = {
  primary: '#1e40af',      // Blue
  secondary: '#3b82f6',
  accent: '#f59e0b',       // Amber
  background: '#0f172a',
  surface: '#1e293b',
  text: '#f8fafc',
  textMuted: '#94a3b8',
}

export const pdTheme: LeagueTheme = {
  primary: '#7c3aed',      // Purple
  secondary: '#a78bfa',
  accent: '#10b981',       // Emerald
  background: '#0f172a',
  surface: '#1e293b',
  text: '#f8fafc',
  textMuted: '#94a3b8',
}

Create packages/shared-ui/composables/useTheme.ts:

import { inject, provide, ref, readonly } from 'vue'
import type { LeagueTheme } from '../styles/themes'
import { sbaTheme } from '../styles/themes'

const THEME_KEY = Symbol('theme')

export function provideTheme(theme: LeagueTheme) {
  const themeRef = ref(theme)
  provide(THEME_KEY, readonly(themeRef))
  return themeRef
}

export function useTheme(): LeagueTheme {
  const theme = inject<LeagueTheme>(THEME_KEY)
  if (!theme) {
    console.warn('No theme provided, using SBA default')
    return sbaTheme
  }
  return theme
}

Step 5: Update Frontends to Use Shared Package (2 hours)

Update frontend-sba/package.json:

{
  "dependencies": {
    "@strat-gameplay/shared-ui": "workspace:*"
  }
}

Update frontend-sba/nuxt.config.ts:

export default defineNuxtConfig({
  modules: [
    '@strat-gameplay/shared-ui',
  ],
  sharedUi: {
    prefix: '',
  },
  // Override theme
  runtimeConfig: {
    public: {
      league: 'sba',
    },
  },
})

Update frontend-sba/app.vue:

<script setup lang="ts">
import { provideTheme, sbaTheme } from '@strat-gameplay/shared-ui'

provideTheme(sbaTheme)
</script>

Step 6: Setup Workspace (1 hour)

Create/update root package.json:

{
  "name": "strat-gameplay-webapp",
  "private": true,
  "workspaces": [
    "packages/*",
    "frontend-sba",
    "frontend-pd"
  ],
  "scripts": {
    "dev:sba": "cd frontend-sba && npm run dev",
    "dev:pd": "cd frontend-pd && npm run dev",
    "build": "npm run build --workspaces",
    "test": "npm run test --workspaces"
  }
}

Install dependencies:

cd /mnt/NV2/Development/strat-gameplay-webapp
npm install  # or bun install

Step 7: Update Imports in Frontends (2 hours)

Update components to import from shared package:

Before:

// frontend-sba/pages/game/[id].vue
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
import { useWebSocket } from '~/composables/useWebSocket'

After:

// frontend-sba/pages/game/[id].vue
// ScoreBoard auto-imported from shared-ui module
// useWebSocket auto-imported from shared-ui module

Step 8: Add Shared Tests (2 hours)

Create packages/shared-ui/tests/:

// packages/shared-ui/tests/components/ScoreBoard.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ScoreBoard from '../components/Game/ScoreBoard.vue'

describe('ScoreBoard', () => {
  it('renders team scores', () => {
    const wrapper = mount(ScoreBoard, {
      props: {
        homeScore: 5,
        awayScore: 3,
        homeTeamName: 'Home Team',
        awayTeamName: 'Away Team',
      },
    })

    expect(wrapper.text()).toContain('5')
    expect(wrapper.text()).toContain('3')
  })

  it('applies theme colors', () => {
    // Test theme integration
  })
})

Step 9: Documentation (1 hour)

Create packages/shared-ui/README.md:

# @strat-gameplay/shared-ui

Shared Vue 3 components for Strat Gameplay frontends.

## Usage

Add to your Nuxt config:

```typescript
export default defineNuxtConfig({
  modules: ['@strat-gameplay/shared-ui'],
})

Components

Game Components

  • ScoreBoard - Displays game score, inning, count
  • GameBoard - Diamond visualization
  • CurrentSituation - Batter/pitcher matchup

UI Components

  • ActionButton - Styled action button
  • ToggleSwitch - Boolean toggle
  • ButtonGroup - Button group container

Composables

  • useWebSocket - WebSocket connection management
  • useGameActions - Type-safe game action emitters
  • useTheme - Theme injection/consumption

Theming

Provide theme at app root:

<script setup>
import { provideTheme, sbaTheme } from '@strat-gameplay/shared-ui'
provideTheme(sbaTheme)
</script>

## Verification Checklist

- [ ] Shared package builds without errors
- [ ] SBA frontend works with shared components
- [ ] PD frontend works with shared components
- [ ] Themes apply correctly per league
- [ ] Tests pass for shared components
- [ ] No duplicate component code remains

## Migration Strategy

1. **Phase 1**: Create package, move UI primitives
2. **Phase 2**: Move game display components
3. **Phase 3**: Move composables and types
4. **Phase 4**: Refactor league-specific components

## Rollback Plan

If issues arise:
1. Remove shared-ui from module list
2. Restore original component imports
3. Keep shared package for future use

## Dependencies

- None (can be implemented independently)

## Notes

- Consider Storybook for component documentation
- May want to publish to private npm registry eventually
- Future: Add design tokens for full design system