2026-06-02 02:03:36 -04:00
|
|
|
/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
|
2025-07-26 00:22:15 -04:00
|
|
|
import { useState } from 'react';
|
2025-07-26 01:12:54 -04:00
|
|
|
import { useRouter } from 'next/router';
|
2026-05-29 10:53:40 -04:00
|
|
|
import { VOCAB } from '../lib/collection-vocabulary.js';
|
2025-07-26 00:22:15 -04:00
|
|
|
|
2025-07-26 10:00:26 -04:00
|
|
|
// Expanding Add Button Component
|
|
|
|
|
function ExpandingAddButton({ card, onAddToCollection, onAddToDeck, onMarkAsOwned }) {
|
|
|
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleMainClick = (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (!isExpanded) {
|
|
|
|
|
setIsExpanded(true);
|
|
|
|
|
} else {
|
|
|
|
|
// Default action when expanded - add to collection
|
|
|
|
|
onAddToCollection([card]);
|
|
|
|
|
setIsExpanded(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOptionClick = (e, action) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
action();
|
|
|
|
|
setIsExpanded(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="relative">
|
|
|
|
|
{/* Expanded Options */}
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="absolute bottom-full left-0 mb-1 flex flex-col space-y-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => handleOptionClick(e, () => onAddToCollection([card]))}
|
|
|
|
|
className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 flex items-center space-x-1 text-xs whitespace-nowrap"
|
|
|
|
|
style={{ backgroundColor: 'var(--accent-flame)', color: 'white' }}
|
2026-05-29 10:53:40 -04:00
|
|
|
title={VOCAB.ADD_TO_LIST}
|
2025-07-26 10:00:26 -04:00
|
|
|
>
|
|
|
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>Collection</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => handleOptionClick(e, () => onAddToDeck([card]))}
|
|
|
|
|
className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 flex items-center space-x-1 text-xs whitespace-nowrap"
|
|
|
|
|
style={{ backgroundColor: 'var(--accent-gold)', color: 'white' }}
|
|
|
|
|
title="Add to Deck"
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>Deck</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => handleOptionClick(e, onMarkAsOwned)}
|
|
|
|
|
className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 flex items-center space-x-1 text-xs whitespace-nowrap"
|
2025-07-26 10:17:22 -04:00
|
|
|
style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}
|
2026-05-29 10:53:40 -04:00
|
|
|
title={VOCAB.ADD_TO_MY_COLLECTION}
|
2025-07-26 10:00:26 -04:00
|
|
|
>
|
|
|
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>Own</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Main Add Button */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleMainClick}
|
|
|
|
|
className="p-1.5 rounded-md shadow-lg transition-all duration-150 hover:scale-105 relative"
|
|
|
|
|
style={{
|
2025-07-26 10:17:22 -04:00
|
|
|
backgroundColor: 'var(--accent-ember)',
|
|
|
|
|
color: 'white',
|
|
|
|
|
boxShadow: '0 0 0 2px var(--accent-ember), 0 4px 6px -1px rgba(216, 67, 21, 0.3)'
|
2025-07-26 10:00:26 -04:00
|
|
|
}}
|
2026-05-29 10:53:40 -04:00
|
|
|
title={isExpanded ? VOCAB.ADD_TO_LIST : 'Show Options'}
|
2025-07-26 10:00:26 -04:00
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
className={`w-4 h-4 transition-transform duration-150 ${isExpanded ? 'rotate-45' : ''}`}
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Click outside to close */}
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-[-1]"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setIsExpanded(false);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 00:22:15 -04:00
|
|
|
export default function CardItem({
|
|
|
|
|
card,
|
|
|
|
|
viewMode = 'grid',
|
|
|
|
|
isSelected = false,
|
|
|
|
|
onToggleSelect,
|
|
|
|
|
onAddToCollection,
|
|
|
|
|
onAddToDeck,
|
|
|
|
|
onToggleFavorite,
|
2025-07-26 09:45:38 -04:00
|
|
|
isFavorited = false
|
2025-07-26 00:22:15 -04:00
|
|
|
}) {
|
|
|
|
|
const [imageError, setImageError] = useState(false);
|
2025-07-26 01:12:54 -04:00
|
|
|
const router = useRouter();
|
2025-07-26 00:22:15 -04:00
|
|
|
|
|
|
|
|
const handleSelectChange = (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onToggleSelect(card);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleImageError = () => {
|
|
|
|
|
setImageError(true);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-26 01:12:54 -04:00
|
|
|
const handleCardClick = (e) => {
|
|
|
|
|
// Don't navigate if clicking on interactive elements
|
|
|
|
|
if (e.target.closest('input, button, a')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
router.push(`/card/${card.id}`);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-26 01:01:04 -04:00
|
|
|
// Get rarity-based styling
|
|
|
|
|
const getRarityEffects = (rarity) => {
|
|
|
|
|
const rarityLower = rarity?.toLowerCase() || '';
|
|
|
|
|
|
|
|
|
|
if (rarityLower.includes('mythic') || rarityLower.includes('legendary')) {
|
|
|
|
|
return {
|
|
|
|
|
glow: 'rarity-glow-mythic',
|
|
|
|
|
particles: 'rarity-particles-mythic',
|
|
|
|
|
border: 'border-yellow-400',
|
|
|
|
|
shadow: 'shadow-yellow-400/50'
|
|
|
|
|
};
|
|
|
|
|
} else if (rarityLower.includes('rare') || rarityLower.includes('holo')) {
|
|
|
|
|
return {
|
|
|
|
|
glow: 'rarity-glow-rare',
|
|
|
|
|
particles: 'rarity-particles-rare',
|
|
|
|
|
border: 'border-purple-400',
|
|
|
|
|
shadow: 'shadow-purple-400/50'
|
|
|
|
|
};
|
|
|
|
|
} else if (rarityLower.includes('uncommon')) {
|
|
|
|
|
return {
|
|
|
|
|
glow: 'rarity-glow-uncommon',
|
|
|
|
|
particles: 'rarity-particles-uncommon',
|
|
|
|
|
border: 'border-blue-400',
|
|
|
|
|
shadow: 'shadow-blue-400/30'
|
|
|
|
|
};
|
|
|
|
|
} else if (rarityLower.includes('enchanted') || rarityLower.includes('secret')) {
|
|
|
|
|
return {
|
|
|
|
|
glow: 'rarity-glow-enchanted',
|
|
|
|
|
particles: 'rarity-particles-enchanted',
|
|
|
|
|
border: 'border-pink-400',
|
|
|
|
|
shadow: 'shadow-pink-400/60'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
glow: '',
|
|
|
|
|
particles: '',
|
|
|
|
|
border: 'border-gray-200',
|
|
|
|
|
shadow: ''
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rarityEffects = getRarityEffects(card.rarity);
|
|
|
|
|
|
2025-07-26 09:45:38 -04:00
|
|
|
// Panel positioning no longer needed - using simple hover buttons
|
2025-07-26 01:49:42 -04:00
|
|
|
|
2025-07-26 00:22:15 -04:00
|
|
|
if (viewMode === 'list') {
|
|
|
|
|
return (
|
2026-06-04 17:49:17 -04:00
|
|
|
<div
|
|
|
|
|
className="flex items-center p-4 rounded-xl transition-all duration-200 cursor-pointer nav-item-hover"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: isSelected
|
|
|
|
|
? 'rgba(255, 110, 0, 0.08)'
|
|
|
|
|
: 'transparent',
|
|
|
|
|
border: '1px solid',
|
|
|
|
|
borderColor: isSelected ? 'var(--accent-ember)' : 'var(--border)',
|
|
|
|
|
boxShadow: isSelected ? 'var(--rim-light-inner)' : 'none',
|
|
|
|
|
}}
|
2025-07-26 01:12:54 -04:00
|
|
|
onClick={handleCardClick}
|
|
|
|
|
>
|
2025-07-26 00:22:15 -04:00
|
|
|
{/* Selection Checkbox */}
|
|
|
|
|
<div className="flex-shrink-0 mr-4">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={isSelected}
|
|
|
|
|
onChange={handleSelectChange}
|
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>
2026-06-04 15:06:22 -04:00
|
|
|
className="w-4 h-4 rounded focus:ring-2"
|
|
|
|
|
style={{
|
|
|
|
|
accentColor: 'var(--accent-ember)',
|
|
|
|
|
'--tw-ring-color': 'var(--accent-ember)',
|
|
|
|
|
}}
|
2025-07-26 00:22:15 -04:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Card Image */}
|
|
|
|
|
<div className="flex-shrink-0 mr-4">
|
2026-06-04 17:49:17 -04:00
|
|
|
<div
|
|
|
|
|
className="w-16 h-22 rounded-lg overflow-hidden"
|
|
|
|
|
style={{ backgroundColor: 'var(--bg-tertiary)' }}
|
|
|
|
|
>
|
2025-07-26 00:22:15 -04:00
|
|
|
{!imageError ? (
|
|
|
|
|
<img
|
|
|
|
|
src={card.image_url}
|
|
|
|
|
alt={card.name}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
onError={handleImageError}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
2026-06-04 17:49:17 -04:00
|
|
|
<div
|
|
|
|
|
className="w-full h-full flex items-center justify-center text-xs"
|
|
|
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
|
|
|
>
|
2025-07-26 00:22:15 -04:00
|
|
|
No Image
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Card Info */}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
2026-06-04 17:49:17 -04:00
|
|
|
<h3
|
|
|
|
|
className="font-semibold truncate"
|
|
|
|
|
style={{ color: 'var(--text-primary)' }}
|
|
|
|
|
>
|
|
|
|
|
{card.name}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
{card.set_name} • {card.rarity}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
{card.card_type}
|
|
|
|
|
</p>
|
2025-07-26 00:22:15 -04:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Game Badge */}
|
|
|
|
|
<div className="flex-shrink-0 mx-4">
|
2026-06-04 17:49:17 -04:00
|
|
|
<span
|
|
|
|
|
className="px-2 py-1 text-xs font-medium rounded-full"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'var(--bg-tertiary)',
|
|
|
|
|
color: 'var(--text-primary)',
|
|
|
|
|
border: '1px solid var(--border)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-07-26 00:22:15 -04:00
|
|
|
{card.game}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Price */}
|
|
|
|
|
<div className="flex-shrink-0 mx-4">
|
2026-06-04 17:49:17 -04:00
|
|
|
<span
|
|
|
|
|
className="text-lg font-bold"
|
|
|
|
|
style={{ color: 'var(--accent-flame)' }}
|
|
|
|
|
>
|
2025-07-26 00:22:15 -04:00
|
|
|
${card.market_price}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
|
|
|
<div className="flex-shrink-0 flex items-center space-x-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onToggleFavorite(card);
|
|
|
|
|
}}
|
2026-06-04 17:49:17 -04:00
|
|
|
className="nav-item-hover p-2 rounded-xl transition-colors"
|
|
|
|
|
style={{
|
|
|
|
|
color: isFavorited
|
|
|
|
|
? 'var(--accent-ember)'
|
|
|
|
|
: 'var(--text-secondary)',
|
|
|
|
|
}}
|
2025-07-26 00:22:15 -04:00
|
|
|
>
|
|
|
|
|
<svg className="w-5 h-5" 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" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onAddToCollection(card);
|
|
|
|
|
}}
|
2026-06-04 17:49:17 -04:00
|
|
|
className="nav-item-hover p-2 rounded-xl transition-colors"
|
|
|
|
|
style={{ color: 'var(--accent-ember)' }}
|
2026-05-29 10:53:40 -04:00
|
|
|
title={VOCAB.ADD_TO_LIST}
|
2025-07-26 00:22:15 -04:00
|
|
|
>
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onAddToDeck(card);
|
|
|
|
|
}}
|
2026-06-04 17:49:17 -04:00
|
|
|
className="nav-item-hover p-2 rounded-xl transition-colors"
|
|
|
|
|
style={{ color: 'var(--accent-flame)' }}
|
2025-07-26 00:22:15 -04:00
|
|
|
title="Add to Deck"
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Grid view
|
|
|
|
|
return (
|
2026-06-04 12:14:53 -04:00
|
|
|
<div className="card-item-container card-grid-outer-glow relative group bg-transparent rounded-xl transition-all duration-150 overflow-visible transform hover:scale-102 hover:z-10">
|
2025-07-26 00:44:03 -04:00
|
|
|
{/* Main Card Container */}
|
2025-07-26 01:12:54 -04:00
|
|
|
<div
|
2025-07-26 09:45:38 -04:00
|
|
|
className={`relative rounded-xl border-2 transition-all duration-200 overflow-hidden cursor-pointer ${
|
2025-07-26 01:12:54 -04:00
|
|
|
isSelected
|
2025-07-26 09:45:38 -04:00
|
|
|
? 'shadow-xl ring-2 ring-opacity-50'
|
|
|
|
|
: 'group-hover:shadow-xl'
|
2025-07-26 01:12:54 -04:00
|
|
|
} ${rarityEffects.glow}`}
|
2025-07-26 09:45:38 -04:00
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
borderColor: isSelected ? 'var(--accent-ember)' : 'var(--border)',
|
|
|
|
|
'--tw-ring-color': isSelected ? 'var(--accent-ember)' : 'transparent'
|
|
|
|
|
}}
|
2025-07-26 01:12:54 -04:00
|
|
|
onClick={handleCardClick}
|
|
|
|
|
>
|
2025-07-26 01:01:04 -04:00
|
|
|
|
|
|
|
|
{/* Rarity Particles */}
|
|
|
|
|
{rarityEffects.particles && (
|
|
|
|
|
<div className={`absolute inset-0 pointer-events-none ${rarityEffects.particles}`}></div>
|
|
|
|
|
)}
|
2025-07-26 00:44:03 -04:00
|
|
|
|
2025-07-26 09:45:38 -04:00
|
|
|
|
2025-07-26 00:22:15 -04:00
|
|
|
|
2025-07-26 00:44:03 -04:00
|
|
|
{/* Card Image */}
|
|
|
|
|
<div className="aspect-[2.5/3.5] bg-gray-200 overflow-hidden">
|
|
|
|
|
{!imageError ? (
|
|
|
|
|
<img
|
|
|
|
|
src={card.image_url}
|
|
|
|
|
alt={card.name}
|
2025-07-26 09:45:38 -04:00
|
|
|
className="w-full h-full object-cover"
|
2025-07-26 00:44:03 -04:00
|
|
|
onError={handleImageError}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-xs">No Image</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-07-26 00:22:15 -04:00
|
|
|
</div>
|
|
|
|
|
|
2025-07-26 09:45:38 -04:00
|
|
|
{/* Hover Action Buttons */}
|
2025-07-26 10:00:26 -04:00
|
|
|
{/* Top Left - Checkbox */}
|
|
|
|
|
<div className="absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-150 z-20">
|
2025-07-26 09:45:38 -04:00
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onToggleSelect(card);
|
|
|
|
|
}}
|
2025-07-26 10:00:26 -04:00
|
|
|
className="p-1 rounded-md shadow-lg transition-all duration-150 hover:scale-105"
|
2025-07-26 09:45:38 -04:00
|
|
|
style={{ backgroundColor: 'var(--bg-primary)' }}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={isSelected}
|
|
|
|
|
onChange={() => {}}
|
2025-07-26 10:00:26 -04:00
|
|
|
className="w-4 h-4 rounded focus:ring-1 focus:ring-offset-1"
|
2025-07-26 09:45:38 -04:00
|
|
|
style={{
|
|
|
|
|
accentColor: 'var(--accent-ember)',
|
|
|
|
|
backgroundColor: 'var(--bg-primary)',
|
|
|
|
|
borderColor: 'var(--border)'
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
2025-07-26 10:00:26 -04:00
|
|
|
</div>
|
2025-07-26 00:55:54 -04:00
|
|
|
|
2025-07-26 10:00:26 -04:00
|
|
|
{/* Top Right - Favorite */}
|
|
|
|
|
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-150 z-20">
|
2025-07-26 09:45:38 -04:00
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onToggleFavorite(card);
|
|
|
|
|
}}
|
2025-07-26 10:00:26 -04:00
|
|
|
className="p-1 rounded-md shadow-lg transition-all duration-150 hover:scale-105"
|
2025-07-26 09:45:38 -04:00
|
|
|
style={{
|
|
|
|
|
backgroundColor: isFavorited ? 'var(--accent-ember)' : 'var(--bg-primary)',
|
|
|
|
|
color: isFavorited ? 'white' : 'var(--accent-ember)'
|
|
|
|
|
}}
|
|
|
|
|
title="Toggle Favorite"
|
|
|
|
|
>
|
|
|
|
|
<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" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-07-26 00:55:54 -04:00
|
|
|
|
2025-07-26 10:00:26 -04:00
|
|
|
{/* Bottom Left - Expanding Add Button */}
|
|
|
|
|
<div className="absolute bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-150 z-20">
|
|
|
|
|
<ExpandingAddButton
|
|
|
|
|
card={card}
|
|
|
|
|
onAddToCollection={onAddToCollection}
|
|
|
|
|
onAddToDeck={onAddToDeck}
|
2026-05-29 10:53:40 -04:00
|
|
|
onMarkAsOwned={() => alert(`${VOCAB.ADD_TO_MY_COLLECTION} (coming soon)`)}
|
2025-07-26 10:00:26 -04:00
|
|
|
/>
|
2025-07-26 00:22:15 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|