mantimon-tcg/frontend/CLAUDE.md
Cal Corum 5424bf9086 Add environment config and Vue Router with guards (F0-003, F0-008)
- Add environment configuration with type-safe config.ts
- Implement navigation guards (requireAuth, requireGuest, requireStarter)
- Update router to match sitePlan routes and layouts
- Create placeholder pages for all sitePlan routes
- Update auth store User interface for OAuth flow
- Add phase plan tracking for F0

Phase F0 progress: 4/8 tasks complete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:59:04 -06:00

14 KiB

Mantimon TCG Frontend - AI Agent Guidelines

Guidelines for AI agents working on the frontend codebase.

Tech Stack

Technology Purpose
Vue 3 UI framework (Composition API + <script setup>)
Phaser 3 Game canvas (matches, pack opening)
TypeScript Type safety (strict mode)
Pinia State management
Tailwind CSS Styling (mobile-first)
Socket.io-client Real-time communication
Vite Build tool
Vitest Unit/component testing

Quick Commands

npm run dev          # Start dev server
npm run build        # Production build
npm run preview      # Preview production build
npm run test         # Run tests
npm run test:watch   # Run tests in watch mode
npm run lint         # ESLint
npm run typecheck    # TypeScript check

Project Structure

src/
├── api/              # API client and composables
│   ├── client.ts     # Base HTTP client with auth
│   └── types.ts      # API response types
├── assets/           # Static assets, global CSS
├── components/       # Reusable Vue components
│   ├── ui/           # Generic UI (Button, Modal, Toast)
│   ├── cards/        # Card display components
│   ├── deck/         # Deck builder components
│   └── game/         # Game UI overlays
├── composables/      # Vue composables (useAuth, useDecks, etc.)
├── game/             # Phaser game code
│   ├── scenes/       # Phaser scenes
│   ├── objects/      # Game objects (Card, Board)
│   ├── animations/   # Animation helpers
│   └── bridge.ts     # Vue-Phaser communication
├── layouts/          # Layout components
├── pages/            # Route pages
├── router/           # Vue Router config
├── socket/           # Socket.IO client
├── stores/           # Pinia stores
├── types/            # TypeScript types
└── utils/            # Utility functions

Code Style

Vue Components

Single File Component structure:

<script setup lang="ts">
// 1. Imports (Vue, then external, then local)
import { ref, computed, onMounted } from 'vue'

import { useGameStore } from '@/stores/game'
import type { Card } from '@/types'

// 2. Props and emits
const props = defineProps<{
  cardId: string
  size?: 'sm' | 'md' | 'lg'
}>()

const emit = defineEmits<{
  click: [cardId: string]
}>()

// 3. Composables and stores
const gameStore = useGameStore()

// 4. Reactive state
const isHovered = ref(false)

// 5. Computed
const card = computed(() => gameStore.getCard(props.cardId))

// 6. Methods
function handleClick() {
  emit('click', props.cardId)
}

// 7. Lifecycle
onMounted(() => {
  // ...
})
</script>

<template>
  <!-- Single root element preferred -->
  <div
    class="card"
    :class="{ 'card--hovered': isHovered }"
    @click="handleClick"
    @mouseenter="isHovered = true"
    @mouseleave="isHovered = false"
  >
    <slot />
  </div>
</template>

<style scoped>
/* Component-specific styles if Tailwind isn't enough */
</style>

TypeScript

Use strict typing:

// Good - explicit types
interface CardProps {
  cardId: string
  size?: 'sm' | 'md' | 'lg'
}

// Good - type imports
import type { Card, GameState } from '@/types'

// Good - const assertions for literals
const SIZES = ['sm', 'md', 'lg'] as const
type Size = typeof SIZES[number]

// Avoid - any
const data: any = response  // NO
const data: unknown = response  // OK if you'll narrow it

Naming Conventions

Type Convention Example
Components PascalCase CardDisplay.vue, DeckBuilder.vue
Composables camelCase with use prefix useAuth.ts, useDeckValidation.ts
Stores camelCase with descriptive name auth.ts, game.ts
Types/Interfaces PascalCase Card, GameState, ApiResponse
Constants UPPER_SNAKE_CASE MAX_HAND_SIZE, API_BASE_URL
CSS classes kebab-case (BEM optional) card-display, card-display--active

Pinia Stores

Use setup store syntax:

// stores/game.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { GameState } from '@/types'

