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>
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 primitivescomponents/Game/ScoreBoard.vue- League-agnosticcomponents/Game/GameBoard.vue- League-agnosticcomponents/Game/CurrentSituation.vue- League-agnosticcomponents/Gameplay/DiceRoller.vue- League-agnosticcomponents/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, countGameBoard- Diamond visualizationCurrentSituation- Batter/pitcher matchup
UI Components
ActionButton- Styled action buttonToggleSwitch- Boolean toggleButtonGroup- Button group container
Composables
useWebSocket- WebSocket connection managementuseGameActions- Type-safe game action emittersuseTheme- 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