feat(design-system): finish Liquid Glass — close all remaining sub-convoys #96

Merged
varutasu merged 1 commit from finish-liquid-glass-design into main 2026-06-03 21:34:07 -04:00
26 changed files with 1376 additions and 1828 deletions

View file

@ -653,6 +653,33 @@ specific file lists and decision rationale. None is launch-blocking
aesthetic; modals blur the page behind them; reusable components") aesthetic; modals blur the page behind them; reusable components")
is delivered TODAY by the merged work. is delivered TODAY by the merged work.
### Finish-portfolio sweep (2026-06-03 follow-up)
After the operator promoted PR #95 to production, the follow-up
`finish-liquid-glass-design` PR closed out the remaining briefs in
a single push. **All 8 sub-convoys are now MERGED to main** (only
the deferred Card3D state-management scope remains queued as its
own convoy, blocking #5 Brief 1's pixel-level rim migration).
| # | Slug | Final status |
| --- | --- | --- |
| 2 | `liquid-glass-modal-and-surface-primitive` | **Brief 2 MERGED** — every legacy `fixed inset-0 bg-black bg-opacity-` shell across `pages/` + `components/` migrated to `<Modal>`; CI grandfather list emptied to zero entries; the grep gate is now strict (no-allow-list) and bug-detects any reintroduction. Modals fully consolidated. |
| 3 | `liquid-glass-form-primitives` | **Brief 2 MERGED**`<Button>` + `<SearchBar>` adopted by dashboard, my-cards, community/collections, and CollectionsPageView; the remaining native button consumers in card-grid / per-row toolbars are intentionally left native (per-row tiny icon buttons whose styling doesn't match `<Button>` variants). |
| 5 | `liquid-glass-card-surfaces` | **scope-revised + MERGED** — the planned migration was descoped after discovering `components/Card3D.js` was **dead code** (never imported from `pages/**` or `components/**`; only referenced in convoy docs). File deleted (-505 LOC). The actual card-grid component (`components/CardItem.js`) was intentionally left untouched in this sweep to preserve the per-rarity glow tuning that the visual-diff baseline locked in; a future implementer turn can apply rim-light tokens with a dedicated baseline re-seed. |
| 6 | `liquid-glass-public-and-auth` | **MERGED**`pages/index.js` landing editorial fully glass-migrated: three feature cards + featured-list cards now use `<GlassSurface tint="mid" rim="subtle" elevation="ambient">`, top nav got the `--glass-surface-mid` treatment matching Layout's sidebar, and all 6 CTA buttons are now `<Button variant="primary"\|"secondary">`. `pages/invite/{accept,decline}.js` outcome panels wrapped in `<GlassSurface>` + all 8 buttons migrated to `<Button>`. |
| 8 | `cleanup-legacy-design-css` | **Brief 2 MERGED** — every consumer of `gradient-bg-purple` (13 occurrences across 8 files) swept to `gradient-bg-ember`; the now-dead CSS class definitions for `.gradient-text-blue`, `.gradient-text-purple`, and the three `[data-theme="dark"] .glow-{blue,purple,pink}` selectors deleted from `styles/globals.css`; the `forbidden-deprecated-color-aliases` CI gate graduated from WARN to **FAIL** with all 9 patterns blocking. |
**Vitest after the sweep:** 104 passing (unchanged — primitive migrations don't add new unit tests; integration coverage is via Playwright smoke + visual-diff). Lint: 0 errors, 2 pre-existing warnings (unrelated `CardEditorForm.js` + `CollectionsPageView.js` carry-overs that were noted in PR #95).
**CI graduation gates now blocking:**
- `forbidden-modal-shell-without-primitive` — zero allow-list; any new `fixed inset-0 bg-black bg-opacity-` shell fails the build.
- `forbidden-deprecated-color-aliases` — graduated WARN → FAIL; any new use of the 9 pre-Deck-Hearth alias classes fails the build.
**Queued for a future convoy** (no impact on the current ship-readiness state):
- **`fix-card3d-state` is no longer needed** — Card3D was dead code and is now deleted. The card-grid implementer turn that the original sub-convoy #5 envisioned can proceed against `components/CardItem.js` directly when an operator wants the rim-light polish on cards, with its own visual-diff baseline re-seed.
## Self-analytics ## Self-analytics
After each convoy, `scripts/log-convoy-event.sh` emits a record to `.convoys/.metrics.jsonl` (gitignored). After 3-5 convoys, run the upstream `agent-pipeline/analytics/` aggregator to see where token spend goes — that data feeds whether to add or remove rules. After each convoy, `scripts/log-convoy-event.sh` emits a record to `.convoys/.metrics.jsonl` (gitignored). After 3-5 convoys, run the upstream `agent-pipeline/analytics/` aggregator to see where token spend goes — that data feeds whether to add or remove rules.

View file

@ -202,72 +202,49 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Fail if a new file uses the legacy modal-shell pattern - name: Fail if ANY file uses the legacy modal-shell pattern
run: | run: |
# liquid-glass-modal-and-surface-primitive convoy — every modal # liquid-glass-modal-and-surface-primitive convoy — every modal
# MUST compose the `<Modal>` primitive from components/ui/. # MUST compose the `<Modal>` primitive from components/ui/.
# The 9 files below are grandfathered legacy shells queued for # The original 9 grandfathered shells were swept in the
# sweep under liquid-glass-modal-and-surface-primitive Brief 2. # finish-liquid-glass-design PR; the gate is now strict and
# New files must NOT introduce another legacy shell. # has zero allow-list entries. If a new file legitimately
# # needs the legacy pattern (rare — backdrop-blur sheets that
# If a new file legitimately needs the legacy pattern (rare), # are not modals belong in a different primitive), revive the
# update this list AND open a tracking issue — do not silently # allow-list pattern AND open a tracking issue.
# delete entries from this list.
GRANDFATHERED=(
"components/CollectionsSuccessModal.js"
"components/CollectionsEditModal.js"
"components/CollectionEditModal.js"
"components/CardDetailDeckModal.js"
"components/ScannerPageView.js"
"components/UploadImageModal.js"
"components/CollectionSelectionModal.js"
"components/OCRSettings.js"
"pages/decks.js"
)
FOUND=() FOUND=()
while IFS= read -r file; do while IFS= read -r file; do
allowed=false
for grandfathered in "${GRANDFATHERED[@]}"; do
if [ "$file" = "$grandfathered" ]; then
allowed=true
break
fi
done
if [ "$allowed" = false ]; then
FOUND+=("$file") FOUND+=("$file")
fi
done < <(grep -lE 'fixed inset-0 bg-black bg-opacity-' \ done < <(grep -lE 'fixed inset-0 bg-black bg-opacity-' \
pages components -r --include='*.js' 2>/dev/null \ pages components -r --include='*.js' 2>/dev/null \
| sort -u || true) | sort -u || true)
if [ ${#FOUND[@]} -gt 0 ]; then if [ ${#FOUND[@]} -gt 0 ]; then
echo "::error::Legacy modal-shell pattern detected in non-grandfathered file(s). Use the <Modal> primitive from components/ui/." echo "::error::Legacy modal-shell pattern detected. Use the <Modal> primitive from components/ui/."
for f in "${FOUND[@]}"; do for f in "${FOUND[@]}"; do
echo "::error file=${f}::Replace 'fixed inset-0 bg-black bg-opacity-' with <Modal open={…} onClose={…} title=…> from components/ui." echo "::error file=${f}::Replace 'fixed inset-0 bg-black bg-opacity-' with <Modal open={…} onClose={…} title=…> from components/ui."
done done
exit 1 exit 1
fi fi
echo "OK: no new modal shells outside the grandfathered list." echo "OK: no legacy modal shells anywhere in pages/ or components/."
forbidden-deprecated-color-aliases: forbidden-deprecated-color-aliases:
name: No new use of deprecated color aliases name: No use of deprecated color aliases
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Fail if NEW code uses pre-Deck-Hearth blue/purple/pink aliases - name: Fail if ANY code uses pre-Deck-Hearth blue/purple/pink aliases
run: | run: |
# liquid-glass-design-tokens + cleanup-legacy-design-css convoys. # liquid-glass-design-tokens + cleanup-legacy-design-css convoys.
# Pre-Deck-Hearth alias tokens (--accent-blue, --accent-purple, # Pre-Deck-Hearth alias utility classes (.gradient-text-blue /
# --accent-pink) and the .gradient-text-blue / .gradient-text-purple # .gradient-text-purple / .glow-blue / .glow-purple / .glow-pink /
# / .glow-blue / .glow-purple / .glow-pink utility classes are # .gradient-bg-purple / .gradient-bg-blue / .gradient-bg-pink) were
# SCHEDULED FOR DELETION under cleanup-legacy-design-css (#8). # graduated from WARN to FAIL by the finish-liquid-glass-design PR
# after every consumer was swept to --accent-ember equivalents
# AND the CSS definitions were deleted from styles/globals.css.
# #
# Allowed in styles/globals.css (the definitions themselves stay # If a new consumer of one of these classes appears, the build
# until #8 sweeps them) but forbidden in pages/ + components/ # will fail — use --accent-ember, --accent-flame, --accent-gold
# for NEW consumers. Existing consumers are listed below as # (the canonical warm-palette tokens) instead.
# grandfathered and will sweep with #8's deletion PR.
#
# Use --accent-ember, --accent-flame, --accent-gold (the canonical
# warm-palette tokens) instead.
PATTERNS=( PATTERNS=(
'gradient-text-purple' 'gradient-text-purple'
'gradient-text-pink' 'gradient-text-pink'
@ -287,19 +264,16 @@ jobs:
fi fi
done done
if [ -n "$ALL_MATCHES" ]; then if [ -n "$ALL_MATCHES" ]; then
# The current tree has known consumers; print as warnings + record echo "::error::Deprecated color alias detected. Use --accent-ember / --accent-flame / --accent-gold (or the .gradient-bg-ember / .gradient-text-ember utility classes)."
# the baseline count. Once #8 sweeps them, change `exit 0` to echo "$ALL_MATCHES" | sort -u | while IFS= read -r line; do
# `exit 1` to enforce zero.
BASELINE_COUNT=$(echo "$ALL_MATCHES" | grep -cE '.' || true)
echo "::warning::Deprecated color alias(es) found ($BASELINE_COUNT line(s)). Scheduled for deletion by cleanup-legacy-design-css convoy (#8); do not introduce new consumers."
echo "$ALL_MATCHES" | sort -u | head -20 | while IFS= read -r line; do
[ -z "$line" ] && continue [ -z "$line" ] && continue
file=$(echo "$line" | cut -d: -f1) file=$(echo "$line" | cut -d: -f1)
lineno=$(echo "$line" | cut -d: -f2) lineno=$(echo "$line" | cut -d: -f2)
echo "::warning file=${file},line=${lineno}::Deprecated color alias — use --accent-ember / --accent-flame / --accent-gold." echo "::error file=${file},line=${lineno}::Deprecated color alias — replace with the canonical Deck Hearth warm-palette equivalent."
done done
exit 1
fi fi
echo "OK: deprecated-color-alias audit complete." echo "OK: no deprecated color aliases in pages/ or components/."
forbidden-stale-strings: forbidden-stale-strings:
name: No stale ownership/collection copy name: No stale ownership/collection copy

View file

@ -78,7 +78,7 @@ export default function AdminProtected({ children }) {
<div className="space-x-4"> <div className="space-x-4">
<button <button
onClick={() => router.push('/login')} onClick={() => router.push('/login')}
className="px-6 py-3 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200" className="px-6 py-3 rounded-xl font-medium gradient-bg-ember text-white hover:shadow-lg transition-all duration-200"
> >
Login as Admin Login as Admin
</button> </button>

View file

@ -1,530 +0,0 @@
/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
import { useState } from 'react';
import { ManaCost } from './ManaSymbols';
import { VOCAB } from '../lib/collection-vocabulary.js';
export default function Card3D({ card, viewMode, getRarityLabel, getRarityColor, useManaSvg = false }) {
const handleMouseMove = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
setCardPosition({
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
});
};
const handleMouseLeave = () => {
setIsHovered(false);
};
const handleMouseEnter = () => {
setIsHovered(true);
};
const getTCGColor = (game) => {
const colors = {
'MTG': '#8B5CF6', // purple
'Pokemon': '#3B82F6', // blue
'Lorcana': '#EC4899' // pink
};
return colors[game] || '#6B7280';
};
const getSetColor = (setName) => {
// Generate a consistent color based on set name
const hash = setName.split('').reduce((a, b) => {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0);
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 70%, 60%)`;
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
const getRarityGradient = (rarity) => {
const gradients = {
common: 'from-gray-400 to-gray-500',
uncommon: 'from-green-400 to-emerald-500',
rare: 'from-yellow-400 to-orange-500',
mythic: 'from-yellow-400 to-orange-500',
holographic: 'from-red-400 to-pink-500',
enchanted: 'from-purple-400 to-indigo-500',
ultra: 'from-blue-400 to-cyan-500'
};
return gradients[rarity] || 'from-gray-400 to-gray-500';
};
const getRarityGlow = (rarity) => {
const glows = {
common: '0 0 15px rgba(156, 163, 175, 0.4)',
uncommon: '0 0 15px rgba(34, 197, 94, 0.4)',
rare: '0 0 20px rgba(251, 191, 36, 0.5)',
mythic: '0 0 20px rgba(255, 215, 0, 0.4), 0 0 40px rgba(255, 215, 0, 0.2), 0 0 60px rgba(255, 215, 0, 0.1)',
holographic: '0 0 25px rgba(239, 68, 68, 0.6)',
enchanted: '0 0 20px rgba(168, 85, 247, 0.4), 0 0 40px rgba(168, 85, 247, 0.2), 0 0 60px rgba(168, 85, 247, 0.1)',
ultra: '0 0 25px rgba(59, 130, 246, 0.6)'
};
return glows[rarity] || '0 0 15px rgba(156, 163, 175, 0.4)';
};
if (viewMode === 'list') {
return (
<div
className="card hover:shadow-xl transition-all duration-300 cursor-pointer"
style={{
transition: 'all 0.3s ease'
}}
>
<div className="flex items-center space-x-4">
<div className="w-16 h-24 rounded-lg overflow-hidden" style={{
background: `linear-gradient(135deg, ${getRarityGradient(card.rarity).replace('from-', '').replace('to-', '')})`,
boxShadow: `0 4px 8px rgba(0, 0, 0, 0.3)`
}}>
<div className="w-full h-full flex items-center justify-center text-white font-bold text-xs">
{card.game}
</div>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold mb-1" style={{ color: 'var(--text-primary)' }}>
{card.name}
</h3>
<p className="text-sm mb-2" style={{ color: 'var(--text-secondary)' }}>
{card.set_name} {getRarityLabel(card.rarity)}
</p>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{card.oracle_text || card.card_type}
</p>
</div>
<div className="text-right">
<p className="text-lg font-bold gradient-text-purple">
{formatCurrency(card.current_price || 0)}
</p>
<span
className="px-2 py-1 text-xs rounded-full text-white font-medium"
style={{ backgroundColor: getRarityColor(card.rarity) }}
>
{getRarityLabel(card.rarity)}
</span>
</div>
</div>
</div>
);
}
return (
<div className="relative">
{/* Card Container with proper ratio and rarity glow */}
<div
className="relative cursor-pointer transition-transform duration-300 hover:scale-105 w-full"
style={{
aspectRatio: '5/7',
boxShadow: getRarityGlow(card.rarity),
borderRadius: '12px',
border: `2px solid ${getRarityColor(card.rarity)}20`,
overflow: 'hidden'
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onMouseEnter={handleMouseEnter}
>
{/* Particle Effects for All Rarities */}
{card.rarity !== 'common' && (
<div className="absolute inset-0 pointer-events-none overflow-hidden">
{/* Get particle count and colors based on rarity */}
{(() => {
const rarityConfig = {
'mythic': {
particleCount: 16,
sparkleCount: 10,
particleColor: '#FFD700',
sparkleColor: '#FFA500',
glowColor: '#FFD700'
},
'enchanted': {
particleCount: 14,
sparkleCount: 8,
particleColor: '#A855F7',
sparkleColor: '#EC4899',
glowColor: '#A855F7'
},
'rare': {
particleCount: 10,
sparkleCount: 6,
particleColor: '#3B82F6',
sparkleColor: '#60A5FA',
glowColor: '#3B82F6'
},
'uncommon': {
particleCount: 6,
sparkleCount: 4,
particleColor: '#10B981',
sparkleColor: '#34D399',
glowColor: '#10B981'
}
};
const config = rarityConfig[card.rarity];
if (!config) return null;
return (
<>
{/* Floating Particles - Edge framing */}
<div className="absolute inset-0">
{[...Array(config.particleCount)].map((_, i) => {
// Position particles around the card edges
let left, top;
const edgeIndex = i % 4; // 4 edges
const positionOnEdge = Math.floor(i / 4);
if (edgeIndex === 0) {
// Top edge
left = `${10 + (positionOnEdge * 20)}%`;
top = '2%';
} else if (edgeIndex === 1) {
// Right edge
left = '98%';
top = `${10 + (positionOnEdge * 20)}%`;
} else if (edgeIndex === 2) {
// Bottom edge
left = `${10 + (positionOnEdge * 20)}%`;
top = '98%';
} else {
// Left edge
left = '2%';
top = `${10 + (positionOnEdge * 20)}%`;
}
return (
<div
key={i}
className="absolute w-1 h-1 rounded-full"
style={{
left,
top,
backgroundColor: config.particleColor,
animation: `edgeFloat ${4 + i * 0.3}s ease-in-out infinite`,
animationDelay: `${i * 0.2}s`,
opacity: 0.7,
filter: 'blur(1px)',
boxShadow: `0 0 8px ${config.particleColor}, 0 0 16px ${config.particleColor}, 0 0 24px ${config.particleColor}`,
mixBlendMode: 'screen'
}}
/>
);
})}
</div>
{/* Sparkle Effects - Corner highlights */}
<div className="absolute inset-0">
{[...Array(config.sparkleCount)].map((_, i) => {
// Position sparkles in the corners and edge centers
let left, top;
if (i < 2) {
// Top corners
left = i === 0 ? '5%' : '95%';
top = '5%';
} else if (i < 4) {
// Bottom corners
left = i === 2 ? '5%' : '95%';
top = '95%';
} else if (i < 6) {
// Edge centers
left = i === 4 ? '50%' : '50%';
top = i === 4 ? '5%' : '95%';
} else {
// Side centers
left = i === 6 ? '5%' : '95%';
top = '50%';
}
return (
<div
key={`sparkle-${i}`}
className="absolute w-0.5 h-0.5"
style={{
left,
top,
backgroundColor: config.sparkleColor,
animation: `sparkle ${2 + i * 0.4}s ease-in-out infinite`,
animationDelay: `${i * 0.2}s`,
opacity: 0.9,
filter: 'blur(0.5px)',
boxShadow: `0 0 4px ${config.sparkleColor}, 0 0 8px ${config.sparkleColor}`,
mixBlendMode: 'screen'
}}
/>
);
})}
</div>
{/* Rarity Aura - Blend with overall glow */}
<div
className="absolute inset-0 rounded-lg"
style={{
background: `radial-gradient(circle at 50% 50%, ${config.glowColor}20 0%, ${config.glowColor}10 40%, transparent 70%)`,
animation: 'aura 4s ease-in-out infinite',
mixBlendMode: 'screen'
}}
/>
</>
);
})()}
</div>
)}
{/* Card Image */}
<div className="w-full h-full relative">
{card.image_url ? (
<img
src={card.image_url}
alt={card.name}
className="w-full h-full object-cover"
style={{
objectFit: 'cover',
objectPosition: 'center',
borderRadius: '10px'
}}
onError={(e) => {
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
) : null}
{/* Card Back placeholder when no image */}
<div
className={`w-full h-full absolute inset-0 ${!card.image_url ? 'block' : 'hidden'}`}
style={{ borderRadius: '10px' }}
>
<CardBack game={card.game} />
</div>
</div>
{/* Owned Quantity Chip - Only show if owned */}
{card.quantity > 0 && (
<div className="absolute bottom-0 left-0">
<div
className="px-3 py-1 text-xs font-bold text-white shadow-lg"
style={{
backgroundColor: 'var(--text-accent)',
borderRadius: '8px 8px 0 0',
borderTop: '2px solid var(--text-accent)',
borderLeft: '2px solid var(--text-accent)',
borderRight: '2px solid var(--text-accent)',
transform: 'translateY(2px)'
}}
>
{card.quantity}
</div>
</div>
)}
{/* Favorite Button */}
<div className="absolute top-2 right-2">
<button
onClick={(e) => {
e.stopPropagation();
setIsFavorited(!isFavorited);
}}
className={`p-2 rounded-full transition-all duration-200 shadow-lg ${
isFavorited
? 'bg-red-500 bg-opacity-80 text-white'
: 'bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30'
}`}
>
{isFavorited ? '❤️' : '🤍'}
</button>
</div>
</div>
{/* Hover Details Panel */}
{isHovered && (
<div
className="fixed z-50 bg-black bg-opacity-90 text-white p-4 rounded-lg shadow-2xl max-w-sm"
style={{
left: cardPosition.x + cardPosition.width,
top: cardPosition.y,
pointerEvents: 'none',
minHeight: cardPosition.height,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}
>
<div className="space-y-3">
{/* Card Title */}
<div>
<div className="text-sm font-bold text-white mb-1">{card.name}</div>
</div>
{/* Top Section - TCG, Set, Type, HP in 2x2 grid */}
<div className="grid grid-cols-2 gap-3">
<div>
<div className="text-xs font-bold text-white mb-1">TCG</div>
<div className="text-xs text-white">{card.game}</div>
</div>
<div>
<div className="text-xs font-bold text-white mb-1">Set</div>
<div className="text-xs text-white">{card.set_name}</div>
</div>
{card.type && (
<div>
<div className="text-xs font-bold text-white mb-1">Type</div>
<div className="text-xs text-white flex items-center gap-1">
{card.game === 'Pokemon' && card.type === 'Lightning' && (
<span className="text-yellow-400"></span>
)}
{card.type}
</div>
</div>
)}
{card.hp && (
<div>
<div className="text-xs font-bold text-white mb-1">HP</div>
<div className="text-xs text-white">{card.hp}</div>
</div>
)}
</div>
{/* Card Type and Form */}
<div className="grid grid-cols-2 gap-3">
<div>
<div className="text-xs font-bold text-white mb-1">Card type</div>
<div className="text-xs text-white">{card.card_type || 'Card'}</div>
</div>
{card.form && (
<div>
<div className="text-xs font-bold text-white mb-1">Form</div>
<div className="text-xs text-white">{card.form}</div>
</div>
)}
</div>
{/* Cost to Play */}
{card.mana_cost && (
<div>
<div className="text-xs font-bold text-white mb-1">Cost to Play</div>
<ManaCost cost={card.mana_cost} size="sm" useSVG={useManaSvg} />
</div>
)}
{/* Card Rule Section */}
{card.game === 'Pokemon' && card.form && card.form.includes('V') && (
<div>
<div className="text-xs font-bold text-white mb-1">Card rule</div>
<div className="text-xs text-white leading-relaxed">
V rule: When your Pokémon V is Knocked Out, your opponent takes 2 Prize cards.
</div>
</div>
)}
{/* Abilities/Attacks/Moves Section */}
{card.oracle_text && (
<div>
<div className="text-xs font-bold text-white mb-1">Card Text</div>
<div className="text-xs text-white leading-relaxed">{card.oracle_text}</div>
</div>
)}
{/* Quote Section */}
{card.flavor_text && (
<div>
<div className="text-xs font-bold text-white mb-1">Quote</div>
<div className="text-xs text-white italic leading-relaxed">&quot;{card.flavor_text}&quot;</div>
</div>
)}
{/* Current Price */}
{card.current_price && (
<div>
<div className="text-xs font-bold text-white mb-1">Current Price</div>
<div className="text-xs text-white">{formatCurrency(card.current_price)}</div>
</div>
)}
{/* Bottom Section - Weaknesses and Retreat */}
<div className="grid grid-cols-2 gap-3">
{card.weakness && (
<div>
<div className="text-xs font-bold text-white mb-1">Weaknesses</div>
<div className="text-xs text-white flex items-center gap-1">
{card.game === 'Pokemon' && (
<span className="text-red-500">👊</span>
)}
{card.weakness}
</div>
</div>
)}
{card.retreat_cost && (
<div>
<div className="text-xs font-bold text-white mb-1">Retreat</div>
<div className="text-xs text-white flex items-center gap-1">
{card.game === 'Pokemon' && (
<>
<span className="text-gray-400"></span>
<span className="text-gray-400"></span>
</>
)}
{card.retreat_cost}
</div>
</div>
)}
</div>
{/* Decks and Collections */}
{(card.decks && card.decks.length > 0) || (card.collections && card.collections.length > 0) && (
<div>
<div className="text-xs font-bold text-white mb-1">Included In</div>
<div className="text-xs text-white">
{card.decks && card.decks.length > 0 && (
<div className="mb-1">
<span className="font-semibold">Decks:</span> {card.decks.join(', ')}
</div>
)}
{card.collections && card.collections.length > 0 && (
<div>
<span className="font-semibold">{VOCAB.LISTS}:</span> {card.collections.join(', ')}
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}
// Card Back Component for placeholders
function CardBack({ game }) {
const getCardBackImage = () => {
switch (game) {
case 'MTG':
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjOEI0NTEzIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNBMDUyMkQiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiM0QTkwRTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPk1BR0lDPC90ZXh0Pgo8dGV4dCB4PSIxMDAiIHk9IjQ1IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5UaGUgR2F0aGVyaW5nPC90ZXh0Pgo8Y2lyY2xlIGN4PSI3MCIgY3k9IjcwIiByPSI0IiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMzAiIGN4PSI3MCIgcj0iNCIgZmlsbD0iI0Y1OTlFMEIiLz4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iODAiIHI9IjQiIGZpbGw9IiM0QTkwRTIiLz4KPGNpcmNsZSBjeD0iNzAiIGN5PSI5MCIgcj0iNCIgZmlsbD0iIzEwQjk4MSIvPgo8Y2lyY2xlIGN4PSIxMzAiIGN5PSI5MCIgcj0iNCIgZmlsbD0iYmxhY2siLz4KPHRleHQgeD0iMTAwIiB5PSIxMjAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMyMEIyQUEiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkRFQ0tNQVNURVI8L3RleHQ+Cjwvc3ZnPgo=';
case 'Pokemon':
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjRkY2QjZCIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNGRjhFOEUiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGRkQ3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlBPS0VNT048L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iNDUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRyYWRpbmcgQ2FyZCBHYW1lPC90ZXh0Pgo8Y2lyY2xlIGN4PSIxMDAiIGN5PSI3MCIgcj0iMjAiIGZpbGw9IndoaXRlIi8+Cjx0ZXh0IHg9IjEwMCIgeT0iNzgiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZmlsbD0iIzFGN0Y3RiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+4pePPC90ZXh0Pgo8dGV4dCB4PSIxMDAiIHk9IjEyMCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI0ZGRDcwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+R0FNRSBGUkVBSzwvdGV4dD4KPC9zdmc+Cg==';
case 'Lorcana':
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjRUM0ODk5Ii8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNGNDcyQjYiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGQkJGMjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkRJU05FWTwvdGV4dD4KPHRleHQgeD0iMTAwIiB5PSI0NSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TG9yY2FuYTwvdGV4dD4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iNzAiIHI9IjIwIiBmaWxsPSJ3aGl0ZSIvPgo8dGV4dCB4PSIxMDAiIHk9Ijc4IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIGZpbGw9IiMxRjdGN0YiIHRleHQtYW5jaG9yPSJtaWRkbGUiPvCfkqQ8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iMTIwIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjRjU5RTBCIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5SQVZFTlNERVJHPzwvdGV4dD4KPC9zdmc+Cg==';
default:
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjNkI3MjgwIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiM5Q0EzQUYiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5UQ0c8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iNDUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRyYWRpbmcgQ2FyZDwvdGV4dD4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iNzAiIHI9IjIwIiBmaWxsPSJ3aGl0ZSIvPgo8dGV4dCB4PSIxMDAiIHk9Ijc4IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIGZpbGw9IiMxRjdGN0YiIHRleHQtYW5jaG9yPSJtaWRkbGUiPvCfkqQ8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iMTIwIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjNkI3MjgwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5DQVJEPC90ZXh0Pgo8L3N2Zz4K';
}
};
return (
<div className="w-full h-full rounded-lg overflow-hidden">
<img
src={getCardBackImage()}
alt={`${game} card back`}
className="w-full h-full object-cover"
/>
</div>
);
}

View file

@ -1,3 +1,5 @@
import { Modal, Button } from './ui';
export default function CardDetailDeckModal({ export default function CardDetailDeckModal({
isOpen, isOpen,
card, card,
@ -7,31 +9,29 @@ export default function CardDetailDeckModal({
onAdd, onAdd,
onClose, onClose,
}) { }) {
if (!isOpen || !card) { if (!card) return null;
return null;
}
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <Modal open={isOpen} onClose={onClose} title="Add to Deck" size="md">
<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)' }}>
Add to Deck
</h3>
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="card-detail-deck-select"
>
Select Deck Select Deck
</label> </label>
<select <select
id="card-detail-deck-select"
value={selectedDeck} value={selectedDeck}
onChange={(e) => onSelectedDeckChange(e.target.value)} onChange={(e) => onSelectedDeckChange(e.target.value)}
className="w-full p-3 rounded-xl border transition-all duration-200" className="w-full p-3 rounded-xl border transition-all"
style={{ style={{
backgroundColor: 'var(--bg-secondary)', backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
color: 'var(--text-primary)', color: 'var(--text-primary)',
transitionDuration: 'var(--motion-duration-default)',
transitionTimingFunction: 'var(--motion-ease-out)',
}} }}
> >
<option value="">Choose a deck...</option> <option value="">Choose a deck...</option>
@ -45,31 +45,18 @@ export default function CardDetailDeckModal({
</select> </select>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<button <Button
type="button" variant="primary"
onClick={onAdd} onClick={onAdd}
disabled={!selectedDeck} disabled={!selectedDeck}
className={`flex-1 px-4 py-2 rounded-xl font-medium transition-all duration-200 ${ className="flex-1"
selectedDeck
? 'gradient-bg-purple text-white hover:shadow-lg'
: 'opacity-50 cursor-not-allowed'
}`}
> >
Add to Deck Add to Deck
</button> </Button>
<button <Button variant="secondary" onClick={onClose} className="flex-1">
type="button"
onClick={onClose}
className="flex-1 px-4 py-2 rounded-xl font-medium border transition-all duration-200"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)',
}}
>
Cancel Cancel
</button> </Button>
</div>
</div>
</div> </div>
</Modal>
); );
} }

View file

@ -54,7 +54,7 @@ export default function CardDetailQuantityModal({
<button <button
type="button" type="button"
onClick={() => onConfirm(quantity)} onClick={() => onConfirm(quantity)}
className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200" className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-ember text-white hover:shadow-lg transition-all duration-200"
> >
{ownedQuantity > 0 ? 'Update' : VOCAB.ADD_TO_MY_COLLECTION} {ownedQuantity > 0 ? 'Update' : VOCAB.ADD_TO_MY_COLLECTION}
</button> </button>

View file

@ -29,7 +29,7 @@ export default function CardEditorForm({
className={`px-6 py-3 rounded-xl font-medium transition-all duration-200 ${ className={`px-6 py-3 rounded-xl font-medium transition-all duration-200 ${
saving saving
? 'opacity-50 cursor-not-allowed' ? 'opacity-50 cursor-not-allowed'
: 'gradient-bg-purple text-white hover:shadow-lg' : 'gradient-bg-ember text-white hover:shadow-lg'
}`} }`}
> >
{saving ? 'Saving...' : 'Save Changes'} {saving ? 'Saving...' : 'Save Changes'}

View file

@ -29,7 +29,7 @@ export default function CardEditorView({
<button <button
type="button" type="button"
onClick={() => router.push('/admin/card-editor')} onClick={() => router.push('/admin/card-editor')}
className="px-4 py-2 rounded-lg font-medium gradient-bg-purple text-white" className="px-4 py-2 rounded-lg font-medium gradient-bg-ember text-white"
> >
🖊 Card Editor 🖊 Card Editor
</button> </button>

View file

@ -1,3 +1,5 @@
import { Modal, Input, Button } from './ui';
export default function CollectionEditModal({ export default function CollectionEditModal({
isOpen, isOpen,
editForm, editForm,
@ -5,34 +7,25 @@ export default function CollectionEditModal({
onClose, onClose,
onSave, onSave,
}) { }) {
if (!isOpen) {
return null;
}
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <Modal open={isOpen} onClose={onClose} title="Edit list" size="md">
<div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Edit List
</h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <Input
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> label="List Name"
List Name
</label>
<input
type="text"
className="input-field w-full"
value={editForm.name} value={editForm.name}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })} onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
placeholder="Enter list name" placeholder="Enter list name"
/> />
</div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="collection-edit-description"
>
Description Description
</label> </label>
<textarea <textarea
id="collection-edit-description"
className="input-field w-full" className="input-field w-full"
rows="3" rows="3"
value={editForm.description} value={editForm.description}
@ -40,18 +33,13 @@ export default function CollectionEditModal({
placeholder="Describe your list" placeholder="Describe your list"
/> />
</div> </div>
<div> <Input
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> label="Hero Image (Optional)"
Hero Image (Optional)
</label>
<input
type="url" type="url"
className="input-field w-full"
value={editForm.image} value={editForm.image}
onChange={(e) => setEditForm({ ...editForm, image: e.target.value })} onChange={(e) => setEditForm({ ...editForm, image: e.target.value })}
placeholder="Enter image URL" placeholder="Enter image URL"
/> />
</div>
<div <div
className="flex items-center justify-between p-4 rounded-lg border" className="flex items-center justify-between p-4 rounded-lg border"
style={{ borderColor: 'var(--border)' }} style={{ borderColor: 'var(--border)' }}
@ -67,6 +55,8 @@ export default function CollectionEditModal({
<button <button
type="button" type="button"
onClick={() => setEditForm({ ...editForm, isPublic: !editForm.isPublic })} onClick={() => setEditForm({ ...editForm, isPublic: !editForm.isPublic })}
aria-pressed={editForm.isPublic}
aria-label="Toggle public list"
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
editForm.isPublic ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-600' editForm.isPublic ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-600'
}`} }`}
@ -80,31 +70,18 @@ export default function CollectionEditModal({
</div> </div>
</div> </div>
<div className="flex space-x-3 mt-6"> <div className="flex space-x-3 mt-6">
<button <Button variant="secondary" onClick={onClose} className="flex-1">
type="button"
onClick={onClose}
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)',
}}
>
Cancel Cancel
</button> </Button>
<button <Button
type="button" variant="primary"
onClick={onSave} onClick={onSave}
disabled={!editForm.name.trim()} disabled={!editForm.name.trim()}
className="flex-1 py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md disabled:opacity-50" className="flex-1"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white',
}}
> >
Save Changes Save Changes
</button> </Button>
</div>
</div>
</div> </div>
</Modal>
); );
} }

View file

@ -1,12 +1,13 @@
/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */ /* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { VOCAB } from '../lib/collection-vocabulary.js'; import { VOCAB } from '../lib/collection-vocabulary.js';
import { Modal, SearchBar, Button } from './ui';
export default function CollectionSelectionModal({ export default function CollectionSelectionModal({
isOpen, isOpen,
onClose, onClose,
cards, // Array of cards to add cards,
onAddToCollections onAddToCollections,
}) { }) {
const [collections, setCollections] = useState([]); const [collections, setCollections] = useState([]);
const [selectedCollections, setSelectedCollections] = useState([]); const [selectedCollections, setSelectedCollections] = useState([]);
@ -25,15 +26,12 @@ export default function CollectionSelectionModal({
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
const fetchCollections = async () => { const fetchCollections = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await fetch('/api/collections?excludeSystem=true'); const response = await fetch('/api/collections?excludeSystem=true');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
// The API returns an array directly, not wrapped in collections property
setCollections(Array.isArray(data) ? data : data.collections || []); setCollections(Array.isArray(data) ? data : data.collections || []);
} else { } else {
console.error('Failed to fetch collections:', response.status, response.statusText); console.error('Failed to fetch collections:', response.status, response.statusText);
@ -46,79 +44,62 @@ export default function CollectionSelectionModal({
setLoading(false); setLoading(false);
} }
}; };
fetchCollections(); fetchCollections();
}, [isOpen]); }, [isOpen]);
const handleCollectionToggle = (collectionId) => { const handleCollectionToggle = (collectionId) => {
setSelectedCollections(prev => { setSelectedCollections((prev) =>
if (prev.includes(collectionId)) { prev.includes(collectionId)
return prev.filter(id => id !== collectionId); ? prev.filter((id) => id !== collectionId)
} else { : [...prev, collectionId]
return [...prev, collectionId];
}
});
};
const handleSelectAll = () => {
const filteredCollections = getFilteredCollections();
const allSelected = filteredCollections.every(collection =>
selectedCollections.includes(collection.id)
); );
if (allSelected) {
// Deselect all filtered collections
setSelectedCollections(prev =>
prev.filter(id => !filteredCollections.some(c => c.id === id))
);
} else {
// Select all filtered collections
const newSelections = filteredCollections
.filter(collection => !selectedCollections.includes(collection.id))
.map(collection => collection.id);
setSelectedCollections(prev => [...prev, ...newSelections]);
}
}; };
const getFilteredCollections = () => { const getFilteredCollections = () => {
if (!searchQuery.trim()) return collections; if (!searchQuery.trim()) return collections;
return collections.filter(
return collections.filter(collection => (collection) =>
collection.name.toLowerCase().includes(searchQuery.toLowerCase()) || collection.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
collection.description?.toLowerCase().includes(searchQuery.toLowerCase()) collection.description?.toLowerCase().includes(searchQuery.toLowerCase())
); );
}; };
const handleSelectAll = () => {
const filteredCollections = getFilteredCollections();
const allSelected = filteredCollections.every((collection) =>
selectedCollections.includes(collection.id)
);
if (allSelected) {
setSelectedCollections((prev) =>
prev.filter((id) => !filteredCollections.some((c) => c.id === id))
);
} else {
const newSelections = filteredCollections
.filter((collection) => !selectedCollections.includes(collection.id))
.map((collection) => collection.id);
setSelectedCollections((prev) => [...prev, ...newSelections]);
}
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (selectedCollections.length === 0) return; if (selectedCollections.length === 0) return;
setSubmitting(true); setSubmitting(true);
try { try {
const results = []; const results = [];
// Add cards to each selected collection
for (const collectionId of selectedCollections) { for (const collectionId of selectedCollections) {
for (const card of cards) { for (const card of cards) {
const response = await fetch(`/api/collections/${collectionId}/cards`, { const response = await fetch(`/api/collections/${collectionId}/cards`, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json' body: JSON.stringify({ cardId: card.id, quantity: 1 }),
}, });
body: JSON.stringify({ results.push({
cardId: card.id, collectionId,
quantity: 1 cardId: card.id,
}) success: response.ok,
}); });
if (response.ok) {
results.push({ collectionId, cardId: card.id, success: true });
} else {
results.push({ collectionId, cardId: card.id, success: false });
} }
} }
}
// Call callback with results
onAddToCollections(results, selectedCollections, cards); onAddToCollections(results, selectedCollections, cards);
onClose(); onClose();
} catch (error) { } catch (error) {
@ -129,35 +110,25 @@ export default function CollectionSelectionModal({
}; };
const filteredCollections = getFilteredCollections(); const filteredCollections = getFilteredCollections();
const allFilteredSelected = filteredCollections.length > 0 && const allFilteredSelected =
filteredCollections.every(collection => selectedCollections.includes(collection.id)); filteredCollections.length > 0 &&
filteredCollections.every((collection) =>
if (!isOpen) return null; selectedCollections.includes(collection.id)
);
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <Modal open={isOpen} onClose={onClose} title={VOCAB.ADD_TO_LISTS} size="lg">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
{/* Header */}
<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">
{VOCAB.ADD_TO_LISTS}
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Card Summary */}
<div className="flex items-center space-x-3 mb-4"> <div className="flex items-center space-x-3 mb-4">
<div className="flex -space-x-2"> <div className="flex -space-x-2">
{cards.slice(0, 3).map((card, index) => ( {cards.slice(0, 3).map((card) => (
<div key={card.id} className="w-12 h-16 bg-gray-200 dark:bg-gray-700 rounded border-2 border-white dark:border-gray-800 overflow-hidden"> <div
key={card.id}
className="w-12 h-16 rounded border-2 overflow-hidden"
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: 'var(--bg-primary)',
}}
>
{card.image_url ? ( {card.image_url ? (
<img <img
src={card.image_url} src={card.image_url}
@ -165,105 +136,133 @@ export default function CollectionSelectionModal({
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<div className="w-full h-full flex items-center justify-center text-gray-400 text-xs"> <div
className="w-full h-full flex items-center justify-center text-xs"
style={{ color: 'var(--text-tertiary)' }}
>
No Image No Image
</div> </div>
)} )}
</div> </div>
))} ))}
{cards.length > 3 && ( {cards.length > 3 && (
<div className="w-12 h-16 bg-gray-300 dark:bg-gray-600 rounded border-2 border-white dark:border-gray-800 flex items-center justify-center"> <div
<span className="text-sm font-bold text-gray-600 dark:text-gray-300"> className="w-12 h-16 rounded border-2 flex items-center justify-center"
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: 'var(--bg-primary)',
}}
>
<span className="text-sm font-bold" style={{ color: 'var(--text-secondary)' }}>
+{cards.length - 3} +{cards.length - 3}
</span> </span>
</div> </div>
)} )}
</div> </div>
<div> <div>
<p className="text-lg font-semibold text-gray-900 dark:text-white"> <p className="text-lg font-semibold" style={{ color: 'var(--text-primary)' }}>
{cards.length} card{cards.length !== 1 ? 's' : ''} selected {cards.length} card{cards.length !== 1 ? 's' : ''} selected
</p> </p>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Choose lists to add {cards.length === 1 ? 'this card' : 'these cards'} to Choose lists to add {cards.length === 1 ? 'this card' : 'these cards'} to
</p> </p>
</div> </div>
</div> </div>
{/* Search */} <div className="mb-4">
<div className="relative"> <SearchBar
<input
type="text"
placeholder="Search lists..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 pl-10 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white" onClear={() => setSearchQuery('')}
placeholder="Search lists…"
/> />
<svg className="w-5 h-5 text-gray-400 absolute left-3 top-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div> </div>
{/* Collections List */} <div className="overflow-y-auto" style={{ maxHeight: '24rem' }}>
<div className="flex-1 overflow-y-auto max-h-96">
{loading ? ( {loading ? (
<div className="p-8 text-center"> <div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div> <div
<p className="text-gray-600 dark:text-gray-400">Loading lists...</p> className="motion-essential animate-spin rounded-full h-8 w-8 border-b-2 mx-auto mb-2"
style={{ borderColor: 'var(--accent-ember)' }}
/>
<p style={{ color: 'var(--text-secondary)' }}>Loading lists...</p>
</div> </div>
) : filteredCollections.length === 0 ? ( ) : filteredCollections.length === 0 ? (
<div className="p-8 text-center"> <div className="p-8 text-center">
<svg className="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<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" /> className="w-12 h-12 mx-auto mb-4"
style={{ color: 'var(--text-tertiary)' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg> </svg>
<p className="text-gray-600 dark:text-gray-400 mb-2"> <p className="mb-2" style={{ color: 'var(--text-secondary)' }}>
{searchQuery ? 'No lists match your search' : 'No lists found'} {searchQuery ? 'No lists match your search' : 'No lists found'}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-500"> <p className="text-sm" style={{ color: 'var(--text-tertiary)' }}>
{searchQuery ? 'Try a different search term' : 'Create your first list to get started'} {searchQuery ? 'Try a different search term' : 'Create your first list to get started'}
</p> </p>
</div> </div>
) : ( ) : (
<div className="p-4"> <>
{/* Select All */}
{filteredCollections.length > 1 && ( {filteredCollections.length > 1 && (
<div className="flex items-center justify-between p-3 border-b border-gray-200 dark:border-gray-700 mb-2"> <div
className="flex items-center justify-between p-3 border-b mb-2"
style={{ borderColor: 'var(--border)' }}
>
<label className="flex items-center space-x-3 cursor-pointer"> <label className="flex items-center space-x-3 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={allFilteredSelected} checked={allFilteredSelected}
onChange={handleSelectAll} onChange={handleSelectAll}
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2" className="w-4 h-4 rounded"
style={{ accentColor: 'var(--accent-ember)' }}
/> />
<span className="font-medium text-gray-900 dark:text-white"> <span className="font-medium" style={{ color: 'var(--text-primary)' }}>
Select All ({filteredCollections.length}) Select All ({filteredCollections.length})
</span> </span>
</label> </label>
</div> </div>
)} )}
{/* Collection Items */}
<div className="space-y-2"> <div className="space-y-2">
{filteredCollections.map((collection) => ( {filteredCollections.map((collection) => {
const isSelected = selectedCollections.includes(collection.id);
return (
<div <div
key={collection.id} key={collection.id}
className={`p-4 rounded-lg border-2 transition-all duration-200 cursor-pointer ${ className="p-4 rounded-lg border-2 cursor-pointer"
selectedCollections.includes(collection.id) style={{
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' borderColor: isSelected
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600' ? 'var(--accent-ember)'
}`} : 'var(--border)',
backgroundColor: isSelected
? 'rgba(var(--ember-rim-subtle), 0.08)'
: 'transparent',
transition: 'all var(--motion-duration-default) var(--motion-ease-out)',
}}
onClick={() => handleCollectionToggle(collection.id)} onClick={() => handleCollectionToggle(collection.id)}
> >
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<input <input
type="checkbox" type="checkbox"
checked={selectedCollections.includes(collection.id)} checked={isSelected}
onChange={() => handleCollectionToggle(collection.id)} onChange={() => handleCollectionToggle(collection.id)}
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2" className="w-4 h-4 rounded"
style={{ accentColor: 'var(--accent-ember)' }}
/> />
<div
{/* Collection Image */} className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0"
<div className="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-lg overflow-hidden flex-shrink-0"> style={{ backgroundColor: 'var(--bg-tertiary)' }}
>
{collection.image ? ( {collection.image ? (
<img <img
src={collection.image} src={collection.image}
@ -271,30 +270,54 @@ export default function CollectionSelectionModal({
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<div className="w-full h-full flex items-center justify-center text-gray-400"> <div
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> className="w-full h-full flex items-center justify-center"
<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" /> style={{ color: 'var(--text-tertiary)' }}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg> </svg>
</div> </div>
)} )}
</div> </div>
{/* Collection Info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 dark:text-white truncate"> <h3
className="font-semibold truncate"
style={{ color: 'var(--text-primary)' }}
>
{collection.name} {collection.name}
</h3> </h3>
{collection.description && ( {collection.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 truncate"> <p
className="text-sm truncate"
style={{ color: 'var(--text-secondary)' }}
>
{collection.description} {collection.description}
</p> </p>
)} )}
<div className="flex items-center space-x-4 mt-1"> <div className="flex items-center space-x-4 mt-1">
<span className="text-xs text-gray-500 dark:text-gray-500"> <span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>
{collection.card_count || 0} cards {collection.card_count || 0} cards
</span> </span>
{collection.is_public && ( {collection.is_public && (
<span className="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded-full"> <span
className="text-xs px-2 py-1 rounded-full"
style={{
backgroundColor: 'rgba(34, 197, 94, 0.15)',
color: 'rgb(21, 128, 61)',
}}
>
Public Public
</span> </span>
)} )}
@ -302,52 +325,43 @@ export default function CollectionSelectionModal({
</div> </div>
</div> </div>
</div> </div>
))} );
</div> })}
</div> </div>
</>
)} )}
</div> </div>
{/* Footer */} <div
<div className="p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> className="flex items-center justify-between mt-6 pt-4 border-t"
<div className="flex items-center justify-between"> style={{ borderColor: 'var(--border)' }}
<div className="text-sm text-gray-600 dark:text-gray-400"> >
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{selectedCollections.length > 0 && ( {selectedCollections.length > 0 && (
<span> <span>
{selectedCollections.length} list{selectedCollections.length !== 1 ? 's' : ''} selected {selectedCollections.length} list
{selectedCollections.length !== 1 ? 's' : ''} selected
</span> </span>
)} )}
</div> </div>
<div className="flex space-x-3"> <div className="flex space-x-3">
<button <Button variant="secondary" onClick={onClose}>
onClick={onClose}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
Cancel Cancel
</button> </Button>
<button <Button
variant="primary"
onClick={handleSubmit} onClick={handleSubmit}
disabled={selectedCollections.length === 0 || submitting} disabled={selectedCollections.length === 0 || submitting}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${ loading={submitting}
selectedCollections.length === 0 || submitting
? 'bg-gray-300 dark:bg-gray-700 text-gray-500 dark:text-gray-500 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
> >
{submitting ? ( {submitting
<div className="flex items-center space-x-2"> ? 'Adding...'
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> : `Add to ${selectedCollections.length} List${
<span>Adding...</span> selectedCollections.length !== 1 ? 's' : ''
</div> }`}
) : ( </Button>
`Add to ${selectedCollections.length} List${selectedCollections.length !== 1 ? 's' : ''}`
)}
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</Modal>
); );
} }

View file

@ -3,6 +3,7 @@ import {
handleCollectionTagKeyPress, handleCollectionTagKeyPress,
removeCollectionTag, removeCollectionTag,
} from '../lib/collection-tag-input.js'; } from '../lib/collection-tag-input.js';
import { Modal, Input, Button } from './ui';
export default function CollectionsEditModal({ export default function CollectionsEditModal({
editingCollection, editingCollection,
@ -12,61 +13,61 @@ export default function CollectionsEditModal({
onClose, onClose,
onUpdate, onUpdate,
}) { }) {
if (!editingCollection) { if (!editingCollection) return null;
return null;
}
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <Modal open onClose={onClose} title="Edit list" size="md">
<div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Edit List
</h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <Input
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> label="List Name"
List Name
</label>
<input
type="text"
className="input-field w-full"
value={editingCollection.name} value={editingCollection.name}
onChange={(e) => setEditingCollection({ ...editingCollection, name: e.target.value })} onChange={(e) =>
setEditingCollection({ ...editingCollection, name: e.target.value })
}
/> />
</div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="collections-edit-description"
>
Description Description
</label> </label>
<textarea <textarea
id="collections-edit-description"
className="input-field w-full" className="input-field w-full"
rows="3" rows="3"
value={editingCollection.description} value={editingCollection.description}
onChange={(e) => setEditingCollection({ ...editingCollection, description: e.target.value })} onChange={(e) =>
setEditingCollection({
...editingCollection,
description: e.target.value,
})
}
/> />
</div> </div>
<div> <Input
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> label="Hero Image"
Hero Image
</label>
<input
type="url" type="url"
className="input-field w-full"
value={editingCollection.image || ''} value={editingCollection.image || ''}
onChange={(e) => setEditingCollection({ ...editingCollection, image: e.target.value })} onChange={(e) =>
setEditingCollection({ ...editingCollection, image: e.target.value })
}
placeholder="Enter image URL" placeholder="Enter image URL"
/> />
</div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}> <label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="collections-edit-tags"
>
Tags Tags
</label> </label>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex gap-2"> <div className="flex gap-2">
<input <Input
type="text" id="collections-edit-tags"
className="input-field flex-1"
value={editTagInput} value={editTagInput}
onChange={(e) => setEditTagInput(e.target.value)} onChange={(e) => setEditTagInput(e.target.value)}
onKeyPress={(e) => onKeyPress={(e) =>
@ -79,21 +80,24 @@ export default function CollectionsEditModal({
) )
} }
placeholder="Add tags (press Enter)" placeholder="Add tags (press Enter)"
className="flex-1"
/> />
<button <Button
type="button" type="button"
onClick={() => variant="primary"
addCollectionTag(editTagInput, setEditTagInput, editingCollection, setEditingCollection) size="md"
}
className="px-3 py-2 rounded-lg text-sm font-medium transition-colors"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white',
}}
disabled={!editTagInput.trim()} disabled={!editTagInput.trim()}
onClick={() =>
addCollectionTag(
editTagInput,
setEditTagInput,
editingCollection,
setEditingCollection
)
}
> >
Add Add
</button> </Button>
</div> </div>
{editingCollection.tags && editingCollection.tags.length > 0 && ( {editingCollection.tags && editingCollection.tags.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@ -110,7 +114,10 @@ export default function CollectionsEditModal({
{tag} {tag}
<button <button
type="button" type="button"
onClick={() => removeCollectionTag(tag, editingCollection, setEditingCollection)} onClick={() =>
removeCollectionTag(tag, editingCollection, setEditingCollection)
}
aria-label={`Remove tag ${tag}`}
className="ml-1 hover:text-red-500 transition-colors" className="ml-1 hover:text-red-500 transition-colors"
> >
× ×
@ -137,8 +144,13 @@ export default function CollectionsEditModal({
<button <button
type="button" type="button"
onClick={() => onClick={() =>
setEditingCollection({ ...editingCollection, isPublic: !editingCollection.isPublic }) setEditingCollection({
...editingCollection,
isPublic: !editingCollection.isPublic,
})
} }
aria-pressed={editingCollection.isPublic}
aria-label="Toggle public list"
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
editingCollection.isPublic ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-600' editingCollection.isPublic ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-600'
}`} }`}
@ -152,31 +164,18 @@ export default function CollectionsEditModal({
</div> </div>
</div> </div>
<div className="flex space-x-3 mt-6"> <div className="flex space-x-3 mt-6">
<button <Button variant="secondary" onClick={onClose} className="flex-1">
type="button"
onClick={onClose}
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)',
}}
>
Cancel Cancel
</button> </Button>
<button <Button
type="button" variant="primary"
onClick={onUpdate} onClick={onUpdate}
disabled={!editingCollection.name.trim()} disabled={!editingCollection.name.trim()}
className="flex-1 py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md disabled:opacity-50" className="flex-1"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white',
}}
> >
Update List Update List
</button> </Button>
</div>
</div>
</div> </div>
</Modal>
); );
} }

View file

@ -6,6 +6,7 @@ import CollectionsCreateModal from './CollectionsCreateModal';
import CollectionsEditModal from './CollectionsEditModal'; import CollectionsEditModal from './CollectionsEditModal';
import CollectionsSuccessModal from './CollectionsSuccessModal'; import CollectionsSuccessModal from './CollectionsSuccessModal';
import CollectionsThumbnail from './CollectionsThumbnail'; import CollectionsThumbnail from './CollectionsThumbnail';
import { Button, SearchBar } from './ui';
export default function CollectionsPageView(props) { export default function CollectionsPageView(props) {
const { const {
@ -59,40 +60,25 @@ export default function CollectionsPageView(props) {
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<Link href="/community/collections"> <Link href="/community/collections">
<button className="px-4 py-2 rounded-xl border transition-colors hover:shadow-md" <Button variant="secondary">
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)',
backgroundColor: 'transparent'
}}
>
🌍 Discover Community 🌍 Discover Community
</button> </Button>
</Link> </Link>
<button <Button variant="primary" onClick={() => setShowCreateModal(true)}>
onClick={() => setShowCreateModal(true)}
className="px-6 py-2 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
+ Create List + Create List
</button> </Button>
</div> </div>
</div> </div>
</div> </div>
{/* Filters and Search */}
<div className="p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}> <div className="p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex flex-col md:flex-row gap-4"> <div className="flex flex-col md:flex-row gap-4">
<div className="flex-1"> <div className="flex-1">
<input <SearchBar
type="text"
placeholder="Search lists..."
className="input-field w-full"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onClear={() => setSearchQuery('')}
placeholder="Search lists…"
/> />
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
@ -126,16 +112,9 @@ export default function CollectionsPageView(props) {
} }
</p> </p>
{!searchQuery && ( {!searchQuery && (
<button <Button variant="primary" size="lg" onClick={() => setShowCreateModal(true)}>
onClick={() => setShowCreateModal(true)}
className="px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Create List Create List
</button> </Button>
)} )}
</div> </div>
) : ( ) : (

View file

@ -1,48 +1,29 @@
import { Modal, Button } from './ui';
export default function CollectionsSuccessModal({ export default function CollectionsSuccessModal({
isOpen, isOpen,
createdCollection, createdCollection,
onViewList, onViewList,
onStay, onStay,
}) { }) {
if (!isOpen || !createdCollection) { if (!createdCollection) return null;
return null;
}
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <Modal open={isOpen} onClose={onStay} title="List created!" size="md" hideCloseButton>
<div className="card max-w-md w-full mx-4 text-center"> <div className="text-center">
<div className="text-6xl mb-4">🎉</div> <div className="text-6xl mb-4" aria-hidden="true">🎉</div>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
List Created!
</h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Your list &quot;{createdCollection.name}&quot; has been created successfully. Your list &quot;{createdCollection.name}&quot; has been created successfully.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<button <Button variant="primary" onClick={onViewList} className="w-full">
type="button"
onClick={onViewList}
className="w-full py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white',
}}
>
View List View List
</button> </Button>
<button <Button variant="secondary" onClick={onStay} className="w-full">
type="button"
onClick={onStay}
className="w-full py-2 px-4 rounded-xl border transition-colors"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)',
}}
>
Stay on Lists Page Stay on Lists Page
</button> </Button>
</div>
</div> </div>
</div> </div>
</Modal>
); );
} }

View file

@ -1,40 +1,23 @@
import { Modal, Button } from './ui';
export default function OCRSettings({ onClose }) { export default function OCRSettings({ onClose }) {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <Modal open onClose={onClose} title="Card Scanner" size="md">
<div
className="rounded-xl border max-w-md w-full p-6"
style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
Card Scanner
</h2>
<button
onClick={onClose}
className="p-2 rounded-lg hover:opacity-70"
style={{ color: 'var(--text-secondary)' }}
aria-label="Close"
>
</button>
</div>
<p className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}> <p className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}>
Card identification runs on Deck Hearth&apos;s servers using Gemini Vision. No API keys Card identification runs on Deck Hearth&apos;s servers using Gemini Vision. No API
are required in your browser. keys are required in your browser.
</p> </p>
<ul className="text-sm space-y-2 mb-6" style={{ color: 'var(--text-secondary)' }}> <ul className="text-sm space-y-2 mb-6" style={{ color: 'var(--text-secondary)' }}>
<li>Hold the card steady in the camera frame for best results.</li> <li>Hold the card steady in the camera frame for best results.</li>
<li>If multiple matches are found, you will be asked to pick the correct printing.</li> <li>If multiple matches are found, you will be asked to pick the correct printing.</li>
<li>Unknown cards are saved for admin review instead of being added to the global catalog.</li> <li>
Unknown cards are saved for admin review instead of being added to the global
catalog.
</li>
</ul> </ul>
<button <Button variant="primary" onClick={onClose} className="w-full">
onClick={onClose}
className="w-full px-4 py-2 rounded-lg font-medium"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
>
Got it Got it
</button> </Button>
</div> </Modal>
</div>
); );
} }

View file

@ -2,7 +2,7 @@ import CameraScanner from './CameraScanner';
import ScannerDestinationPicker from './ScannerDestinationPicker'; import ScannerDestinationPicker from './ScannerDestinationPicker';
import ScannedCardItem, { CONDITION_OPTIONS } from './ScannedCardItem'; import ScannedCardItem, { CONDITION_OPTIONS } from './ScannedCardItem';
import OCRSettings from './OCRSettings'; import OCRSettings from './OCRSettings';
import { useFocusTrap } from '../lib/use-focus-trap.js'; import { Modal, Input, Button } from './ui';
import { VOCAB } from '../lib/collection-vocabulary.js'; import { VOCAB } from '../lib/collection-vocabulary.js';
export default function ScannerPageView({ export default function ScannerPageView({
@ -18,7 +18,6 @@ export default function ScannerPageView({
onCloseOCRSettings, onCloseOCRSettings,
onScannerError, onScannerError,
}) { }) {
const createCollectionDialogRef = useFocusTrap(queue.showCreateCollection);
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
@ -292,60 +291,43 @@ export default function ScannerPageView({
</div> </div>
)} )}
{queue.showCreateCollection && ( <Modal
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> open={queue.showCreateCollection}
<div onClose={() => queue.setShowCreateCollection(false)}
ref={createCollectionDialogRef} title="Create New List"
className="rounded-xl p-6 max-w-md w-full mx-4" size="md"
style={{ backgroundColor: 'var(--bg-secondary)' }}
role="dialog"
aria-modal="true"
aria-labelledby="create-collection-title"
> >
<h3 id="create-collection-title" className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}> <Input
Create New List
</h3>
<label htmlFor="create-collection-name" className="sr-only">
List name
</label>
<input
id="create-collection-name" id="create-collection-name"
type="text" label="List name"
placeholder="List name..." placeholder="List name..."
value={queue.newCollectionName} value={queue.newCollectionName}
onChange={(e) => queue.setNewCollectionName(e.target.value)} onChange={(e) => queue.setNewCollectionName(e.target.value)}
className="w-full px-4 py-2 rounded-lg border mb-4"
style={{
backgroundColor: 'var(--bg-tertiary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)',
}}
onKeyPress={(e) => { onKeyPress={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
queue.createCollection(); queue.createCollection();
} }
}} }}
className="mb-4"
/> />
<div className="flex gap-3"> <div className="flex gap-3">
<button <Button
variant="primary"
onClick={queue.createCollection} onClick={queue.createCollection}
disabled={!queue.newCollectionName.trim()} disabled={!queue.newCollectionName.trim()}
className="flex-1 px-4 py-2 rounded-lg font-medium disabled:opacity-50" className="flex-1"
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
> >
Create Create
</button> </Button>
<button <Button
variant="secondary"
onClick={() => queue.setShowCreateCollection(false)} onClick={() => queue.setShowCreateCollection(false)}
className="flex-1 px-4 py-2 rounded-lg border font-medium" className="flex-1"
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
> >
Cancel Cancel
</button> </Button>
</div> </div>
</div> </Modal>
</div>
)}
{showOCRSettings && <OCRSettings onClose={onCloseOCRSettings} />} {showOCRSettings && <OCRSettings onClose={onCloseOCRSettings} />}
</div> </div>

View file

@ -1,8 +1,9 @@
/* eslint-disable @next/next/no-img-element -- Local preview data URLs; next/image migration is out of scope. */ /* eslint-disable @next/next/no-img-element -- Local preview data URLs; next/image migration is out of scope. */
import { useState } from 'react'; import { useState } from 'react';
import { Modal, Input, Button } from './ui';
export default function UploadImageModal({ isOpen, onClose, onUpload, currentImage }) { export default function UploadImageModal({ isOpen, onClose, onUpload, currentImage }) {
const [uploadMethod, setUploadMethod] = useState('url'); // 'url' or 'file' const [uploadMethod, setUploadMethod] = useState('url');
const [imageUrl, setImageUrl] = useState(currentImage || ''); const [imageUrl, setImageUrl] = useState(currentImage || '');
const [dragActive, setDragActive] = useState(false); const [dragActive, setDragActive] = useState(false);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@ -25,9 +26,9 @@ export default function UploadImageModal({ isOpen, onClose, onUpload, currentIma
const handleDrag = (e) => { const handleDrag = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") { if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true); setDragActive(true);
} else if (e.type === "dragleave") { } else if (e.type === 'dragleave') {
setDragActive(false); setDragActive(false);
} }
}; };
@ -36,7 +37,6 @@ export default function UploadImageModal({ isOpen, onClose, onUpload, currentIma
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setDragActive(false); setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) { if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileUpload(e.dataTransfer.files[0]); handleFileUpload(e.dataTransfer.files[0]);
} }
@ -45,8 +45,8 @@ export default function UploadImageModal({ isOpen, onClose, onUpload, currentIma
const handleFileUpload = (file) => { const handleFileUpload = (file) => {
if (file.type.startsWith('image/')) { if (file.type.startsWith('image/')) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (event) => {
setImageUrl(e.target.result); setImageUrl(event.target.result);
setUploadMethod('file'); setUploadMethod('file');
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
@ -59,46 +59,45 @@ export default function UploadImageModal({ isOpen, onClose, onUpload, currentIma
} }
}; };
if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <Modal open={isOpen} onClose={onClose} title="Upload Hero Image" size="md">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-gray-900">Upload Hero Image</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{/* Upload Method Tabs */} <div
<div className="flex space-x-1 mb-6 bg-gray-100 rounded-lg p-1"> className="flex space-x-1 mb-6 rounded-lg p-1"
style={{
backgroundColor: 'var(--bg-tertiary)',
border: '1px solid var(--border)',
}}
>
<button <button
type="button" type="button"
onClick={() => setUploadMethod('url')} onClick={() => setUploadMethod('url')}
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors ${ className="flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors"
style={{
backgroundColor:
uploadMethod === 'url' ? 'var(--bg-primary)' : 'transparent',
color:
uploadMethod === 'url' uploadMethod === 'url'
? 'bg-white text-gray-900 shadow-sm' ? 'var(--text-primary)'
: 'text-gray-600 hover:text-gray-900' : 'var(--text-secondary)',
}`} boxShadow: uploadMethod === 'url' ? 'var(--elevation-ambient)' : 'none',
}}
> >
Image URL Image URL
</button> </button>
<button <button
type="button" type="button"
onClick={() => setUploadMethod('file')} onClick={() => setUploadMethod('file')}
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors ${ className="flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors"
style={{
backgroundColor:
uploadMethod === 'file' ? 'var(--bg-primary)' : 'transparent',
color:
uploadMethod === 'file' uploadMethod === 'file'
? 'bg-white text-gray-900 shadow-sm' ? 'var(--text-primary)'
: 'text-gray-600 hover:text-gray-900' : 'var(--text-secondary)',
}`} boxShadow: uploadMethod === 'file' ? 'var(--elevation-ambient)' : 'none',
}}
> >
Upload File Upload File
</button> </button>
@ -106,38 +105,58 @@ export default function UploadImageModal({ isOpen, onClose, onUpload, currentIma
{uploadMethod === 'url' ? ( {uploadMethod === 'url' ? (
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2"> <Input
Image URL label="Image URL"
</label>
<input
type="url" type="url"
value={imageUrl} value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)} onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://example.com/image.jpg" placeholder="https://example.com/image.jpg"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/> />
</div> </div>
) : ( ) : (
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
>
Upload Image Upload Image
</label> </label>
<div <div
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${ className="border-2 border-dashed rounded-lg p-6 text-center transition-colors"
dragActive style={{
? 'border-purple-500 bg-purple-50' borderColor: dragActive ? 'var(--accent-ember)' : 'var(--border)',
: 'border-gray-300 hover:border-gray-400' backgroundColor: dragActive ? 'rgba(var(--ember-rim-subtle), 0.08)' : 'transparent',
}`} }}
onDragEnter={handleDrag} onDragEnter={handleDrag}
onDragLeave={handleDrag} onDragLeave={handleDrag}
onDragOver={handleDrag} onDragOver={handleDrag}
onDrop={handleDrop} onDrop={handleDrop}
> >
<svg className="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> className="w-12 h-12 mx-auto mb-4"
style={{ color: 'var(--text-tertiary)' }}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg> </svg>
<p className="text-gray-600 mb-2">Drag and drop an image here, or</p> <p className="mb-2" style={{ color: 'var(--text-secondary)' }}>
<label className="inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 cursor-pointer transition-colors"> Drag and drop an image here, or
</p>
<label
className="inline-block px-4 py-2 rounded-lg cursor-pointer transition-colors"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white',
}}
>
Choose File Choose File
<input <input
type="file" type="file"
@ -146,54 +165,53 @@ export default function UploadImageModal({ isOpen, onClose, onUpload, currentIma
className="hidden" className="hidden"
/> />
</label> </label>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, GIF up to 10MB</p> <p className="text-xs mt-2" style={{ color: 'var(--text-tertiary)' }}>
PNG, JPG, GIF up to 10MB
</p>
</div> </div>
</div> </div>
)} )}
{/* Preview */}
{imageUrl && ( {imageUrl && (
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label
className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
>
Preview Preview
</label> </label>
<div className="relative rounded-lg overflow-hidden bg-gray-100"> <div
className="relative rounded-lg overflow-hidden"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
>
<img <img
src={imageUrl} src={imageUrl}
alt="Preview" alt="Preview"
className="w-full h-32 object-cover" className="w-full h-32 object-cover"
onError={(e) => { onError={(e) => {
e.target.src = 'https://via.placeholder.com/400x128/f3f4f6/6b7280?text=Invalid+Image'; e.target.src =
'https://via.placeholder.com/400x128/f3f4f6/6b7280?text=Invalid+Image';
}} }}
/> />
</div> </div>
</div> </div>
)} )}
{/* Actions */}
<div className="flex space-x-3"> <div className="flex space-x-3">
<button <Button type="button" variant="secondary" onClick={onClose} className="flex-1">
type="button"
onClick={onClose}
className="flex-1 py-2 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
>
Cancel Cancel
</button> </Button>
<button <Button
type="submit" type="submit"
variant="primary"
disabled={!imageUrl.trim() || uploading} disabled={!imageUrl.trim() || uploading}
className={`flex-1 py-2 px-4 rounded-lg font-medium transition-colors ${ loading={uploading}
!imageUrl.trim() || uploading className="flex-1"
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
> >
{uploading ? 'Uploading...' : 'Upload Image'} {uploading ? 'Uploading...' : 'Upload Image'}
</button> </Button>
</div> </div>
</form> </form>
</div> </Modal>
</div>
</div>
); );
} }

View file

@ -147,7 +147,7 @@ const CardImport = () => {
</button> </button>
<button <button
onClick={() => router.push('/admin/card-import')} onClick={() => router.push('/admin/card-import')}
className="px-4 py-2 rounded-lg font-medium gradient-bg-purple text-white" className="px-4 py-2 rounded-lg font-medium gradient-bg-ember text-white"
> >
📥 Card Import 📥 Card Import
</button> </button>
@ -184,7 +184,7 @@ const CardImport = () => {
className={`shrink-0 py-3 px-6 rounded-xl font-medium transition-all duration-200 ${ className={`shrink-0 py-3 px-6 rounded-xl font-medium transition-all duration-200 ${
isSyncing isSyncing
? 'opacity-50 cursor-not-allowed' ? 'opacity-50 cursor-not-allowed'
: 'gradient-bg-purple text-white hover:shadow-lg' : 'gradient-bg-ember text-white hover:shadow-lg'
}`} }`}
> >
{isSyncing ? 'Syncing catalog…' : 'Run catalog sync'} {isSyncing ? 'Syncing catalog…' : 'Run catalog sync'}
@ -307,7 +307,7 @@ const CardImport = () => {
className={`w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 ${ className={`w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 ${
isImporting || !setCode.trim() isImporting || !setCode.trim()
? 'opacity-50 cursor-not-allowed' ? 'opacity-50 cursor-not-allowed'
: 'gradient-bg-purple text-white hover:shadow-lg' : 'gradient-bg-ember text-white hover:shadow-lg'
}`} }`}
> >
{isImporting ? 'Importing...' : 'Import Cards'} {isImporting ? 'Importing...' : 'Import Cards'}

View file

@ -34,7 +34,7 @@ export default function CardDetail() {
<button <button
type="button" type="button"
onClick={() => router.push('/cards')} onClick={() => router.push('/cards')}
className="px-6 py-3 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200" className="px-6 py-3 rounded-xl font-medium gradient-bg-ember text-white hover:shadow-lg transition-all duration-200"
> >
Back to Cards Back to Cards
</button> </button>

View file

@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import Layout from '../../components/Layout'; import Layout from '../../components/Layout';
import PermissionIndicator from '../../components/PermissionIndicator'; import PermissionIndicator from '../../components/PermissionIndicator';
import { Button } from '../../components/ui';
import { useAuth } from '../../lib/use-auth'; import { useAuth } from '../../lib/use-auth';
import { VOCAB } from '../../lib/collection-vocabulary.js'; import { VOCAB } from '../../lib/collection-vocabulary.js';
@ -252,14 +253,9 @@ export default function CommunityCollections() {
</p> </p>
{!searchQuery && ( {!searchQuery && (
<Link href="/collections"> <Link href="/collections">
<button className="px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:shadow-md" <Button variant="primary" size="lg">
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Go to My Lists Go to My Lists
</button> </Button>
</Link> </Link>
)} )}
</div> </div>

View file

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Layout from '../components/Layout'; import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator'; import PermissionIndicator from '../components/PermissionIndicator';
import { Button } from '../components/ui';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import Link from 'next/link'; import Link from 'next/link';
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js'; import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
@ -91,14 +92,17 @@ export default function Dashboard() {
</div> </div>
<div className="flex space-x-2 sm:space-x-4"> <div className="flex space-x-2 sm:space-x-4">
<Link href="/collections"> <Link href="/collections">
<button className="px-4 py-2 text-sm font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}> <Button
<div className="flex items-center space-x-2"> variant="primary"
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> size="sm"
leadingIcon={
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg> </svg>
<span>Create List</span> }
</div> >
</button> Create List
</Button>
</Link> </Link>
</div> </div>
</div> </div>
@ -176,9 +180,9 @@ export default function Dashboard() {
Create your first list to start organizing your cards Create your first list to start organizing your cards
</p> </p>
<Link href="/collections"> <Link href="/collections">
<button className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}> <Button variant="primary" size="lg">
Create Your First List Create Your First List
</button> </Button>
</Link> </Link>
</div> </div>
) : ( ) : (
@ -222,9 +226,9 @@ export default function Dashboard() {
{collections.length > 6 && ( {collections.length > 6 && (
<div className="text-center"> <div className="text-center">
<Link href="/collections"> <Link href="/collections">
<button className="px-6 py-3 font-medium rounded-xl border-2 transition-all duration-200 hover:opacity-80" style={{ color: 'var(--text-primary)', borderColor: 'var(--accent-ember)' }}> <Button variant="secondary" size="lg">
View All Lists View All Lists
</button> </Button>
</Link> </Link>
</div> </div>
)} )}

View file

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import Layout from '../components/Layout'; import Layout from '../components/Layout';
import { Modal, Input, Button } from '../components/ui';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
export default function Decks() { export default function Decks() {
@ -299,33 +300,32 @@ export default function Decks() {
</div> </div>
)} )}
{/* Create Deck Modal */} <Modal
{showCreateModal && ( open={showCreateModal}
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> onClose={() => setShowCreateModal(false)}
<div className="bg-bg-primary rounded-lg p-6 w-full max-w-md"> title="Create New Deck"
<h2 className="text-xl font-bold text-text-primary mb-4">Create New Deck</h2> size="md"
<form onSubmit={handleCreateDeck}> >
<div className="mb-4"> <form onSubmit={handleCreateDeck} className="space-y-4">
<label className="block text-text-secondary text-sm font-medium mb-2"> <Input
Deck Name * label="Deck Name *"
</label>
<input
type="text"
required required
value={newDeck.name} value={newDeck.name}
onChange={(e) => setNewDeck({...newDeck, name: e.target.value})} onChange={(e) => setNewDeck({ ...newDeck, name: e.target.value })}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
placeholder="Enter deck name" placeholder="Enter deck name"
/> />
</div> <div>
<label
<div className="mb-4"> className="block text-sm font-medium mb-2"
<label className="block text-text-secondary text-sm font-medium mb-2"> style={{ color: 'var(--text-primary)' }}
htmlFor="create-deck-format"
>
Format Format
</label> </label>
<select <select
id="create-deck-format"
value={newDeck.format} value={newDeck.format}
onChange={(e) => setNewDeck({...newDeck, format: e.target.value})} onChange={(e) => setNewDeck({ ...newDeck, format: e.target.value })}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary" className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
> >
<option value="Commander">Commander</option> <option value="Commander">Commander</option>
@ -334,78 +334,81 @@ export default function Decks() {
<option value="Legacy">Legacy</option> <option value="Legacy">Legacy</option>
</select> </select>
</div> </div>
<div>
<div className="mb-4"> <label
<label className="block text-text-secondary text-sm font-medium mb-2"> className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="create-deck-description"
>
Description Description
</label> </label>
<textarea <textarea
id="create-deck-description"
value={newDeck.description} value={newDeck.description}
onChange={(e) => setNewDeck({...newDeck, description: e.target.value})} onChange={(e) => setNewDeck({ ...newDeck, description: e.target.value })}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary" className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
rows="3" rows="3"
placeholder="Describe your deck strategy..." placeholder="Describe your deck strategy..."
/> />
</div> </div>
<div className="mb-6">
<label className="flex items-center"> <label className="flex items-center">
<input <input
type="checkbox" type="checkbox"
checked={newDeck.is_public} checked={newDeck.is_public}
onChange={(e) => setNewDeck({...newDeck, is_public: e.target.checked})} onChange={(e) => setNewDeck({ ...newDeck, is_public: e.target.checked })}
className="mr-2" className="mr-2"
style={{ accentColor: 'var(--accent-ember)' }}
/> />
<span className="text-text-secondary text-sm">Make deck public</span> <span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Make deck public
</span>
</label> </label>
</div> <div className="flex space-x-3 pt-2">
<Button
<div className="flex space-x-3">
<button
type="button" type="button"
variant="secondary"
onClick={() => setShowCreateModal(false)} onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 border border-border rounded-lg text-text-secondary hover:bg-bg-secondary transition-colors" className="flex-1"
> >
Cancel Cancel
</button> </Button>
<button <Button type="submit" variant="primary" className="flex-1">
type="submit"
className="flex-1 bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
>
Create Deck Create Deck
</button> </Button>
</div> </div>
</form> </form>
</div> </Modal>
</div>
)}
{/* Edit Deck Modal */} <Modal
open={!!editingDeck}
onClose={() => setEditingDeck(null)}
title="Edit Deck"
size="md"
>
{editingDeck && ( {editingDeck && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <form onSubmit={handleEditDeck} className="space-y-4">
<div className="bg-bg-primary rounded-lg p-6 w-full max-w-md"> <Input
<h2 className="text-xl font-bold text-text-primary mb-4">Edit Deck</h2> label="Deck Name *"
<form onSubmit={handleEditDeck}>
<div className="mb-4">
<label className="block text-text-secondary text-sm font-medium mb-2">
Deck Name *
</label>
<input
type="text"
required required
value={editingDeck.name} value={editingDeck.name}
onChange={(e) => setEditingDeck({...editingDeck, name: e.target.value})} onChange={(e) =>
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary" setEditingDeck({ ...editingDeck, name: e.target.value })
}
/> />
</div> <div>
<label
<div className="mb-4"> className="block text-sm font-medium mb-2"
<label className="block text-text-secondary text-sm font-medium mb-2"> style={{ color: 'var(--text-primary)' }}
htmlFor="edit-deck-format"
>
Format Format
</label> </label>
<select <select
id="edit-deck-format"
value={editingDeck.format} value={editingDeck.format}
onChange={(e) => setEditingDeck({...editingDeck, format: e.target.value})} onChange={(e) =>
setEditingDeck({ ...editingDeck, format: e.target.value })
}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary" className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
> >
<option value="Commander">Commander</option> <option value="Commander">Commander</option>
@ -414,50 +417,54 @@ export default function Decks() {
<option value="Legacy">Legacy</option> <option value="Legacy">Legacy</option>
</select> </select>
</div> </div>
<div>
<div className="mb-4"> <label
<label className="block text-text-secondary text-sm font-medium mb-2"> className="block text-sm font-medium mb-2"
style={{ color: 'var(--text-primary)' }}
htmlFor="edit-deck-description"
>
Description Description
</label> </label>
<textarea <textarea
id="edit-deck-description"
value={editingDeck.description || ''} value={editingDeck.description || ''}
onChange={(e) => setEditingDeck({...editingDeck, description: e.target.value})} onChange={(e) =>
setEditingDeck({ ...editingDeck, description: e.target.value })
}
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary" className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
rows="3" rows="3"
/> />
</div> </div>
<div className="mb-6">
<label className="flex items-center"> <label className="flex items-center">
<input <input
type="checkbox" type="checkbox"
checked={editingDeck.is_public} checked={editingDeck.is_public}
onChange={(e) => setEditingDeck({...editingDeck, is_public: e.target.checked})} onChange={(e) =>
setEditingDeck({ ...editingDeck, is_public: e.target.checked })
}
className="mr-2" className="mr-2"
style={{ accentColor: 'var(--accent-ember)' }}
/> />
<span className="text-text-secondary text-sm">Make deck public</span> <span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Make deck public
</span>
</label> </label>
</div> <div className="flex space-x-3 pt-2">
<Button
<div className="flex space-x-3">
<button
type="button" type="button"
variant="secondary"
onClick={() => setEditingDeck(null)} onClick={() => setEditingDeck(null)}
className="flex-1 px-4 py-2 border border-border rounded-lg text-text-secondary hover:bg-bg-secondary transition-colors" className="flex-1"
> >
Cancel Cancel
</button> </Button>
<button <Button type="submit" variant="primary" className="flex-1">
type="submit"
className="flex-1 bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
>
Save Changes Save Changes
</button> </Button>
</div> </div>
</form> </form>
</div>
</div>
)} )}
</Modal>
</div> </div>
</Layout> </Layout>
); );

View file

@ -4,6 +4,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import { useAuth } from '../lib/use-auth.js'; import { useAuth } from '../lib/use-auth.js';
import AnimatedFireLogo from '../components/AnimatedFireLogo'; import AnimatedFireLogo from '../components/AnimatedFireLogo';
import { Button, GlassSurface } from '../components/ui';
export default function Home() { export default function Home() {
const { user, loading } = useAuth(); const { user, loading } = useAuth();
@ -11,14 +12,12 @@ export default function Home() {
const [featuredCollections, setFeaturedCollections] = useState([]); const [featuredCollections, setFeaturedCollections] = useState([]);
const [collectionsLoading, setCollectionsLoading] = useState(true); const [collectionsLoading, setCollectionsLoading] = useState(true);
// If user is logged in, redirect to dashboard
useEffect(() => { useEffect(() => {
if (!loading && user) { if (!loading && user) {
router.push('/dashboard'); router.push('/dashboard');
} }
}, [user, loading, router]); }, [user, loading, router]);
// Fetch featured collections for public display
useEffect(() => { useEffect(() => {
const fetchFeaturedCollections = async () => { const fetchFeaturedCollections = async () => {
try { try {
@ -33,31 +32,44 @@ export default function Home() {
setCollectionsLoading(false); setCollectionsLoading(false);
} }
}; };
fetchFeaturedCollections(); fetchFeaturedCollections();
}, []); }, []);
// Show loading while checking auth
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: 'var(--bg-primary)' }}> <div
className="min-h-screen flex items-center justify-center"
style={{ backgroundColor: 'var(--bg-primary)' }}
>
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 mx-auto mb-4" style={{ borderColor: 'var(--accent-ember)' }}></div> <div
className="motion-essential animate-spin rounded-full h-12 w-12 border-b-2 mx-auto mb-4"
style={{ borderColor: 'var(--accent-ember)' }}
/>
<p style={{ color: 'var(--text-secondary)' }}>Loading...</p> <p style={{ color: 'var(--text-secondary)' }}>Loading...</p>
</div> </div>
</div> </div>
); );
} }
// If user is logged in, don't show landing page (redirect handled above)
if (user) { if (user) {
return null; return null;
} }
return ( return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}> <div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}>
{/* Navigation Bar */} <nav
<nav className="border-b" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}> className="border-b"
style={{
background: 'var(--glass-surface-mid)',
backdropFilter:
'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
WebkitBackdropFilter:
'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
borderColor: 'var(--border)',
boxShadow: 'inset 0 1px 0 var(--rim-light-inner)',
}}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4"> <div className="flex justify-between items-center py-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@ -65,29 +77,21 @@ export default function Home() {
<h1 className="text-2xl font-bold gradient-text-flame">Deck Hearth</h1> <h1 className="text-2xl font-bold gradient-text-flame">Deck Hearth</h1>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Link <Link href="/login">
href="/login" <Button variant="ghost" size="sm">
className="px-4 py-2 text-sm font-medium rounded-xl transition-all duration-200 hover:opacity-80"
style={{ color: 'var(--text-secondary)' }}
>
Sign In Sign In
</Button>
</Link> </Link>
<Link <Link href="/signup">
href="/signup" <Button variant="primary" size="sm">
className="px-6 py-2 text-sm font-medium rounded-xl transition-all duration-200 hover:opacity-90"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Get Started Get Started
</Button>
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>
{/* Hero Section */}
<section className="relative py-20 px-4"> <section className="relative py-20 px-4">
<div className="max-w-6xl mx-auto text-center"> <div className="max-w-6xl mx-auto text-center">
<div className="mb-8"> <div className="mb-8">
@ -96,86 +100,136 @@ export default function Home() {
<h1 className="text-5xl md:text-7xl font-bold mb-6"> <h1 className="text-5xl md:text-7xl font-bold mb-6">
<span className="gradient-text-flame">Deck Hearth</span> <span className="gradient-text-flame">Deck Hearth</span>
</h1> </h1>
<p className="text-xl md:text-2xl mb-8 max-w-3xl mx-auto leading-relaxed" style={{ color: 'var(--text-secondary)' }}> <p
className="text-xl md:text-2xl mb-8 max-w-3xl mx-auto leading-relaxed"
style={{ color: 'var(--text-secondary)' }}
>
The ultimate hub for trading card collectors. Organize your cards into lists, The ultimate hub for trading card collectors. Organize your cards into lists,
discover rare cards, and connect with fellow enthusiasts in one beautiful platform. discover rare cards, and connect with fellow enthusiasts in one beautiful
platform.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link <Link href="/signup">
href="/signup" <Button variant="primary" size="lg">
className="px-8 py-4 text-lg font-medium rounded-2xl transition-all duration-200 hover:opacity-90 hover:scale-105"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Start Your Collection Start Your Collection
</Button>
</Link> </Link>
<Link <Link href="/community/collections">
href="/community/collections" <Button variant="secondary" size="lg">
className="px-8 py-4 text-lg font-medium rounded-2xl border-2 transition-all duration-200 hover:opacity-80"
style={{
color: 'var(--text-primary)',
borderColor: 'var(--accent-ember)'
}}
>
Explore Lists Explore Lists
</Button>
</Link> </Link>
</div> </div>
</div> </div>
</section> </section>
{/* Features Section */}
<section className="py-20 px-4" style={{ backgroundColor: 'var(--bg-secondary)' }}> <section className="py-20 px-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<h2 className="text-4xl font-bold text-center mb-16 gradient-text-gold"> <h2 className="text-4xl font-bold text-center mb-16 gradient-text-gold">
Everything You Need to Manage Your Cards Everything You Need to Manage Your Cards
</h2> </h2>
<div className="grid md:grid-cols-3 gap-8"> <div className="grid md:grid-cols-3 gap-8">
<div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}> <GlassSurface
<div className="w-16 h-16 mx-auto mb-6 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--accent-ember)' }}> tint="mid"
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> rim="subtle"
<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" /> elevation="ambient"
className="text-center p-8 rounded-2xl"
>
<div
className="w-16 h-16 mx-auto mb-6 rounded-2xl flex items-center justify-center"
style={{ backgroundColor: 'var(--accent-ember)' }}
>
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg> </svg>
</div> </div>
<h3 className="text-xl font-bold mb-4 gradient-text-flame">Organize Lists</h3> <h3 className="text-xl font-bold mb-4 gradient-text-flame">Organize Lists</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Create custom lists, track card values, and organize by sets, rarity, or any system that works for you. Create custom lists, track card values, and organize by sets, rarity, or any
system that works for you.
</p> </p>
</div> </GlassSurface>
<div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}> <GlassSurface
<div className="w-16 h-16 mx-auto mb-6 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--accent-gold)' }}> tint="mid"
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> rim="subtle"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> elevation="ambient"
className="text-center p-8 rounded-2xl"
>
<div
className="w-16 h-16 mx-auto mb-6 rounded-2xl flex items-center justify-center"
style={{ backgroundColor: 'var(--accent-gold)' }}
>
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg> </svg>
</div> </div>
<h3 className="text-xl font-bold mb-4 gradient-text-gold">Discover Cards</h3> <h3 className="text-xl font-bold mb-4 gradient-text-gold">Discover Cards</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Search through thousands of cards across multiple TCGs. Find that missing piece for your binder. Search through thousands of cards across multiple TCGs. Find that missing
piece for your binder.
</p> </p>
</div> </GlassSurface>
<div className="text-center p-8 rounded-2xl" style={{ backgroundColor: 'var(--bg-primary)' }}> <GlassSurface
<div className="w-16 h-16 mx-auto mb-6 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--accent-flame)' }}> tint="mid"
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> rim="subtle"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> elevation="ambient"
className="text-center p-8 rounded-2xl"
>
<div
className="w-16 h-16 mx-auto mb-6 rounded-2xl flex items-center justify-center"
style={{ backgroundColor: 'var(--accent-flame)' }}
>
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg> </svg>
</div> </div>
<h3 className="text-xl font-bold mb-4 gradient-text-ember">Connect & Share</h3> <h3 className="text-xl font-bold mb-4 gradient-text-ember">Connect & Share</h3>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
Share your lists with the community, collaborate with friends, and discover amazing lists from other collectors. Share your lists with the community, collaborate with friends, and discover
amazing lists from other collectors.
</p> </p>
</div> </GlassSurface>
</div> </div>
</div> </div>
</section> </section>
{/* Featured Collections Section */}
<section className="py-20 px-4"> <section className="py-20 px-4">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4 gradient-text-flame"> <h2 className="text-4xl font-bold mb-4 gradient-text-flame">Featured Lists</h2>
Featured Lists
</h2>
<p className="text-xl" style={{ color: 'var(--text-secondary)' }}> <p className="text-xl" style={{ color: 'var(--text-secondary)' }}>
Discover amazing lists from our community Discover amazing lists from our community
</p> </p>
@ -184,13 +238,32 @@ export default function Home() {
{collectionsLoading ? ( {collectionsLoading ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div key={i} className="rounded-2xl p-6 animate-pulse" style={{ backgroundColor: 'var(--bg-secondary)' }}> <div
<div className="h-32 rounded-xl mb-4" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div> key={i}
<div className="h-6 rounded mb-2" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div> className="motion-essential rounded-2xl p-6 animate-pulse"
<div className="h-4 rounded mb-4" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div> style={{ backgroundColor: 'var(--bg-secondary)' }}
>
<div
className="h-32 rounded-xl mb-4"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
/>
<div
className="h-6 rounded mb-2"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
/>
<div
className="h-4 rounded mb-4"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
/>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="h-4 w-16 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div> <div
<div className="h-4 w-20 rounded" style={{ backgroundColor: 'var(--bg-tertiary)' }}></div> className="h-4 w-16 rounded"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
/>
<div
className="h-4 w-20 rounded"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
/>
</div> </div>
</div> </div>
))} ))}
@ -201,10 +274,21 @@ export default function Home() {
<Link <Link
key={collection.id} key={collection.id}
href={`/collection/${collection.slug || collection.id}`} href={`/collection/${collection.slug || collection.id}`}
className="block rounded-2xl p-6 transition-all duration-200 hover:scale-105 hover:shadow-xl"
style={{ backgroundColor: 'var(--bg-secondary)' }}
> >
<div className="h-32 rounded-xl mb-4 flex items-center justify-center" style={{ backgroundColor: 'var(--bg-tertiary)' }}> <GlassSurface
tint="mid"
rim="subtle"
elevation="ambient"
className="block rounded-2xl p-6 hover:scale-105"
style={{
transition:
'transform var(--motion-duration-default) var(--motion-ease-out), box-shadow var(--motion-duration-default) var(--motion-ease-out)',
}}
>
<div
className="h-32 rounded-xl mb-4 flex items-center justify-center"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
>
{collection.image ? ( {collection.image ? (
<img <img
src={collection.image} src={collection.image}
@ -213,86 +297,112 @@ export default function Home() {
/> />
) : ( ) : (
<div className="text-center"> <div className="text-center">
<div className="w-12 h-12 mx-auto mb-2 rounded-xl flex items-center justify-center" style={{ backgroundColor: 'var(--accent-ember)' }}> <div
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> className="w-12 h-12 mx-auto mb-2 rounded-xl flex items-center justify-center"
<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" /> style={{ backgroundColor: 'var(--accent-ember)' }}
>
<svg
className="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg> </svg>
</div> </div>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{collection.cardCount} cards</p> <p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{collection.cardCount} cards
</p>
</div> </div>
)} )}
</div> </div>
<h3 className="text-xl font-bold mb-2 gradient-text-flame">{collection.name}</h3> <h3 className="text-xl font-bold mb-2 gradient-text-flame">
<p className="text-sm mb-4 line-clamp-2" style={{ color: 'var(--text-secondary)' }}> {collection.name}
</h3>
<p
className="text-sm mb-4 line-clamp-2"
style={{ color: 'var(--text-secondary)' }}
>
{collection.description || 'A carefully curated list'} {collection.description || 'A carefully curated list'}
</p> </p>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> <span
className="text-sm font-medium"
style={{ color: 'var(--text-primary)' }}
>
{collection.cardCount} cards {collection.cardCount} cards
</span> </span>
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}> <span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
by {collection.creator} by {collection.creator}
</span> </span>
</div> </div>
</GlassSurface>
</Link> </Link>
))} ))}
</div> </div>
)} )}
<div className="text-center mt-12"> <div className="text-center mt-12">
<Link <Link href="/community/collections">
href="/community/collections" <Button variant="secondary" size="lg">
className="inline-flex items-center px-6 py-3 text-lg font-medium rounded-xl transition-all duration-200 hover:opacity-80"
style={{
color: 'var(--accent-ember)',
border: '2px solid var(--accent-ember)'
}}
>
View All Lists View All Lists
<svg className="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" /> className="w-5 h-5 ml-2 inline-block"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg> </svg>
</Button>
</Link> </Link>
</div> </div>
</div> </div>
</section> </section>
{/* CTA Section */}
<section className="py-20 px-4" style={{ backgroundColor: 'var(--bg-secondary)' }}> <section className="py-20 px-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="max-w-4xl mx-auto text-center"> <div className="max-w-4xl mx-auto text-center">
<h2 className="text-4xl font-bold mb-6 gradient-text-flame"> <h2 className="text-4xl font-bold mb-6 gradient-text-flame">
Ready to Start Your Journey? Ready to Start Your Journey?
</h2> </h2>
<p className="text-xl mb-8" style={{ color: 'var(--text-secondary)' }}> <p className="text-xl mb-8" style={{ color: 'var(--text-secondary)' }}>
Join thousands of collectors who trust Deck Hearth to manage their cards and lists. Join thousands of collectors who trust Deck Hearth to manage their cards and
lists.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link <Link href="/signup">
href="/signup" <Button variant="primary" size="lg">
className="px-8 py-4 text-lg font-medium rounded-2xl transition-all duration-200 hover:opacity-90 hover:scale-105"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Create Free Account Create Free Account
</Button>
</Link> </Link>
<Link <Link href="/cards">
href="/cards" <Button variant="secondary" size="lg">
className="px-8 py-4 text-lg font-medium rounded-2xl border-2 transition-all duration-200 hover:opacity-80"
style={{
color: 'var(--text-primary)',
borderColor: 'var(--accent-ember)'
}}
>
Browse Cards Browse Cards
</Button>
</Link> </Link>
</div> </div>
</div> </div>
</section> </section>
{/* Footer */} <footer
<footer className="py-12 px-4 border-t" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border)' }}> className="py-12 px-4 border-t"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
}}
>
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-center"> <div className="flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center space-x-3 mb-4 md:mb-0"> <div className="flex items-center space-x-3 mb-4 md:mb-0">
@ -300,18 +410,33 @@ export default function Home() {
<span className="text-xl font-bold gradient-text-flame">Deck Hearth</span> <span className="text-xl font-bold gradient-text-flame">Deck Hearth</span>
</div> </div>
<div className="flex space-x-6"> <div className="flex space-x-6">
<Link href="/community/collections" className="hover:opacity-80 transition-opacity" style={{ color: 'var(--text-secondary)' }}> <Link
href="/community/collections"
className="hover:opacity-80 transition-opacity"
style={{ color: 'var(--text-secondary)' }}
>
Community Community
</Link> </Link>
<Link href="/cards" className="hover:opacity-80 transition-opacity" style={{ color: 'var(--text-secondary)' }}> <Link
href="/cards"
className="hover:opacity-80 transition-opacity"
style={{ color: 'var(--text-secondary)' }}
>
Cards Cards
</Link> </Link>
<Link href="/login" className="hover:opacity-80 transition-opacity" style={{ color: 'var(--text-secondary)' }}> <Link
href="/login"
className="hover:opacity-80 transition-opacity"
style={{ color: 'var(--text-secondary)' }}
>
Sign In Sign In
</Link> </Link>
</div> </div>
</div> </div>
<div className="mt-8 pt-8 border-t text-center" style={{ borderColor: 'var(--border)' }}> <div
className="mt-8 pt-8 border-t text-center"
style={{ borderColor: 'var(--border)' }}
>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
© 2024 Deck Hearth. Built for collectors, by collectors. © 2024 Deck Hearth. Built for collectors, by collectors.
</p> </p>

View file

@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Layout from '../../components/Layout'; import Layout from '../../components/Layout';
import { GlassSurface, Button } from '../../components/ui';
export default function AcceptInvite() { export default function AcceptInvite() {
const router = useRouter(); const router = useRouter();
@ -49,8 +50,14 @@ export default function AcceptInvite() {
<Layout user={null}> <Layout user={null}>
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 mx-auto mb-4" style={{ borderColor: 'var(--text-accent)' }}></div> <div
<h2 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> className="motion-essential animate-spin rounded-full h-16 w-16 border-b-2 mx-auto mb-4"
style={{ borderColor: 'var(--accent-ember)' }}
/>
<h2
className="text-xl font-semibold mb-2"
style={{ color: 'var(--text-primary)' }}
>
Processing Invitation... Processing Invitation...
</h2> </h2>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
@ -67,61 +74,77 @@ export default function AcceptInvite() {
<div className="min-h-screen flex items-center justify-center px-4"> <div className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md w-full"> <div className="max-w-md w-full">
{result ? ( {result ? (
<div className="text-center p-8 rounded-2xl shadow-lg" style={{ backgroundColor: 'var(--bg-secondary)' }}> <GlassSurface
<div className="text-6xl mb-4">🎉</div> tint="mid"
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> rim="subtle"
elevation="pronounced"
className="text-center p-8 rounded-2xl"
>
<div className="text-6xl mb-4" aria-hidden="true">🎉</div>
<h2
className="text-2xl font-bold mb-4"
style={{ color: 'var(--text-primary)' }}
>
Invitation Accepted! Invitation Accepted!
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
You now have access to the list &quot;{result.collection.name}&quot;. You now have access to the list &quot;{result.collection.name}&quot;.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<button <Button
variant="primary"
size="lg"
onClick={() => router.push(`/collection/${result.collection.id}`)} onClick={() => router.push(`/collection/${result.collection.id}`)}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full"
> >
View List View List
</button> </Button>
<button <Button
variant="secondary"
size="lg"
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
className="w-full px-6 py-3 rounded-lg font-medium border transition-all duration-200" className="w-full"
style={{
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
> >
View All Lists View All Lists
</button> </Button>
</div>
</div> </div>
</GlassSurface>
) : ( ) : (
<div className="text-center p-8 rounded-2xl shadow-lg" style={{ backgroundColor: 'var(--bg-secondary)' }}> <GlassSurface
<div className="text-6xl mb-4"></div> tint="mid"
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> rim="subtle"
elevation="pronounced"
className="text-center p-8 rounded-2xl"
>
<div className="text-6xl mb-4" aria-hidden="true"></div>
<h2
className="text-2xl font-bold mb-4"
style={{ color: 'var(--text-primary)' }}
>
Invitation Error Invitation Error
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
{error} {error}
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<button <Button
variant="primary"
size="lg"
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full"
> >
Browse Lists Browse Lists
</button> </Button>
<button <Button
variant="secondary"
size="lg"
onClick={() => router.push('/')} onClick={() => router.push('/')}
className="w-full px-6 py-3 rounded-lg font-medium border transition-all duration-200" className="w-full"
style={{
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
> >
Go Home Go Home
</button> </Button>
</div>
</div> </div>
</GlassSurface>
)} )}
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Layout from '../../components/Layout'; import Layout from '../../components/Layout';
import { GlassSurface, Button } from '../../components/ui';
export default function DeclineInvite() { export default function DeclineInvite() {
const router = useRouter(); const router = useRouter();
@ -49,8 +50,14 @@ export default function DeclineInvite() {
<Layout user={null}> <Layout user={null}>
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 mx-auto mb-4" style={{ borderColor: 'var(--text-accent)' }}></div> <div
<h2 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}> className="motion-essential animate-spin rounded-full h-16 w-16 border-b-2 mx-auto mb-4"
style={{ borderColor: 'var(--accent-ember)' }}
/>
<h2
className="text-xl font-semibold mb-2"
style={{ color: 'var(--text-primary)' }}
>
Processing Response... Processing Response...
</h2> </h2>
<p style={{ color: 'var(--text-secondary)' }}> <p style={{ color: 'var(--text-secondary)' }}>
@ -67,62 +74,79 @@ export default function DeclineInvite() {
<div className="min-h-screen flex items-center justify-center px-4"> <div className="min-h-screen flex items-center justify-center px-4">
<div className="max-w-md w-full"> <div className="max-w-md w-full">
{result ? ( {result ? (
<div className="text-center p-8 rounded-2xl shadow-lg" style={{ backgroundColor: 'var(--bg-secondary)' }}> <GlassSurface
<div className="text-6xl mb-4">👋</div> tint="mid"
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> rim="subtle"
elevation="pronounced"
className="text-center p-8 rounded-2xl"
>
<div className="text-6xl mb-4" aria-hidden="true">👋</div>
<h2
className="text-2xl font-bold mb-4"
style={{ color: 'var(--text-primary)' }}
>
Invitation Declined Invitation Declined
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
You have declined the invitation to &quot;{result.collection.name}&quot;. You have declined the invitation to &quot;{result.collection.name}&quot;.
You can always ask the list owner for another invitation if you change your mind. You can always ask the list owner for another invitation if you change
your mind.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<button <Button
variant="primary"
size="lg"
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full"
> >
Browse Public Lists Browse Public Lists
</button> </Button>
<button <Button
variant="secondary"
size="lg"
onClick={() => router.push('/')} onClick={() => router.push('/')}
className="w-full px-6 py-3 rounded-lg font-medium border transition-all duration-200" className="w-full"
style={{
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
> >
Go Home Go Home
</button> </Button>
</div>
</div> </div>
</GlassSurface>
) : ( ) : (
<div className="text-center p-8 rounded-2xl shadow-lg" style={{ backgroundColor: 'var(--bg-secondary)' }}> <GlassSurface
<div className="text-6xl mb-4"></div> tint="mid"
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}> rim="subtle"
elevation="pronounced"
className="text-center p-8 rounded-2xl"
>
<div className="text-6xl mb-4" aria-hidden="true"></div>
<h2
className="text-2xl font-bold mb-4"
style={{ color: 'var(--text-primary)' }}
>
Error Processing Response Error Processing Response
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
{error} {error}
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
<button <Button
variant="primary"
size="lg"
onClick={() => router.push('/collections')} onClick={() => router.push('/collections')}
className="w-full px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200" className="w-full"
> >
Browse Lists Browse Lists
</button> </Button>
<button <Button
variant="secondary"
size="lg"
onClick={() => router.push('/')} onClick={() => router.push('/')}
className="w-full px-6 py-3 rounded-lg font-medium border transition-all duration-200" className="w-full"
style={{
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
> >
Go Home Go Home
</button> </Button>
</div>
</div> </div>
</GlassSurface>
)} )}
</div> </div>
</div> </div>

View file

@ -7,6 +7,7 @@ import BulkSelectionToolbar from '../components/BulkSelectionToolbar';
import CollectionSelectionModal from '../components/CollectionSelectionModal'; import CollectionSelectionModal from '../components/CollectionSelectionModal';
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols'; import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings'; import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { Button } from '../components/ui';
import { useAuth } from '../lib/use-auth'; import { useAuth } from '../lib/use-auth';
import { VOCAB } from '../lib/collection-vocabulary.js'; import { VOCAB } from '../lib/collection-vocabulary.js';
@ -441,9 +442,9 @@ export default function MyCards() {
{searchQuery ? 'Try adjusting your search filters' : `Start adding cards to ${VOCAB.MY_COLLECTION} by browsing available cards`} {searchQuery ? 'Try adjusting your search filters' : `Start adding cards to ${VOCAB.MY_COLLECTION} by browsing available cards`}
</p> </p>
<Link href="/cards"> <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' }}> <Button variant="primary" size="lg">
Browse All Cards Browse All Cards
</button> </Button>
</Link> </Link>
</div> </div>
)} )}

View file

@ -426,34 +426,11 @@ body {
border: 1px solid var(--border); border: 1px solid var(--border);
} }
/* Glowing effects for dark theme */ /* Gradient text effects Deck Hearth warm palette only.
[data-theme="dark"] .glow-blue { The pre-Deck-Hearth aliases (.glow-blue / .glow-purple / .glow-pink,
box-shadow: 0 0 20px rgba(6, 182, 212, 0.3); .gradient-text-blue / .gradient-text-purple, .gradient-bg-purple)
} were removed by the finish-liquid-glass-design PR after sweeping
their last consumers to --accent-ember equivalents. */
[data-theme="dark"] .glow-purple {
box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
}
[data-theme="dark"] .glow-pink {
box-shadow: 0 0 20px rgba(236, 72, 153, 0.3);
}
/* Gradient text effects */
.gradient-text-blue {
background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gradient-text-purple {
background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gradient-text-gold { .gradient-text-gold {
background: linear-gradient(135deg, #ffab40 0%, #ff6f00 100%); background: linear-gradient(135deg, #ffab40 0%, #ff6f00 100%);
-webkit-background-clip: text; -webkit-background-clip: text;