refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive (#117) (#117)
Comprehensive design sweep across the rest of the app following the shipped Liquid Glass + corner-border-light system (#116). ## Three classes of finding ### 1. Broken Tailwind token classes (HIGH — pages were unstyled) The decks / deck-builder / deck-detail cluster relied on Tailwind classes that don't exist in `tailwind.config.js` (no `bg-bg-*`, `text-text-*`, `border-border`, `bg-accent-ember`, `focus:ring-accent-ember`, `hover:bg-accent-ember-dark`). Those classes produced ZERO CSS — backgrounds were transparent, borders invisible, hover states absent. Rewrote with inline `style={{ ... CSS vars ... }}` + the `<Button>` / `<SearchBar>` primitives + `glass-panel` surfaces: - `pages/decks.js` (full page) - `pages/deck/[id].js` (header, stats sidebar, group-by controls, card list) - `pages/deck-builder.js` (loading spinner) - `components/DeckBuilderView.js` (toolbar + main panel) - `components/DeckBuilderCardBrowser.js` (full rewrite; integrated `<SearchBar>` for the card-picker input) - `components/DeckBuilderDeckList.js` (full rewrite) - `components/DeckBuilderStatsBar.js` - `components/ManaSymbolSettings.js` - `components/ManaSymbols.js` (single `text-text-secondary`) - `pages/admin/card-editor.js` cluster was already clean ### 2. Duplicative / stale page searches Replaced raw `<input>` search controls with the `<SearchBar>` primitive (adds clear button, ember focus ring, system-consistent rounded corners). Kept page-specific filter searches (they filter the visible list — distinct from the global TopSearchBar command palette): - `pages/my-cards.js` - `pages/community/collections.js` - `components/CardsPageView.js` - `components/CollectionPageView.js` - `components/DeckBuilderCardBrowser.js` `pages/my-cards.js` filter wrapper also lifted into a `glass-panel` chip instead of a solid `var(--bg-primary)` band. ### 3. Square corners + stale palette in shared views - `components/CollectionPageView.js`: 10 action buttons (`rounded-lg` + `hover:bg-gray-50`) → `rounded-xl` + `nav-item-hover`; 4 filter selects (`focus:ring-purple-500 rounded-lg`) → `.input-field`; view-mode toggle (`bg-white text-gray-900` — invisible in dark mode) → tokenised; SYSTEM badge gradient (`from-blue-500 to-purple-600`) → ember↔flame; tooltip (`bg-gray-900`) → `glass-panel-strong`; search-results dropdown (`bg-white border-gray-200` — invisible in dark mode) → `glass-panel-strong`; Activity / game-count / TCG-game badges palette-aligned. - `components/CardsPageView.js`: "Load More Cards" button (`bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg`) → `<Button variant="primary" size="lg">`. - `components/CollectionsPageView.js`: matching SYSTEM badge + tooltip cleanup. - `components/ShareModal.js`: user-search dropdown (`border-gray-200 hover:bg-gray-50`) and email-invite card moved onto `glass-panel` + `nav-item-hover`; social-share buttons `rounded-lg hover:bg-gray-50` → `rounded-xl nav-item-hover`. - `components/Layout.js`: profile-menu dropdown row (`hover:bg-gray-50 dark:hover:bg-gray-700`) → `nav-item-hover`. - `components/CardItem.js`: bulk-select checkbox `focus:ring-purple-500` → ember. ### 4. `dark:` modifier classes (broken with `[data-theme]` theming) This app uses `[data-theme="dark"]` CSS selector theming, not Tailwind's `class` strategy, so `dark:bg-green-900/20` etc. produced no CSS in dark mode. Affected alerts on `pages/settings.js` and `pages/profile.js` — replaced with `glass-panel` + semantic border colour (flame for success, #dc2626 for error). `pages/settings.js` sidebar nav also moved off its hardcoded full-ember fill onto the system `nav-item` / `nav-item-active` / `nav-item-hover` pattern for consistency with the global sidebar. ## Verification - `npm run build` — green (Next 16 + Turbopack) - `npm run lint` — 0 errors, 1 unrelated pre-existing warning - `npm run test:run` — 113/113 pass (no test changes needed) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c57b5d0406
commit
22364de8c9
19 changed files with 1397 additions and 1188 deletions
|
|
@ -194,7 +194,11 @@ export default function CardItem({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
className="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500 focus:ring-2"
|
className="w-4 h-4 rounded focus:ring-2"
|
||||||
|
style={{
|
||||||
|
accentColor: 'var(--accent-ember)',
|
||||||
|
'--tw-ring-color': 'var(--accent-ember)',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import CardItem from './CardItem';
|
import CardItem from './CardItem';
|
||||||
import BulkSelectionToolbar from './BulkSelectionToolbar';
|
import BulkSelectionToolbar from './BulkSelectionToolbar';
|
||||||
import CollectionSelectionModal from './CollectionSelectionModal';
|
import CollectionSelectionModal from './CollectionSelectionModal';
|
||||||
|
import { Button, SearchBar } from './ui';
|
||||||
|
|
||||||
export default function CardsPageView(props) {
|
export default function CardsPageView(props) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -146,39 +147,28 @@ export default function CardsPageView(props) {
|
||||||
<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)' }}>
|
||||||
Search
|
Search
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<SearchBar
|
||||||
<input
|
value={searchQuery}
|
||||||
type="text"
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
placeholder="Search cards..."
|
onClear={() => {
|
||||||
className="search-bar w-full pr-12"
|
handleSearchChange('');
|
||||||
value={searchQuery}
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
setCards([]);
|
||||||
onKeyPress={(e) => {
|
setHasMore(true);
|
||||||
if (e.key === 'Enter') {
|
hasMoreRef.current = true;
|
||||||
setPagination(prev => ({ ...prev, page: 1 }));
|
fetchCards(false);
|
||||||
setCards([]);
|
}}
|
||||||
setHasMore(true);
|
onKeyDown={(e) => {
|
||||||
hasMoreRef.current = true;
|
if (e.key === 'Enter') {
|
||||||
fetchCards(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setPagination(prev => ({ ...prev, page: 1 }));
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||||||
setCards([]);
|
setCards([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
hasMoreRef.current = true;
|
hasMoreRef.current = true;
|
||||||
fetchCards(false);
|
fetchCards(false);
|
||||||
}}
|
}
|
||||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 rounded-lg hover:bg-opacity-20 transition-all duration-200"
|
}}
|
||||||
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
placeholder="Search cards…"
|
||||||
>
|
/>
|
||||||
<svg className="w-4 h-4" 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>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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)' }}>
|
||||||
|
|
@ -358,12 +348,9 @@ export default function CardsPageView(props) {
|
||||||
{/* Load More Button */}
|
{/* Load More Button */}
|
||||||
{hasMore && !loadingMore && (
|
{hasMore && !loadingMore && (
|
||||||
<div className="flex justify-center py-8">
|
<div className="flex justify-center py-8">
|
||||||
<button
|
<Button variant="primary" size="lg" onClick={loadMoreCards}>
|
||||||
onClick={loadMoreCards}
|
|
||||||
className="px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg hover:from-blue-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
|
|
||||||
>
|
|
||||||
Load More Cards
|
Load More Cards
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import ManaSymbolSettings from './ManaSymbolSettings';
|
||||||
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
|
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
|
||||||
import CollectionEditModal from './CollectionEditModal';
|
import CollectionEditModal from './CollectionEditModal';
|
||||||
import CollectionDeleteModal from './CollectionDeleteModal';
|
import CollectionDeleteModal from './CollectionDeleteModal';
|
||||||
|
import { Button, SearchBar } from './ui';
|
||||||
export default function CollectionPageView(props) {
|
export default function CollectionPageView(props) {
|
||||||
const {
|
const {
|
||||||
cards,
|
cards,
|
||||||
|
|
@ -97,9 +98,9 @@ export default function CollectionPageView(props) {
|
||||||
{/* Edit and Delete buttons - only show for owner and non-system collections */}
|
{/* Edit and Delete buttons - only show for owner and non-system collections */}
|
||||||
{collection.userRole === 'owner' && !collection.isSystemCollection && (
|
{collection.userRole === 'owner' && !collection.isSystemCollection && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowEditModal(true)}
|
onClick={() => setShowEditModal(true)}
|
||||||
className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50 flex items-center space-x-2"
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover flex items-center space-x-2"
|
||||||
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -107,9 +108,9 @@ export default function CollectionPageView(props) {
|
||||||
</svg>
|
</svg>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDeleteModal(true)}
|
onClick={() => setShowDeleteModal(true)}
|
||||||
className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-red-50 hover:border-red-200 hover:text-red-600 flex items-center space-x-2"
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover flex items-center space-x-2"
|
||||||
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -119,15 +120,18 @@ export default function CollectionPageView(props) {
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowUploadModal(true)}
|
onClick={() => setShowUploadModal(true)}
|
||||||
className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50"
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
||||||
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||||
>
|
>
|
||||||
Upload Image
|
Upload Image
|
||||||
</button>
|
</button>
|
||||||
<button className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50" style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}>
|
<button
|
||||||
|
className="px-4 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
||||||
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
🪄 Generate AI Image
|
🪄 Generate AI Image
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -142,16 +146,31 @@ export default function CollectionPageView(props) {
|
||||||
{/* System collection indicator */}
|
{/* System collection indicator */}
|
||||||
{collection.isSystemCollection && (
|
{collection.isSystemCollection && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<span className="px-2.5 py-1 text-xs font-bold rounded-full bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm border-2 border-blue-200">
|
<span
|
||||||
|
className="px-2.5 py-1 text-xs font-bold rounded-full text-white shadow-sm border-2"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
|
||||||
|
borderColor: 'var(--accent-flame)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
🔒 SYSTEM
|
🔒 SYSTEM
|
||||||
</span>
|
</span>
|
||||||
<div className="group relative">
|
<div className="group relative">
|
||||||
<svg className="h-4 w-4 text-blue-500 cursor-help" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
|
className="h-4 w-4 cursor-help"
|
||||||
|
style={{ color: 'var(--accent-ember)' }}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap">
|
<div
|
||||||
|
className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs rounded-xl shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap glass-panel-strong"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
|
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
|
||||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -163,11 +182,15 @@ export default function CollectionPageView(props) {
|
||||||
|
|
||||||
{/* TCG Tags */}
|
{/* TCG Tags */}
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
{Object.entries(gameStats).map(([game, count]) =>
|
{Object.entries(gameStats).map(([game, count]) =>
|
||||||
count > 0 ? (
|
count > 0 ? (
|
||||||
<span
|
<span
|
||||||
key={game}
|
key={game}
|
||||||
className="px-3 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800"
|
className="px-3 py-1 text-xs font-medium rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{game}
|
{game}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -202,7 +225,7 @@ export default function CollectionPageView(props) {
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowShareModal(true)}
|
onClick={() => setShowShareModal(true)}
|
||||||
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50"
|
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
||||||
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -213,10 +236,18 @@ export default function CollectionPageView(props) {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleFavorite}
|
onClick={toggleFavorite}
|
||||||
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg ${
|
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl ${
|
||||||
isFavorited ? 'bg-red-50 border-red-200 text-red-600' : 'hover:bg-gray-50'
|
isFavorited ? '' : 'nav-item-hover'
|
||||||
}`}
|
}`}
|
||||||
style={!isFavorited ? { borderColor: 'var(--border)', color: 'var(--text-primary)' } : {}}
|
style={
|
||||||
|
isFavorited
|
||||||
|
? {
|
||||||
|
borderColor: 'var(--accent-ember)',
|
||||||
|
color: 'var(--accent-ember)',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
}
|
||||||
|
: { borderColor: 'var(--border)', color: 'var(--text-primary)' }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill={isFavorited ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill={isFavorited ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||||
|
|
@ -224,9 +255,9 @@ export default function CollectionPageView(props) {
|
||||||
<span>Favorite</span>
|
<span>Favorite</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadCSV}
|
onClick={handleDownloadCSV}
|
||||||
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50"
|
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl nav-item-hover"
|
||||||
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -238,7 +269,15 @@ export default function CollectionPageView(props) {
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<div className="flex items-center space-x-1 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<span>Activity</span>
|
<span>Activity</span>
|
||||||
<span className="px-2 py-1 bg-purple-100 text-purple-800 rounded-full text-xs font-medium">123</span>
|
<span
|
||||||
|
className="px-2 py-1 rounded-full text-xs font-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
123
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -248,28 +287,25 @@ export default function CollectionPageView(props) {
|
||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="relative">
|
<div className="w-64">
|
||||||
<input
|
<SearchBar
|
||||||
type="text"
|
|
||||||
placeholder="Search cards..."
|
|
||||||
value={searchCards}
|
value={searchCards}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchCards(e.target.value);
|
setSearchCards(e.target.value);
|
||||||
handleSearchCards(e.target.value);
|
handleSearchCards(e.target.value);
|
||||||
}}
|
}}
|
||||||
className="w-64 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
onClear={() => {
|
||||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
setSearchCards('');
|
||||||
|
handleSearchCards('');
|
||||||
|
}}
|
||||||
|
placeholder="Search cards…"
|
||||||
/>
|
/>
|
||||||
<svg className="absolute right-3 top-2.5 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--text-secondary)' }}>
|
|
||||||
<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>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedRarity}
|
value={selectedRarity}
|
||||||
onChange={(e) => setSelectedRarity(e.target.value)}
|
onChange={(e) => setSelectedRarity(e.target.value)}
|
||||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
className="input-field"
|
||||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
||||||
>
|
>
|
||||||
<option>All Rarities</option>
|
<option>All Rarities</option>
|
||||||
<option>Common</option>
|
<option>Common</option>
|
||||||
|
|
@ -278,12 +314,11 @@ export default function CollectionPageView(props) {
|
||||||
<option>Mythic</option>
|
<option>Mythic</option>
|
||||||
<option>Legendary</option>
|
<option>Legendary</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedType}
|
value={selectedType}
|
||||||
onChange={(e) => setSelectedType(e.target.value)}
|
onChange={(e) => setSelectedType(e.target.value)}
|
||||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
className="input-field"
|
||||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
||||||
>
|
>
|
||||||
<option>All Types</option>
|
<option>All Types</option>
|
||||||
<option>Creature</option>
|
<option>Creature</option>
|
||||||
|
|
@ -292,23 +327,21 @@ export default function CollectionPageView(props) {
|
||||||
<option>Artifact</option>
|
<option>Artifact</option>
|
||||||
<option>Enchantment</option>
|
<option>Enchantment</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={groupBy}
|
value={groupBy}
|
||||||
onChange={(e) => setGroupBy(e.target.value)}
|
onChange={(e) => setGroupBy(e.target.value)}
|
||||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
className="input-field"
|
||||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
||||||
>
|
>
|
||||||
<option>Group by Game</option>
|
<option>Group by Game</option>
|
||||||
<option>Group by Rarity</option>
|
<option>Group by Rarity</option>
|
||||||
<option>Group by Type</option>
|
<option>Group by Type</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
onChange={(e) => setSortBy(e.target.value)}
|
||||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
className="input-field"
|
||||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
||||||
>
|
>
|
||||||
<option>Sort by Name</option>
|
<option>Sort by Name</option>
|
||||||
<option>Sort by Price</option>
|
<option>Sort by Price</option>
|
||||||
|
|
@ -318,11 +351,22 @@ export default function CollectionPageView(props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="flex rounded-lg border" style={{ borderColor: 'var(--border)' }}>
|
<div
|
||||||
|
className="flex rounded-xl border"
|
||||||
|
style={{ borderColor: 'var(--border)' }}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('grid')}
|
onClick={() => setViewMode('grid')}
|
||||||
className={`p-2 rounded-l-lg ${viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`}
|
className="p-2 rounded-l-xl transition-colors"
|
||||||
style={viewMode !== 'grid' ? { color: 'var(--text-secondary)' } : {}}
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
viewMode === 'grid' ? 'var(--bg-tertiary)' : 'transparent',
|
||||||
|
color:
|
||||||
|
viewMode === 'grid'
|
||||||
|
? 'var(--text-primary)'
|
||||||
|
: 'var(--text-secondary)',
|
||||||
|
}}
|
||||||
|
aria-label="Grid view"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||||
|
|
@ -330,27 +374,39 @@ export default function CollectionPageView(props) {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('list')}
|
onClick={() => setViewMode('list')}
|
||||||
className={`p-2 rounded-r-lg ${viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`}
|
className="p-2 rounded-r-xl transition-colors"
|
||||||
style={viewMode !== 'list' ? { color: 'var(--text-secondary)' } : {}}
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
viewMode === 'list' ? 'var(--bg-tertiary)' : 'transparent',
|
||||||
|
color:
|
||||||
|
viewMode === 'list'
|
||||||
|
? 'var(--text-primary)'
|
||||||
|
: 'var(--text-secondary)',
|
||||||
|
}}
|
||||||
|
aria-label="List view"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
variant="primary"
|
||||||
onClick={() => router.push('/cards')}
|
onClick={() => router.push('/cards')}
|
||||||
className="px-4 py-2 text-white rounded-lg flex items-center space-x-2"
|
leadingIcon={
|
||||||
style={{ backgroundColor: 'var(--accent-ember)' }}
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Cards
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 border rounded-xl nav-item-hover flex items-center space-x-2"
|
||||||
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
<span>Add Cards</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button className="px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center space-x-2" style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
|
@ -366,7 +422,13 @@ export default function CollectionPageView(props) {
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-xl font-bold flex items-center space-x-3" style={{ color: 'var(--text-primary)' }}>
|
<h2 className="text-xl font-bold flex items-center space-x-3" style={{ color: 'var(--text-primary)' }}>
|
||||||
<span>{game === 'MTG' ? 'Magic The Gathering' : game}</span>
|
<span>{game === 'MTG' ? 'Magic The Gathering' : game}</span>
|
||||||
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-sm font-medium">
|
<span
|
||||||
|
className="px-2 py-1 rounded-xl text-sm font-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{gameCards.length}
|
{gameCards.length}
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -399,21 +461,23 @@ export default function CollectionPageView(props) {
|
||||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Add cards to get started with your collection
|
Add cards to get started with your collection
|
||||||
</p>
|
</p>
|
||||||
<button className="px-6 py-3 text-white rounded-lg" style={{ backgroundColor: 'var(--accent-ember)' }}>
|
<Button variant="primary" onClick={() => router.push('/cards')}>
|
||||||
Browse Cards to Add
|
Browse Cards to Add
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Results Dropdown */}
|
{/* Search Results Dropdown */}
|
||||||
{showSearchResults && searchResults.length > 0 && (
|
{showSearchResults && searchResults.length > 0 && (
|
||||||
<div className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg max-h-64 overflow-y-auto z-50">
|
<div
|
||||||
|
className="absolute top-full left-0 right-0 glass-panel-strong rounded-xl max-h-64 overflow-y-auto z-50"
|
||||||
|
>
|
||||||
{searchResults.map(card => (
|
{searchResults.map(card => (
|
||||||
<div
|
<div
|
||||||
key={card.id}
|
key={card.id}
|
||||||
onClick={() => handleAddCard(card)}
|
onClick={() => handleAddCard(card)}
|
||||||
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
|
className="flex items-center p-3 cursor-pointer nav-item-hover"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={card.image_url}
|
src={card.image_url}
|
||||||
|
|
@ -424,8 +488,12 @@ export default function CollectionPageView(props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{card.name}</div>
|
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
<div className="text-sm text-gray-500">{card.set_name} • ${card.market_price}</div>
|
{card.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
{card.set_name} • ${card.market_price}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -140,16 +140,31 @@ export default function CollectionsPageView(props) {
|
||||||
{/* System collection indicator */}
|
{/* System collection indicator */}
|
||||||
{collection.isSystemCollection && (
|
{collection.isSystemCollection && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<span className="px-2.5 py-1 text-xs font-bold rounded-full bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm border-2 border-blue-200">
|
<span
|
||||||
|
className="px-2.5 py-1 text-xs font-bold rounded-full text-white shadow-sm border-2"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
|
||||||
|
borderColor: 'var(--accent-flame)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
🔒 SYSTEM
|
🔒 SYSTEM
|
||||||
</span>
|
</span>
|
||||||
<div className="group relative">
|
<div className="group relative">
|
||||||
<svg className="h-4 w-4 text-blue-500 cursor-help" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
|
className="h-4 w-4 cursor-help"
|
||||||
|
style={{ color: 'var(--accent-ember)' }}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap">
|
<div
|
||||||
|
className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs rounded-xl shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap glass-panel-strong"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
|
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
|
||||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,27 @@
|
||||||
/* 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 { ManaCost } from './ManaSymbols';
|
import { ManaCost } from './ManaSymbols';
|
||||||
|
import { Button } from './ui';
|
||||||
|
|
||||||
|
/* 2026-06-04 design-sweep pass: same broken Tailwind tokens
|
||||||
|
(bg-bg-*, text-text-*, bg-accent-ember, hover:bg-accent-ember-dark)
|
||||||
|
that the other deck builder files had — none of those classes are
|
||||||
|
defined in tailwind.config.js, so this list rendered with no
|
||||||
|
visible card surfaces or hover affordances. Sweep replaces with
|
||||||
|
inline CSS variables + Button primitive. Rarity badges reuse the
|
||||||
|
neutral palette from DeckBuilderCardBrowser. */
|
||||||
|
|
||||||
|
const rarityBadgeStyle = (rarity) => {
|
||||||
|
switch (rarity) {
|
||||||
|
case 'mythic':
|
||||||
|
return { backgroundColor: 'var(--accent-ember)', color: '#ffffff' };
|
||||||
|
case 'rare':
|
||||||
|
return { backgroundColor: 'var(--accent-flame)', color: '#ffffff' };
|
||||||
|
case 'uncommon':
|
||||||
|
return { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-primary)' };
|
||||||
|
default:
|
||||||
|
return { backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-secondary)' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function DeckBuilderDeckList({
|
export default function DeckBuilderDeckList({
|
||||||
deckCards,
|
deckCards,
|
||||||
|
|
@ -10,76 +32,112 @@ export default function DeckBuilderDeckList({
|
||||||
onRemoveCard,
|
onRemoveCard,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto space-y-2">
|
<div className="flex-1 overflow-y-auto space-y-2">
|
||||||
{deckCards.length === 0 ? (
|
{deckCards.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="text-6xl mb-4">🃏</div>
|
<div className="text-6xl mb-4">🃏</div>
|
||||||
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3>
|
<h3
|
||||||
<p className="text-text-secondary mb-4">Start building your deck by searching for cards</p>
|
className="text-xl font-semibold mb-2"
|
||||||
{!sidebarOpen && (
|
style={{ color: 'var(--text-primary)' }}
|
||||||
<button
|
>
|
||||||
onClick={onOpenSidebar}
|
Empty Deck
|
||||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
</h3>
|
||||||
|
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
Start building your deck by searching for cards
|
||||||
|
</p>
|
||||||
|
{!sidebarOpen && (
|
||||||
|
<Button variant="primary" onClick={onOpenSidebar}>
|
||||||
|
Open Card Browser
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
deckCards
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((card) => (
|
||||||
|
<div
|
||||||
|
key={`${card.card_id}-${card.id}`}
|
||||||
|
className="flex items-center justify-between p-4 rounded-xl transition-colors nav-item-hover"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{card.image_url && (
|
||||||
|
<img
|
||||||
|
src={card.image_url}
|
||||||
|
alt={card.name}
|
||||||
|
className="w-14 h-20 object-cover rounded shadow-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h4
|
||||||
|
className="font-semibold text-lg"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
{card.name}
|
||||||
|
</h4>
|
||||||
|
<p
|
||||||
|
className="text-sm"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{card.set_name}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-3 mt-1">
|
||||||
|
{card.mana_cost && (
|
||||||
|
<div
|
||||||
|
className="px-2 py-1 rounded"
|
||||||
|
style={{ backgroundColor: 'var(--bg-secondary)' }}
|
||||||
>
|
>
|
||||||
Open Card Browser
|
<ManaCost
|
||||||
</button>
|
cost={card.mana_cost}
|
||||||
|
size="sm"
|
||||||
|
useSVG={manaSymbolSettings.useSVG}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{card.rarity && (
|
||||||
|
<span
|
||||||
|
className="text-xs px-2 py-1 rounded capitalize"
|
||||||
|
style={rarityBadgeStyle(card.rarity)}
|
||||||
|
>
|
||||||
|
{card.rarity}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
deckCards
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.map((card) => (
|
|
||||||
<div key={`${card.card_id}-${card.id}`} className="flex items-center justify-between p-4 bg-bg-primary rounded-lg hover:bg-bg-tertiary transition-colors">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
{card.image_url && (
|
|
||||||
<img
|
|
||||||
src={card.image_url}
|
|
||||||
alt={card.name}
|
|
||||||
className="w-14 h-20 object-cover rounded shadow-md"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-text-primary text-lg">{card.name}</h4>
|
|
||||||
<p className="text-text-secondary text-sm">{card.set_name}</p>
|
|
||||||
<div className="flex items-center space-x-3 mt-1">
|
|
||||||
{card.mana_cost && (
|
|
||||||
<div className="bg-bg-secondary px-2 py-1 rounded">
|
|
||||||
<ManaCost cost={card.mana_cost} size="sm" useSVG={manaSymbolSettings.useSVG} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{card.rarity && (
|
|
||||||
<span className={`text-xs px-2 py-1 rounded capitalize ${
|
|
||||||
card.rarity === 'mythic' ? 'bg-orange-100 text-orange-800' :
|
|
||||||
card.rarity === 'rare' ? 'bg-yellow-100 text-yellow-800' :
|
|
||||||
card.rarity === 'uncommon' ? 'bg-gray-100 text-gray-800' :
|
|
||||||
'bg-green-100 text-green-800'
|
|
||||||
}`}>
|
|
||||||
{card.rarity}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<span className="text-text-primary font-bold text-lg">{card.quantity}x</span>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<button
|
|
||||||
onClick={() => onRemoveCard(card.card_id, 1)}
|
|
||||||
className="w-8 h-8 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors flex items-center justify-center font-bold"
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onAddCard(card, 1)}
|
|
||||||
className="w-8 h-8 bg-accent-ember text-white rounded-full hover:bg-accent-ember-dark transition-colors flex items-center justify-center font-bold"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span
|
||||||
|
className="font-bold text-lg"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
{card.quantity}x
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<button
|
||||||
|
onClick={() => onRemoveCard(card.card_id, 1)}
|
||||||
|
className="w-8 h-8 rounded-full transition-colors flex items-center justify-center font-bold text-white"
|
||||||
|
style={{ backgroundColor: '#dc2626' }}
|
||||||
|
aria-label="Remove one"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onAddCard(card, 1)}
|
||||||
|
className="w-8 h-8 rounded-full transition-transform hover:scale-105 active:scale-95 flex items-center justify-center font-bold text-white"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
|
||||||
|
boxShadow: 'var(--rim-light-inner), var(--ember-rim-subtle)',
|
||||||
|
}}
|
||||||
|
aria-label="Add one"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,41 @@
|
||||||
*/
|
*/
|
||||||
export default function DeckBuilderStatsBar({ stats }) {
|
export default function DeckBuilderStatsBar({ stats }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 p-4 bg-bg-primary rounded-lg">
|
<div
|
||||||
|
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 p-4 rounded-xl"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-lg font-bold text-text-primary">{stats.totalCards}/100</div>
|
<div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
<div className="text-text-secondary text-sm">Cards</div>
|
{stats.totalCards}/100
|
||||||
|
</div>
|
||||||
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
Cards
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-lg font-bold text-text-primary">{stats.avgCmc}</div>
|
<div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
<div className="text-text-secondary text-sm">Avg CMC</div>
|
{stats.avgCmc}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
Avg CMC
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-lg font-bold text-text-primary">
|
<div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
{Object.keys(stats.colorCounts).length}
|
{Object.keys(stats.colorCounts).length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-text-secondary text-sm">Colors</div>
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
Colors
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-lg font-bold text-text-primary">
|
<div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
{Object.keys(stats.typeCounts).length}
|
{Object.keys(stats.typeCounts).length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-text-secondary text-sm">Types</div>
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
Types
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,16 @@ import Link from 'next/link';
|
||||||
import DeckBuilderCardBrowser from './DeckBuilderCardBrowser';
|
import DeckBuilderCardBrowser from './DeckBuilderCardBrowser';
|
||||||
import DeckBuilderDeckList from './DeckBuilderDeckList';
|
import DeckBuilderDeckList from './DeckBuilderDeckList';
|
||||||
import DeckBuilderStatsBar from './DeckBuilderStatsBar';
|
import DeckBuilderStatsBar from './DeckBuilderStatsBar';
|
||||||
|
import { Button } from './ui';
|
||||||
import { computeDeckStats } from '../lib/deck-builder-stats';
|
import { computeDeckStats } from '../lib/deck-builder-stats';
|
||||||
|
|
||||||
|
/* 2026-06-04 design-sweep pass: replaced broken Tailwind token
|
||||||
|
classes (bg-bg-secondary, text-text-primary, bg-accent-ember, etc.
|
||||||
|
— none of which are defined in tailwind.config.js and produced
|
||||||
|
zero CSS) with inline style={{ ... CSS vars ... }} + the Button
|
||||||
|
primitive + `glass-panel` + `rounded-2xl` so this view actually
|
||||||
|
renders with the Liquid Glass design system. */
|
||||||
|
|
||||||
export default function DeckBuilderView({
|
export default function DeckBuilderView({
|
||||||
addCardToDeck,
|
addCardToDeck,
|
||||||
clearFilters,
|
clearFilters,
|
||||||
|
|
@ -37,46 +45,46 @@ export default function DeckBuilderView({
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Link href="/decks" className="text-accent-ember hover:underline">
|
<Link
|
||||||
|
href="/decks"
|
||||||
|
className="hover:underline"
|
||||||
|
style={{ color: 'var(--accent-ember)' }}
|
||||||
|
>
|
||||||
← Back to Decks
|
← Back to Decks
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-text-primary mt-2">{deck.name}</h1>
|
<h1 className="text-3xl font-bold mt-2" style={{ color: 'var(--text-primary)' }}>
|
||||||
<p className="text-text-secondary">
|
{deck.name}
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: 'var(--text-secondary)' }}>
|
||||||
{deck.format} • {stats.totalCards}/100 cards
|
{deck.format} • {stats.totalCards}/100 cards
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<button
|
<Button type="button" variant="secondary">
|
||||||
type="button"
|
|
||||||
className="bg-bg-secondary text-text-primary px-4 py-2 rounded-lg hover:bg-bg-tertiary transition-colors"
|
|
||||||
>
|
|
||||||
Save Deck
|
Save Deck
|
||||||
</button>
|
</Button>
|
||||||
<Link
|
<Link href={`/deck/${deck.id}`}>
|
||||||
href={`/deck/${deck.id}`}
|
<Button variant="primary">View Deck</Button>
|
||||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
|
||||||
>
|
|
||||||
View Deck
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-6 h-[calc(100vh-12rem)]">
|
<div className="flex gap-6 h-[calc(100vh-12rem)]">
|
||||||
<div className="flex-1 transition-all duration-300">
|
<div className="flex-1 transition-all duration-300">
|
||||||
<div className="bg-bg-secondary rounded-lg p-6 h-full flex flex-col">
|
<div className="glass-panel rounded-2xl p-6 h-full flex flex-col">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-xl font-semibold text-text-primary">
|
<h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||||
Deck Cards ({stats.totalCards})
|
Deck Cards ({stats.totalCards})
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="primary"
|
||||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors flex items-center space-x-2"
|
trailingIcon={<span aria-hidden="true">{sidebarOpen ? '→' : '←'}</span>}
|
||||||
>
|
>
|
||||||
<span>{sidebarOpen ? 'Hide' : 'Show'} Browser</span>
|
{sidebarOpen ? 'Hide' : 'Show'} Browser
|
||||||
<span>{sidebarOpen ? '→' : '←'}</span>
|
</Button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeckBuilderStatsBar stats={stats} />
|
<DeckBuilderStatsBar stats={stats} />
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,7 @@ function UserProfileDropdown({ user, onMobileMenuClose }) {
|
||||||
{profileMenuItems.map((item) => (
|
{profileMenuItems.map((item) => (
|
||||||
<Link key={item.name} href={item.href}>
|
<Link key={item.name} href={item.href}>
|
||||||
<div
|
<div
|
||||||
className={`
|
className="flex items-center px-4 py-3 text-sm cursor-pointer nav-item-hover"
|
||||||
flex items-center px-4 py-3 text-sm transition-all duration-200 cursor-pointer
|
|
||||||
hover:bg-gray-50 dark:hover:bg-gray-700
|
|
||||||
${item.isLogout ? 'text-red-600 dark:text-red-400' : ''}
|
|
||||||
`}
|
|
||||||
style={{
|
style={{
|
||||||
color: item.isLogout ? 'var(--accent-ember)' : 'var(--text-primary)'
|
color: item.isLogout ? 'var(--accent-ember)' : 'var(--text-primary)'
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -31,18 +31,27 @@ export default function ManaSymbolSettings({ onSettingsChange }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-3 p-3 bg-bg-secondary rounded-lg">
|
<div
|
||||||
|
className="flex items-center space-x-3 p-3 rounded-xl"
|
||||||
|
style={{ backgroundColor: 'var(--bg-secondary)' }}
|
||||||
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="text-sm font-medium text-text-primary">Mana Symbol Style</h4>
|
<h4
|
||||||
<p className="text-xs text-text-secondary">
|
className="text-sm font-medium"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
Mana Symbol Style
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{useSVG ? 'Using official Scryfall SVG symbols' : 'Using custom circular symbols'}
|
{useSVG ? 'Using official Scryfall SVG symbols' : 'Using custom circular symbols'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
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"
|
||||||
useSVG ? 'bg-accent-ember' : 'bg-gray-300'
|
style={{
|
||||||
}`}
|
backgroundColor: useSVG ? 'var(--accent-ember)' : 'var(--bg-tertiary)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||||
|
|
@ -52,4 +61,4 @@ export default function ManaSymbolSettings({ onSettingsChange }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export function AdvancedManaCost({ cost, showAnalysis = false, useSVG = false })
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<ManaCost cost={analysis.cost} useSVG={useSVG} />
|
<ManaCost cost={analysis.cost} useSVG={useSVG} />
|
||||||
{showAnalysis && (
|
{showAnalysis && (
|
||||||
<div className="text-xs text-text-secondary space-y-1">
|
<div className="text-xs space-y-1" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<div>CMC: {analysis.cmc}</div>
|
<div>CMC: {analysis.cmc}</div>
|
||||||
{analysis.colors.length > 0 && (
|
{analysis.colors.length > 0 && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
|
|
|
||||||
|
|
@ -184,21 +184,33 @@ export default function ShareModal({
|
||||||
|
|
||||||
{/* Search Results */}
|
{/* Search Results */}
|
||||||
{searchResults.length > 0 && (
|
{searchResults.length > 0 && (
|
||||||
<div className="mt-2 border border-gray-200 rounded-lg max-h-40 overflow-y-auto">
|
<div
|
||||||
|
className="mt-2 rounded-xl max-h-40 overflow-y-auto glass-panel"
|
||||||
|
>
|
||||||
{searchResults.map((user) => (
|
{searchResults.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
key={user.id}
|
||||||
onClick={() => handleInvite(user)}
|
onClick={() => handleInvite(user)}
|
||||||
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
|
className="flex items-center p-3 cursor-pointer nav-item-hover"
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 bg-purple-600 rounded-full flex items-center justify-center mr-3">
|
<div
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center mr-3"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span className="text-white text-sm font-bold">
|
<span className="text-white text-sm font-bold">
|
||||||
{user.email.charAt(0).toUpperCase()}
|
{user.email.charAt(0).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900">{user.email}</div>
|
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
<div className="text-sm text-gray-500">Click to invite as viewer</div>
|
{user.email}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
Click to invite as viewer
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -207,19 +219,32 @@ export default function ShareModal({
|
||||||
|
|
||||||
{/* Email invite option */}
|
{/* Email invite option */}
|
||||||
{searchQuery && isValidEmail(searchQuery) && !searchResults.some(u => u.email === searchQuery) && (
|
{searchQuery && isValidEmail(searchQuery) && !searchResults.some(u => u.email === searchQuery) && (
|
||||||
<div className="mt-2 border border-gray-200 rounded-lg">
|
<div className="mt-2 rounded-xl glass-panel">
|
||||||
<div
|
<div
|
||||||
onClick={() => handleInvite(searchQuery)}
|
onClick={() => handleInvite(searchQuery)}
|
||||||
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
|
className="flex items-center p-3 cursor-pointer nav-item-hover"
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center mr-3">
|
<div
|
||||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
className="w-8 h-8 rounded-full flex items-center justify-center mr-3"
|
||||||
|
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-gray-900">Invite {searchQuery}</div>
|
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
<div className="text-sm text-gray-500">Send email invitation as viewer</div>
|
Invite {searchQuery}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
Send email invitation as viewer
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -310,7 +335,8 @@ export default function ShareModal({
|
||||||
<button
|
<button
|
||||||
key={social.platform}
|
key={social.platform}
|
||||||
onClick={() => handleSocialShare(social.platform)}
|
onClick={() => handleSocialShare(social.platform)}
|
||||||
className="flex flex-col items-center p-3 rounded-lg border hover:bg-gray-50 transition-colors"
|
className="flex flex-col items-center p-3 rounded-xl border nav-item-hover transition-colors"
|
||||||
|
style={{ borderColor: 'var(--border)' }}
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 mb-2 text-gray-600">
|
<div className="w-8 h-8 mb-2 text-gray-600">
|
||||||
{social.icon === 'twitter' && (
|
{social.icon === 'twitter' && (
|
||||||
|
|
|
||||||
|
|
@ -4,7 +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 { Button, SearchBar } 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';
|
||||||
|
|
||||||
|
|
@ -213,12 +213,11 @@ export default function CommunityCollections() {
|
||||||
{/* Search and Sort */}
|
{/* Search and Sort */}
|
||||||
<div className="flex items-center justify-between mt-6">
|
<div className="flex items-center justify-between mt-6">
|
||||||
<div className="flex-1 max-w-md">
|
<div className="flex-1 max-w-md">
|
||||||
<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">
|
||||||
|
|
@ -311,10 +310,10 @@ export default function CommunityCollections() {
|
||||||
{collection.tags.slice(0, 2).map((tag, index) => (
|
{collection.tags.slice(0, 2).map((tag, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="px-2 py-1 rounded-md text-xs font-medium"
|
className="px-2 py-1 rounded-xl text-xs font-medium"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-tertiary)',
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
color: 'var(--text-secondary)'
|
color: 'var(--text-secondary)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
|
|
@ -322,10 +321,10 @@ export default function CommunityCollections() {
|
||||||
))}
|
))}
|
||||||
{collection.tags.length > 2 && (
|
{collection.tags.length > 2 && (
|
||||||
<span
|
<span
|
||||||
className="px-2 py-1 rounded-md text-xs font-medium"
|
className="px-2 py-1 rounded-xl text-xs font-medium"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-tertiary)',
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
color: 'var(--text-secondary)'
|
color: 'var(--text-secondary)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
+{collection.tags.length - 2}
|
+{collection.tags.length - 2}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ export default function DeckBuilder() {
|
||||||
return (
|
return (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember" />
|
<div
|
||||||
|
className="animate-spin rounded-full h-32 w-32 border-b-2"
|
||||||
|
style={{ borderColor: 'var(--accent-ember)' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,18 @@ 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 { ManaCost, ColorIdentity } from '../../components/ManaSymbols';
|
import { ManaCost, ColorIdentity } from '../../components/ManaSymbols';
|
||||||
|
import { Button } from '../../components/ui';
|
||||||
import { useAuth } from '../../lib/use-auth';
|
import { useAuth } from '../../lib/use-auth';
|
||||||
import { getColorIdentity } from '../../lib/mana-symbols';
|
import { getColorIdentity } from '../../lib/mana-symbols';
|
||||||
|
|
||||||
|
/* 2026-06-04 design-sweep pass: this page used the broken Tailwind
|
||||||
|
token classes (bg-bg-*, text-text-*, bg-accent-ember,
|
||||||
|
hover:bg-accent-ember-dark, border-accent-ember) that don't
|
||||||
|
resolve in tailwind.config.js. Sweep replaces them with inline
|
||||||
|
CSS variables + Button primitive + glass-panel surface + the
|
||||||
|
nav-item-active / nav-item-hover utilities so the page renders
|
||||||
|
with the Liquid Glass design system. */
|
||||||
|
|
||||||
export default function DeckDetail() {
|
export default function DeckDetail() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -136,7 +145,10 @@ export default function DeckDetail() {
|
||||||
return (
|
return (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
<div
|
||||||
|
className="animate-spin rounded-full h-32 w-32 border-b-2"
|
||||||
|
style={{ borderColor: 'var(--accent-ember)' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
@ -147,8 +159,17 @@ export default function DeckDetail() {
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Deck not found</h1>
|
<h1
|
||||||
<Link href="/decks" className="text-accent-ember hover:underline">
|
className="text-2xl font-bold mb-4"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
Deck not found
|
||||||
|
</h1>
|
||||||
|
<Link
|
||||||
|
href="/decks"
|
||||||
|
className="hover:underline"
|
||||||
|
style={{ color: 'var(--accent-ember)' }}
|
||||||
|
>
|
||||||
Back to Decks
|
Back to Decks
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -168,34 +189,51 @@ export default function DeckDetail() {
|
||||||
<div className="flex justify-between items-start mb-8">
|
<div className="flex justify-between items-start mb-8">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
<Link href="/decks" className="text-accent-ember hover:underline">
|
<Link
|
||||||
|
href="/decks"
|
||||||
|
className="hover:underline"
|
||||||
|
style={{ color: 'var(--accent-ember)' }}
|
||||||
|
>
|
||||||
← Back to Decks
|
← Back to Decks
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
<span className="text-3xl">{getFormatIcon(deck.format)}</span>
|
<span className="text-3xl">{getFormatIcon(deck.format)}</span>
|
||||||
<h1 className="text-3xl font-bold text-text-primary">{deck.name}</h1>
|
<h1
|
||||||
|
className="text-3xl font-bold"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
{deck.name}
|
||||||
|
</h1>
|
||||||
{deck.is_public && (
|
{deck.is_public && (
|
||||||
<span className="bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs font-medium">
|
<span
|
||||||
|
className="px-2 py-1 rounded-full text-xs font-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
|
color: 'var(--accent-ember)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
Public
|
Public
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-text-secondary mb-2">
|
<p className="mb-2" style={{ color: 'var(--text-secondary)' }}>
|
||||||
by {deck.creator_username} • {deck.format} • {stats.totalCards} cards
|
by {deck.creator_username} • {deck.format} • {stats.totalCards} cards
|
||||||
</p>
|
</p>
|
||||||
{deck.description && (
|
{deck.description && (
|
||||||
<p className="text-text-secondary max-w-2xl">{deck.description}</p>
|
<p
|
||||||
|
className="max-w-2xl"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{deck.description}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Link
|
<Link href={`/deck-builder?deck=${deck.id}`}>
|
||||||
href={`/deck-builder?deck=${deck.id}`}
|
<Button variant="primary">Edit Deck</Button>
|
||||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
|
||||||
>
|
|
||||||
Edit Deck
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -204,38 +242,64 @@ export default function DeckDetail() {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
{/* Stats Sidebar */}
|
{/* Stats Sidebar */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="bg-bg-secondary rounded-lg p-6 mb-6">
|
<div className="glass-panel rounded-2xl p-6 mb-6">
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Statistics</h3>
|
<h3
|
||||||
|
className="text-lg font-semibold mb-4"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
Statistics
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-text-secondary">Total Cards:</span>
|
<span style={{ color: 'var(--text-secondary)' }}>Total Cards:</span>
|
||||||
<span className="text-text-primary font-medium">{stats.totalCards}</span>
|
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{stats.totalCards}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-text-secondary">Avg. CMC:</span>
|
<span style={{ color: 'var(--text-secondary)' }}>Avg. CMC:</span>
|
||||||
<span className="text-text-primary font-medium">{stats.avgCmc}</span>
|
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{stats.avgCmc}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-text-secondary">Format:</span>
|
<span style={{ color: 'var(--text-secondary)' }}>Format:</span>
|
||||||
<span className="text-text-primary font-medium">{deck.format}</span>
|
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{deck.format}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Color Distribution */}
|
{/* Color Distribution */}
|
||||||
{Object.keys(stats.colorCounts).length > 0 && (
|
{Object.keys(stats.colorCounts).length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h4 className="text-text-secondary text-sm font-medium mb-3">Color Distribution</h4>
|
<h4
|
||||||
|
className="text-sm font-medium mb-3"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
Color Distribution
|
||||||
|
</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(stats.colorCounts)
|
{Object.entries(stats.colorCounts)
|
||||||
.sort(([,a], [,b]) => b - a)
|
.sort(([, a], [, b]) => b - a)
|
||||||
.map(([color, count]) => (
|
.map(([color, count]) => (
|
||||||
<div key={color} className="flex justify-between items-center">
|
<div key={color} className="flex justify-between items-center">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-lg">{color}</span>
|
<span className="text-lg">{color}</span>
|
||||||
<span className="text-text-secondary text-sm">{color}</span>
|
<span
|
||||||
|
className="text-sm"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{color}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-text-primary font-medium">{count}</span>
|
<span
|
||||||
|
className="font-medium"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -245,15 +309,30 @@ export default function DeckDetail() {
|
||||||
{/* Type Distribution */}
|
{/* Type Distribution */}
|
||||||
{Object.keys(stats.typeCounts).length > 0 && (
|
{Object.keys(stats.typeCounts).length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h4 className="text-text-secondary text-sm font-medium mb-3">Card Types</h4>
|
<h4
|
||||||
|
className="text-sm font-medium mb-3"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
Card Types
|
||||||
|
</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(stats.typeCounts)
|
{Object.entries(stats.typeCounts)
|
||||||
.sort(([,a], [,b]) => b - a)
|
.sort(([, a], [, b]) => b - a)
|
||||||
.slice(0, 8)
|
.slice(0, 8)
|
||||||
.map(([type, count]) => (
|
.map(([type, count]) => (
|
||||||
<div key={type} className="flex justify-between">
|
<div key={type} className="flex justify-between">
|
||||||
<span className="text-text-secondary text-sm">{type}</span>
|
<span
|
||||||
<span className="text-text-primary font-medium">{count}</span>
|
className="text-sm"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="font-medium"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -262,22 +341,25 @@ export default function DeckDetail() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Group By Controls */}
|
{/* Group By Controls */}
|
||||||
<div className="bg-bg-secondary rounded-lg p-6">
|
<div className="glass-panel rounded-2xl p-6">
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Group Cards By</h3>
|
<h3
|
||||||
|
className="text-lg font-semibold mb-4"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
Group Cards By
|
||||||
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{[
|
{[
|
||||||
{ value: 'type', label: 'Card Type' },
|
{ value: 'type', label: 'Card Type' },
|
||||||
{ value: 'cmc', label: 'Mana Cost' },
|
{ value: 'cmc', label: 'Mana Cost' },
|
||||||
{ value: 'color', label: 'Color' },
|
{ value: 'color', label: 'Color' },
|
||||||
{ value: 'rarity', label: 'Rarity' }
|
{ value: 'rarity', label: 'Rarity' },
|
||||||
].map(option => (
|
].map(option => (
|
||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onClick={() => setGroupBy(option.value)}
|
onClick={() => setGroupBy(option.value)}
|
||||||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
|
className={`nav-item w-full text-left px-3 py-2 ${
|
||||||
groupBy === option.value
|
groupBy === option.value ? 'nav-item-active' : 'nav-item-hover'
|
||||||
? 'bg-accent-ember text-white'
|
|
||||||
: 'text-text-secondary hover:bg-bg-tertiary'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
@ -289,12 +371,19 @@ export default function DeckDetail() {
|
||||||
|
|
||||||
{/* Card List */}
|
{/* Card List */}
|
||||||
<div className="lg:col-span-3">
|
<div className="lg:col-span-3">
|
||||||
<div className="bg-bg-secondary rounded-lg p-6">
|
<div className="glass-panel rounded-2xl p-6">
|
||||||
{deck.cards && deck.cards.length === 0 ? (
|
{deck.cards && deck.cards.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="text-6xl mb-4">🃏</div>
|
<div className="text-6xl mb-4">🃏</div>
|
||||||
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3>
|
<h3
|
||||||
<p className="text-text-secondary">This deck doesn't have any cards yet</p>
|
className="text-xl font-semibold mb-2"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
Empty Deck
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
This deck doesn't have any cards yet
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -302,14 +391,24 @@ export default function DeckDetail() {
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.map(([group, cards]) => (
|
.map(([group, cards]) => (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-3 border-b border-border pb-2">
|
<h3
|
||||||
|
className="text-lg font-semibold mb-3 border-b pb-2"
|
||||||
|
style={{
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
borderColor: 'var(--border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{group} ({cards.reduce((sum, card) => sum + card.quantity, 0)})
|
{group} ({cards.reduce((sum, card) => sum + card.quantity, 0)})
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{cards
|
{cards
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((card) => (
|
.map((card) => (
|
||||||
<div key={`${card.card_id}-${card.id}`} className="flex items-center space-x-3 p-3 bg-bg-primary rounded-lg hover:bg-bg-tertiary transition-colors">
|
<div
|
||||||
|
key={`${card.card_id}-${card.id}`}
|
||||||
|
className="flex items-center space-x-3 p-3 rounded-xl transition-colors nav-item-hover"
|
||||||
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
||||||
|
>
|
||||||
{card.image_url && (
|
{card.image_url && (
|
||||||
<img
|
<img
|
||||||
src={card.image_url}
|
src={card.image_url}
|
||||||
|
|
@ -319,15 +418,35 @@ export default function DeckDetail() {
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="font-medium text-text-primary truncate">{card.name}</h4>
|
<h4
|
||||||
<span className="text-text-primary font-medium ml-2">{card.quantity}x</span>
|
className="font-medium truncate"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
{card.name}
|
||||||
|
</h4>
|
||||||
|
<span
|
||||||
|
className="font-medium ml-2"
|
||||||
|
style={{ color: 'var(--text-primary)' }}
|
||||||
|
>
|
||||||
|
{card.quantity}x
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-text-secondary text-sm">{card.set_name}</p>
|
<p
|
||||||
<div className="flex items-center space-x-2 text-xs text-text-secondary">
|
className="text-sm"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{card.set_name}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="flex items-center space-x-2 text-xs"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
{card.mana_cost && (
|
{card.mana_cost && (
|
||||||
<ManaCost cost={card.mana_cost} size="xs" />
|
<ManaCost cost={card.mana_cost} size="xs" />
|
||||||
)}
|
)}
|
||||||
{card.rarity && <span className="capitalize">{card.rarity}</span>}
|
{card.rarity && (
|
||||||
|
<span className="capitalize">{card.rarity}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
243
pages/decks.js
243
pages/decks.js
|
|
@ -5,6 +5,17 @@ import Layout from '../components/Layout';
|
||||||
import { Modal, Input, Button } from '../components/ui';
|
import { Modal, Input, Button } from '../components/ui';
|
||||||
import { useAuth } from '../lib/use-auth';
|
import { useAuth } from '../lib/use-auth';
|
||||||
|
|
||||||
|
/* 2026-06-04 design-sweep pass: the Tailwind token classes that this
|
||||||
|
page relied on (bg-bg-secondary, text-text-primary, border-border,
|
||||||
|
bg-accent-ember, hover:bg-accent-ember-dark, focus:ring-accent-ember)
|
||||||
|
are NOT defined in tailwind.config.js — they produced zero CSS,
|
||||||
|
leaving the page visually unstyled (transparent backgrounds, no
|
||||||
|
borders, no hover states). Sweep replaces those with inline
|
||||||
|
style={{ ... CSS vars ... }} and the Button / SearchBar primitives
|
||||||
|
so the page actually renders with the Liquid Glass + corner-border
|
||||||
|
design system. `rounded-lg` → `rounded-xl` across the board to match
|
||||||
|
the system standard. */
|
||||||
|
|
||||||
export default function Decks() {
|
export default function Decks() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -16,7 +27,7 @@ export default function Decks() {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
format: 'Commander',
|
format: 'Commander',
|
||||||
is_public: false
|
is_public: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchDecks = async () => {
|
const fetchDecks = async () => {
|
||||||
|
|
@ -24,8 +35,8 @@ export default function Decks() {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch('/api/decks', {
|
const response = await fetch('/api/decks', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
@ -39,7 +50,7 @@ export default function Decks() {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
@ -48,20 +59,17 @@ export default function Decks() {
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
;
|
|
||||||
|
|
||||||
const handleCreateDeck = async (e) => {
|
const handleCreateDeck = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch('/api/decks', {
|
const response = await fetch('/api/decks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(newDeck)
|
body: JSON.stringify(newDeck),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
@ -69,8 +77,6 @@ export default function Decks() {
|
||||||
setDecks([createdDeck, ...decks]);
|
setDecks([createdDeck, ...decks]);
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
setNewDeck({ name: '', description: '', format: 'Commander', is_public: false });
|
setNewDeck({ name: '', description: '', format: 'Commander', is_public: false });
|
||||||
|
|
||||||
// Navigate to deck builder for the new deck
|
|
||||||
router.push(`/deck-builder?deck=${createdDeck.id}`);
|
router.push(`/deck-builder?deck=${createdDeck.id}`);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to create deck');
|
console.error('Failed to create deck');
|
||||||
|
|
@ -82,21 +88,20 @@ export default function Decks() {
|
||||||
|
|
||||||
const handleEditDeck = async (e) => {
|
const handleEditDeck = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`/api/decks/${editingDeck.id}`, {
|
const response = await fetch(`/api/decks/${editingDeck.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(editingDeck)
|
body: JSON.stringify(editingDeck),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const updatedDeck = await response.json();
|
const updatedDeck = await response.json();
|
||||||
setDecks(decks.map(deck => deck.id === updatedDeck.id ? updatedDeck : deck));
|
setDecks(decks.map((deck) => (deck.id === updatedDeck.id ? updatedDeck : deck)));
|
||||||
setEditingDeck(null);
|
setEditingDeck(null);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to update deck');
|
console.error('Failed to update deck');
|
||||||
|
|
@ -110,18 +115,15 @@ export default function Decks() {
|
||||||
if (!confirm('Are you sure you want to delete this deck? This action cannot be undone.')) {
|
if (!confirm('Are you sure you want to delete this deck? This action cannot be undone.')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`/api/decks/${deckId}`, {
|
const response = await fetch(`/api/decks/${deckId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setDecks(decks.filter(deck => deck.id !== deckId));
|
setDecks(decks.filter((deck) => deck.id !== deckId));
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to delete deck');
|
console.error('Failed to delete deck');
|
||||||
}
|
}
|
||||||
|
|
@ -132,41 +134,46 @@ export default function Decks() {
|
||||||
|
|
||||||
const getFormatIcon = (format) => {
|
const getFormatIcon = (format) => {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'Commander':
|
case 'Commander': return '⚔️';
|
||||||
return '⚔️';
|
case 'Standard': return '🏆';
|
||||||
case 'Standard':
|
case 'Modern': return '🔥';
|
||||||
return '🏆';
|
case 'Legacy': return '💎';
|
||||||
case 'Modern':
|
default: return '🃏';
|
||||||
return '🔥';
|
|
||||||
case 'Legacy':
|
|
||||||
return '💎';
|
|
||||||
default:
|
|
||||||
return '🃏';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFormatColor = (format) => {
|
// Single neutral chip style for format badges — the prior
|
||||||
switch (format) {
|
// getFormatColor() returned 5 stale Tailwind colour pairs
|
||||||
case 'Commander':
|
// (bg-purple-100, bg-blue-100, etc.) that don't fit the
|
||||||
return 'bg-purple-100 text-purple-800';
|
// Deck Hearth palette and don't render in dark theme anyway.
|
||||||
case 'Standard':
|
const formatBadgeStyle = {
|
||||||
return 'bg-blue-100 text-blue-800';
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
case 'Modern':
|
color: 'var(--text-primary)',
|
||||||
return 'bg-red-100 text-red-800';
|
|
||||||
case 'Legacy':
|
|
||||||
return 'bg-yellow-100 text-yellow-800';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-100 text-gray-800';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reusable inline-style for the .card surfaces used as stat
|
||||||
|
// cards + deck cards. We can't use the .glass-panel class +
|
||||||
|
// rounded-xl directly because we want the same gradient-border
|
||||||
|
// catch-light effect that the chrome chips have, and inline
|
||||||
|
// styles for that pattern are verbose. Easiest path: opt into
|
||||||
|
// the existing .glass-panel class for the multi-layer bg, then
|
||||||
|
// override border-radius via className. (.glass-panel doesn't
|
||||||
|
// set border-radius so we control it via Tailwind.)
|
||||||
|
const chipBgStyle = {};
|
||||||
|
|
||||||
|
// textarea / select use .input-field which is the Deck-Hearth
|
||||||
|
// standard input class (rounded-2xl + ember focus ring).
|
||||||
|
const inputFieldClass = 'input-field w-full';
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold mb-4">Please log in to view your decks</h1>
|
<h1 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||||||
<Link href="/login" className="text-accent-ember hover:underline">
|
Please log in to view your decks
|
||||||
|
</h1>
|
||||||
|
<Link href="/login" style={{ color: 'var(--accent-ember)' }} className="hover:underline">
|
||||||
Go to Login
|
Go to Login
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -179,7 +186,10 @@ export default function Decks() {
|
||||||
return (
|
return (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-accent-ember"></div>
|
<div
|
||||||
|
className="animate-spin rounded-full h-32 w-32 border-b-2"
|
||||||
|
style={{ borderColor: 'var(--accent-ember)' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
@ -191,42 +201,43 @@ export default function Decks() {
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-text-primary">My Decks</h1>
|
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
<p className="text-text-secondary mt-2">
|
My Decks
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Build and manage your MTG decks
|
Build and manage your MTG decks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button variant="primary" onClick={() => setShowCreateModal(true)}>
|
||||||
onClick={() => setShowCreateModal(true)}
|
|
||||||
className="bg-accent-ember text-white px-6 py-3 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
|
||||||
>
|
|
||||||
Create New Deck
|
Create New Deck
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
<div className="bg-bg-secondary rounded-lg p-6">
|
<div className="glass-panel rounded-2xl p-6" style={chipBgStyle}>
|
||||||
<div className="text-2xl font-bold text-text-primary">{decks.length}</div>
|
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
<div className="text-text-secondary">Total Decks</div>
|
{decks.length}
|
||||||
</div>
|
|
||||||
<div className="bg-bg-secondary rounded-lg p-6">
|
|
||||||
<div className="text-2xl font-bold text-text-primary">
|
|
||||||
{decks.filter(d => d.format === 'Commander').length}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-text-secondary">Commander</div>
|
<div style={{ color: 'var(--text-secondary)' }}>Total Decks</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-bg-secondary rounded-lg p-6">
|
<div className="glass-panel rounded-2xl p-6">
|
||||||
<div className="text-2xl font-bold text-text-primary">
|
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
{decks.filter(d => d.is_public).length}
|
{decks.filter((d) => d.format === 'Commander').length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-text-secondary">Public</div>
|
<div style={{ color: 'var(--text-secondary)' }}>Commander</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-bg-secondary rounded-lg p-6">
|
<div className="glass-panel rounded-2xl p-6">
|
||||||
<div className="text-2xl font-bold text-text-primary">
|
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{decks.filter((d) => d.is_public).length}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: 'var(--text-secondary)' }}>Public</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-panel rounded-2xl p-6">
|
||||||
|
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||||
{decks.reduce((sum, deck) => sum + (deck.card_count || 0), 0)}
|
{decks.reduce((sum, deck) => sum + (deck.card_count || 0), 0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-text-secondary">Total Cards</div>
|
<div style={{ color: 'var(--text-secondary)' }}>Total Cards</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -234,65 +245,83 @@ export default function Decks() {
|
||||||
{decks.length === 0 ? (
|
{decks.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<div className="text-6xl mb-4">🃏</div>
|
<div className="text-6xl mb-4">🃏</div>
|
||||||
<h3 className="text-xl font-semibold text-text-primary mb-2">No decks yet</h3>
|
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||||
<p className="text-text-secondary mb-6">Create your first deck to get started</p>
|
No decks yet
|
||||||
<button
|
</h3>
|
||||||
onClick={() => setShowCreateModal(true)}
|
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||||
className="bg-accent-ember text-white px-6 py-3 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
Create your first deck to get started
|
||||||
>
|
</p>
|
||||||
|
<Button variant="primary" onClick={() => setShowCreateModal(true)}>
|
||||||
Create Your First Deck
|
Create Your First Deck
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{decks.map((deck) => (
|
{decks.map((deck) => (
|
||||||
<div key={deck.id} className="bg-bg-secondary rounded-lg p-6 hover:shadow-lg transition-shadow">
|
<div key={deck.id} className="glass-panel rounded-2xl p-6 transition-shadow">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-2xl">{getFormatIcon(deck.format)}</span>
|
<span className="text-2xl">{getFormatIcon(deck.format)}</span>
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getFormatColor(deck.format)}`}>
|
<span
|
||||||
|
className="px-2 py-1 rounded-full text-xs font-medium"
|
||||||
|
style={formatBadgeStyle}
|
||||||
|
>
|
||||||
{deck.format}
|
{deck.format}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingDeck({...deck})}
|
onClick={() => setEditingDeck({ ...deck })}
|
||||||
className="text-text-secondary hover:text-accent-ember transition-colors"
|
className="transition-colors"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
aria-label="Edit deck"
|
||||||
>
|
>
|
||||||
✏️
|
✏️
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteDeck(deck.id)}
|
onClick={() => handleDeleteDeck(deck.id)}
|
||||||
className="text-text-secondary hover:text-red-500 transition-colors"
|
className="transition-colors"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
aria-label="Delete deck"
|
||||||
>
|
>
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-xl font-bold text-text-primary mb-2">{deck.name}</h3>
|
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
{deck.name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
{deck.description && (
|
{deck.description && (
|
||||||
<p className="text-text-secondary text-sm mb-4 line-clamp-2">{deck.description}</p>
|
<p
|
||||||
|
className="text-sm mb-4 line-clamp-2"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
|
{deck.description}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between items-center text-sm text-text-secondary mb-4">
|
<div
|
||||||
|
className="flex justify-between items-center text-sm mb-4"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
>
|
||||||
<span>{deck.card_count || 0} cards</span>
|
<span>{deck.card_count || 0} cards</span>
|
||||||
{deck.is_public && <span className="text-green-600">Public</span>}
|
{deck.is_public && (
|
||||||
|
<span style={{ color: 'var(--accent-ember)' }}>Public</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Link
|
<Link href={`/deck-builder?deck=${deck.id}`} className="flex-1">
|
||||||
href={`/deck-builder?deck=${deck.id}`}
|
<Button variant="primary" size="md" className="w-full">
|
||||||
className="flex-1 bg-accent-ember text-white text-center py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
Edit Deck
|
||||||
>
|
</Button>
|
||||||
Edit Deck
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link href={`/deck/${deck.id}`} className="flex-1">
|
||||||
href={`/deck/${deck.id}`}
|
<Button variant="secondary" size="md" className="w-full">
|
||||||
className="flex-1 bg-bg-tertiary text-text-primary text-center py-2 rounded-lg hover:bg-bg-primary transition-colors"
|
View
|
||||||
>
|
</Button>
|
||||||
View
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -326,7 +355,7 @@ export default function Decks() {
|
||||||
id="create-deck-format"
|
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={inputFieldClass}
|
||||||
>
|
>
|
||||||
<option value="Commander">Commander</option>
|
<option value="Commander">Commander</option>
|
||||||
<option value="Standard">Standard</option>
|
<option value="Standard">Standard</option>
|
||||||
|
|
@ -346,7 +375,7 @@ export default function Decks() {
|
||||||
id="create-deck-description"
|
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={inputFieldClass}
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="Describe your deck strategy..."
|
placeholder="Describe your deck strategy..."
|
||||||
/>
|
/>
|
||||||
|
|
@ -391,9 +420,7 @@ export default function Decks() {
|
||||||
label="Deck Name *"
|
label="Deck Name *"
|
||||||
required
|
required
|
||||||
value={editingDeck.name}
|
value={editingDeck.name}
|
||||||
onChange={(e) =>
|
onChange={(e) => setEditingDeck({ ...editingDeck, name: e.target.value })}
|
||||||
setEditingDeck({ ...editingDeck, name: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|
@ -406,10 +433,8 @@ export default function Decks() {
|
||||||
<select
|
<select
|
||||||
id="edit-deck-format"
|
id="edit-deck-format"
|
||||||
value={editingDeck.format}
|
value={editingDeck.format}
|
||||||
onChange={(e) =>
|
onChange={(e) => setEditingDeck({ ...editingDeck, format: e.target.value })}
|
||||||
setEditingDeck({ ...editingDeck, format: e.target.value })
|
className={inputFieldClass}
|
||||||
}
|
|
||||||
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>
|
||||||
<option value="Standard">Standard</option>
|
<option value="Standard">Standard</option>
|
||||||
|
|
@ -428,10 +453,8 @@ export default function Decks() {
|
||||||
<textarea
|
<textarea
|
||||||
id="edit-deck-description"
|
id="edit-deck-description"
|
||||||
value={editingDeck.description || ''}
|
value={editingDeck.description || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) => setEditingDeck({ ...editingDeck, description: e.target.value })}
|
||||||
setEditingDeck({ ...editingDeck, description: e.target.value })
|
className={inputFieldClass}
|
||||||
}
|
|
||||||
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>
|
||||||
|
|
@ -439,9 +462,7 @@ export default function Decks() {
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={editingDeck.is_public}
|
checked={editingDeck.is_public}
|
||||||
onChange={(e) =>
|
onChange={(e) => setEditingDeck({ ...editingDeck, is_public: e.target.checked })}
|
||||||
setEditingDeck({ ...editingDeck, is_public: e.target.checked })
|
|
||||||
}
|
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
style={{ accentColor: 'var(--accent-ember)' }}
|
style={{ accentColor: 'var(--accent-ember)' }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +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 { Button, SearchBar } 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';
|
||||||
|
|
||||||
|
|
@ -310,52 +310,40 @@ export default function MyCards() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="p-4 sm:p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
<div className="p-4 sm:p-6">
|
||||||
<div className="flex flex-wrap gap-4 mb-6">
|
<div className="glass-panel rounded-2xl p-4 mb-6">
|
||||||
<input
|
<div className="flex flex-wrap gap-4">
|
||||||
type="text"
|
<div className="flex-1 min-w-[200px]">
|
||||||
placeholder="Search your cards..."
|
<SearchBar
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="flex-1 min-w-[200px] px-4 py-2 rounded-xl border"
|
onClear={() => setSearchQuery('')}
|
||||||
style={{
|
placeholder="Search your cards…"
|
||||||
backgroundColor: 'var(--bg-secondary)',
|
/>
|
||||||
borderColor: 'var(--border)',
|
</div>
|
||||||
color: 'var(--text-primary)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<select
|
|
||||||
value={selectedTCG}
|
|
||||||
onChange={(e) => setSelectedTCG(e.target.value)}
|
|
||||||
className="px-4 py-2 rounded-xl border"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'var(--bg-secondary)',
|
|
||||||
borderColor: 'var(--border)',
|
|
||||||
color: 'var(--text-primary)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="all">All Games</option>
|
|
||||||
{filters.games.map(game => (
|
|
||||||
<option key={game} value={game}>{game}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={selectedRarity}
|
value={selectedTCG}
|
||||||
onChange={(e) => setSelectedRarity(e.target.value)}
|
onChange={(e) => setSelectedTCG(e.target.value)}
|
||||||
className="px-4 py-2 rounded-xl border"
|
className="input-field"
|
||||||
style={{
|
>
|
||||||
backgroundColor: 'var(--bg-secondary)',
|
<option value="all">All Games</option>
|
||||||
borderColor: 'var(--border)',
|
{filters.games.map(game => (
|
||||||
color: 'var(--text-primary)'
|
<option key={game} value={game}>{game}</option>
|
||||||
}}
|
))}
|
||||||
>
|
</select>
|
||||||
<option value="all">All Rarities</option>
|
|
||||||
{filters.rarities.map(rarity => (
|
<select
|
||||||
<option key={rarity} value={rarity}>{rarity}</option>
|
value={selectedRarity}
|
||||||
))}
|
onChange={(e) => setSelectedRarity(e.target.value)}
|
||||||
</select>
|
className="input-field"
|
||||||
|
>
|
||||||
|
<option value="all">All Rarities</option>
|
||||||
|
{filters.rarities.map(rarity => (
|
||||||
|
<option key={rarity} value={rarity}>{rarity}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results Summary */}
|
{/* Results Summary */}
|
||||||
|
|
|
||||||
|
|
@ -275,11 +275,15 @@ export default function Profile() {
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
{message.text && (
|
{message.text && (
|
||||||
<div className={`mb-6 p-4 rounded-xl ${
|
<div
|
||||||
message.type === 'success'
|
className="mb-6 p-4 rounded-xl glass-panel"
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
style={{
|
||||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
color:
|
||||||
}`}>
|
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||||
|
borderColor:
|
||||||
|
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -251,11 +251,15 @@ export default function Settings() {
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
{message.text && (
|
{message.text && (
|
||||||
<div className={`mb-6 p-4 rounded-xl ${
|
<div
|
||||||
message.type === 'success'
|
className="mb-6 p-4 rounded-xl glass-panel"
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
style={{
|
||||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
color:
|
||||||
}`}>
|
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||||
|
borderColor:
|
||||||
|
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -269,13 +273,9 @@ export default function Settings() {
|
||||||
<button
|
<button
|
||||||
key={section.id}
|
key={section.id}
|
||||||
onClick={() => setActiveSection(section.id)}
|
onClick={() => setActiveSection(section.id)}
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl font-medium transition-all duration-200 text-left ${
|
className={`nav-item w-full flex items-center gap-3 px-4 py-3 font-medium text-left ${
|
||||||
activeSection === section.id ? 'shadow-lg' : 'hover:shadow-md'
|
activeSection === section.id ? 'nav-item-active' : 'nav-item-hover'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
|
||||||
backgroundColor: activeSection === section.id ? 'var(--accent-ember)' : 'transparent',
|
|
||||||
color: activeSection === section.id ? 'white' : 'var(--text-primary)'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span>{section.icon}</span>
|
<span>{section.icon}</span>
|
||||||
{section.name}
|
{section.name}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue