Align UI copy with My Collection vs Lists vocabulary.

Replace stale ownership/list labels across pages and components, add
lib/collection-vocabulary.js as the single copy source, document the
taxonomy in AGENTS.md, and gate retired strings in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-05-28 14:55:01 -05:00
parent c51ec6a04c
commit a6d72e19b7
24 changed files with 228 additions and 120 deletions

View file

@ -59,3 +59,11 @@ Don't duplicate navigation in a page — extend `NavigationContent` inside Layou
## 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).
## 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
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:
name: Unit tests (vitest)
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
> 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
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 { VOCAB } from '../lib/collection-vocabulary.js';
export default function BulkSelectionToolbar({
selectedCards,
@ -96,7 +97,7 @@ export default function BulkSelectionToolbar({
<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" />
</svg>
<span className="hidden sm:inline">Own</span>
<span className="hidden sm:inline">{VOCAB.MY_COLLECTION}</span>
</button>
</div>
@ -137,7 +138,7 @@ export default function BulkSelectionToolbar({
<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" />
</svg>
<span>Remove from Owned</span>
<span>{VOCAB.REMOVE_FROM_MY_COLLECTION}</span>
</button>
<div className="border-t border-gray-200 my-1"></div>

View file

@ -1,5 +1,6 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { VOCAB } from '../lib/collection-vocabulary.js';
// Expanding Add Button Component
function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwned }) {
@ -31,7 +32,7 @@ function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwne
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"
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">
<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)}
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' }}
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
@ -74,7 +75,7 @@ function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwne
color: 'white',
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
className={`w-4 h-4 transition-transform duration-150 ${isExpanded ? 'rotate-45' : ''}`}
@ -259,7 +260,7 @@ export default function CardItem({
onAddToCollection(card);
}}
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">
<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}
onAddToCollection={onAddToCollection}
onAddToDeck={onAddToDeck}
onMarkAsOwned={() => alert('Mark as owned (coming soon)')}
onMarkAsOwned={() => alert(`${VOCAB.ADD_TO_MY_COLLECTION} (coming soon)`)}
/>
</div>
</div>

View file

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function CollectionSelectionModal({
isOpen,
@ -132,7 +133,7 @@ export default function CollectionSelectionModal({
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Add to Collections
{VOCAB.ADD_TO_LIST}s
</h2>
<button
onClick={onClose}
@ -175,7 +176,7 @@ export default function CollectionSelectionModal({
{cards.length} card{cards.length !== 1 ? 's' : ''} selected
</p>
<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>
</div>
</div>
@ -184,7 +185,7 @@ export default function CollectionSelectionModal({
<div className="relative">
<input
type="text"
placeholder="Search collections..."
placeholder="Search lists..."
value={searchQuery}
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"
@ -200,7 +201,7 @@ export default function CollectionSelectionModal({
{loading ? (
<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>
<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>
) : filteredCollections.length === 0 ? (
<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" />
</svg>
<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 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>
</div>
) : (
@ -305,7 +306,7 @@ export default function CollectionSelectionModal({
<div className="text-sm text-gray-600 dark:text-gray-400">
{selectedCollections.length > 0 && (
<span>
{selectedCollections.length} collection{selectedCollections.length !== 1 ? 's' : ''} selected
{selectedCollections.length} list{selectedCollections.length !== 1 ? 's' : ''} selected
</span>
)}
</div>
@ -332,7 +333,7 @@ export default function CollectionSelectionModal({
<span>Adding...</span>
</div>
) : (
`Add to ${selectedCollections.length} Collection${selectedCollections.length !== 1 ? 's' : ''}`
`Add to ${selectedCollections.length} List${selectedCollections.length !== 1 ? 's' : ''}`
)}
</button>
</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',
expanded: true,
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: 'Decks', href: '/decks', active: router.pathname === '/decks' },
{ name: 'Analytics', href: '/analytics', active: router.pathname === '/analytics', isPlaceholder: true }
@ -201,7 +201,7 @@ function NavigationContent({ user, router, onItemClick }) {
active: router.pathname.startsWith('/community'),
expanded: isCommunityExpanded,
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: 'Forums', href: '/community/forums', active: router.pathname === '/community/forums' }
]

View file

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

View file

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

View file

@ -23,8 +23,8 @@
| --- | --- | --- |
| **Identity** | `users`, `user_settings`, `user_avatars` | Accounts, profile, preferences |
| **Catalog** | `cards` | Master card list across MTG / Pokémon / Lorcana |
| **Ownership** | `user_cards`, `user_favorites` | What a user owns / has favorited |
| **Collections** | `collections`, `collection_cards`, `collection_permissions`, `collection_activity` | Curated card lists with sharing |
| **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 (UI: **Lists**) |
| **Decks** | `decks`, `deck_cards` | Playable deck definitions |
| **Scanning** | `card_submissions`, `scan_attempts` | Unknown-card review queue + scan telemetry |
| **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** |
| `tcg` | `VARCHAR(50)` default `'MTG'` | |
| `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` | |
### 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 { ManaCost, ColorIdentity, AdvancedManaCost } from '../../components/ManaSymbols';
import ManaSymbolSettings from '../../components/ManaSymbolSettings';
import { VOCAB } from '../../lib/collection-vocabulary.js';
export default function CardDetail() {
const router = useRouter();
@ -468,7 +469,7 @@ export default function CardDetail() {
<div className="flex items-center gap-2">
{ownedQuantity > 0 && (
<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>
)}
<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"
>
<span>📦</span>
{ownedQuantity > 0 ? 'Update Quantity' : 'Mark as Owned'}
{ownedQuantity > 0 ? 'Update Quantity' : VOCAB.ADD_TO_MY_COLLECTION}
</button>
<button
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"
>
<span>📁</span>
Add to Collection
{VOCAB.ADD_TO_LIST}
</button>
<button
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="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)' }}>
{ownedQuantity > 0 ? 'Update Quantity' : 'Mark as Owned'}
{ownedQuantity > 0 ? 'Update Quantity' : VOCAB.ADD_TO_MY_COLLECTION}
</h3>
<div className="mb-6">
<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)}
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
onClick={() => setShowQuantityModal(false)}

View file

@ -9,6 +9,7 @@ import ProtectedRoute from '../components/ProtectedRoute';
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth';
import { VOCAB } from '../lib/collection-vocabulary.js';
// Public fallback component for non-authenticated users
function PublicCardsView() {
@ -461,7 +462,7 @@ function AuthenticatedCards() {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
alert('Please log in to mark cards as owned');
alert(`Please log in to ${VOCAB.ADD_TO_MY_COLLECTION.toLowerCase()}`);
return;
}
@ -486,16 +487,16 @@ function AuthenticatedCards() {
}
if (successCount === cards.length) {
alert(`Successfully marked ${cards.length} cards as owned!`);
alert(`Successfully added ${cards.length} cards to ${VOCAB.MY_COLLECTION}!`);
} 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
setSelectedCards([]);
} catch (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 {
const token = localStorage.getItem('auth_token');
if (!token) {
alert('Please log in to remove cards from owned');
alert(`Please log in to ${VOCAB.REMOVE_FROM_MY_COLLECTION.toLowerCase()}`);
return;
}
@ -528,16 +529,16 @@ function AuthenticatedCards() {
}
if (successCount === cards.length) {
alert(`Successfully removed ${cards.length} cards from owned!`);
alert(`Successfully removed ${cards.length} cards from ${VOCAB.MY_COLLECTION}!`);
} 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
setSelectedCards([]);
} catch (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;
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 {
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 && (
<div>
<span className="font-semibold">Collections:</span> {card.collections.join(', ')}
<span className="font-semibold">{VOCAB.LISTS}:</span> {card.collections.join(', ')}
</div>
)}
</div>

View file

@ -10,6 +10,7 @@ import LoginCTA from '../../components/LoginCTA';
import { ManaCost, ColorFilterSymbol } from '../../components/ManaSymbols';
import ManaSymbolSettings from '../../components/ManaSymbolSettings';
import { useAuth } from '../../lib/use-auth';
import { VOCAB, collectionDisplayName } from '../../lib/collection-vocabulary.js';
export default function CollectionView() {
const router = useRouter();
@ -518,11 +519,11 @@ export default function CollectionView() {
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Collection not found
List not found
</h2>
<Link href="/collections">
<button className="px-4 py-2 rounded-lg text-white" style={{ backgroundColor: 'var(--accent-ember)' }}>
Back to Collections
Back to Lists
</button>
</Link>
</div>
@ -594,7 +595,7 @@ export default function CollectionView() {
<div className="mb-6">
<div className="flex items-center space-x-3 mb-2">
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
{collection.name}
{collectionDisplayName(collection)}
</h1>
{/* System collection indicator */}
{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" />
</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">
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>
</div>
@ -851,7 +852,7 @@ export default function CollectionView() {
<div className="text-center py-12">
<div className="text-6xl mb-4">🃏</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Start Building Your Collection
Start Building Your List
</h3>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
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="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Edit Collection
Edit List
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name
List Name
</label>
<input
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>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Public Collection
Public List
</label>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
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="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Delete Collection
Delete List
</h2>
<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>
<div className="flex space-x-3">
<button
@ -1009,7 +1010,7 @@ export default function CollectionView() {
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"
>
Delete Collection
Delete List
</button>
</div>
</div>

View file

@ -4,6 +4,7 @@ import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator';
import { useAuth } from '../lib/use-auth';
import Link from 'next/link';
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
export default function Collections() {
const router = useRouter();
@ -154,7 +155,7 @@ export default function Collections() {
fetchCollections();
} else {
const error = await response.json();
alert(error.error || 'Failed to create collection');
alert(error.error || 'Failed to create list');
}
} catch (error) {
console.error('Error creating collection:', error);
@ -185,7 +186,7 @@ export default function Collections() {
setEditTagInput(''); // Clear the tag input
} else {
const error = await response.json();
alert(error.error || 'Failed to update collection');
alert(error.error || 'Failed to update list');
}
} catch (error) {
console.error('Error updating collection:', error);
@ -194,7 +195,7 @@ export default function Collections() {
};
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 {
const response = await fetch(`/api/collections/${collectionId}`, {
method: 'DELETE'
@ -204,7 +205,7 @@ export default function Collections() {
fetchCollections(); // Refresh the list
} else {
const error = await response.json();
alert(error.error || 'Failed to delete collection');
alert(error.error || 'Failed to delete list');
}
} catch (error) {
console.error('Error deleting collection:', error);
@ -388,10 +389,10 @@ export default function Collections() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
My Collections
{VOCAB.LISTS}
</h1>
<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>
</div>
<div className="flex gap-3">
@ -414,7 +415,7 @@ export default function Collections() {
color: 'white'
}}
>
+ Create Collection
+ Create List
</button>
</div>
</div>
@ -426,7 +427,7 @@ export default function Collections() {
<div className="flex-1">
<input
type="text"
placeholder="Search collections..."
placeholder="Search lists..."
className="input-field w-full"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
@ -454,12 +455,12 @@ export default function Collections() {
<div className="text-center py-12">
<div className="text-6xl mb-4">📦</div>
<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>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
{searchQuery
? 'Try adjusting your search terms'
: 'Create your first collection to get started'
: 'Create your first list to get started'
}
</p>
{!searchQuery && (
@ -471,7 +472,7 @@ export default function Collections() {
color: 'white'
}}
>
Create Collection
Create List
</button>
)}
</div>
@ -493,7 +494,7 @@ export default function Collections() {
<div className="flex items-start justify-between">
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold leading-tight" style={{ color: 'var(--text-primary)' }}>
{collection.name}
{collectionDisplayName(collection)}
</h3>
{/* System collection indicator */}
{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" />
</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">
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>
</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="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Create New Collection
Create New List
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name
List Name
</label>
<input
type="text"
className="input-field w-full"
value={newCollection.name}
onChange={(e) => setNewCollection({...newCollection, name: e.target.value})}
placeholder="Enter collection name"
placeholder="Enter list name"
/>
</div>
<div>
@ -662,7 +663,7 @@ export default function Collections() {
rows="3"
value={newCollection.description}
onChange={(e) => setNewCollection({...newCollection, description: e.target.value})}
placeholder="Describe your collection"
placeholder="Describe your list"
/>
</div>
<div>
@ -734,7 +735,7 @@ export default function Collections() {
</div>
)}
<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>
</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>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Public Collection
Public List
</label>
<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>
</div>
<button
@ -786,7 +787,7 @@ export default function Collections() {
color: 'white'
}}
>
Create Collection
Create List
</button>
</div>
</div>
@ -799,10 +800,10 @@ export default function Collections() {
<div className="card max-w-md w-full mx-4 text-center">
<div className="text-6xl mb-4">🎉</div>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Collection Created!
List Created!
</h2>
<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>
<div className="space-y-3">
<button
@ -816,7 +817,7 @@ export default function Collections() {
color: 'white'
}}
>
View Collection
View List
</button>
<button
onClick={() => setShowSuccessModal(false)}
@ -826,7 +827,7 @@ export default function Collections() {
color: 'var(--text-secondary)'
}}
>
Stay on Collections Page
Stay on Lists Page
</button>
</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="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Edit Collection
Edit List
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name
List Name
</label>
<input
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>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Public Collection
Public List
</label>
<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>
</div>
<button
@ -978,7 +979,7 @@ export default function Collections() {
color: 'white'
}}
>
Update Collection
Update List
</button>
</div>
</div>

View file

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

View file

@ -4,6 +4,7 @@ import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator';
import { useAuth } from '../lib/use-auth';
import Link from 'next/link';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function Dashboard() {
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>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
My Collections
{VOCAB.LISTS}
</h1>
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<span>Create Collection</span>
<span>Create List</span>
</div>
</button>
</Link>
@ -119,7 +120,7 @@ export default function Dashboard() {
</div>
<div>
<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>
@ -159,7 +160,7 @@ export default function Dashboard() {
{/* Collections Grid */}
<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 ? (
<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)' }}>
@ -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" />
</svg>
</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)' }}>
Create your first collection to start organizing your cards
</p>
<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' }}>
Create Your First Collection
Create Your First List
</button>
</Link>
</div>
@ -219,7 +220,7 @@ export default function Dashboard() {
<div className="text-center">
<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)' }}>
View All Collections
View All Lists
</button>
</Link>
</div>

View file

@ -118,7 +118,7 @@ export default function Home() {
borderColor: 'var(--accent-ember)'
}}
>
Explore Collections
Explore Lists
</Link>
</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" />
</svg>
</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)' }}>
Create custom collections, track card values, and organize by sets, rarity, or any system that works for you.
</p>
@ -173,7 +173,7 @@ export default function Home() {
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4 gradient-text-flame">
Featured Collections
Featured Lists
</h2>
<p className="text-xl" style={{ color: 'var(--text-secondary)' }}>
Discover amazing collections from our community
@ -247,7 +247,7 @@ export default function Home() {
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>

View file

@ -76,7 +76,7 @@ export default function AcceptInvite() {
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"
>
View Collection
View List
</button>
<button
onClick={() => router.push('/collections')}
@ -86,7 +86,7 @@ export default function AcceptInvite() {
color: 'var(--text-primary)'
}}
>
View All Collections
View All Lists
</button>
</div>
</div>
@ -104,7 +104,7 @@ export default function AcceptInvite() {
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"
>
Browse Collections
Browse Lists
</button>
<button
onClick={() => router.push('/')}

View file

@ -77,7 +77,7 @@ export default function DeclineInvite() {
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"
>
Browse Public Collections
Browse Public Lists
</button>
<button
onClick={() => router.push('/')}
@ -105,7 +105,7 @@ export default function DeclineInvite() {
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"
>
Browse Collections
Browse Lists
</button>
<button
onClick={() => router.push('/')}

View file

@ -8,6 +8,7 @@ import CollectionSelectionModal from '../components/CollectionSelectionModal';
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function MyCards() {
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>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
My Cards
{VOCAB.MY_COLLECTION}
</h1>
<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>
</div>
<div className="flex space-x-2 sm:space-x-4">
@ -356,7 +357,7 @@ export default function MyCards() {
{/* Results Summary */}
<div className="flex items-center justify-between mb-4">
<p style={{ color: 'var(--text-secondary)' }}>
{pagination.total} owned cards found
{pagination.total} cards in {VOCAB.MY_COLLECTION}
</p>
{selectedCards.length > 0 && (
<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" />
</svg>
</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)' }}>
{searchQuery ? 'Try adjusting your search filters' : 'Start building your collection by browsing available cards'}
</p>

View file

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

View file

@ -9,10 +9,11 @@ import { ManaCost, ColorIdentity } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth';
import { useFocusTrap } from '../lib/use-focus-trap.js';
import { VOCAB } from '../lib/collection-vocabulary.js';
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 };
function loadSavedScannerSession() {
@ -120,7 +121,7 @@ export default function Scanner() {
});
if (response.ok) {
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);
setCollections(userCollections);
}
@ -367,7 +368,7 @@ export default function Scanner() {
});
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' }}
>
<span aria-hidden="true">💎 </span>
Mark Owned
{VOCAB.ADD_TO_MY_COLLECTION}
</button>
{collections.length > 0 && (
@ -703,9 +704,9 @@ export default function Scanner() {
disabled={isProcessing}
className="px-4 py-2 rounded-lg font-medium"
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 => (
<option key={collection.id} value={collection.id}>
{collection.name}

View file

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