export const useGameStore = defineStore('game', () => {
  // State
  const gameState = ref<GameState | null>(null)
  const isConnected = ref(false)

  // Getters (computed)
  const myHand = computed(() => gameState.value?.myHand ?? [])
  const isMyTurn = computed(() => gameState.value?.currentPlayer === 'me')

  // Actions
  function setGameState(state: GameState) {
    gameState.value = state
  }

  function clearGame() {
    gameState.value = null
    isConnected.value = false
  }

  return {
    // State
    gameState,
    isConnected,
    // Getters
    myHand,
    isMyTurn,
    // Actions
    setGameState,
    clearGame,
  }
})

Composables

Pattern for API composables:

// composables/useDecks.ts
import { ref } from 'vue'
import { apiClient } from '@/api/client'
import type { Deck, DeckCreate } from '@/types'

export function useDecks() {
  const decks = ref<Deck[]>([])
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  async function fetchDecks() {
    isLoading.value = true
    error.value = null
    try {
      decks.value = await apiClient.get<Deck[]>('/api/decks')
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Failed to fetch decks'
    } finally {
      isLoading.value = false
    }
  }

  async function createDeck(data: DeckCreate) {
    isLoading.value = true
    error.value = null
    try {
      const deck = await apiClient.post<Deck>('/api/decks', data)
      decks.value.push(deck)
      return deck
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Failed to create deck'
      throw e
    } finally {
      isLoading.value = false
    }
  }

  return {
    decks,
    isLoading,
    error,
    fetchDecks,
    createDeck,
  }
}

Vue-Phaser Integration

Phaser is for rendering only. Game logic lives in backend.

Communication Pattern

// Vue -> Phaser (intentions)
phaserGame.value?.events.emit('card:play', { cardId, targetZone })
phaserGame.value?.events.emit('attack:select', { attackIndex })

// Phaser -> Vue (completions/UI requests)
phaserGame.value?.events.on('animation:complete', handleAnimationComplete)
phaserGame.value?.events.on('card:clicked', handleCardClicked)

State Sync

// Phaser scene reads from Pinia store
import { useGameStore } from '@/stores/game'

class MatchScene extends Phaser.Scene {
  private gameStore = useGameStore()

  update() {
    // React to store changes
    if (this.gameStore.gameState) {
      this.renderState(this.gameStore.gameState)
    }
  }
}

Tailwind Guidelines

Mobile-first responsive:

<!-- Base styles for mobile, override for larger screens -->
<div class="p-2 md:p-4 lg:p-6">
  <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
    <!-- ... -->
  </div>
</div>

Use design system colors:

<!-- Pokemon type colors defined in tailwind.config.js -->
<div class="bg-type-fire text-white">Fire Type</div>
<div class="bg-type-water text-white">Water Type</div>

<!-- UI colors -->
<button class="bg-primary hover:bg-primary-dark">Action</button>

Testing

Component tests with Vitest + Vue Test Utils:

import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import CardDisplay from '@/components/cards/CardDisplay.vue'

