Align UI copy: My Collection vs Lists #54
27 changed files with 258 additions and 148 deletions
|
|
@ -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/`.
|
||||
|
|
|
|||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
14
AGENTS.md
14
AGENTS.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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_LISTS}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
]
|
||||
|
|
@ -791,7 +791,7 @@ export default function Layout({ children, user = null, showSearch = false }) {
|
|||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search cards, decks, or collections..."
|
||||
placeholder="Search cards, decks, or lists..."
|
||||
className="search-bar pr-12"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
export default function LoginCTA({ message = "Sign up to create your own collections and more!" }) {
|
||||
export default function LoginCTA({ message = 'Sign up to create your own lists and more!' }) {
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -200,7 +202,7 @@ export default function ScannerDestinationPicker({
|
|||
|
||||
{destination?.type === 'collection' && filteredCollections.length === 0 && (
|
||||
<p className="mt-3 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
No collections match this game filter.
|
||||
No lists match this game filter.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export default function ShareModal({
|
|||
|
||||
const handleSocialShare = (platform) => {
|
||||
const url = window.location.href;
|
||||
const title = `Check out this collection on Deck Hearth`;
|
||||
const title = 'Check out this list on Deck Hearth';
|
||||
|
||||
const shareUrls = {
|
||||
twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
|
||||
|
|
@ -176,7 +176,7 @@ export default function ShareModal({
|
|||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
This collection will be available in the community.
|
||||
This list will be available in the community.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -244,7 +244,7 @@ export default function ShareModal({
|
|||
{/* Current Permissions */}
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-gray-700 mb-3">
|
||||
Only those invited can view or collaborate on this collection.
|
||||
Only those invited can view or collaborate on this list.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
40
lib/collection-vocabulary.js
Normal file
40
lib/collection-vocabulary.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 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',
|
||||
ADD_TO_LISTS: 'Add to Lists',
|
||||
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;
|
||||
}
|
||||
|
|
@ -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, collectionDisplayName } 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)}
|
||||
|
|
@ -541,7 +542,7 @@ export default function CardDetail() {
|
|||
{cardCollections.map(collection => (
|
||||
<div key={collection.id} className="flex items-center gap-2 text-sm">
|
||||
<span>📁</span>
|
||||
<span>{collection.name}</span>
|
||||
<span>{collectionDisplayName(collection)}</span>
|
||||
</div>
|
||||
))}
|
||||
{cardDecks.map(deck => (
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -177,7 +178,7 @@ export default function CollectionView() {
|
|||
setShowEditModal(false);
|
||||
} 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);
|
||||
|
|
@ -198,7 +199,7 @@ export default function CollectionView() {
|
|||
router.push('/collections');
|
||||
} 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);
|
||||
|
|
@ -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,19 +895,19 @@ 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"
|
||||
className="input-field w-full"
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm({...editForm, name: e.target.value})}
|
||||
placeholder="Enter collection name"
|
||||
placeholder="Enter list name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -918,7 +919,7 @@ export default function CollectionView() {
|
|||
rows="3"
|
||||
value={editForm.description}
|
||||
onChange={(e) => setEditForm({...editForm, description: e.target.value})}
|
||||
placeholder="Describe your collection"
|
||||
placeholder="Describe your list"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1037,7 +1038,7 @@ export default function CollectionView() {
|
|||
|
||||
{/* Show login CTA for non-authenticated users */}
|
||||
{!user && collection?.isPublic && (
|
||||
<LoginCTA message="Sign up to create your own collections and collaborate with others!" />
|
||||
<LoginCTA message="Sign up to create your own lists and collaborate with others!" />
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Link from 'next/link';
|
|||
import Layout from '../../components/Layout';
|
||||
import PermissionIndicator from '../../components/PermissionIndicator';
|
||||
import { useAuth } from '../../lib/use-auth';
|
||||
import { VOCAB } from '../../lib/collection-vocabulary.js';
|
||||
|
||||
export default function CommunityCollections() {
|
||||
const router = useRouter();
|
||||
|
|
@ -198,10 +199,10 @@ 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
|
||||
Discover public lists shared by the community
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -211,7 +212,7 @@ export default function CommunityCollections() {
|
|||
<div className="flex-1 max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search collections..."
|
||||
placeholder="Search lists..."
|
||||
className="input-field w-full"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
|
|
@ -239,12 +240,12 @@ export default function CommunityCollections() {
|
|||
<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 public collections yet'}
|
||||
{searchQuery ? 'No lists found' : 'No public lists yet'}
|
||||
</h3>
|
||||
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
{searchQuery
|
||||
? 'Try adjusting your search terms'
|
||||
: 'Be the first to share a public collection with the community!'
|
||||
: 'Be the first to share a public list with the community!'
|
||||
}
|
||||
</p>
|
||||
{!searchQuery && (
|
||||
|
|
@ -255,7 +256,7 @@ export default function CommunityCollections() {
|
|||
color: 'white'
|
||||
}}
|
||||
>
|
||||
Go to My Collections
|
||||
Go to My Lists
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -223,7 +223,7 @@ export default function Home() {
|
|||
</div>
|
||||
<h3 className="text-xl font-bold mb-2 gradient-text-flame">{collection.name}</h3>
|
||||
<p className="text-sm mb-4 line-clamp-2" style={{ color: 'var(--text-secondary)' }}>
|
||||
{collection.description || 'A carefully curated collection'}
|
||||
{collection.description || 'A carefully curated list'}
|
||||
</p>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -69,14 +69,14 @@ export default function AcceptInvite() {
|
|||
Invitation Accepted!
|
||||
</h2>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
You now have access to the collection "{result.collection.name}".
|
||||
You now have access to the list "{result.collection.name}".
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
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('/')}
|
||||
|
|
|
|||
|
|
@ -69,15 +69,15 @@ export default function DeclineInvite() {
|
|||
Invitation Declined
|
||||
</h2>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
You have declined the invitation to "{result.collection.name}".
|
||||
You can always ask the collection owner for another invitation if you change your mind.
|
||||
You have declined the invitation to "{result.collection.name}".
|
||||
You can always ask the list owner for another invitation if you change your mind.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
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('/')}
|
||||
|
|
|
|||
|
|
@ -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,9 +433,9 @@ 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'}
|
||||
{searchQuery ? 'Try adjusting your search filters' : `Start adding cards to ${VOCAB.MY_COLLECTION} by browsing available cards`}
|
||||
</p>
|
||||
<Link href="/cards">
|
||||
<button className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -769,15 +770,15 @@ export default function Scanner() {
|
|||
aria-labelledby="create-collection-title"
|
||||
>
|
||||
<h3 id="create-collection-title" className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||||
Create New Collection
|
||||
Create New List
|
||||
</h3>
|
||||
<label htmlFor="create-collection-name" className="sr-only">
|
||||
Collection name
|
||||
List name
|
||||
</label>
|
||||
<input
|
||||
id="create-collection-name"
|
||||
type="text"
|
||||
placeholder="Collection name..."
|
||||
placeholder="List name..."
|
||||
value={newCollectionName}
|
||||
onChange={(e) => setNewCollectionName(e.target.value)}
|
||||
className="w-full px-4 py-2 rounded-lg border mb-4"
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ export default function Settings() {
|
|||
|
||||
const handleDeleteAccount = async () => {
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to delete your account? This action cannot be undone and will permanently delete all your cards, collections, and decks.'
|
||||
'Are you sure you want to delete your account? This action cannot be undone and will permanently delete all your cards, lists, and decks.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
|
@ -316,7 +316,7 @@ export default function Settings() {
|
|||
{/* Collection Visibility */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||
Default Collection Visibility
|
||||
Default List Visibility
|
||||
</label>
|
||||
<select
|
||||
value={settings.collection_visibility}
|
||||
|
|
@ -328,7 +328,7 @@ export default function Settings() {
|
|||
<option value="unlisted">Unlisted</option>
|
||||
</select>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
||||
Controls who can see your new collections by default
|
||||
Controls who can see your new lists by default
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -546,7 +546,7 @@ export default function Settings() {
|
|||
Email Notifications
|
||||
</p>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Receive notifications about your collections and cards
|
||||
Receive notifications about your lists and cards
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue