Align UI copy: My Collection vs Lists #54

Merged
varutasu merged 2 commits from feat/rename-collections-vocabulary into main 2026-05-29 10:53:41 -04:00
24 changed files with 228 additions and 120 deletions
Showing only changes of commit a6d72e19b7 - Show all commits

View file

@ -59,3 +59,11 @@ Don't duplicate navigation in a page — extend `NavigationContent` inside Layou
## Branding ## Branding
The canonical product brand is **Deck Hearth** (two words, internal cap), ratified 2026-05-24 in the `pick-a-name` convoy. The repo directory + GitHub project name remain `tcg-vault` until the queued `rename-repo-and-vercel-project` convoy ships. New UI copy MUST use `Deck Hearth` verbatim — do not introduce a third name, do not abbreviate to "DH" outside the logo glyph (currently used in `components/Layout.js` lines 621, 714 as the sidebar logo monogram). The canonical product brand is **Deck Hearth** (two words, internal cap), ratified 2026-05-24 in the `pick-a-name` convoy. The repo directory + GitHub project name remain `tcg-vault` until the queued `rename-repo-and-vercel-project` convoy ships. New UI copy MUST use `Deck Hearth` verbatim — do not introduce a third name, do not abbreviate to "DH" outside the logo glyph (currently used in `components/Layout.js` lines 621, 714 as the sidebar logo monogram).
## Product vocabulary (ownership vs lists)
- **My Collection** — global ownership (`user_cards`, `/my-cards`). Never say "Owned Cards" or "Mark Owned" in UI.
- **Lists** — curated binders (`collections` table, `/collections`). Say "List" in buttons/modals; URL stays `/collections` until a future slug convoy.
- **Synced binder** — display name for the system collection (`is_system_collection`); DB literal `'All My Cards'` is internal only.
- Import copy from `lib/collection-vocabulary.js` (`VOCAB`, `collectionDisplayName`, `formatProcessedDestination`).
- CI `forbidden-stale-strings` enforces the three retired phrases in `pages/` + `components/`.

View file

@ -208,6 +208,40 @@ jobs:
fi fi
echo "OK: no client-side LLM key leakage patterns detected." echo "OK: no client-side LLM key leakage patterns detected."
forbidden-stale-strings:
name: No stale ownership/collection copy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail if forbidden UI strings appear in pages/ or components/
run: |
# rename-collections-vocabulary convoy — ownership vs curated-list taxonomy.
# API/DB literals (e.g. system collection name) live under pages/api/ only.
PATTERNS=(
'Mark Owned'
'Owned Cards'
'All My Cards'
)
FOUND=()
for pattern in "${PATTERNS[@]}"; do
while IFS= read -r line; do
FOUND+=("$line")
done < <(grep -rFn "$pattern" pages/ components/ \
--include='*.js' \
--exclude-dir=api 2>/dev/null || true)
done
if [ ${#FOUND[@]} -gt 0 ]; then
echo "::error::Forbidden stale UI string(s) in pages/ or components/. Use lib/collection-vocabulary.js labels — see AGENTS.md § Product vocabulary."
printf '%s\n' "${FOUND[@]}" | sort -u | while IFS= read -r line; do
file=$(echo "$line" | cut -d: -f1)
lineno=$(echo "$line" | cut -d: -f2)
text=$(echo "$line" | cut -d: -f3-)
echo "::error file=${file},line=${lineno}::${text}"
done
exit 1
fi
echo "OK: no forbidden stale strings in pages/ or components/."
test: test:
name: Unit tests (vitest) name: Unit tests (vitest)
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -12,6 +12,20 @@ Guidance for agents and humans working in this repo. Prefer existing patterns ov
> as a historical regression-lock per Risk 4 of the pick-a-name > as a historical regression-lock per Risk 4 of the pick-a-name
> convoy. > convoy.
## Product vocabulary
User-facing copy distinguishes **ownership** (everything you own) from **curated lists** (binders/subsets). Import labels from `lib/collection-vocabulary.js` (`VOCAB`, `collectionDisplayName`) rather than hardcoding strings.
| Concept | UI label | Route / schema | Notes |
| --- | --- | --- | --- |
| Global ownership | **My Collection** | `/my-cards`, `user_cards` | Scanner default destination; replaces "Owned" / "Mark Owned" |
| Curated list / binder | **List** / **Lists** | `/collections`, `/collection/[id]`, `collections` table | URL slug unchanged in v1; nav says "Lists" |
| Auto-sync system list | **Synced binder** (display) | `collections` row with `is_system_collection = true` | DB name stays `'All My Cards'` — never render; use `collectionDisplayName()` |
| Add ownership | **Add to My Collection** | `POST /api/user-cards`, scanner bulk | Replaces "Mark Owned" / "Mark as Owned" |
| Add to curated list | **Add to List** | `POST /api/collections/:id/cards` | Replaces "Add to Collection" in scanner/card flows |
CI job `forbidden-stale-strings` blocks `"Mark Owned"`, `"Owned Cards"`, and `"All My Cards"` in `pages/` + `components/` (API literals exempt).
## 1. Project overview ## 1. Project overview
A web app for managing trading-card-game collections (Magic, Pokémon, Lorcana). Users authenticate, build collections + decks, scan physical cards via a camera+AI-OCR flow, and share publicly. Admin users curate the card database. A web app for managing trading-card-game collections (Magic, Pokémon, Lorcana). Users authenticate, build collections + decks, scan physical cards via a camera+AI-OCR flow, and share publicly. Admin users curate the card database.

View file

@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function BulkSelectionToolbar({ export default function BulkSelectionToolbar({
selectedCards, selectedCards,
@ -96,7 +97,7 @@ export default function BulkSelectionToolbar({
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span className="hidden sm:inline">Own</span> <span className="hidden sm:inline">{VOCAB.MY_COLLECTION}</span>
</button> </button>
</div> </div>
@ -137,7 +138,7 @@ export default function BulkSelectionToolbar({
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
<span>Remove from Owned</span> <span>{VOCAB.REMOVE_FROM_MY_COLLECTION}</span>
</button> </button>
<div className="border-t border-gray-200 my-1"></div> <div className="border-t border-gray-200 my-1"></div>

View file

@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { VOCAB } from '../lib/collection-vocabulary.js';
// Expanding Add Button Component // Expanding Add Button Component
function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwned }) { function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwned }) {
@ -31,7 +32,7 @@ function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwne
onClick={(e) => handleOptionClick(e, () => onAddToCollection([card]))} onClick={(e) => handleOptionClick(e, () => onAddToCollection([card]))}
className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 flex items-center space-x-1 text-xs whitespace-nowrap" className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 flex items-center space-x-1 text-xs whitespace-nowrap"
style={{ backgroundColor: 'var(--accent-flame)', color: 'white' }} style={{ backgroundColor: 'var(--accent-flame)', color: 'white' }}
title="Add to Collection" title={VOCAB.ADD_TO_LIST}
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
@ -55,7 +56,7 @@ function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwne
onClick={(e) => handleOptionClick(e, onMarkAsOwned)} onClick={(e) => handleOptionClick(e, onMarkAsOwned)}
className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 flex items-center space-x-1 text-xs whitespace-nowrap" className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 flex items-center space-x-1 text-xs whitespace-nowrap"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }} style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
title="Mark as Owned" title={VOCAB.ADD_TO_MY_COLLECTION}
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
@ -74,7 +75,7 @@ function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwne
color: 'white', color: 'white',
boxShadow: '0 0 0 2px var(--accent-ember), 0 4px 6px -1px rgba(216, 67, 21, 0.3)' boxShadow: '0 0 0 2px var(--accent-ember), 0 4px 6px -1px rgba(216, 67, 21, 0.3)'
}} }}
title={isExpanded ? "Add to Collection" : "Show Options"} title={isExpanded ? VOCAB.ADD_TO_LIST : 'Show Options'}
> >
<svg <svg
className={`w-4 h-4 transition-transform duration-150 ${isExpanded ? 'rotate-45' : ''}`} className={`w-4 h-4 transition-transform duration-150 ${isExpanded ? 'rotate-45' : ''}`}
@ -259,7 +260,7 @@ export default function CardItem({
onAddToCollection(card); onAddToCollection(card);
}} }}
className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors" className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors"
title="Add to Collection" title={VOCAB.ADD_TO_LIST}
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
@ -381,7 +382,7 @@ export default function CardItem({
card={card} card={card}
onAddToCollection={onAddToCollection} onAddToCollection={onAddToCollection}
onAddToDeck={onAddToDeck} onAddToDeck={onAddToDeck}
onMarkAsOwned={() => alert('Mark as owned (coming soon)')} onMarkAsOwned={() => alert(`${VOCAB.ADD_TO_MY_COLLECTION} (coming soon)`)}
/> />
</div> </div>
</div> </div>

View file

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function CollectionSelectionModal({ export default function CollectionSelectionModal({
isOpen, isOpen,
@ -132,7 +133,7 @@ export default function CollectionSelectionModal({
<div className="p-6 border-b border-gray-200 dark:border-gray-700"> <div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Add to Collections {VOCAB.ADD_TO_LIST}s
</h2> </h2>
<button <button
onClick={onClose} onClick={onClose}
@ -175,7 +176,7 @@ export default function CollectionSelectionModal({
{cards.length} card{cards.length !== 1 ? 's' : ''} selected {cards.length} card{cards.length !== 1 ? 's' : ''} selected
</p> </p>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Choose collections to add {cards.length === 1 ? 'this card' : 'these cards'} to Choose lists to add {cards.length === 1 ? 'this card' : 'these cards'} to
</p> </p>
</div> </div>
</div> </div>
@ -184,7 +185,7 @@ export default function CollectionSelectionModal({
<div className="relative"> <div className="relative">
<input <input
type="text" type="text"
placeholder="Search collections..." placeholder="Search lists..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 pl-10 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white" className="w-full px-4 py-2 pl-10 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
@ -200,7 +201,7 @@ export default function CollectionSelectionModal({
{loading ? ( {loading ? (
<div className="p-8 text-center"> <div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
<p className="text-gray-600 dark:text-gray-400">Loading collections...</p> <p className="text-gray-600 dark:text-gray-400">Loading lists...</p>
</div> </div>
) : filteredCollections.length === 0 ? ( ) : filteredCollections.length === 0 ? (
<div className="p-8 text-center"> <div className="p-8 text-center">
@ -208,10 +209,10 @@ export default function CollectionSelectionModal({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg> </svg>
<p className="text-gray-600 dark:text-gray-400 mb-2"> <p className="text-gray-600 dark:text-gray-400 mb-2">
{searchQuery ? 'No collections match your search' : 'No collections found'} {searchQuery ? 'No lists match your search' : 'No lists found'}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-500">
{searchQuery ? 'Try a different search term' : 'Create your first collection to get started'} {searchQuery ? 'Try a different search term' : 'Create your first list to get started'}
</p> </p>
</div> </div>
) : ( ) : (
@ -305,7 +306,7 @@ export default function CollectionSelectionModal({
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
{selectedCollections.length > 0 && ( {selectedCollections.length > 0 && (
<span> <span>
{selectedCollections.length} collection{selectedCollections.length !== 1 ? 's' : ''} selected {selectedCollections.length} list{selectedCollections.length !== 1 ? 's' : ''} selected
</span> </span>
)} )}
</div> </div>
@ -332,7 +333,7 @@ export default function CollectionSelectionModal({
<span>Adding...</span> <span>Adding...</span>
</div> </div>
) : ( ) : (
`Add to ${selectedCollections.length} Collection${selectedCollections.length !== 1 ? 's' : ''}` `Add to ${selectedCollections.length} List${selectedCollections.length !== 1 ? 's' : ''}`
)} )}
</button> </button>
</div> </div>

View file

@ -176,7 +176,7 @@ function NavigationContent({ user, router, onItemClick }) {
active: router.pathname === '/dashboard' || router.pathname === '/collections' || router.pathname === '/my-cards' || router.pathname === '/decks' || router.pathname === '/analytics', active: router.pathname === '/dashboard' || router.pathname === '/collections' || router.pathname === '/my-cards' || router.pathname === '/decks' || router.pathname === '/analytics',
expanded: true, expanded: true,
items: [ items: [
{ name: 'Collections', href: '/collections', active: router.pathname === '/collections' }, { name: 'Lists', href: '/collections', active: router.pathname === '/collections' },
{ name: 'Cards', href: '/my-cards', active: router.pathname === '/my-cards' }, { name: 'Cards', href: '/my-cards', active: router.pathname === '/my-cards' },
{ name: 'Decks', href: '/decks', active: router.pathname === '/decks' }, { name: 'Decks', href: '/decks', active: router.pathname === '/decks' },
{ name: 'Analytics', href: '/analytics', active: router.pathname === '/analytics', isPlaceholder: true } { name: 'Analytics', href: '/analytics', active: router.pathname === '/analytics', isPlaceholder: true }
@ -201,7 +201,7 @@ function NavigationContent({ user, router, onItemClick }) {
active: router.pathname.startsWith('/community'), active: router.pathname.startsWith('/community'),
expanded: isCommunityExpanded, expanded: isCommunityExpanded,
items: [ items: [
{ name: 'Collections', href: '/community/collections', active: router.pathname === '/community/collections' }, { name: 'Lists', href: '/community/collections', active: router.pathname === '/community/collections' },
{ name: 'Decks', href: '/community/decks', active: router.pathname === '/community/decks' }, { name: 'Decks', href: '/community/decks', active: router.pathname === '/community/decks' },
{ name: 'Forums', href: '/community/forums', active: router.pathname === '/community/forums' } { name: 'Forums', href: '/community/forums', active: router.pathname === '/community/forums' }
] ]

View file

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { formatProcessedDestination, VOCAB } from '../lib/collection-vocabulary.js';
export const CONDITION_OPTIONS = ['NM', 'LP', 'MP', 'HP', 'DMG']; export const CONDITION_OPTIONS = ['NM', 'LP', 'MP', 'HP', 'DMG'];
@ -223,7 +224,7 @@ export default function ScannedCardItem({
style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }} style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }}
> >
<span aria-hidden="true">💎 </span> <span aria-hidden="true">💎 </span>
{isAdding ? 'Adding…' : 'Mark Owned'} {isAdding ? 'Adding…' : VOCAB.ADD_TO_MY_COLLECTION}
</button> </button>
<button <button
type="button" type="button"
@ -251,9 +252,9 @@ export default function ScannedCardItem({
border: '1px solid var(--border)', border: '1px solid var(--border)',
}} }}
defaultValue="" defaultValue=""
aria-label={`Add ${card.name} to a collection`} aria-label={`Add ${card.name} to a list`}
> >
<option value="">📚 Add to Collection</option> <option value="">{`📚 ${VOCAB.ADD_TO_LIST}`}</option>
{collections.map((collection) => ( {collections.map((collection) => (
<option key={collection.id} value={collection.id}> <option key={collection.id} value={collection.id}>
{collection.name} {collection.name}
@ -291,7 +292,7 @@ export default function ScannedCardItem({
) : ( ) : (
<div className="text-sm flex items-center gap-2" style={{ color: 'var(--accent-ember)' }}> <div className="text-sm flex items-center gap-2" style={{ color: 'var(--accent-ember)' }}>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span>Added to {card.processedAction}</span> <span>Added to {formatProcessedDestination(card.processedAction)}</span>
</div> </div>
)} )}
</div> </div>

View file

@ -1,3 +1,5 @@
import { VOCAB } from '../lib/collection-vocabulary.js';
const GAME_OPTIONS = [ const GAME_OPTIONS = [
{ value: 'All', label: 'All games' }, { value: 'All', label: 'All games' },
{ value: 'MTG', label: 'Magic' }, { value: 'MTG', label: 'Magic' },
@ -30,7 +32,7 @@ export default function ScannerDestinationPicker({
const handleTypeChange = (type) => { const handleTypeChange = (type) => {
if (type === 'owned') { if (type === 'owned') {
onDestinationChange({ type: 'owned', id: null, label: 'My owned cards' }); onDestinationChange({ type: 'owned', id: null, label: VOCAB.MY_COLLECTION });
return; return;
} }
if (type === 'collection' && filteredCollections.length > 0) { if (type === 'collection' && filteredCollections.length > 0) {
@ -122,8 +124,8 @@ export default function ScannerDestinationPicker({
<fieldset className="flex flex-wrap gap-2" disabled={disabled}> <fieldset className="flex flex-wrap gap-2" disabled={disabled}>
<legend className="sr-only">Destination type</legend> <legend className="sr-only">Destination type</legend>
{[ {[
{ type: 'owned', label: '💎 Owned' }, { type: 'owned', label: `💎 ${VOCAB.MY_COLLECTION}` },
{ type: 'collection', label: '📚 Collection' }, { type: 'collection', label: `📚 ${VOCAB.LIST}` },
{ type: 'deck', label: '🃏 Deck' }, { type: 'deck', label: '🃏 Deck' },
].map((option) => { ].map((option) => {
const isActive = destination?.type === option.type; const isActive = destination?.type === option.type;
@ -154,7 +156,7 @@ export default function ScannerDestinationPicker({
{destination?.type === 'collection' && filteredCollections.length > 0 && ( {destination?.type === 'collection' && filteredCollections.length > 0 && (
<label className="flex flex-col gap-1 mt-4 text-sm"> <label className="flex flex-col gap-1 mt-4 text-sm">
<span style={{ color: 'var(--text-secondary)' }}>Collection</span> <span style={{ color: 'var(--text-secondary)' }}>{VOCAB.LIST}</span>
<select <select
value={String(destination.id)} value={String(destination.id)}
onChange={handleTargetChange} onChange={handleTargetChange}

View file

@ -23,8 +23,8 @@
| --- | --- | --- | | --- | --- | --- |
| **Identity** | `users`, `user_settings`, `user_avatars` | Accounts, profile, preferences | | **Identity** | `users`, `user_settings`, `user_avatars` | Accounts, profile, preferences |
| **Catalog** | `cards` | Master card list across MTG / Pokémon / Lorcana | | **Catalog** | `cards` | Master card list across MTG / Pokémon / Lorcana |
| **Ownership** | `user_cards`, `user_favorites` | What a user owns / has favorited | | **Ownership** | `user_cards`, `user_favorites` | What a user owns / has favorited (UI: **My Collection**) |
| **Collections** | `collections`, `collection_cards`, `collection_permissions`, `collection_activity` | Curated card lists with sharing | | **Collections** | `collections`, `collection_cards`, `collection_permissions`, `collection_activity` | Curated card lists with sharing (UI: **Lists**) |
| **Decks** | `decks`, `deck_cards` | Playable deck definitions | | **Decks** | `decks`, `deck_cards` | Playable deck definitions |
| **Scanning** | `card_submissions`, `scan_attempts` | Unknown-card review queue + scan telemetry | | **Scanning** | `card_submissions`, `scan_attempts` | Unknown-card review queue + scan telemetry |
| **Invitations** | `invitations` (referenced; verify) | Pending share requests | | **Invitations** | `invitations` (referenced; verify) | Pending share requests |
@ -124,7 +124,7 @@
| `visibility` | `VARCHAR(20)` default `'private'` | **Coexists with `is_public`; verify single source of truth** | | `visibility` | `VARCHAR(20)` default `'private'` | **Coexists with `is_public`; verify single source of truth** |
| `tcg` | `VARCHAR(50)` default `'MTG'` | | | `tcg` | `VARCHAR(50)` default `'MTG'` | |
| `tags` | `TEXT` | Comma-separated; consider migrating to JSONB array | | `tags` | `TEXT` | Comma-separated; consider migrating to JSONB array |
| `is_system_collection` | `BOOLEAN` default `false` | E.g. "All My Cards" auto-collection | | `is_system_collection` | `BOOLEAN` default `false` | Auto-sync list; DB name `'All My Cards'` (internal — UI: **Synced binder** via `collectionDisplayName()`) |
| `created_at`, `updated_at` | `TIMESTAMP` | | | `created_at`, `updated_at` | `TIMESTAMP` | |
### collection_cards ### collection_cards

View file

@ -0,0 +1,39 @@
/**
* Product vocabulary ownership vs curated lists.
* See AGENTS.md § Product vocabulary and docs/SCHEMA_MAP.md glossary.
*/
/** Internal DB name for the auto-sync system collection — never render in UI. */
export const SYSTEM_COLLECTION_DB_NAME = 'All My Cards';
export const VOCAB = {
MY_COLLECTION: 'My Collection',
LISTS: 'Lists',
LIST: 'List',
ADD_TO_MY_COLLECTION: 'Add to My Collection',
REMOVE_FROM_MY_COLLECTION: 'Remove from My Collection',
ADD_TO_LIST: 'Add to List',
SYNCED_BINDER: 'Synced binder',
SYSTEM_COLLECTION_SYNC_HINT: 'Automatically syncs with My Collection',
};
/** User-facing label for a collection row (maps system collection DB name). */
export function collectionDisplayName(collection) {
if (!collection) return '';
if (
collection.isSystemCollection ||
collection.is_system_collection ||
collection.name === SYSTEM_COLLECTION_DB_NAME
) {
return VOCAB.SYNCED_BINDER;
}
return collection.name;
}
/** Scanner / bulk-action success line: "Added to …" */
export function formatProcessedDestination(action) {
if (action === 'owned') return VOCAB.MY_COLLECTION;
if (action === 'collection') return VOCAB.LIST;
if (action === 'deck') return 'deck';
return action;
}

View file

@ -5,6 +5,7 @@ import { useAuth } from '../../lib/use-auth';
import CollectionSelectionModal from '../../components/CollectionSelectionModal'; import CollectionSelectionModal from '../../components/CollectionSelectionModal';
import { ManaCost, ColorIdentity, AdvancedManaCost } from '../../components/ManaSymbols'; import { ManaCost, ColorIdentity, AdvancedManaCost } from '../../components/ManaSymbols';
import ManaSymbolSettings from '../../components/ManaSymbolSettings'; import ManaSymbolSettings from '../../components/ManaSymbolSettings';
import { VOCAB } from '../../lib/collection-vocabulary.js';
export default function CardDetail() { export default function CardDetail() {
const router = useRouter(); const router = useRouter();
@ -468,7 +469,7 @@ export default function CardDetail() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{ownedQuantity > 0 && ( {ownedQuantity > 0 && (
<span className="px-3 py-1 rounded-full text-sm font-medium bg-green-500 bg-opacity-80"> <span className="px-3 py-1 rounded-full text-sm font-medium bg-green-500 bg-opacity-80">
Owned ({ownedQuantity}) In {VOCAB.MY_COLLECTION} ({ownedQuantity})
</span> </span>
)} )}
<button <button
@ -489,14 +490,14 @@ export default function CardDetail() {
className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2" className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2"
> >
<span>📦</span> <span>📦</span>
{ownedQuantity > 0 ? 'Update Quantity' : 'Mark as Owned'} {ownedQuantity > 0 ? 'Update Quantity' : VOCAB.ADD_TO_MY_COLLECTION}
</button> </button>
<button <button
onClick={() => setShowCollectionModal(true)} onClick={() => setShowCollectionModal(true)}
className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2" className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2"
> >
<span>📁</span> <span>📁</span>
Add to Collection {VOCAB.ADD_TO_LIST}
</button> </button>
<button <button
onClick={() => setShowDeckModal(true)} onClick={() => setShowDeckModal(true)}
@ -759,7 +760,7 @@ export default function CardDetail() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl p-6 max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-primary)' }}> <div className="bg-white rounded-2xl p-6 max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<h3 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> <h3 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
{ownedQuantity > 0 ? 'Update Quantity' : 'Mark as Owned'} {ownedQuantity > 0 ? 'Update Quantity' : VOCAB.ADD_TO_MY_COLLECTION}
</h3> </h3>
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
@ -798,7 +799,7 @@ export default function CardDetail() {
onClick={() => handleOwnershipUpdate(quantity)} onClick={() => handleOwnershipUpdate(quantity)}
className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200" className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200"
> >
{ownedQuantity > 0 ? 'Update' : 'Mark as Owned'} {ownedQuantity > 0 ? 'Update' : VOCAB.ADD_TO_MY_COLLECTION}
</button> </button>
<button <button
onClick={() => setShowQuantityModal(false)} onClick={() => setShowQuantityModal(false)}

View file

@ -9,6 +9,7 @@ import ProtectedRoute from '../components/ProtectedRoute';
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols'; import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings'; import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import { VOCAB } from '../lib/collection-vocabulary.js';
// Public fallback component for non-authenticated users // Public fallback component for non-authenticated users
function PublicCardsView() { function PublicCardsView() {
@ -461,7 +462,7 @@ function AuthenticatedCards() {
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
if (!token) { if (!token) {
alert('Please log in to mark cards as owned'); alert(`Please log in to ${VOCAB.ADD_TO_MY_COLLECTION.toLowerCase()}`);
return; return;
} }
@ -486,16 +487,16 @@ function AuthenticatedCards() {
} }
if (successCount === cards.length) { if (successCount === cards.length) {
alert(`Successfully marked ${cards.length} cards as owned!`); alert(`Successfully added ${cards.length} cards to ${VOCAB.MY_COLLECTION}!`);
} else { } else {
alert(`Marked ${successCount} out of ${cards.length} cards as owned. Some operations may have failed.`); alert(`Added ${successCount} out of ${cards.length} cards to ${VOCAB.MY_COLLECTION}. Some operations may have failed.`);
} }
// Clear selection after successful operation // Clear selection after successful operation
setSelectedCards([]); setSelectedCards([]);
} catch (error) { } catch (error) {
console.error('Error in bulk mark as owned:', error); console.error('Error in bulk mark as owned:', error);
alert('Failed to mark cards as owned. Please try again.'); alert(`Failed to add cards to ${VOCAB.MY_COLLECTION}. Please try again.`);
} }
}; };
@ -503,7 +504,7 @@ function AuthenticatedCards() {
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
if (!token) { if (!token) {
alert('Please log in to remove cards from owned'); alert(`Please log in to ${VOCAB.REMOVE_FROM_MY_COLLECTION.toLowerCase()}`);
return; return;
} }
@ -528,16 +529,16 @@ function AuthenticatedCards() {
} }
if (successCount === cards.length) { if (successCount === cards.length) {
alert(`Successfully removed ${cards.length} cards from owned!`); alert(`Successfully removed ${cards.length} cards from ${VOCAB.MY_COLLECTION}!`);
} else { } else {
alert(`Removed ${successCount} out of ${cards.length} cards from owned. Some operations may have failed.`); alert(`Removed ${successCount} out of ${cards.length} cards from ${VOCAB.MY_COLLECTION}. Some operations may have failed.`);
} }
// Clear selection after successful operation // Clear selection after successful operation
setSelectedCards([]); setSelectedCards([]);
} catch (error) { } catch (error) {
console.error('Error in bulk remove from owned:', error); console.error('Error in bulk remove from owned:', error);
alert('Failed to remove cards from owned. Please try again.'); alert(`Failed to remove cards from ${VOCAB.MY_COLLECTION}. Please try again.`);
} }
}; };
@ -582,7 +583,7 @@ function AuthenticatedCards() {
const totalAttempts = results.length; const totalAttempts = results.length;
if (successCount === totalAttempts) { if (successCount === totalAttempts) {
alert(`Successfully added ${cards.length} card${cards.length !== 1 ? 's' : ''} to ${selectedCollectionIds.length} collection${selectedCollectionIds.length !== 1 ? 's' : ''}!`); alert(`Successfully added ${cards.length} card${cards.length !== 1 ? 's' : ''} to ${selectedCollectionIds.length} list${selectedCollectionIds.length !== 1 ? 's' : ''}!`);
} else { } else {
alert(`Added ${successCount} out of ${totalAttempts} cards. Some additions may have failed.`); alert(`Added ${successCount} out of ${totalAttempts} cards. Some additions may have failed.`);
} }
@ -1459,7 +1460,7 @@ function Card3D({ card, viewMode, getRarityLabel, getRarityColor }) {
)} )}
{card.collections && card.collections.length > 0 && ( {card.collections && card.collections.length > 0 && (
<div> <div>
<span className="font-semibold">Collections:</span> {card.collections.join(', ')} <span className="font-semibold">{VOCAB.LISTS}:</span> {card.collections.join(', ')}
</div> </div>
)} )}
</div> </div>

View file

@ -10,6 +10,7 @@ import LoginCTA from '../../components/LoginCTA';
import { ManaCost, ColorFilterSymbol } from '../../components/ManaSymbols'; import { ManaCost, ColorFilterSymbol } from '../../components/ManaSymbols';
import ManaSymbolSettings from '../../components/ManaSymbolSettings'; import ManaSymbolSettings from '../../components/ManaSymbolSettings';
import { useAuth } from '../../lib/use-auth'; import { useAuth } from '../../lib/use-auth';
import { VOCAB, collectionDisplayName } from '../../lib/collection-vocabulary.js';
export default function CollectionView() { export default function CollectionView() {
const router = useRouter(); const router = useRouter();
@ -518,11 +519,11 @@ export default function CollectionView() {
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-center"> <div className="text-center">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Collection not found List not found
</h2> </h2>
<Link href="/collections"> <Link href="/collections">
<button className="px-4 py-2 rounded-lg text-white" style={{ backgroundColor: 'var(--accent-ember)' }}> <button className="px-4 py-2 rounded-lg text-white" style={{ backgroundColor: 'var(--accent-ember)' }}>
Back to Collections Back to Lists
</button> </button>
</Link> </Link>
</div> </div>
@ -594,7 +595,7 @@ export default function CollectionView() {
<div className="mb-6"> <div className="mb-6">
<div className="flex items-center space-x-3 mb-2"> <div className="flex items-center space-x-3 mb-2">
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}> <h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
{collection.name} {collectionDisplayName(collection)}
</h1> </h1>
{/* System collection indicator */} {/* System collection indicator */}
{collection.isSystemCollection && ( {collection.isSystemCollection && (
@ -607,7 +608,7 @@ export default function CollectionView() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap"> <div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap">
Automatically syncs with your owned cards {VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div> <div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div> </div>
</div> </div>
@ -851,7 +852,7 @@ export default function CollectionView() {
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-6xl mb-4">🃏</div> <div className="text-6xl mb-4">🃏</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> <h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Start Building Your Collection Start Building Your List
</h3> </h3>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Add cards to get started with your collection Add cards to get started with your collection
@ -894,12 +895,12 @@ export default function CollectionView() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4"> <div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Edit Collection Edit List
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name List Name
</label> </label>
<input <input
type="text" type="text"
@ -936,7 +937,7 @@ export default function CollectionView() {
<div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}> <div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}>
<div> <div>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> <label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Public Collection Public List
</label> </label>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}> <p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Make this collection discoverable by other users Make this collection discoverable by other users
@ -989,10 +990,10 @@ export default function CollectionView() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4"> <div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Delete Collection Delete List
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Are you sure you want to delete "{collection.name}"? This action cannot be undone and will permanently remove all cards and data associated with this collection. Are you sure you want to delete "{collectionDisplayName(collection)}"? This action cannot be undone and will permanently remove all cards and data associated with this list.
</p> </p>
<div className="flex space-x-3"> <div className="flex space-x-3">
<button <button
@ -1009,7 +1010,7 @@ export default function CollectionView() {
onClick={handleDeleteCollection} onClick={handleDeleteCollection}
className="flex-1 py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md bg-red-600 text-white hover:bg-red-700" className="flex-1 py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md bg-red-600 text-white hover:bg-red-700"
> >
Delete Collection Delete List
</button> </button>
</div> </div>
</div> </div>

View file

@ -4,6 +4,7 @@ import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator'; import PermissionIndicator from '../components/PermissionIndicator';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import Link from 'next/link'; import Link from 'next/link';
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
export default function Collections() { export default function Collections() {
const router = useRouter(); const router = useRouter();
@ -154,7 +155,7 @@ export default function Collections() {
fetchCollections(); fetchCollections();
} else { } else {
const error = await response.json(); const error = await response.json();
alert(error.error || 'Failed to create collection'); alert(error.error || 'Failed to create list');
} }
} catch (error) { } catch (error) {
console.error('Error creating collection:', error); console.error('Error creating collection:', error);
@ -185,7 +186,7 @@ export default function Collections() {
setEditTagInput(''); // Clear the tag input setEditTagInput(''); // Clear the tag input
} else { } else {
const error = await response.json(); const error = await response.json();
alert(error.error || 'Failed to update collection'); alert(error.error || 'Failed to update list');
} }
} catch (error) { } catch (error) {
console.error('Error updating collection:', error); console.error('Error updating collection:', error);
@ -194,7 +195,7 @@ export default function Collections() {
}; };
const handleDeleteCollection = async (collectionId) => { const handleDeleteCollection = async (collectionId) => {
if (confirm('Are you sure you want to delete this collection? This action cannot be undone.')) { if (confirm('Are you sure you want to delete this list? This action cannot be undone.')) {
try { try {
const response = await fetch(`/api/collections/${collectionId}`, { const response = await fetch(`/api/collections/${collectionId}`, {
method: 'DELETE' method: 'DELETE'
@ -204,7 +205,7 @@ export default function Collections() {
fetchCollections(); // Refresh the list fetchCollections(); // Refresh the list
} else { } else {
const error = await response.json(); const error = await response.json();
alert(error.error || 'Failed to delete collection'); alert(error.error || 'Failed to delete list');
} }
} catch (error) { } catch (error) {
console.error('Error deleting collection:', error); console.error('Error deleting collection:', error);
@ -388,10 +389,10 @@ export default function Collections() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}> <h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
My Collections {VOCAB.LISTS}
</h1> </h1>
<p className="mt-2" style={{ color: 'var(--text-secondary)' }}> <p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
Collections you own, collaborate on, or have been shared with you Lists you own, collaborate on, or have been shared with you
</p> </p>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
@ -414,7 +415,7 @@ export default function Collections() {
color: 'white' color: 'white'
}} }}
> >
+ Create Collection + Create List
</button> </button>
</div> </div>
</div> </div>
@ -426,7 +427,7 @@ export default function Collections() {
<div className="flex-1"> <div className="flex-1">
<input <input
type="text" type="text"
placeholder="Search collections..." placeholder="Search lists..."
className="input-field w-full" className="input-field w-full"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
@ -454,12 +455,12 @@ export default function Collections() {
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-6xl mb-4">📦</div> <div className="text-6xl mb-4">📦</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> <h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
{searchQuery ? 'No collections found' : 'No collections yet'} {searchQuery ? 'No lists found' : 'No lists yet'}
</h3> </h3>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
{searchQuery {searchQuery
? 'Try adjusting your search terms' ? 'Try adjusting your search terms'
: 'Create your first collection to get started' : 'Create your first list to get started'
} }
</p> </p>
{!searchQuery && ( {!searchQuery && (
@ -471,7 +472,7 @@ export default function Collections() {
color: 'white' color: 'white'
}} }}
> >
Create Collection Create List
</button> </button>
)} )}
</div> </div>
@ -493,7 +494,7 @@ export default function Collections() {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold leading-tight" style={{ color: 'var(--text-primary)' }}> <h3 className="text-lg font-semibold leading-tight" style={{ color: 'var(--text-primary)' }}>
{collection.name} {collectionDisplayName(collection)}
</h3> </h3>
{/* System collection indicator */} {/* System collection indicator */}
{collection.isSystemCollection && ( {collection.isSystemCollection && (
@ -506,7 +507,7 @@ export default function Collections() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap"> <div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap">
Automatically syncs with your owned cards {VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div> <div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div> </div>
</div> </div>
@ -638,19 +639,19 @@ export default function Collections() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4"> <div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Create New Collection Create New List
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name List Name
</label> </label>
<input <input
type="text" type="text"
className="input-field w-full" className="input-field w-full"
value={newCollection.name} value={newCollection.name}
onChange={(e) => setNewCollection({...newCollection, name: e.target.value})} onChange={(e) => setNewCollection({...newCollection, name: e.target.value})}
placeholder="Enter collection name" placeholder="Enter list name"
/> />
</div> </div>
<div> <div>
@ -662,7 +663,7 @@ export default function Collections() {
rows="3" rows="3"
value={newCollection.description} value={newCollection.description}
onChange={(e) => setNewCollection({...newCollection, description: e.target.value})} onChange={(e) => setNewCollection({...newCollection, description: e.target.value})}
placeholder="Describe your collection" placeholder="Describe your list"
/> />
</div> </div>
<div> <div>
@ -734,7 +735,7 @@ export default function Collections() {
</div> </div>
)} )}
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}> <p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
Add tags to help organize and categorize your collection Add tags to help organize and categorize your list
</p> </p>
</div> </div>
</div> </div>
@ -742,10 +743,10 @@ export default function Collections() {
<div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}> <div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}>
<div> <div>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> <label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Public Collection Public List
</label> </label>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}> <p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Make this collection discoverable by other users Make this list discoverable by other users
</p> </p>
</div> </div>
<button <button
@ -786,7 +787,7 @@ export default function Collections() {
color: 'white' color: 'white'
}} }}
> >
Create Collection Create List
</button> </button>
</div> </div>
</div> </div>
@ -799,10 +800,10 @@ export default function Collections() {
<div className="card max-w-md w-full mx-4 text-center"> <div className="card max-w-md w-full mx-4 text-center">
<div className="text-6xl mb-4">🎉</div> <div className="text-6xl mb-4">🎉</div>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Collection Created! List Created!
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Your collection "{createdCollection.name}" has been created successfully. Your list "{createdCollection.name}" has been created successfully.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<button <button
@ -816,7 +817,7 @@ export default function Collections() {
color: 'white' color: 'white'
}} }}
> >
View Collection View List
</button> </button>
<button <button
onClick={() => setShowSuccessModal(false)} onClick={() => setShowSuccessModal(false)}
@ -826,7 +827,7 @@ export default function Collections() {
color: 'var(--text-secondary)' color: 'var(--text-secondary)'
}} }}
> >
Stay on Collections Page Stay on Lists Page
</button> </button>
</div> </div>
</div> </div>
@ -838,12 +839,12 @@ export default function Collections() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4"> <div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Edit Collection Edit List
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name List Name
</label> </label>
<input <input
type="text" type="text"
@ -934,10 +935,10 @@ export default function Collections() {
<div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}> <div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}>
<div> <div>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> <label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Public Collection Public List
</label> </label>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}> <p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Make this collection discoverable by other users Make this list discoverable by other users
</p> </p>
</div> </div>
<button <button
@ -978,7 +979,7 @@ export default function Collections() {
color: 'white' color: 'white'
}} }}
> >
Update Collection Update List
</button> </button>
</div> </div>
</div> </div>

View file

@ -198,7 +198,7 @@ export default function CommunityCollections() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}> <h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
Community Collections Community Lists
</h1> </h1>
<p className="mt-2" style={{ color: 'var(--text-secondary)' }}> <p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
Discover public collections shared by the community Discover public collections shared by the community
@ -255,7 +255,7 @@ export default function CommunityCollections() {
color: 'white' color: 'white'
}} }}
> >
Go to My Collections Go to My Lists
</button> </button>
</Link> </Link>
)} )}

View file

@ -4,6 +4,7 @@ import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator'; import PermissionIndicator from '../components/PermissionIndicator';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import Link from 'next/link'; import Link from 'next/link';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function Dashboard() { export default function Dashboard() {
const router = useRouter(); const router = useRouter();
@ -79,7 +80,7 @@ export default function Dashboard() {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}> <h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
My Collections {VOCAB.LISTS}
</h1> </h1>
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}> <p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
Manage your collection of trading cards and decks Manage your collection of trading cards and decks
@ -92,7 +93,7 @@ export default function Dashboard() {
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg> </svg>
<span>Create Collection</span> <span>Create List</span>
</div> </div>
</button> </button>
</Link> </Link>
@ -119,7 +120,7 @@ export default function Dashboard() {
</div> </div>
<div> <div>
<p className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{collections.length}</p> <p className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{collections.length}</p>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>Collections</p> <p className="text-sm" style={{ color: 'var(--text-secondary)' }}>Lists</p>
</div> </div>
</div> </div>
</div> </div>
@ -159,7 +160,7 @@ export default function Dashboard() {
{/* Collections Grid */} {/* Collections Grid */}
<div className="mb-6"> <div className="mb-6">
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>Recent Collections</h2> <h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>Recent Lists</h2>
{collections.length === 0 ? ( {collections.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--bg-secondary)' }}> <div className="w-16 h-16 mx-auto mb-4 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--bg-secondary)' }}>
@ -167,13 +168,13 @@ export default function Dashboard() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg> </svg>
</div> </div>
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>No Collections Yet</h3> <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>No Lists Yet</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}> <p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
Create your first collection to start organizing your cards Create your first collection to start organizing your cards
</p> </p>
<Link href="/collections"> <Link href="/collections">
<button className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}> <button className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}>
Create Your First Collection Create Your First List
</button> </button>
</Link> </Link>
</div> </div>
@ -219,7 +220,7 @@ export default function Dashboard() {
<div className="text-center"> <div className="text-center">
<Link href="/collections"> <Link href="/collections">
<button className="px-6 py-3 font-medium rounded-xl border-2 transition-all duration-200 hover:opacity-80" style={{ color: 'var(--text-primary)', borderColor: 'var(--accent-ember)' }}> <button className="px-6 py-3 font-medium rounded-xl border-2 transition-all duration-200 hover:opacity-80" style={{ color: 'var(--text-primary)', borderColor: 'var(--accent-ember)' }}>
View All Collections View All Lists
</button> </button>
</Link> </Link>
</div> </div>

View file

@ -118,7 +118,7 @@ export default function Home() {
borderColor: 'var(--accent-ember)' borderColor: 'var(--accent-ember)'
}} }}
> >
Explore Collections Explore Lists
</Link> </Link>
</div> </div>
</div> </div>
@ -137,7 +137,7 @@ export default function Home() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg> </svg>
</div> </div>
<h3 className="text-xl font-bold mb-4 gradient-text-flame">Organize Collections</h3> <h3 className="text-xl font-bold mb-4 gradient-text-flame">Organize Lists</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Create custom collections, track card values, and organize by sets, rarity, or any system that works for you. Create custom collections, track card values, and organize by sets, rarity, or any system that works for you.
</p> </p>
@ -173,7 +173,7 @@ export default function Home() {
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4 gradient-text-flame"> <h2 className="text-4xl font-bold mb-4 gradient-text-flame">
Featured Collections Featured Lists
</h2> </h2>
<p className="text-xl" style={{ color: 'var(--text-secondary)' }}> <p className="text-xl" style={{ color: 'var(--text-secondary)' }}>
Discover amazing collections from our community Discover amazing collections from our community
@ -247,7 +247,7 @@ export default function Home() {
border: '2px solid var(--accent-ember)' border: '2px solid var(--accent-ember)'
}} }}
> >
View All Collections View All Lists
<svg className="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg> </svg>

View file

@ -76,7 +76,7 @@ export default function AcceptInvite() {
onClick={() => router.push(`/collection/${result.collection.id}`)} onClick={() => router.push(`/collection/${result.collection.id}`)}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
> >
View Collection View List
</button> </button>
<button <button
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
@ -86,7 +86,7 @@ export default function AcceptInvite() {
color: 'var(--text-primary)' color: 'var(--text-primary)'
}} }}
> >
View All Collections View All Lists
</button> </button>
</div> </div>
</div> </div>
@ -104,7 +104,7 @@ export default function AcceptInvite() {
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
> >
Browse Collections Browse Lists
</button> </button>
<button <button
onClick={() => router.push('/')} onClick={() => router.push('/')}

View file

@ -77,7 +77,7 @@ export default function DeclineInvite() {
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
> >
Browse Public Collections Browse Public Lists
</button> </button>
<button <button
onClick={() => router.push('/')} onClick={() => router.push('/')}
@ -105,7 +105,7 @@ export default function DeclineInvite() {
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
> >
Browse Collections Browse Lists
</button> </button>
<button <button
onClick={() => router.push('/')} onClick={() => router.push('/')}

View file

@ -8,6 +8,7 @@ import CollectionSelectionModal from '../components/CollectionSelectionModal';
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols'; import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings'; import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function MyCards() { export default function MyCards() {
const router = useRouter(); const router = useRouter();
@ -261,10 +262,10 @@ export default function MyCards() {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}> <h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
My Cards {VOCAB.MY_COLLECTION}
</h1> </h1>
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}> <p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
Browse and manage your owned trading cards Browse and manage cards in your collection
</p> </p>
</div> </div>
<div className="flex space-x-2 sm:space-x-4"> <div className="flex space-x-2 sm:space-x-4">
@ -356,7 +357,7 @@ export default function MyCards() {
{/* Results Summary */} {/* Results Summary */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
{pagination.total} owned cards found {pagination.total} cards in {VOCAB.MY_COLLECTION}
</p> </p>
{selectedCards.length > 0 && ( {selectedCards.length > 0 && (
<p style={{ color: 'var(--text-primary)' }}> <p style={{ color: 'var(--text-primary)' }}>
@ -432,7 +433,7 @@ export default function MyCards() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg> </svg>
</div> </div>
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>No Owned Cards Found</h3> <h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>No cards in {VOCAB.MY_COLLECTION}</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}> <p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
{searchQuery ? 'Try adjusting your search filters' : 'Start building your collection by browsing available cards'} {searchQuery ? 'Try adjusting your search filters' : 'Start building your collection by browsing available cards'}
</p> </p>

View file

@ -393,7 +393,7 @@ export default function Profile() {
{/* Stats Card */} {/* Stats Card */}
<div className="card p-6 mt-6"> <div className="card p-6 mt-6">
<h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}> <h3 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Collection Stats List Stats
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -403,7 +403,7 @@ export default function Profile() {
</span> </span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span style={{ color: 'var(--text-secondary)' }}>Collections</span> <span style={{ color: 'var(--text-secondary)' }}>Lists</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}> <span className="font-semibold" style={{ color: 'var(--text-primary)' }}>
{stats.totalCollections} {stats.totalCollections}
</span> </span>

View file

@ -9,10 +9,11 @@ import { ManaCost, ColorIdentity } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings'; import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import { useFocusTrap } from '../lib/use-focus-trap.js'; import { useFocusTrap } from '../lib/use-focus-trap.js';
import { VOCAB } from '../lib/collection-vocabulary.js';
const SESSION_STORAGE_KEY = 'deckhearth:scanner-session'; const SESSION_STORAGE_KEY = 'deckhearth:scanner-session';
const DEFAULT_DESTINATION = { type: 'owned', id: null, label: 'My owned cards' }; const DEFAULT_DESTINATION = { type: 'owned', id: null, label: VOCAB.MY_COLLECTION };
const DEFAULT_SCAN_DEFAULTS = { condition: 'NM', isFoil: false }; const DEFAULT_SCAN_DEFAULTS = { condition: 'NM', isFoil: false };
function loadSavedScannerSession() { function loadSavedScannerSession() {
@ -120,7 +121,7 @@ export default function Scanner() {
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
// Filter out system collections (like "All My Cards") // Filter out auto-sync system lists (not user-curated)
const userCollections = data.filter(collection => !collection.is_system_collection); const userCollections = data.filter(collection => !collection.is_system_collection);
setCollections(userCollections); setCollections(userCollections);
} }
@ -367,7 +368,7 @@ export default function Scanner() {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to add to owned cards'); throw new Error(`Failed to add to ${VOCAB.MY_COLLECTION}`);
} }
}; };
@ -688,7 +689,7 @@ export default function Scanner() {
style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }} style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }}
> >
<span aria-hidden="true">💎 </span> <span aria-hidden="true">💎 </span>
Mark Owned {VOCAB.ADD_TO_MY_COLLECTION}
</button> </button>
{collections.length > 0 && ( {collections.length > 0 && (
@ -703,9 +704,9 @@ export default function Scanner() {
disabled={isProcessing} disabled={isProcessing}
className="px-4 py-2 rounded-lg font-medium" className="px-4 py-2 rounded-lg font-medium"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white', border: 'none' }} style={{ backgroundColor: 'var(--accent-ember)', color: 'white', border: 'none' }}
aria-label="Add selected cards to collection" aria-label="Add selected cards to list"
> >
<option value="">📚 Add to Collection</option> <option value="">{`📚 ${VOCAB.ADD_TO_LIST}`}</option>
{collections.map(collection => ( {collections.map(collection => (
<option key={collection.id} value={collection.id}> <option key={collection.id} value={collection.id}>
{collection.name} {collection.name}

View file

@ -22,15 +22,15 @@ describe('ScannerDestinationPicker', () => {
<ScannerDestinationPicker <ScannerDestinationPicker
gameFilter="All" gameFilter="All"
onGameFilterChange={vi.fn()} onGameFilterChange={vi.fn()}
destination={{ type: 'owned', id: null, label: 'My owned cards' }} destination={{ type: 'owned', id: null, label: 'My Collection' }}
onDestinationChange={vi.fn()} onDestinationChange={vi.fn()}
collections={COLLECTIONS} collections={COLLECTIONS}
decks={DECKS} decks={DECKS}
/> />
); );
expect(screen.getByRole('button', { name: /owned/i }).getAttribute('aria-pressed')).toBe('true'); expect(screen.getByRole('button', { name: /my collection/i }).getAttribute('aria-pressed')).toBe('true');
expect(screen.getByRole('button', { name: /collection/i }).getAttribute('aria-pressed')).toBe('false'); expect(screen.getByRole('button', { name: /list/i }).getAttribute('aria-pressed')).toBe('false');
}); });
it('calls onDestinationChange when switching to a collection', () => { it('calls onDestinationChange when switching to a collection', () => {
@ -40,14 +40,14 @@ describe('ScannerDestinationPicker', () => {
<ScannerDestinationPicker <ScannerDestinationPicker
gameFilter="All" gameFilter="All"
onGameFilterChange={vi.fn()} onGameFilterChange={vi.fn()}
destination={{ type: 'owned', id: null, label: 'My owned cards' }} destination={{ type: 'owned', id: null, label: 'My Collection' }}
onDestinationChange={onDestinationChange} onDestinationChange={onDestinationChange}
collections={COLLECTIONS} collections={COLLECTIONS}
decks={DECKS} decks={DECKS}
/> />
); );
fireEvent.click(screen.getByRole('button', { name: /collection/i })); fireEvent.click(screen.getByRole('button', { name: /^📚 list$/i }));
expect(onDestinationChange).toHaveBeenCalledWith({ expect(onDestinationChange).toHaveBeenCalledWith({
type: 'collection', type: 'collection',
@ -79,7 +79,7 @@ describe('ScannerDestinationPicker', () => {
<ScannerDestinationPicker <ScannerDestinationPicker
gameFilter="All" gameFilter="All"
onGameFilterChange={onGameFilterChange} onGameFilterChange={onGameFilterChange}
destination={{ type: 'owned', id: null, label: 'My owned cards' }} destination={{ type: 'owned', id: null, label: 'My Collection' }}
onDestinationChange={vi.fn()} onDestinationChange={vi.fn()}
collections={COLLECTIONS} collections={COLLECTIONS}
decks={DECKS} decks={DECKS}