refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive #117
19 changed files with 1397 additions and 1188 deletions
|
|
@ -194,7 +194,11 @@ export default function CardItem({
|
|||
type="checkbox"
|
||||
checked={isSelected}
|
||||
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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import CardItem from './CardItem';
|
||||
import BulkSelectionToolbar from './BulkSelectionToolbar';
|
||||
import CollectionSelectionModal from './CollectionSelectionModal';
|
||||
import { Button, SearchBar } from './ui';
|
||||
|
||||
export default function CardsPageView(props) {
|
||||
const {
|
||||
|
|
@ -146,14 +147,18 @@ export default function CardsPageView(props) {
|
|||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||
Search
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search cards..."
|
||||
className="search-bar w-full pr-12"
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
onClear={() => {
|
||||
handleSearchChange('');
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
setCards([]);
|
||||
setHasMore(true);
|
||||
hasMoreRef.current = true;
|
||||
fetchCards(false);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
setCards([]);
|
||||
|
|
@ -162,23 +167,8 @@ export default function CardsPageView(props) {
|
|||
fetchCards(false);
|
||||
}
|
||||
}}
|
||||
placeholder="Search cards…"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
setCards([]);
|
||||
setHasMore(true);
|
||||
hasMoreRef.current = true;
|
||||
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)' }}
|
||||
>
|
||||
<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>
|
||||
<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 */}
|
||||
{hasMore && !loadingMore && (
|
||||
<div className="flex justify-center py-8">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Button variant="primary" size="lg" onClick={loadMoreCards}>
|
||||
Load More Cards
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ManaSymbolSettings from './ManaSymbolSettings';
|
|||
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
|
||||
import CollectionEditModal from './CollectionEditModal';
|
||||
import CollectionDeleteModal from './CollectionDeleteModal';
|
||||
import { Button, SearchBar } from './ui';
|
||||
export default function CollectionPageView(props) {
|
||||
const {
|
||||
cards,
|
||||
|
|
@ -99,7 +100,7 @@ export default function CollectionPageView(props) {
|
|||
<>
|
||||
<button
|
||||
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)' }}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -109,7 +110,7 @@ export default function CollectionPageView(props) {
|
|||
</button>
|
||||
<button
|
||||
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)' }}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -122,12 +123,15 @@ export default function CollectionPageView(props) {
|
|||
|
||||
<button
|
||||
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)' }}
|
||||
>
|
||||
Upload Image
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -142,16 +146,31 @@ export default function CollectionPageView(props) {
|
|||
{/* System collection indicator */}
|
||||
{collection.isSystemCollection && (
|
||||
<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
|
||||
</span>
|
||||
<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" />
|
||||
</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}
|
||||
<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>
|
||||
|
|
@ -167,7 +186,11 @@ export default function CollectionPageView(props) {
|
|||
count > 0 ? (
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
|
|
@ -202,7 +225,7 @@ export default function CollectionPageView(props) {
|
|||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
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)' }}
|
||||
>
|
||||
<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
|
||||
onClick={toggleFavorite}
|
||||
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg ${
|
||||
isFavorited ? 'bg-red-50 border-red-200 text-red-600' : 'hover:bg-gray-50'
|
||||
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-xl ${
|
||||
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">
|
||||
<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" />
|
||||
|
|
@ -226,7 +257,7 @@ export default function CollectionPageView(props) {
|
|||
|
||||
<button
|
||||
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)' }}
|
||||
>
|
||||
<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)' }}>
|
||||
<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>
|
||||
|
|
@ -248,28 +287,25 @@ export default function CollectionPageView(props) {
|
|||
{/* Search and Filters */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search cards..."
|
||||
<div className="w-64">
|
||||
<SearchBar
|
||||
value={searchCards}
|
||||
onChange={(e) => {
|
||||
setSearchCards(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"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
||||
onClear={() => {
|
||||
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>
|
||||
|
||||
<select
|
||||
value={selectedRarity}
|
||||
onChange={(e) => setSelectedRarity(e.target.value)}
|
||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
||||
className="input-field"
|
||||
>
|
||||
<option>All Rarities</option>
|
||||
<option>Common</option>
|
||||
|
|
@ -282,8 +318,7 @@ export default function CollectionPageView(props) {
|
|||
<select
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
||||
className="input-field"
|
||||
>
|
||||
<option>All Types</option>
|
||||
<option>Creature</option>
|
||||
|
|
@ -296,8 +331,7 @@ export default function CollectionPageView(props) {
|
|||
<select
|
||||
value={groupBy}
|
||||
onChange={(e) => setGroupBy(e.target.value)}
|
||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
||||
className="input-field"
|
||||
>
|
||||
<option>Group by Game</option>
|
||||
<option>Group by Rarity</option>
|
||||
|
|
@ -307,8 +341,7 @@ export default function CollectionPageView(props) {
|
|||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
||||
className="input-field"
|
||||
>
|
||||
<option>Sort by Name</option>
|
||||
<option>Sort by Price</option>
|
||||
|
|
@ -318,11 +351,22 @@ export default function CollectionPageView(props) {
|
|||
</div>
|
||||
|
||||
<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
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded-l-lg ${viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`}
|
||||
style={viewMode !== 'grid' ? { color: 'var(--text-secondary)' } : {}}
|
||||
className="p-2 rounded-l-xl transition-colors"
|
||||
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">
|
||||
<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,8 +374,16 @@ export default function CollectionPageView(props) {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded-r-lg ${viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`}
|
||||
style={viewMode !== 'list' ? { color: 'var(--text-secondary)' } : {}}
|
||||
className="p-2 rounded-r-xl transition-colors"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
|
|
@ -339,18 +391,22 @@ export default function CollectionPageView(props) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => router.push('/cards')}
|
||||
className="px-4 py-2 text-white rounded-lg flex items-center space-x-2"
|
||||
style={{ backgroundColor: 'var(--accent-ember)' }}
|
||||
>
|
||||
leadingIcon={
|
||||
<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>
|
||||
}
|
||||
>
|
||||
Add Cards
|
||||
</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)' }}>
|
||||
<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="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" />
|
||||
|
|
@ -366,7 +422,13 @@ export default function CollectionPageView(props) {
|
|||
<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)' }}>
|
||||
<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}
|
||||
</span>
|
||||
</h2>
|
||||
|
|
@ -399,21 +461,23 @@ export default function CollectionPageView(props) {
|
|||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
Add cards to get started with your collection
|
||||
</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
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
{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 => (
|
||||
<div
|
||||
key={card.id}
|
||||
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
|
||||
src={card.image_url}
|
||||
|
|
@ -424,8 +488,12 @@ export default function CollectionPageView(props) {
|
|||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium">{card.name}</div>
|
||||
<div className="text-sm text-gray-500">{card.set_name} • ${card.market_price}</div>
|
||||
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{card.name}
|
||||
</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
{card.set_name} • ${card.market_price}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -140,16 +140,31 @@ export default function CollectionsPageView(props) {
|
|||
{/* System collection indicator */}
|
||||
{collection.isSystemCollection && (
|
||||
<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
|
||||
</span>
|
||||
<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" />
|
||||
</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}
|
||||
<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>
|
||||
|
|
|
|||
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. */
|
||||
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({
|
||||
deckCards,
|
||||
|
|
@ -14,22 +36,30 @@ export default function DeckBuilderDeckList({
|
|||
{deckCards.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🃏</div>
|
||||
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3>
|
||||
<p className="text-text-secondary mb-4">Start building your deck by searching for cards</p>
|
||||
{!sidebarOpen && (
|
||||
<button
|
||||
onClick={onOpenSidebar}
|
||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
<h3
|
||||
className="text-xl font-semibold mb-2"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
Empty Deck
|
||||
</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>
|
||||
</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 bg-bg-primary rounded-lg hover:bg-bg-tertiary transition-colors">
|
||||
<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
|
||||
|
|
@ -39,21 +69,36 @@ export default function DeckBuilderDeckList({
|
|||
/>
|
||||
)}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-primary text-lg">{card.name}</h4>
|
||||
<p className="text-text-secondary text-sm">{card.set_name}</p>
|
||||
<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="bg-bg-secondary px-2 py-1 rounded">
|
||||
<ManaCost cost={card.mana_cost} size="sm" useSVG={manaSymbolSettings.useSVG} />
|
||||
<div
|
||||
className="px-2 py-1 rounded"
|
||||
style={{ backgroundColor: 'var(--bg-secondary)' }}
|
||||
>
|
||||
<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'
|
||||
}`}>
|
||||
<span
|
||||
className="text-xs px-2 py-1 rounded capitalize"
|
||||
style={rarityBadgeStyle(card.rarity)}
|
||||
>
|
||||
{card.rarity}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -61,17 +106,30 @@ export default function DeckBuilderDeckList({
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-text-primary font-bold text-lg">{card.quantity}x</span>
|
||||
<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 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors flex items-center justify-center font-bold"
|
||||
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 bg-accent-ember text-white rounded-full hover:bg-accent-ember-dark transition-colors flex items-center justify-center font-bold"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -4,26 +4,41 @@
|
|||
*/
|
||||
export default function DeckBuilderStatsBar({ stats }) {
|
||||
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-lg font-bold text-text-primary">{stats.totalCards}/100</div>
|
||||
<div className="text-text-secondary text-sm">Cards</div>
|
||||
<div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
{stats.totalCards}/100
|
||||
</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Cards
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-text-primary">{stats.avgCmc}</div>
|
||||
<div className="text-text-secondary text-sm">Avg CMC</div>
|
||||
<div className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
{stats.avgCmc}
|
||||
</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Avg CMC
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
<div className="text-text-secondary text-sm">Colors</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Colors
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
<div className="text-text-secondary text-sm">Types</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Types
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,16 @@ import Link from 'next/link';
|
|||
import DeckBuilderCardBrowser from './DeckBuilderCardBrowser';
|
||||
import DeckBuilderDeckList from './DeckBuilderDeckList';
|
||||
import DeckBuilderStatsBar from './DeckBuilderStatsBar';
|
||||
import { Button } from './ui';
|
||||
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({
|
||||
addCardToDeck,
|
||||
clearFilters,
|
||||
|
|
@ -37,46 +45,46 @@ export default function DeckBuilderView({
|
|||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<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
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-text-primary mt-2">{deck.name}</h1>
|
||||
<p className="text-text-secondary">
|
||||
<h1 className="text-3xl font-bold mt-2" style={{ color: 'var(--text-primary)' }}>
|
||||
{deck.name}
|
||||
</h1>
|
||||
<p style={{ color: 'var(--text-secondary)' }}>
|
||||
{deck.format} • {stats.totalCards}/100 cards
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-bg-secondary text-text-primary px-4 py-2 rounded-lg hover:bg-bg-tertiary transition-colors"
|
||||
>
|
||||
<Button type="button" variant="secondary">
|
||||
Save Deck
|
||||
</button>
|
||||
<Link
|
||||
href={`/deck/${deck.id}`}
|
||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
>
|
||||
View Deck
|
||||
</Button>
|
||||
<Link href={`/deck/${deck.id}`}>
|
||||
<Button variant="primary">View Deck</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 h-[calc(100vh-12rem)]">
|
||||
<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">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
<h2 className="text-xl font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||
Deck Cards ({stats.totalCards})
|
||||
</h2>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
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>
|
||||
<span>{sidebarOpen ? '→' : '←'}</span>
|
||||
</button>
|
||||
{sidebarOpen ? 'Hide' : 'Show'} Browser
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DeckBuilderStatsBar stats={stats} />
|
||||
|
|
|
|||
|
|
@ -99,11 +99,7 @@ function UserProfileDropdown({ user, onMobileMenuClose }) {
|
|||
{profileMenuItems.map((item) => (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<div
|
||||
className={`
|
||||
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' : ''}
|
||||
`}
|
||||
className="flex items-center px-4 py-3 text-sm cursor-pointer nav-item-hover"
|
||||
style={{
|
||||
color: item.isLogout ? 'var(--accent-ember)' : 'var(--text-primary)'
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -31,18 +31,27 @@ export default function ManaSymbolSettings({ onSettingsChange }) {
|
|||
};
|
||||
|
||||
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">
|
||||
<h4 className="text-sm font-medium text-text-primary">Mana Symbol Style</h4>
|
||||
<p className="text-xs text-text-secondary">
|
||||
<h4
|
||||
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'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
useSVG ? 'bg-accent-ember' : 'bg-gray-300'
|
||||
}`}
|
||||
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||
style={{
|
||||
backgroundColor: useSVG ? 'var(--accent-ember)' : 'var(--bg-tertiary)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ export function AdvancedManaCost({ cost, showAnalysis = false, useSVG = false })
|
|||
<div className="space-y-2">
|
||||
<ManaCost cost={analysis.cost} useSVG={useSVG} />
|
||||
{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>
|
||||
{analysis.colors.length > 0 && (
|
||||
<div className="flex items-center space-x-1">
|
||||
|
|
|
|||
|
|
@ -184,21 +184,33 @@ export default function ShareModal({
|
|||
|
||||
{/* Search Results */}
|
||||
{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) => (
|
||||
<div
|
||||
key={user.id}
|
||||
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 rounded-full flex items-center justify-center mr-3"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, var(--accent-ember) 0%, var(--accent-flame) 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="w-8 h-8 bg-purple-600 rounded-full flex items-center justify-center mr-3">
|
||||
<span className="text-white text-sm font-bold">
|
||||
{user.email.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{user.email}</div>
|
||||
<div className="text-sm text-gray-500">Click to invite as viewer</div>
|
||||
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{user.email}
|
||||
</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Click to invite as viewer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -207,19 +219,32 @@ export default function ShareModal({
|
|||
|
||||
{/* Email invite option */}
|
||||
{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
|
||||
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 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"
|
||||
>
|
||||
<div className="w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center mr-3">
|
||||
<svg className="w-4 h-4 text-white" 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" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Invite {searchQuery}</div>
|
||||
<div className="text-sm text-gray-500">Send email invitation as viewer</div>
|
||||
<div className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
Invite {searchQuery}
|
||||
</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Send email invitation as viewer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -310,7 +335,8 @@ export default function ShareModal({
|
|||
<button
|
||||
key={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">
|
||||
{social.icon === 'twitter' && (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
|
|||
import Link from 'next/link';
|
||||
import Layout from '../../components/Layout';
|
||||
import PermissionIndicator from '../../components/PermissionIndicator';
|
||||
import { Button } from '../../components/ui';
|
||||
import { Button, SearchBar } from '../../components/ui';
|
||||
import { useAuth } from '../../lib/use-auth';
|
||||
import { VOCAB } from '../../lib/collection-vocabulary.js';
|
||||
|
||||
|
|
@ -213,12 +213,11 @@ export default function CommunityCollections() {
|
|||
{/* Search and Sort */}
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<div className="flex-1 max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search lists..."
|
||||
className="input-field w-full"
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onClear={() => setSearchQuery('')}
|
||||
placeholder="Search lists…"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
|
|
@ -311,10 +310,10 @@ export default function CommunityCollections() {
|
|||
{collection.tags.slice(0, 2).map((tag, index) => (
|
||||
<span
|
||||
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={{
|
||||
backgroundColor: 'var(--bg-tertiary)',
|
||||
color: 'var(--text-secondary)'
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
|
|
@ -322,10 +321,10 @@ export default function CommunityCollections() {
|
|||
))}
|
||||
{collection.tags.length > 2 && (
|
||||
<span
|
||||
className="px-2 py-1 rounded-md text-xs font-medium"
|
||||
className="px-2 py-1 rounded-xl text-xs font-medium"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-tertiary)',
|
||||
color: 'var(--text-secondary)'
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
+{collection.tags.length - 2}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ export default function DeckBuilder() {
|
|||
return (
|
||||
<Layout user={user}>
|
||||
<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>
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,18 @@ import { useRouter } from 'next/router';
|
|||
import Link from 'next/link';
|
||||
import Layout from '../../components/Layout';
|
||||
import { ManaCost, ColorIdentity } from '../../components/ManaSymbols';
|
||||
import { Button } from '../../components/ui';
|
||||
import { useAuth } from '../../lib/use-auth';
|
||||
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() {
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
|
|
@ -136,7 +145,10 @@ export default function DeckDetail() {
|
|||
return (
|
||||
<Layout user={user}>
|
||||
<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>
|
||||
</Layout>
|
||||
);
|
||||
|
|
@ -147,8 +159,17 @@ export default function DeckDetail() {
|
|||
<Layout user={user}>
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Deck not found</h1>
|
||||
<Link href="/decks" className="text-accent-ember hover:underline">
|
||||
<h1
|
||||
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
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -168,34 +189,51 @@ export default function DeckDetail() {
|
|||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<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
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<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 && (
|
||||
<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
|
||||
</span>
|
||||
)}
|
||||
</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
|
||||
</p>
|
||||
{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>
|
||||
|
||||
{isOwner && (
|
||||
<div className="flex space-x-3">
|
||||
<Link
|
||||
href={`/deck-builder?deck=${deck.id}`}
|
||||
className="bg-accent-ember text-white px-4 py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
>
|
||||
Edit Deck
|
||||
<Link href={`/deck-builder?deck=${deck.id}`}>
|
||||
<Button variant="primary">Edit Deck</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -204,28 +242,44 @@ export default function DeckDetail() {
|
|||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Stats Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-bg-secondary rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Statistics</h3>
|
||||
<div className="glass-panel rounded-2xl p-6 mb-6">
|
||||
<h3
|
||||
className="text-lg font-semibold mb-4"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
Statistics
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Total Cards:</span>
|
||||
<span className="text-text-primary font-medium">{stats.totalCards}</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Total Cards:</span>
|
||||
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{stats.totalCards}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Avg. CMC:</span>
|
||||
<span className="text-text-primary font-medium">{stats.avgCmc}</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Avg. CMC:</span>
|
||||
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{stats.avgCmc}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Format:</span>
|
||||
<span className="text-text-primary font-medium">{deck.format}</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Format:</span>
|
||||
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{deck.format}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Color Distribution */}
|
||||
{Object.keys(stats.colorCounts).length > 0 && (
|
||||
<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">
|
||||
{Object.entries(stats.colorCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
|
|
@ -233,9 +287,19 @@ export default function DeckDetail() {
|
|||
<div key={color} className="flex justify-between items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<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>
|
||||
<span className="text-text-primary font-medium">{count}</span>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -245,15 +309,30 @@ export default function DeckDetail() {
|
|||
{/* Type Distribution */}
|
||||
{Object.keys(stats.typeCounts).length > 0 && (
|
||||
<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">
|
||||
{Object.entries(stats.typeCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 8)
|
||||
.map(([type, count]) => (
|
||||
<div key={type} className="flex justify-between">
|
||||
<span className="text-text-secondary text-sm">{type}</span>
|
||||
<span className="text-text-primary font-medium">{count}</span>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -262,22 +341,25 @@ export default function DeckDetail() {
|
|||
</div>
|
||||
|
||||
{/* Group By Controls */}
|
||||
<div className="bg-bg-secondary rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Group Cards By</h3>
|
||||
<div className="glass-panel rounded-2xl p-6">
|
||||
<h3
|
||||
className="text-lg font-semibold mb-4"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
Group Cards By
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ value: 'type', label: 'Card Type' },
|
||||
{ value: 'cmc', label: 'Mana Cost' },
|
||||
{ value: 'color', label: 'Color' },
|
||||
{ value: 'rarity', label: 'Rarity' }
|
||||
{ value: 'rarity', label: 'Rarity' },
|
||||
].map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => setGroupBy(option.value)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
|
||||
groupBy === option.value
|
||||
? 'bg-accent-ember text-white'
|
||||
: 'text-text-secondary hover:bg-bg-tertiary'
|
||||
className={`nav-item w-full text-left px-3 py-2 ${
|
||||
groupBy === option.value ? 'nav-item-active' : 'nav-item-hover'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
|
|
@ -289,12 +371,19 @@ export default function DeckDetail() {
|
|||
|
||||
{/* Card List */}
|
||||
<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 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🃏</div>
|
||||
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3>
|
||||
<p className="text-text-secondary">This deck doesn't have any cards yet</p>
|
||||
<h3
|
||||
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 className="space-y-6">
|
||||
|
|
@ -302,14 +391,24 @@ export default function DeckDetail() {
|
|||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([group, cards]) => (
|
||||
<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)})
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{cards
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.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 && (
|
||||
<img
|
||||
src={card.image_url}
|
||||
|
|
@ -319,15 +418,35 @@ export default function DeckDetail() {
|
|||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-text-primary truncate">{card.name}</h4>
|
||||
<span className="text-text-primary font-medium ml-2">{card.quantity}x</span>
|
||||
<h4
|
||||
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>
|
||||
<p className="text-text-secondary text-sm">{card.set_name}</p>
|
||||
<div className="flex items-center space-x-2 text-xs text-text-secondary">
|
||||
<p
|
||||
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 && (
|
||||
<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>
|
||||
|
|
|
|||
233
pages/decks.js
233
pages/decks.js
|
|
@ -5,6 +5,17 @@ import Layout from '../components/Layout';
|
|||
import { Modal, Input, Button } from '../components/ui';
|
||||
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() {
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
|
|
@ -16,7 +27,7 @@ export default function Decks() {
|
|||
name: '',
|
||||
description: '',
|
||||
format: 'Commander',
|
||||
is_public: false
|
||||
is_public: false,
|
||||
});
|
||||
|
||||
const fetchDecks = async () => {
|
||||
|
|
@ -24,8 +35,8 @@ export default function Decks() {
|
|||
const token = localStorage.getItem('auth_token');
|
||||
const response = await fetch('/api/decks', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
|
@ -39,7 +50,7 @@ export default function Decks() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
|
|
@ -48,20 +59,17 @@ export default function Decks() {
|
|||
}
|
||||
}, [user]);
|
||||
|
||||
;
|
||||
|
||||
const handleCreateDeck = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const response = await fetch('/api/decks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(newDeck)
|
||||
body: JSON.stringify(newDeck),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
|
@ -69,8 +77,6 @@ export default function Decks() {
|
|||
setDecks([createdDeck, ...decks]);
|
||||
setShowCreateModal(false);
|
||||
setNewDeck({ name: '', description: '', format: 'Commander', is_public: false });
|
||||
|
||||
// Navigate to deck builder for the new deck
|
||||
router.push(`/deck-builder?deck=${createdDeck.id}`);
|
||||
} else {
|
||||
console.error('Failed to create deck');
|
||||
|
|
@ -82,21 +88,20 @@ export default function Decks() {
|
|||
|
||||
const handleEditDeck = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const response = await fetch(`/api/decks/${editingDeck.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(editingDeck)
|
||||
body: JSON.stringify(editingDeck),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
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);
|
||||
} else {
|
||||
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.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const response = await fetch(`/api/decks/${deckId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setDecks(decks.filter(deck => deck.id !== deckId));
|
||||
setDecks(decks.filter((deck) => deck.id !== deckId));
|
||||
} else {
|
||||
console.error('Failed to delete deck');
|
||||
}
|
||||
|
|
@ -132,41 +134,46 @@ export default function Decks() {
|
|||
|
||||
const getFormatIcon = (format) => {
|
||||
switch (format) {
|
||||
case 'Commander':
|
||||
return '⚔️';
|
||||
case 'Standard':
|
||||
return '🏆';
|
||||
case 'Modern':
|
||||
return '🔥';
|
||||
case 'Legacy':
|
||||
return '💎';
|
||||
default:
|
||||
return '🃏';
|
||||
case 'Commander': return '⚔️';
|
||||
case 'Standard': return '🏆';
|
||||
case 'Modern': return '🔥';
|
||||
case 'Legacy': return '💎';
|
||||
default: return '🃏';
|
||||
}
|
||||
};
|
||||
|
||||
const getFormatColor = (format) => {
|
||||
switch (format) {
|
||||
case 'Commander':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'Standard':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'Modern':
|
||||
return 'bg-red-100 text-red-800';
|
||||
case 'Legacy':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
// Single neutral chip style for format badges — the prior
|
||||
// getFormatColor() returned 5 stale Tailwind colour pairs
|
||||
// (bg-purple-100, bg-blue-100, etc.) that don't fit the
|
||||
// Deck Hearth palette and don't render in dark theme anyway.
|
||||
const formatBadgeStyle = {
|
||||
backgroundColor: 'var(--bg-tertiary)',
|
||||
color: 'var(--text-primary)',
|
||||
};
|
||||
|
||||
// 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) {
|
||||
return (
|
||||
<Layout user={user}>
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Please log in to view your decks</h1>
|
||||
<Link href="/login" className="text-accent-ember hover:underline">
|
||||
<h1 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||||
Please log in to view your decks
|
||||
</h1>
|
||||
<Link href="/login" style={{ color: 'var(--accent-ember)' }} className="hover:underline">
|
||||
Go to Login
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -179,7 +186,10 @@ export default function Decks() {
|
|||
return (
|
||||
<Layout user={user}>
|
||||
<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>
|
||||
</Layout>
|
||||
);
|
||||
|
|
@ -191,42 +201,43 @@ export default function Decks() {
|
|||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-text-primary">My Decks</h1>
|
||||
<p className="text-text-secondary mt-2">
|
||||
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
My Decks
|
||||
</h1>
|
||||
<p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
|
||||
Build and manage your MTG decks
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="bg-accent-ember text-white px-6 py-3 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
>
|
||||
<Button variant="primary" onClick={() => setShowCreateModal(true)}>
|
||||
Create New Deck
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<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="text-2xl font-bold text-text-primary">{decks.length}</div>
|
||||
<div className="text-text-secondary">Total Decks</div>
|
||||
<div className="glass-panel rounded-2xl p-6" style={chipBgStyle}>
|
||||
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
{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 style={{ color: 'var(--text-secondary)' }}>Total Decks</div>
|
||||
</div>
|
||||
<div className="text-text-secondary">Commander</div>
|
||||
<div className="glass-panel rounded-2xl p-6">
|
||||
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
{decks.filter((d) => d.format === 'Commander').length}
|
||||
</div>
|
||||
<div className="bg-bg-secondary rounded-lg p-6">
|
||||
<div className="text-2xl font-bold text-text-primary">
|
||||
{decks.filter(d => d.is_public).length}
|
||||
<div style={{ color: 'var(--text-secondary)' }}>Commander</div>
|
||||
</div>
|
||||
<div className="text-text-secondary">Public</div>
|
||||
<div className="glass-panel rounded-2xl p-6">
|
||||
<div className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
{decks.filter((d) => d.is_public).length}
|
||||
</div>
|
||||
<div className="bg-bg-secondary rounded-lg p-6">
|
||||
<div className="text-2xl font-bold text-text-primary">
|
||||
<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)}
|
||||
</div>
|
||||
<div className="text-text-secondary">Total Cards</div>
|
||||
<div style={{ color: 'var(--text-secondary)' }}>Total Cards</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -234,65 +245,83 @@ export default function Decks() {
|
|||
{decks.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🃏</div>
|
||||
<h3 className="text-xl font-semibold text-text-primary mb-2">No decks yet</h3>
|
||||
<p className="text-text-secondary mb-6">Create your first deck to get started</p>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="bg-accent-ember text-white px-6 py-3 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
||||
No decks yet
|
||||
</h3>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
Create your first deck to get started
|
||||
</p>
|
||||
<Button variant="primary" onClick={() => setShowCreateModal(true)}>
|
||||
Create Your First Deck
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{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 items-center space-x-2">
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
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
|
||||
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>
|
||||
</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 && (
|
||||
<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>
|
||||
{deck.is_public && <span className="text-green-600">Public</span>}
|
||||
{deck.is_public && (
|
||||
<span style={{ color: 'var(--accent-ember)' }}>Public</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Link
|
||||
href={`/deck-builder?deck=${deck.id}`}
|
||||
className="flex-1 bg-accent-ember text-white text-center py-2 rounded-lg hover:bg-accent-ember-dark transition-colors"
|
||||
>
|
||||
<Link href={`/deck-builder?deck=${deck.id}`} className="flex-1">
|
||||
<Button variant="primary" size="md" className="w-full">
|
||||
Edit Deck
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/deck/${deck.id}`}
|
||||
className="flex-1 bg-bg-tertiary text-text-primary text-center py-2 rounded-lg hover:bg-bg-primary transition-colors"
|
||||
>
|
||||
<Link href={`/deck/${deck.id}`} className="flex-1">
|
||||
<Button variant="secondary" size="md" className="w-full">
|
||||
View
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -326,7 +355,7 @@ export default function Decks() {
|
|||
id="create-deck-format"
|
||||
value={newDeck.format}
|
||||
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="Standard">Standard</option>
|
||||
|
|
@ -346,7 +375,7 @@ export default function Decks() {
|
|||
id="create-deck-description"
|
||||
value={newDeck.description}
|
||||
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"
|
||||
placeholder="Describe your deck strategy..."
|
||||
/>
|
||||
|
|
@ -391,9 +420,7 @@ export default function Decks() {
|
|||
label="Deck Name *"
|
||||
required
|
||||
value={editingDeck.name}
|
||||
onChange={(e) =>
|
||||
setEditingDeck({ ...editingDeck, name: e.target.value })
|
||||
}
|
||||
onChange={(e) => setEditingDeck({ ...editingDeck, name: e.target.value })}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
|
|
@ -406,10 +433,8 @@ export default function Decks() {
|
|||
<select
|
||||
id="edit-deck-format"
|
||||
value={editingDeck.format}
|
||||
onChange={(e) =>
|
||||
setEditingDeck({ ...editingDeck, format: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
|
||||
onChange={(e) => setEditingDeck({ ...editingDeck, format: e.target.value })}
|
||||
className={inputFieldClass}
|
||||
>
|
||||
<option value="Commander">Commander</option>
|
||||
<option value="Standard">Standard</option>
|
||||
|
|
@ -428,10 +453,8 @@ export default function Decks() {
|
|||
<textarea
|
||||
id="edit-deck-description"
|
||||
value={editingDeck.description || ''}
|
||||
onChange={(e) =>
|
||||
setEditingDeck({ ...editingDeck, description: e.target.value })
|
||||
}
|
||||
className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-ember bg-bg-secondary text-text-primary"
|
||||
onChange={(e) => setEditingDeck({ ...editingDeck, description: e.target.value })}
|
||||
className={inputFieldClass}
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -439,9 +462,7 @@ export default function Decks() {
|
|||
<input
|
||||
type="checkbox"
|
||||
checked={editingDeck.is_public}
|
||||
onChange={(e) =>
|
||||
setEditingDeck({ ...editingDeck, is_public: e.target.checked })
|
||||
}
|
||||
onChange={(e) => setEditingDeck({ ...editingDeck, is_public: e.target.checked })}
|
||||
className="mr-2"
|
||||
style={{ accentColor: 'var(--accent-ember)' }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import BulkSelectionToolbar from '../components/BulkSelectionToolbar';
|
|||
import CollectionSelectionModal from '../components/CollectionSelectionModal';
|
||||
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
|
||||
import ManaSymbolSettings from '../components/ManaSymbolSettings';
|
||||
import { Button } from '../components/ui';
|
||||
import { Button, SearchBar } from '../components/ui';
|
||||
import { useAuth } from '../lib/use-auth';
|
||||
import { VOCAB } from '../lib/collection-vocabulary.js';
|
||||
|
||||
|
|
@ -310,30 +310,22 @@ export default function MyCards() {
|
|||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="p-4 sm:p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||
<div className="flex flex-wrap gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search your cards..."
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="glass-panel rounded-2xl p-4 mb-6">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="flex-1 min-w-[200px] px-4 py-2 rounded-xl border"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
onClear={() => setSearchQuery('')}
|
||||
placeholder="Search your cards…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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)'
|
||||
}}
|
||||
className="input-field"
|
||||
>
|
||||
<option value="all">All Games</option>
|
||||
{filters.games.map(game => (
|
||||
|
|
@ -344,12 +336,7 @@ export default function MyCards() {
|
|||
<select
|
||||
value={selectedRarity}
|
||||
onChange={(e) => setSelectedRarity(e.target.value)}
|
||||
className="px-4 py-2 rounded-xl border"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border)',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
className="input-field"
|
||||
>
|
||||
<option value="all">All Rarities</option>
|
||||
{filters.rarities.map(rarity => (
|
||||
|
|
@ -357,6 +344,7 @@ export default function MyCards() {
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Summary */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
|
|
|||
|
|
@ -275,11 +275,15 @@ export default function Profile() {
|
|||
|
||||
{/* Message */}
|
||||
{message.text && (
|
||||
<div className={`mb-6 p-4 rounded-xl ${
|
||||
message.type === 'success'
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
||||
}`}>
|
||||
<div
|
||||
className="mb-6 p-4 rounded-xl glass-panel"
|
||||
style={{
|
||||
color:
|
||||
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||
borderColor:
|
||||
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -251,11 +251,15 @@ export default function Settings() {
|
|||
|
||||
{/* Message */}
|
||||
{message.text && (
|
||||
<div className={`mb-6 p-4 rounded-xl ${
|
||||
message.type === 'success'
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
||||
}`}>
|
||||
<div
|
||||
className="mb-6 p-4 rounded-xl glass-panel"
|
||||
style={{
|
||||
color:
|
||||
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||
borderColor:
|
||||
message.type === 'success' ? 'var(--accent-flame)' : '#dc2626',
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -269,13 +273,9 @@ export default function Settings() {
|
|||
<button
|
||||
key={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 ${
|
||||
activeSection === section.id ? 'shadow-lg' : 'hover:shadow-md'
|
||||
className={`nav-item w-full flex items-center gap-3 px-4 py-3 font-medium text-left ${
|
||||
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>
|
||||
{section.name}
|
||||
|
|
|
|||
Loading…
Reference in a new issue