describe('CardDisplay', () => {
  it('renders card name', () => {
    /**
     * Test that the card component displays the card name.
     *
     * Card names must be visible for players to identify cards
     * in their hand and on the board.
     */
    const wrapper = mount(CardDisplay, {
      props: { card: { id: '1', name: 'Pikachu', hp: 60 } }
    })
    expect(wrapper.text()).toContain('Pikachu')
  })

  it('emits click event with card id', async () => {
    /**
     * Test that clicking a card emits the correct event.
     *
     * Card clicks drive all game interactions - playing cards,
     * selecting targets, viewing details.
     */
    const wrapper = mount(CardDisplay, {
      props: { card: { id: '1', name: 'Pikachu', hp: 60 } }
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')?.[0]).toEqual(['1'])
  })
})

Critical Rules

  1. Mobile-first - Base styles for mobile, use md: and lg: for larger screens
  2. TypeScript strict - No any, explicit types for props/emits/returns
  3. Composition API only - No Options API, use <script setup>
  4. Phaser for rendering - No game logic in Phaser, only visualization
  5. Backend is authoritative - Never trust client state for game logic
  6. Test docstrings - Every test needs a docstring explaining what and why

Path Aliases

// Use @/ for src/
import { useAuth } from '@/composables/useAuth'
import CardDisplay from '@/components/cards/CardDisplay.vue'
import type { Card } from '@/types'

// Don't use relative paths for deeply nested imports
import { useAuth } from '../../../composables/useAuth'  // NO

Environment Variables

# .env.development
VITE_API_BASE_URL=http://localhost:8000
VITE_WS_URL=http://localhost:8000

# .env.production
VITE_API_BASE_URL=https://api.pocket.manticorum.com
VITE_WS_URL=https://api.pocket.manticorum.com

Access in code:

const apiUrl = import.meta.env.VITE_API_BASE_URL

Common Patterns

Loading States

<template>
  <div v-if="isLoading" class="flex justify-center p-8">
    <LoadingSpinner />
  </div>
  <div v-else-if="error" class="text-error p-4">
    {{ error }}
  </div>
  <div v-else>
    <!-- Content -->
  </div>
</template>

Route Guards

// router/guards.ts
export function requireAuth(to, from, next) {
  const auth = useAuthStore()
  if (!auth.isAuthenticated) {
    next({ name: 'Login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
}

API Error Handling

try {
  await apiClient.post('/api/games', data)
} catch (e) {
  if (e instanceof ApiError) {
    if (e.status === 401) {
      // Token expired, redirect to login
      auth.logout()
    } else {
      toast.error(e.message)
    }
  }
}

Backend API Reference

REST Endpoints

Auth (/api/auth/)

Method Endpoint Description
GET /auth/google Start Google OAuth (redirect)
GET /auth/google/callback Google OAuth callback
GET /auth/discord Start Discord OAuth (redirect)
GET /auth/discord/callback Discord OAuth callback
POST /auth/refresh Refresh access token
POST /auth/logout Revoke refresh token
POST /auth/logout-all Revoke all user tokens
GET /auth/link/google Link Google account (authed)
GET /auth/link/discord Link Discord account (authed)

Users (/api/users/)

Method Endpoint Description
GET /users/me Get current user profile
PATCH /users/me Update profile (display_name, avatar_url)
GET /users/me/linked-accounts List linked OAuth accounts
DELETE /users/me/link/{provider} Unlink OAuth provider
GET /users/me/sessions Get active session count
GET /users/me/starter-status Check if starter deck selected
POST /users/me/starter-deck Select starter deck

Collections (/api/collections/)

Method Endpoint Description
GET /collections/me Get user's card collection
GET /collections/me/cards/{card_id} Get specific card quantity

Decks (/api/decks/)

Method Endpoint Description
GET /decks List user's decks
POST /decks Create new deck
GET /decks/{id} Get deck by ID
PUT /decks/{id} Update deck
DELETE /decks/{id} Delete deck
POST /decks/validate Validate deck without saving

Games (/api/games/)

Method Endpoint Description
POST /games Create new game
GET /games/{id} Get game info
GET /games/me/active List user's active games
POST /games/{id}/resign Resign from game (HTTP fallback)

WebSocket Events (Socket.IO)

Namespace: /game

Client → Server:

Event Payload Description
game:join { game_id } Join/rejoin a game room
game:action { action_type, ...data } Execute game action
game:resign { game_id } Resign from game
game:spectate { game_id } Join as spectator
game:leave_spectate { game_id } Leave spectator mode

Server → Client:

Event Payload Description
game:state GameState Full game state (visibility filtered)
game:action_result ActionResult Result of player action
game:error { message, code } Error notification
game:turn_timeout_warning { remaining_seconds } Turn timer warning
game:game_over { winner, reason } Game ended
game:opponent_connected { connected } Opponent connection status
game:reconnected { game_id, state } Auto-reconnected to game
game:spectator_count { count } Spectator count update

Key Backend Files

File Description
backend/app/api/auth.py Auth endpoints implementation
backend/app/api/users.py User endpoints implementation
backend/app/api/games.py Game REST endpoints
backend/app/schemas/ Pydantic request/response schemas
backend/app/schemas/ws_messages.py WebSocket message schemas
backend/app/socketio/game_namespace.py WebSocket event handlers

Type Definitions to Mirror

When creating frontend types, reference these backend schemas:

  • backend/app/schemas/user.pyUserResponse, UserUpdate
  • backend/app/schemas/deck.pyDeckResponse, DeckCreate, DeckUpdate
  • backend/app/schemas/game.pyGameCreateRequest, GameResponse
  • backend/app/schemas/ws_messages.py → All WebSocket message types

See Also

  • PROJECT_PLAN_FRONTEND.json - Phase plan and tasks
  • ../backend/CLAUDE.md - Backend guidelines
  • ../backend/app/schemas/ - API schema definitions
  • ../backend/app/socketio/README.md - WebSocket documentation
  • ../CLAUDE.md - Project-wide guidelines