Phase F1 - Authentication: - OAuth callback handling with token management - Auth guards for protected routes - Account linking composable - Profile page updates Phase F2 - Deck Management: - Collection page with card filtering and display - Decks page with CRUD operations - Deck builder with drag-drop support - Collection and deck Pinia stores Phase F3 - Phaser Integration: - Game bridge composable for Vue-Phaser communication - Game page with Phaser canvas mounting - Socket.io event types for real-time gameplay - Game store with match state management - Phaser scene scaffolding and type definitions Also includes: - New UI components (ConfirmDialog, EmptyState, FilterBar, etc.) - Toast notification system - Game config composable for dynamic rule loading - Comprehensive test coverage for new features
625 lines
28 KiB
JSON
625 lines
28 KiB
JSON
{
|
|
"meta": {
|
|
"phaseId": "PHASE_F2",
|
|
"name": "Deck Management",
|
|
"version": "1.3.0",
|
|
"created": "2026-01-30",
|
|
"lastUpdated": "2026-01-31",
|
|
"totalTasks": 12,
|
|
"completedTasks": 12,
|
|
"status": "COMPLETE",
|
|
"completedDate": "2026-01-31",
|
|
"auditStatus": "PASSED",
|
|
"auditDate": "2026-01-31",
|
|
"auditNotes": "0 issues, 3 minor warnings. All previous issues resolved: DeckContents refactored (527->292 lines), hardcoded values replaced with useGameConfig, toast feedback added, validation errors surfaced.",
|
|
"description": "Collection viewing, deck building, and card management with drag-and-drop deck editor and real-time validation.",
|
|
"designReference": "src/styles/DESIGN_REFERENCE.md"
|
|
},
|
|
"stylingPrinciples": [
|
|
"Follow patterns in DESIGN_REFERENCE.md for visual consistency",
|
|
"Type-colored accents on all card components (borders, badges)",
|
|
"Hover states with scale/shadow transitions on interactive cards",
|
|
"Skeleton loaders for all loading states (not spinners)",
|
|
"Empty states with icon, message, and CTA",
|
|
"Mobile-first responsive grids",
|
|
"Business rules (deck size, card limits) from constants/API, never hardcoded"
|
|
],
|
|
"dependencies": {
|
|
"phases": ["PHASE_F0", "PHASE_F1"],
|
|
"backend": [
|
|
"GET /api/collections/me - Get user's card collection",
|
|
"GET /api/collections/me/cards/{card_id} - Get specific card quantity",
|
|
"GET /api/decks - List user's decks",
|
|
"POST /api/decks - Create new deck",
|
|
"GET /api/decks/{id} - Get deck by ID",
|
|
"PUT /api/decks/{id} - Update deck",
|
|
"DELETE /api/decks/{id} - Delete deck",
|
|
"POST /api/decks/validate - Validate deck without saving",
|
|
"GET /api/cards/definitions - Get card definitions (for display)"
|
|
]
|
|
},
|
|
"tasks": [
|
|
{
|
|
"id": "F2-001",
|
|
"name": "Create collection and deck stores",
|
|
"description": "Create Pinia stores for managing collection and deck state",
|
|
"category": "stores",
|
|
"priority": 1,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{"path": "src/stores/collection.ts", "status": "create"},
|
|
{"path": "src/stores/collection.spec.ts", "status": "create"},
|
|
{"path": "src/stores/deck.ts", "status": "create"},
|
|
{"path": "src/stores/deck.spec.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Create useCollectionStore with state: cards (CollectionCard[]), isLoading, error",
|
|
"Add getters: totalCards, uniqueCards, getCardQuantity(definitionId)",
|
|
"Add actions: fetchCollection(), clearCollection()",
|
|
"Create useDeckStore with state: decks (Deck[]), currentDeck, isLoading, error",
|
|
"Add getters: deckCount, getDeckById(id), starterDeck",
|
|
"Add actions: fetchDecks(), fetchDeck(id), createDeck(), updateDeck(), deleteDeck(), setCurrentDeck()",
|
|
"Follow setup store pattern from auth.ts",
|
|
"Do NOT fetch via apiClient directly in store - just state management"
|
|
],
|
|
"acceptance": [
|
|
"Both stores created with typed state",
|
|
"Stores follow setup store pattern",
|
|
"Unit tests cover all getters and actions",
|
|
"Stores integrate with existing type definitions"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-002",
|
|
"name": "Create useCollection composable",
|
|
"description": "Composable for fetching and managing the user's card collection",
|
|
"category": "composables",
|
|
"priority": 2,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-001"],
|
|
"files": [
|
|
{"path": "src/composables/useCollection.ts", "status": "create"},
|
|
{"path": "src/composables/useCollection.spec.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Wrap collection store with loading/error handling",
|
|
"Implement fetchCollection() calling GET /api/collections/me",
|
|
"Parse response into CollectionCard[] format",
|
|
"Handle empty collection gracefully",
|
|
"Provide filtering helpers: byType(type), byCategory(category), byRarity(rarity)",
|
|
"Provide search helper: searchByName(query)",
|
|
"Return readonly state wrappers per composable pattern",
|
|
"Handle network errors with user-friendly messages"
|
|
],
|
|
"acceptance": [
|
|
"Composable fetches collection from API",
|
|
"Filtering and search work correctly",
|
|
"Loading and error states properly managed",
|
|
"Tests cover API success, empty collection, and errors"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-003",
|
|
"name": "Create useDecks composable",
|
|
"description": "Composable for CRUD operations on user decks",
|
|
"category": "composables",
|
|
"priority": 3,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-001"],
|
|
"files": [
|
|
{"path": "src/composables/useDecks.ts", "status": "create"},
|
|
{"path": "src/composables/useDecks.spec.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Wrap deck store with loading/error handling",
|
|
"Implement fetchDecks() calling GET /api/decks",
|
|
"Implement fetchDeck(id) calling GET /api/decks/{id}",
|
|
"Implement createDeck(data) calling POST /api/decks",
|
|
"Implement updateDeck(id, data) calling PUT /api/decks/{id}",
|
|
"Implement deleteDeck(id) calling DELETE /api/decks/{id} with confirmation",
|
|
"Return result objects: { success, error?, data? }",
|
|
"Handle 404 (deck not found), 403 (not owner), validation errors",
|
|
"Track separate loading states for different operations if needed"
|
|
],
|
|
"acceptance": [
|
|
"All CRUD operations work correctly",
|
|
"Error handling covers common cases",
|
|
"Result objects returned consistently",
|
|
"Tests cover success and error paths for each operation"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-004",
|
|
"name": "Create card display components",
|
|
"description": "Reusable components for displaying cards at various sizes",
|
|
"category": "components",
|
|
"priority": 4,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{"path": "src/components/cards/CardImage.vue", "status": "create"},
|
|
{"path": "src/components/cards/CardDisplay.vue", "status": "create"},
|
|
{"path": "src/components/cards/CardDisplay.spec.ts", "status": "create"},
|
|
{"path": "src/components/cards/TypeBadge.vue", "status": "create"}
|
|
],
|
|
"details": [
|
|
"CardImage: Simple img wrapper with loading state and fallback",
|
|
"CardImage props: src (string), alt (string), size ('sm' | 'md' | 'lg')",
|
|
"CardImage: Handle image load error with placeholder",
|
|
"CardDisplay: Full card component with name, HP, type badge",
|
|
"CardDisplay props: card (CardDefinition), size, showQuantity (number), selectable, selected, disabled",
|
|
"CardDisplay emits: click, select",
|
|
"TypeBadge: Reusable type indicator with icon and colored background",
|
|
"Apply type-colored border based on card.type (use type color mapping from DESIGN_REFERENCE)",
|
|
"Mobile-friendly touch targets (min 44px)",
|
|
"Support thumbnail (grid), medium (list), large (detail) sizes"
|
|
],
|
|
"styling": [
|
|
"Use 'Card with Type Accent' pattern from DESIGN_REFERENCE.md",
|
|
"Hover: scale-[1.02], -translate-y-1, shadow-xl with transition-all duration-200",
|
|
"Selected: ring-2 ring-primary ring-offset-2 ring-offset-background",
|
|
"Disabled: opacity-50 grayscale cursor-not-allowed",
|
|
"Quantity badge: absolute positioned, bg-primary, rounded-full (see 'Quantity Badge' pattern)",
|
|
"Type badge: inline-flex with type background color (see 'Type Badge' pattern)",
|
|
"Card container: bg-surface rounded-xl border-2 shadow-md",
|
|
"Image loading: pulse animation placeholder, smooth fade-in on load"
|
|
],
|
|
"acceptance": [
|
|
"CardImage handles loading and errors gracefully",
|
|
"CardDisplay shows card info with type-appropriate styling",
|
|
"Three sizes work correctly",
|
|
"Hover and selection states visible and smooth",
|
|
"Disabled state clearly distinguishable",
|
|
"Tests verify rendering and events"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-005",
|
|
"name": "Create card detail modal",
|
|
"description": "Full card view with all details in a modal overlay",
|
|
"category": "components",
|
|
"priority": 5,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-004"],
|
|
"files": [
|
|
{"path": "src/components/cards/CardDetailModal.vue", "status": "create"},
|
|
{"path": "src/components/cards/CardDetailModal.spec.ts", "status": "create"},
|
|
{"path": "src/components/cards/AttackDisplay.vue", "status": "create"},
|
|
{"path": "src/components/cards/EnergyCost.vue", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Props: card (CardDefinition | null), isOpen (boolean), ownedQuantity (number)",
|
|
"Emits: close",
|
|
"Large card image with CardImage component",
|
|
"Display all card fields: name, HP, type, category, rarity",
|
|
"AttackDisplay: Reusable attack row with energy cost, name, damage, effect",
|
|
"EnergyCost: Row of energy type icons for attack costs",
|
|
"Display weakness, resistance, retreat cost",
|
|
"Display owned quantity badge",
|
|
"Display set info (setId, setNumber)",
|
|
"Use Teleport to render in body",
|
|
"Close on backdrop click, Escape key, or close button",
|
|
"Trap focus within modal for accessibility",
|
|
"Animate in/out with transition"
|
|
],
|
|
"styling": [
|
|
"Backdrop: bg-black/60 backdrop-blur-sm (see 'Modal Backdrop' pattern)",
|
|
"Container: bg-surface rounded-2xl shadow-2xl max-w-lg (see 'Modal Container' pattern)",
|
|
"Header: border-b border-surface-light with close button (see 'Modal Header' pattern)",
|
|
"Enter animation: fade-in zoom-in-95 duration-200",
|
|
"Card image: centered with type-colored border glow effect",
|
|
"Attack rows: bg-surface-light/50 rounded-lg p-3 with hover highlight",
|
|
"Energy icons: small colored circles or actual energy symbols",
|
|
"Stats section: grid layout for weakness/resistance/retreat",
|
|
"Owned quantity: prominent badge in corner of card image area"
|
|
],
|
|
"acceptance": [
|
|
"Modal displays all card information",
|
|
"Attacks rendered with energy costs",
|
|
"Owned quantity visible",
|
|
"Close methods all work",
|
|
"Smooth enter/exit animations",
|
|
"Keyboard accessible (Escape closes, focus trapped)"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-006",
|
|
"name": "Create collection page",
|
|
"description": "Grid view of owned cards with filtering and search",
|
|
"category": "pages",
|
|
"priority": 6,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-002", "F2-004", "F2-005"],
|
|
"files": [
|
|
{"path": "src/pages/CollectionPage.vue", "status": "create"},
|
|
{"path": "src/pages/CollectionPage.spec.ts", "status": "create"},
|
|
{"path": "src/components/ui/FilterBar.vue", "status": "create"},
|
|
{"path": "src/components/ui/SkeletonCard.vue", "status": "create"},
|
|
{"path": "src/components/ui/EmptyState.vue", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Fetch collection on mount using useCollection()",
|
|
"Display cards in responsive grid (2 cols mobile, 3 md, 4 lg, 5 xl)",
|
|
"FilterBar component: type dropdown, category dropdown, rarity dropdown, search input",
|
|
"Show card count: 'Showing X of Y cards'",
|
|
"Each card shows quantity badge overlay",
|
|
"Click card to open CardDetailModal",
|
|
"EmptyState component: reusable empty state with icon, message, CTA",
|
|
"SkeletonCard component: reusable loading placeholder",
|
|
"Loading state: grid of SkeletonCards (match expected count or 8-12)",
|
|
"Error state: inline error with retry button",
|
|
"Debounce search input (300ms)",
|
|
"Filters persist in URL query params for sharing/bookmarking"
|
|
],
|
|
"styling": [
|
|
"Page header: text-2xl font-bold with card count subtitle",
|
|
"Filter bar: bg-surface rounded-xl p-4 mb-6 (see 'Filter Bar' pattern)",
|
|
"Search input: with SearchIcon, focus:border-primary focus:ring-1",
|
|
"Dropdowns: matching input styling for consistency",
|
|
"Grid: gap-3 md:gap-4, cards fill width of column",
|
|
"Skeleton cards: pulse animation, match card aspect ratio (see 'Skeleton Card' pattern)",
|
|
"Empty state: centered, py-16, icon + message + CTA (see 'Empty States' pattern)",
|
|
"Error state: bg-error/10 border-error/20 with AlertIcon (see 'Inline Error' pattern)",
|
|
"Smooth fade-in when cards load (transition-opacity)"
|
|
],
|
|
"acceptance": [
|
|
"Collection grid displays all owned cards",
|
|
"All filters work correctly",
|
|
"Search filters by card name",
|
|
"Card modal opens on click",
|
|
"Loading state shows skeleton grid",
|
|
"Empty state is visually polished",
|
|
"Error state allows retry",
|
|
"Mobile responsive"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-007",
|
|
"name": "Create deck list page",
|
|
"description": "View all user decks with create/edit/delete actions",
|
|
"category": "pages",
|
|
"priority": 7,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-003"],
|
|
"files": [
|
|
{"path": "src/pages/DecksPage.vue", "status": "create"},
|
|
{"path": "src/pages/DecksPage.spec.ts", "status": "create"},
|
|
{"path": "src/components/deck/DeckCard.vue", "status": "create"},
|
|
{"path": "src/components/deck/DeckCard.spec.ts", "status": "create"},
|
|
{"path": "src/components/ui/ConfirmDialog.vue", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Fetch decks on mount using useDecks()",
|
|
"Display decks as cards in a grid (1 col mobile, 2 sm, 3 lg)",
|
|
"DeckCard component shows: name, card count (from API), validation status icon, starter badge",
|
|
"DeckCard shows mini preview of 3-4 Pokemon thumbnails from deck",
|
|
"Click deck to navigate to deck editor (/decks/:id)",
|
|
"'Create New Deck' button in header -> navigate to /decks/new",
|
|
"Delete button (trash icon) with ConfirmDialog",
|
|
"ConfirmDialog: reusable confirmation modal component",
|
|
"Show deck limit from API response: 'X / Y decks'",
|
|
"Empty state: 'No decks yet. Create your first deck!' with + button",
|
|
"Loading state: skeleton DeckCards",
|
|
"Starter deck: show badge, hide/disable delete button"
|
|
],
|
|
"styling": [
|
|
"Page header: flex justify-between with title and 'New Deck' button",
|
|
"New Deck button: btn-primary with PlusIcon",
|
|
"Grid: gap-4, cards have consistent height",
|
|
"DeckCard: bg-surface rounded-xl p-4 shadow-md hover:shadow-lg transition",
|
|
"DeckCard hover: subtle lift effect (translate-y, shadow)",
|
|
"Card preview: row of 3-4 mini card images (32x44px) with overlap",
|
|
"Validity icon: CheckCircle (text-success) or XCircle (text-error)",
|
|
"Starter badge: small pill 'Starter' with bg-primary/20 text-primary",
|
|
"Delete button: icon-only, text-text-muted hover:text-error, positioned top-right",
|
|
"ConfirmDialog: danger variant with red confirm button",
|
|
"Skeleton: match DeckCard dimensions with pulse animation",
|
|
"Empty state: centered with DeckIcon, use EmptyState component"
|
|
],
|
|
"acceptance": [
|
|
"All user decks displayed in polished cards",
|
|
"Can create new deck (navigates to builder)",
|
|
"Can delete non-starter decks with confirmation",
|
|
"Deck validity status clearly visible",
|
|
"Starter deck visually distinguished",
|
|
"Loading and empty states polished"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-008",
|
|
"name": "Create useDeckBuilder composable",
|
|
"description": "Composable for deck editing state and validation",
|
|
"category": "composables",
|
|
"priority": 8,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-003"],
|
|
"files": [
|
|
{"path": "src/composables/useDeckBuilder.ts", "status": "create"},
|
|
{"path": "src/composables/useDeckBuilder.spec.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Manage draft deck state (not yet saved)",
|
|
"State: deckName, deckCards (Map<cardDefinitionId, quantity>), energyConfig",
|
|
"Track isDirty (has unsaved changes)",
|
|
"addCard(cardDefinitionId, quantity=1) - respect 4-card limit per card (except basic energy)",
|
|
"removeCard(cardDefinitionId, quantity=1)",
|
|
"setDeckName(name)",
|
|
"clearDeck() - reset to empty",
|
|
"loadDeck(deck) - populate from existing deck for editing",
|
|
"Computed: totalCards, cardList (sorted), canAddCard(cardId)",
|
|
"Validation: call POST /api/decks/validate with debounce (500ms)",
|
|
"Track validationErrors array, isValid computed",
|
|
"save() - calls createDeck or updateDeck based on isNew flag"
|
|
],
|
|
"acceptance": [
|
|
"Can add/remove cards with limits enforced",
|
|
"Draft state separate from persisted decks",
|
|
"Validation runs on changes with debounce",
|
|
"isDirty tracks unsaved changes",
|
|
"save() creates or updates correctly"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-009",
|
|
"name": "Create deck builder page",
|
|
"description": "Two-panel deck editor with collection picker and deck contents",
|
|
"category": "pages",
|
|
"priority": 9,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-002", "F2-004", "F2-008"],
|
|
"files": [
|
|
{"path": "src/pages/DeckBuilderPage.vue", "status": "create"},
|
|
{"path": "src/pages/DeckBuilderPage.spec.ts", "status": "create"},
|
|
{"path": "src/components/deck/DeckEditor.vue", "status": "create"},
|
|
{"path": "src/components/deck/DeckContents.vue", "status": "create"},
|
|
{"path": "src/components/deck/CollectionPicker.vue", "status": "create"},
|
|
{"path": "src/components/deck/DeckCardRow.vue", "status": "create"},
|
|
{"path": "src/components/ui/ProgressBar.vue", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Route: /decks/new (create) or /decks/:id (edit)",
|
|
"Load existing deck if editing (from route param)",
|
|
"Two-panel layout: CollectionPicker (left/top) + DeckContents (right/bottom)",
|
|
"Mobile: Stack panels vertically with tab switcher (Collection | Deck)",
|
|
"CollectionPicker: Filterable collection grid, click/tap to add card to deck",
|
|
"DeckContents: Scrollable list of cards with quantity controls (+/-)",
|
|
"DeckCardRow: Single card row with thumbnail, name, quantity stepper",
|
|
"Show cards grayed out if not available (quantity in collection exhausted)",
|
|
"Deck name input at top (editable)",
|
|
"ProgressBar: Reusable progress component with current/target props",
|
|
"Card count progress bar showing current/target from API/config",
|
|
"Validation errors displayed inline below progress bar",
|
|
"Save button (disabled if invalid or not dirty)",
|
|
"Cancel button with unsaved changes confirmation",
|
|
"Energy configuration section (collapsible, basic energy type buttons)"
|
|
],
|
|
"styling": [
|
|
"Page layout: sticky header with name input + actions, scrollable content below",
|
|
"Header: bg-surface border-b, flex items-center justify-between p-4",
|
|
"Deck name input: text-xl font-bold, minimal border (border-transparent focus:border-primary)",
|
|
"Two-panel: lg:flex lg:gap-6, CollectionPicker lg:w-2/3, DeckContents lg:w-1/3",
|
|
"Mobile tabs: sticky below header, bg-surface-light rounded-lg p-1, active tab bg-surface",
|
|
"CollectionPicker: bg-surface rounded-xl p-4, includes FilterBar and card grid",
|
|
"Cards in picker: show remaining quantity badge, disabled state if exhausted",
|
|
"DeckContents: bg-surface rounded-xl p-4, sticky on desktop",
|
|
"DeckCardRow: flex items-center gap-3, small thumbnail, name, quantity stepper",
|
|
"Quantity stepper: rounded-lg border, - and + buttons with number between",
|
|
"Progress bar: use 'Card Count Bar' pattern from DESIGN_REFERENCE (current/target props)",
|
|
"Validation errors: use 'Inline Error' pattern, list format if multiple",
|
|
"Save button: btn-primary, Cancel: btn-secondary",
|
|
"Energy section: collapsible with ChevronIcon, grid of energy type buttons"
|
|
],
|
|
"acceptance": [
|
|
"Can create new deck from scratch",
|
|
"Can edit existing deck",
|
|
"Collection filtering works in picker",
|
|
"Cannot add more cards than owned",
|
|
"Card limits enforced (from API validation)",
|
|
"Validation errors shown clearly with proper styling",
|
|
"Save/cancel work correctly with appropriate feedback",
|
|
"Unsaved changes prompt on navigation",
|
|
"Mobile tab switching works smoothly"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-010",
|
|
"name": "Add drag-and-drop support",
|
|
"description": "Optional drag-and-drop for adding/removing cards",
|
|
"category": "components",
|
|
"priority": 10,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-009"],
|
|
"files": [
|
|
{"path": "src/composables/useDragDrop.ts", "status": "create"},
|
|
{"path": "src/composables/useDragDrop.spec.ts", "status": "create"},
|
|
{"path": "src/components/deck/DeckEditor.vue", "status": "modify"},
|
|
{"path": "src/components/deck/CollectionPicker.vue", "status": "modify"},
|
|
{"path": "src/components/deck/DeckContents.vue", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Use HTML5 Drag and Drop API (no library needed)",
|
|
"Create useDragDrop composable for drag state management",
|
|
"Track: isDragging, draggedCard, dropTarget",
|
|
"Make collection cards draggable (draggable='true')",
|
|
"DeckContents as drop target",
|
|
"Touch support: use long-press (500ms) to initiate drag on mobile",
|
|
"Fallback: click-to-add still works (drag is enhancement)",
|
|
"Drop on collection area to remove from deck (or drag out of drop zone)",
|
|
"Accessible: all actions available via click as well"
|
|
],
|
|
"styling": [
|
|
"Dragging card: opacity-50 on original, cursor-grabbing",
|
|
"Drag ghost: slightly rotated (rotate-3), scale-105, shadow-2xl",
|
|
"Valid drop target: ring-2 ring-primary ring-dashed, bg-primary/5",
|
|
"Invalid drop target (card limit reached): ring-2 ring-error ring-dashed, bg-error/5",
|
|
"Drop zone highlight: animate pulse when dragging over",
|
|
"Card being dragged over deck: subtle insert indicator line",
|
|
"Long-press feedback on touch: scale down slightly before drag starts",
|
|
"Transition all visual states smoothly (duration-150)"
|
|
],
|
|
"acceptance": [
|
|
"Can drag cards from collection to deck",
|
|
"Can drag cards out of deck to remove",
|
|
"Visual feedback clearly indicates valid/invalid drops",
|
|
"Click-to-add still works as fallback",
|
|
"Touch devices can use long-press",
|
|
"All animations are smooth"
|
|
],
|
|
"notes": "This is an enhancement - if time constrained, click-to-add/remove is sufficient for MVP"
|
|
},
|
|
{
|
|
"id": "F2-011",
|
|
"name": "Add routes and navigation",
|
|
"description": "Configure routes for collection and deck pages",
|
|
"category": "setup",
|
|
"priority": 11,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-006", "F2-007", "F2-009"],
|
|
"files": [
|
|
{"path": "src/router/index.ts", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Add /collection route -> CollectionPage",
|
|
"Add /decks route -> DecksPage",
|
|
"Add /decks/new route -> DeckBuilderPage (create mode)",
|
|
"Add /decks/:id route -> DeckBuilderPage (edit mode)",
|
|
"All routes require auth and starter deck (meta: requiresAuth, requiresStarter)",
|
|
"Lazy load pages for code splitting",
|
|
"Add navigation guard for unsaved deck changes"
|
|
],
|
|
"acceptance": [
|
|
"All routes work correctly",
|
|
"Routes protected by auth",
|
|
"Lazy loading configured",
|
|
"Navigation warns about unsaved changes in deck builder"
|
|
]
|
|
},
|
|
{
|
|
"id": "F2-012",
|
|
"name": "Update types and API contracts",
|
|
"description": "Ensure frontend types match backend API contracts",
|
|
"category": "api",
|
|
"priority": 12,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["F2-001"],
|
|
"files": [
|
|
{"path": "src/types/index.ts", "status": "modify"},
|
|
{"path": "src/types/api.ts", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Review backend schemas: deck.py, collection.py, card.py",
|
|
"Ensure Deck, DeckCard, CollectionCard types match response shapes",
|
|
"Add API request/response types: DeckCreateRequest, DeckUpdateRequest",
|
|
"Add validation types: DeckValidationResponse, ValidationError",
|
|
"Add energy configuration types: EnergyConfig, EnergyCard",
|
|
"Export all types from index.ts",
|
|
"Document any frontend-only derived types"
|
|
],
|
|
"acceptance": [
|
|
"Frontend types match backend API exactly",
|
|
"All API operations are type-safe",
|
|
"No runtime type mismatches"
|
|
]
|
|
}
|
|
],
|
|
"apiContracts": {
|
|
"collection": {
|
|
"get": {
|
|
"endpoint": "GET /api/collections/me",
|
|
"response": {
|
|
"total_unique_cards": "number",
|
|
"total_card_count": "number",
|
|
"entries": [
|
|
{
|
|
"card_definition_id": "string",
|
|
"quantity": "number",
|
|
"source": "string",
|
|
"obtained_at": "ISO datetime"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"decks": {
|
|
"list": {
|
|
"endpoint": "GET /api/decks",
|
|
"response": {
|
|
"decks": ["DeckResponse[]"],
|
|
"deck_count": "number",
|
|
"deck_limit": "number"
|
|
}
|
|
},
|
|
"get": {
|
|
"endpoint": "GET /api/decks/{id}",
|
|
"response": "DeckResponse"
|
|
},
|
|
"create": {
|
|
"endpoint": "POST /api/decks",
|
|
"request": {
|
|
"name": "string",
|
|
"description": "string | null",
|
|
"cards": "Record<cardDefinitionId, quantity>",
|
|
"energy_cards": "Record<energyType, quantity>",
|
|
"deck_config": "DeckConfig | null"
|
|
},
|
|
"response": "DeckResponse"
|
|
},
|
|
"update": {
|
|
"endpoint": "PUT /api/decks/{id}",
|
|
"request": "Partial<DeckCreateRequest>",
|
|
"response": "DeckResponse"
|
|
},
|
|
"delete": {
|
|
"endpoint": "DELETE /api/decks/{id}",
|
|
"response": "204 No Content"
|
|
},
|
|
"validate": {
|
|
"endpoint": "POST /api/decks/validate",
|
|
"request": {
|
|
"cards": "Record<cardDefinitionId, quantity>",
|
|
"energy_cards": "Record<energyType, quantity>"
|
|
},
|
|
"response": {
|
|
"is_valid": "boolean",
|
|
"errors": ["string[]"]
|
|
}
|
|
}
|
|
},
|
|
"deckResponse": {
|
|
"id": "UUID",
|
|
"name": "string",
|
|
"description": "string | null",
|
|
"cards": "Record<cardDefinitionId, quantity>",
|
|
"energy_cards": "Record<energyType, quantity>",
|
|
"is_valid": "boolean",
|
|
"validation_errors": ["string[]"],
|
|
"is_starter": "boolean",
|
|
"starter_type": "string | null",
|
|
"created_at": "ISO datetime",
|
|
"updated_at": "ISO datetime"
|
|
}
|
|
},
|
|
"notes": [
|
|
"F2-003 (starter selection) was already implemented in F1 - this phase focuses on post-starter deck management",
|
|
"Drag-and-drop (F2-010) is an enhancement - click-based editing is sufficient for MVP",
|
|
"Energy configuration may be simplified if backend doesn't require it yet",
|
|
"Card images may need placeholder/fallback handling if CDN not yet configured",
|
|
"Consider virtualized scrolling for large collections in future optimization",
|
|
"All UI tasks include 'styling' arrays - follow patterns in src/styles/DESIGN_REFERENCE.md",
|
|
"Create reusable UI components (EmptyState, SkeletonCard, ConfirmDialog, ProgressBar, FilterBar) for consistency"
|
|
]
|
|
}
|