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>
452 lines
10 KiB
Markdown
452 lines
10 KiB
Markdown
# 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)
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```bash
|
|
# 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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"@strat-gameplay/shared-ui": "workspace:*"
|
|
}
|
|
}
|
|
```
|
|
|
|
Update `frontend-sba/nuxt.config.ts`:
|
|
|
|
```typescript
|
|
export default defineNuxtConfig({
|
|
modules: [
|
|
'@strat-gameplay/shared-ui',
|
|
],
|
|
sharedUi: {
|
|
prefix: '',
|
|
},
|
|
// Override theme
|
|
runtimeConfig: {
|
|
public: {
|
|
league: 'sba',
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
Update `frontend-sba/app.vue`:
|
|
|
|
```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`:
|
|
|
|
```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:
|
|
|
|
```bash
|
|
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:**
|
|
```typescript
|
|
// frontend-sba/pages/game/[id].vue
|
|
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
|
|
import { useWebSocket } from '~/composables/useWebSocket'
|
|
```
|
|
|
|
**After:**
|
|
```typescript
|
|
// 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/`:
|
|
|
|
```typescript
|
|
// 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`:
|
|
|
|
```markdown
|
|
# @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:
|
|
|
|
```vue
|
|
<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
|