🎨 Redesign Collections Page with Card Thumbnails

📱 Layout Improvements:
- Removed TCG grouping for cleaner, unified view
- Added responsive grid layout (1-4 columns based on screen size)
- Implemented proper sorting options (name, value, card count, date)
- Moved metadata below thumbnails for better visual hierarchy

🖼️ Beautiful Card Thumbnails:
- Created CollectionThumbnail component with 2/3 + 1/3 layout
- Main card (rarest) displayed prominently with rarity glow effects
- Grid of 4 additional cards in smaller tiles
- Card name and rarity overlays on main card
- Fallback to hero image if user uploads custom thumbnail
- Elegant placeholder for empty collections

🔧 Enhanced Functionality:
- Smart thumbnail API fetches top 5 rarest cards by rarity priority
- Rarity ordering: mythic > legendary > rare > uncommon > common
- Secondary sorting by market price and name
- Proper access control for collection thumbnails
- Hover effects reveal edit/delete buttons

💅 Visual Polish:
- Compact stats display (cards count + value + date)
- Less prominent metadata positioning
- Improved spacing and typography
- Fire-themed color scheme throughout
- Smooth hover transitions and interactions
- Better mobile responsiveness

🎯 User Experience:
- Intuitive sorting controls in header
- Search functionality maintained
- Quick access to collection actions
- Visual feedback for empty states
- Consistent with Deck Hearth branding

The collections page now showcases beautiful card thumbnails that highlight the rarest cards in each collection! 🔥
This commit is contained in:
Randall Stillwell 2025-07-26 21:51:58 -05:00
parent a7ee884d02
commit b95d972e95
3 changed files with 479 additions and 285 deletions

View file

@ -0,0 +1,113 @@
import { sql } from '@vercel/postgres';
import { getUserFromRequest } from '../../../../lib/permission-middleware';
export default async function handler(req, res) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle preflight requests
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Get authenticated user
const user = await getUserFromRequest(req);
if (!user) {
return res.status(401).json({ error: 'Authentication required' });
}
const { id: collectionId } = req.query;
if (!collectionId) {
return res.status(400).json({ error: 'Collection ID is required' });
}
// Verify user has access to this collection
const collectionResult = await sql`
SELECT c.*, cp.role as user_role
FROM collections c
LEFT JOIN collection_permissions cp ON c.id = cp.collection_id AND cp.user_id = ${user.userId} AND cp.status = 'active'
WHERE c.id = ${collectionId}
AND (
c.user_id = ${user.userId} OR
cp.id IS NOT NULL OR
c.is_public = true
)
`;
if (collectionResult.rows.length === 0) {
return res.status(404).json({ error: 'Collection not found or access denied' });
}
// Define rarity priority order (highest to lowest value)
const rarityOrder = {
'mythic': 8,
'legendary': 7,
'rare': 6,
'uncommon': 5,
'common': 4,
'special': 3,
'promo': 2,
'token': 1
};
// Get the top 5 rarest cards from the collection
const thumbnailsResult = await sql`
SELECT DISTINCT
cards.id,
cards.name,
cards.rarity,
cards.image_url,
cards.stock_image_url,
cards.market_price,
cards.game,
cards.set_name,
cc.quantity
FROM collection_cards cc
JOIN cards ON cc.card_id = cards.id
WHERE cc.collection_id = ${collectionId}
AND (cards.image_url IS NOT NULL OR cards.stock_image_url IS NOT NULL)
ORDER BY
CASE cards.rarity
WHEN 'mythic' THEN 8
WHEN 'legendary' THEN 7
WHEN 'rare' THEN 6
WHEN 'uncommon' THEN 5
WHEN 'common' THEN 4
WHEN 'special' THEN 3
WHEN 'promo' THEN 2
WHEN 'token' THEN 1
ELSE 0
END DESC,
cards.market_price DESC NULLS LAST,
cards.name ASC
LIMIT 5
`;
const thumbnails = thumbnailsResult.rows.map(card => ({
id: card.id,
name: card.name,
rarity: card.rarity,
image_url: card.image_url,
stock_image_url: card.stock_image_url,
market_price: parseFloat(card.market_price) || 0,
game: card.game,
set_name: card.set_name,
quantity: parseInt(card.quantity) || 1
}));
res.status(200).json(thumbnails);
} catch (error) {
console.error('Error fetching collection thumbnails:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

View file

@ -17,71 +17,7 @@ export default function Collections() {
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [editingCollection, setEditingCollection] = useState(null); const [editingCollection, setEditingCollection] = useState(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedTCG, setSelectedTCG] = useState('all'); const [sortBy, setSortBy] = useState('name'); // name, value, cardCount, createdAt
// Sample data - replace with API calls
const [sampleCollections] = useState([
{
id: 1,
name: "Modern Masters 2021",
description: "Complete set of Modern Masters 2021",
tcg: "MTG",
cardCount: 254,
value: 2847.50,
lastViewed: "2024-01-15",
createdAt: "2024-01-10",
isPublic: true,
tags: ["modern", "masters", "complete"]
},
{
id: 2,
name: "Pokemon Base Set",
description: "Original Pokemon base set collection",
tcg: "Pokemon",
cardCount: 102,
value: 1250.00,
lastViewed: "2024-01-14",
createdAt: "2024-01-05",
isPublic: false,
tags: ["base", "original", "holographic"]
},
{
id: 3,
name: "Lorcana First Chapter",
description: "Disney Lorcana First Chapter collection",
tcg: "Lorcana",
cardCount: 204,
value: 890.00,
lastViewed: "2024-01-13",
createdAt: "2024-01-08",
isPublic: true,
tags: ["disney", "first-chapter", "enchanted"]
},
{
id: 4,
name: "Commander Staples",
description: "Essential cards for Commander format",
tcg: "MTG",
cardCount: 156,
value: 2100.00,
lastViewed: "2024-01-12",
createdAt: "2024-01-03",
isPublic: true,
tags: ["commander", "staples", "multiplayer"]
},
{
id: 5,
name: "Vintage Pokemon",
description: "Rare vintage Pokemon cards",
tcg: "Pokemon",
cardCount: 45,
value: 3500.00,
lastViewed: "2024-01-11",
createdAt: "2024-01-01",
isPublic: false,
tags: ["vintage", "rare", "holographic"]
}
]);
const [newCollection, setNewCollection] = useState({ const [newCollection, setNewCollection] = useState({
name: '', name: '',
@ -98,14 +34,27 @@ export default function Collections() {
const response = await fetch('/api/collections'); const response = await fetch('/api/collections');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setCollections(data); // Fetch thumbnail cards for each collection
const collectionsWithThumbnails = await Promise.all(
data.map(async (collection) => {
try {
const thumbnailResponse = await fetch(`/api/collections/${collection.id}/thumbnails`);
const thumbnails = thumbnailResponse.ok ? await thumbnailResponse.json() : [];
return { ...collection, thumbnails };
} catch (error) {
console.error(`Error fetching thumbnails for collection ${collection.id}:`, error);
return { ...collection, thumbnails: [] };
}
})
);
setCollections(collectionsWithThumbnails);
} else { } else {
console.error('Failed to fetch collections'); console.error('Failed to fetch collections');
setCollections(sampleCollections); // Fallback to sample data setCollections([]); // Empty array on error
} }
} catch (error) { } catch (error) {
console.error('Error fetching collections:', error); console.error('Error fetching collections:', error);
setCollections(sampleCollections); // Fallback to sample data setCollections([]); // Empty array on error
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -115,28 +64,37 @@ export default function Collections() {
fetchCollections(); fetchCollections();
}, []); }, []);
const tcgOptions = [ const sortOptions = [
{ value: 'MTG', label: 'Magic: The Gathering', color: 'purple' }, { value: 'name', label: 'Name (A-Z)' },
{ value: 'Pokemon', label: 'Pokemon', color: 'blue' }, { value: 'value', label: 'Value (High to Low)' },
{ value: 'Lorcana', label: 'Disney Lorcana', color: 'pink' }, { value: 'cardCount', label: 'Card Count (High to Low)' },
{ value: 'YuGiOh', label: 'Yu-Gi-Oh!', color: 'yellow' }, { value: 'createdAt', label: 'Date Created (Newest)' }
{ value: 'Digimon', label: 'Digimon', color: 'orange' }
]; ];
const sortCollections = (collections, sortBy) => {
return [...collections].sort((a, b) => {
switch (sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'value':
return b.value - a.value;
case 'cardCount':
return b.cardCount - a.cardCount;
case 'createdAt':
return new Date(b.createdAt) - new Date(a.createdAt);
default:
return 0;
}
});
};
const filteredCollections = collections.filter(collection => { const filteredCollections = collections.filter(collection => {
const matchesSearch = collection.name.toLowerCase().includes(searchQuery.toLowerCase()) || const matchesSearch = collection.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
collection.description.toLowerCase().includes(searchQuery.toLowerCase()); collection.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesTCG = selectedTCG === 'all' || collection.tcg === selectedTCG; return matchesSearch;
return matchesSearch && matchesTCG;
}); });
const groupedCollections = filteredCollections.reduce((acc, collection) => { const sortedCollections = sortCollections(filteredCollections, sortBy);
if (!acc[collection.tcg]) {
acc[collection.tcg] = [];
}
acc[collection.tcg].push(collection);
return acc;
}, {});
const handleCreateCollection = async () => { const handleCreateCollection = async () => {
try { try {
@ -194,11 +152,6 @@ export default function Collections() {
} }
}; };
const getTCGColor = (tcg) => {
const tcgOption = tcgOptions.find(option => option.value === tcg);
return tcgOption ? tcgOption.color : 'gray';
};
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', { return new Intl.NumberFormat('en-US', {
style: 'currency', style: 'currency',
@ -210,11 +163,100 @@ export default function Collections() {
return new Date(dateString).toLocaleDateString(); return new Date(dateString).toLocaleDateString();
}; };
const CollectionThumbnail = ({ collection }) => {
const { thumbnails = [], image } = collection;
// If collection has a custom hero image, use it
if (image) {
return (
<div className="w-full h-48 rounded-xl overflow-hidden mb-4" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<img
src={image}
alt={collection.name}
className="w-full h-full object-cover"
/>
</div>
);
}
// If no thumbnails available, show placeholder
if (!thumbnails || thumbnails.length === 0) {
return (
<div className="w-full h-48 rounded-xl mb-4 flex items-center justify-center" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<div className="text-center">
<div className="text-4xl mb-2">📦</div>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>No cards yet</p>
</div>
</div>
);
}
// Show main card (rarest) and grid of 4 others
const mainCard = thumbnails[0]; // Rarest card
const gridCards = thumbnails.slice(1, 5); // Next 4 cards
return (
<div className="w-full h-48 rounded-xl overflow-hidden mb-4 flex gap-2">
{/* Main card (rarest) - takes up 2/3 of the space */}
<div className="flex-2 h-full relative group">
{mainCard ? (
<div className="relative h-full">
<img
src={mainCard.image_url || mainCard.stock_image_url || '/placeholder-card.png'}
alt={mainCard.name}
className="w-full h-full object-cover rounded-lg"
/>
{/* Rarity glow effect */}
<div className={`absolute inset-0 rounded-lg rarity-glow-${mainCard.rarity?.toLowerCase() || 'common'}`}></div>
{/* Card name overlay */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2 rounded-b-lg">
<p className="text-white text-xs font-medium truncate">{mainCard.name}</p>
<p className="text-white/80 text-xs capitalize">{mainCard.rarity}</p>
</div>
</div>
) : (
<div className="w-full h-full bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
<span className="text-gray-400 text-xs">No image</span>
</div>
)}
</div>
{/* Grid of 4 other cards - takes up 1/3 of the space */}
<div className="flex-1 h-full">
<div className="grid grid-cols-2 gap-1 h-full">
{Array.from({ length: 4 }).map((_, index) => {
const card = gridCards[index];
return (
<div key={index} className="relative group">
{card ? (
<div className="relative h-full">
<img
src={card.image_url || card.stock_image_url || '/placeholder-card.png'}
alt={card.name}
className="w-full h-full object-cover rounded"
/>
{/* Subtle rarity glow for grid cards */}
<div className={`absolute inset-0 rounded rarity-glow-${card.rarity?.toLowerCase() || 'common'} opacity-50`}></div>
</div>
) : (
<div className="w-full h-full bg-gray-100 dark:bg-gray-800 rounded flex items-center justify-center">
<span className="text-gray-300 text-xs">+</span>
</div>
)}
</div>
);
})}
</div>
</div>
</div>
);
};
if (loading) { if (loading) {
return ( return (
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2" style={{ borderColor: 'var(--text-accent-light)' }}></div> <div className="animate-spin rounded-full h-32 w-32 border-b-2" style={{ borderColor: 'var(--accent-ember)' }}></div>
</div> </div>
</Layout> </Layout>
); );
@ -223,19 +265,23 @@ export default function Collections() {
return ( return (
<Layout user={user}> <Layout user={user}>
{/* Header */} {/* Header */}
<div className="p-6" style={{ backgroundColor: 'var(--bg-secondary-light)', borderBottom: '1px solid var(--border-light)' }}> <div className="p-6" style={{ backgroundColor: 'var(--bg-secondary)', borderBottom: '1px solid var(--border)' }}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary-light)' }}> <h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
My Collections My Collections
</h1> </h1>
<p className="text-lg" style={{ color: 'var(--text-secondary-light)' }}> <p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
Organize and manage your card collections Organize and manage your card collections
</p> </p>
</div> </div>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className="action-btn-primary flex items-center space-x-2" className="px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:shadow-md flex items-center space-x-2"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
> >
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@ -246,25 +292,24 @@ export default function Collections() {
</div> </div>
{/* Filters and Search */} {/* Filters and Search */}
<div className="p-6" style={{ backgroundColor: 'var(--bg-secondary-light)' }}> <div className="p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex flex-col md:flex-row gap-4"> <div className="flex flex-col md:flex-row gap-4">
<div className="flex-1"> <div className="flex-1">
<input <input
type="text" type="text"
placeholder="Search collections..." placeholder="Search collections..."
className="search-bar" className="input-field w-full"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<select <select
value={selectedTCG} value={sortBy}
onChange={(e) => setSelectedTCG(e.target.value)} onChange={(e) => setSortBy(e.target.value)}
className="input-field w-48" className="input-field w-48"
> >
<option value="all">All TCGs</option> {sortOptions.map(option => (
{tcgOptions.map(option => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
{option.label} {option.label}
</option> </option>
@ -276,136 +321,137 @@ export default function Collections() {
{/* Collections Grid */} {/* Collections Grid */}
<div className="p-6"> <div className="p-6">
{Object.keys(groupedCollections).length === 0 ? ( {sortedCollections.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-6xl mb-4">📦</div> <div className="text-6xl mb-4">📦</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary-light)' }}> <h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
No collections found {searchQuery ? 'No collections found' : 'No collections yet'}
</h3> </h3>
<p className="mb-6" style={{ color: 'var(--text-secondary-light)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Create your first collection to get started {searchQuery
? 'Try adjusting your search terms'
: 'Create your first collection to get started'
}
</p> </p>
<button {!searchQuery && (
onClick={() => setShowCreateModal(true)} <button
className="action-btn-primary" onClick={() => setShowCreateModal(true)}
> className="px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
Create Collection style={{
</button> backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Create Collection
</button>
)}
</div> </div>
) : ( ) : (
<div className="space-y-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Object.entries(groupedCollections).map(([tcg, collections]) => ( {sortedCollections.map(collection => (
<div key={tcg} className="space-y-4"> <div
<div className="flex items-center space-x-3"> key={collection.id}
<div className="card hover:shadow-xl transition-all duration-300 cursor-pointer group"
className={`w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold text-sm bg-${getTCGColor(tcg)}-500`} onClick={() => router.push(`/collection/${collection.id}`)}
> >
{tcg} {/* Thumbnail Section */}
</div> <CollectionThumbnail collection={collection} />
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary-light)' }}>
{tcgOptions.find(opt => opt.value === tcg)?.label || tcg}
</h2>
<span className="text-sm px-3 py-1 rounded-full" style={{
backgroundColor: 'var(--bg-tertiary-light)',
color: 'var(--text-secondary-light)'
}}>
{collections.length} collection{collections.length !== 1 ? 's' : ''}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {/* Collection Info */}
{collections.map(collection => ( <div className="space-y-3">
<div {/* Header with name and actions */}
key={collection.id} <div className="flex items-start justify-between">
className="card hover:shadow-xl transition-all duration-300 cursor-pointer" <div className="flex-1 min-w-0">
onClick={() => router.push(`/collection/${collection.id}`)} <h3 className="text-lg font-semibold truncate" style={{ color: 'var(--text-primary)' }}>
> {collection.name}
<div className="flex items-start justify-between mb-4"> </h3>
<div className="flex-1"> {collection.description && (
<div className="flex items-center justify-between mb-2"> <p className="text-sm mt-1 line-clamp-2" style={{ color: 'var(--text-secondary)' }}>
<h3 className="text-xl font-semibold" style={{ color: 'var(--text-primary-light)' }}> {collection.description}
{collection.name} </p>
</h3>
<PermissionIndicator
userRole={collection.userRole}
isPublic={collection.isPublic}
showTooltip={false}
/>
</div>
<p className="text-sm mb-3" style={{ color: 'var(--text-secondary-light)' }}>
{collection.description}
</p>
</div>
<div className="flex space-x-2">
<button
onClick={() => setEditingCollection(collection)}
className="p-2 rounded-lg transition-colors"
style={{
backgroundColor: 'var(--bg-tertiary-light)',
color: 'var(--text-secondary-light)'
}}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDeleteCollection(collection.id)}
className="p-2 rounded-lg transition-colors"
style={{
backgroundColor: 'var(--bg-tertiary-light)',
color: '#ef4444'
}}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="text-center p-3 rounded-lg" style={{ backgroundColor: 'var(--bg-tertiary-light)' }}>
<div className="text-2xl font-bold gradient-text-blue">{collection.cardCount}</div>
<div className="text-xs" style={{ color: 'var(--text-secondary-light)' }}>Cards</div>
</div>
<div className="text-center p-3 rounded-lg" style={{ backgroundColor: 'var(--bg-tertiary-light)' }}>
<div className="text-2xl font-bold gradient-text-purple">{formatCurrency(collection.value)}</div>
<div className="text-xs" style={{ color: 'var(--text-secondary-light)' }}>Value</div>
</div>
</div>
<div className="flex items-center justify-between text-xs" style={{ color: 'var(--text-secondary-light)' }}>
<span>Created {formatDate(collection.createdAt)}</span>
<span>Viewed {formatDate(collection.lastViewed)}</span>
</div>
{collection.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-3">
{collection.tags.slice(0, 3).map(tag => (
<span
key={tag}
className="px-2 py-1 text-xs rounded-full"
style={{
backgroundColor: 'var(--bg-tertiary-light)',
color: 'var(--text-secondary-light)'
}}
>
{tag}
</span>
))}
{collection.tags.length > 3 && (
<span className="px-2 py-1 text-xs rounded-full" style={{
backgroundColor: 'var(--bg-tertiary-light)',
color: 'var(--text-secondary-light)'
}}>
+{collection.tags.length - 3}
</span>
)}
</div>
)} )}
</div> </div>
))} <div className="flex items-center space-x-1 ml-2">
<PermissionIndicator
userRole={collection.userRole}
isPublic={collection.isPublic}
showTooltip={false}
/>
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex space-x-1">
<button
onClick={(e) => {
e.stopPropagation();
setEditingCollection(collection);
}}
className="p-1.5 rounded-lg transition-colors hover:scale-105"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)'
}}
>
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteCollection(collection.id);
}}
className="p-1.5 rounded-lg transition-colors hover:scale-105"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: '#ef4444'
}}
>
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
</div>
{/* Compact Stats */}
<div className="flex items-center justify-between text-sm" style={{ color: 'var(--text-secondary)' }}>
<div className="flex items-center space-x-4">
<span className="font-medium" style={{ color: 'var(--text-primary)' }}>
{collection.cardCount} cards
</span>
<span className="font-medium" style={{ color: 'var(--accent-ember)' }}>
{formatCurrency(collection.value)}
</span>
</div>
<span className="text-xs">
{formatDate(collection.createdAt)}
</span>
</div>
{/* Tags */}
{collection.tags && collection.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{collection.tags.slice(0, 2).map(tag => (
<span
key={tag}
className="px-2 py-1 text-xs rounded-full"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)'
}}
>
{tag}
</span>
))}
{collection.tags.length > 2 && (
<span className="px-2 py-1 text-xs rounded-full" style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-secondary)'
}}>
+{collection.tags.length - 2}
</span>
)}
</div>
)}
</div> </div>
</div> </div>
))} ))}
@ -417,28 +463,28 @@ export default function Collections() {
{showCreateModal && ( {showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4"> <div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary-light)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Create New Collection Create New Collection
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary-light)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name Collection Name
</label> </label>
<input <input
type="text" type="text"
className="input-field" className="input-field w-full"
value={newCollection.name} value={newCollection.name}
onChange={(e) => setNewCollection({...newCollection, name: e.target.value})} onChange={(e) => setNewCollection({...newCollection, name: e.target.value})}
placeholder="Enter collection name" placeholder="Enter collection name"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary-light)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Description Description
</label> </label>
<textarea <textarea
className="input-field" className="input-field w-full"
rows="3" rows="3"
value={newCollection.description} value={newCollection.description}
onChange={(e) => setNewCollection({...newCollection, description: e.target.value})} onChange={(e) => setNewCollection({...newCollection, description: e.target.value})}
@ -446,47 +492,51 @@ export default function Collections() {
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary-light)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Image (Optional) Hero Image (Optional)
</label> </label>
<input <input
type="url" type="url"
className="input-field" className="input-field w-full"
value={newCollection.image} value={newCollection.image}
onChange={(e) => setNewCollection({...newCollection, image: e.target.value})} onChange={(e) => setNewCollection({...newCollection, image: e.target.value})}
placeholder="Enter image URL" placeholder="Enter image URL or upload later"
/> />
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary-light)' }}> <p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Add a hero image for your collection Add a custom thumbnail image, or we'll use your rarest cards
</p> </p>
</div> </div>
<div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border-light)' }}> <div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}>
<div> <div>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary-light)' }}> <label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Show in Community Public Collection
</label> </label>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary-light)' }}> <p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Make this collection discoverable by other users Make this collection discoverable by other users
</p> </p>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer"> <button
<input type="button"
type="checkbox" onClick={() => setNewCollection({...newCollection, isPublic: !newCollection.isPublic})}
className="sr-only peer" className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
checked={newCollection.isPublic} newCollection.isPublic ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-600'
onChange={(e) => setNewCollection({...newCollection, isPublic: e.target.checked})} }`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
newCollection.isPublic ? 'translate-x-6' : 'translate-x-1'
}`}
/> />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div> </button>
</label>
</div> </div>
</div> </div>
<div className="flex space-x-3 mt-6"> <div className="flex space-x-3 mt-6">
<button <button
onClick={() => setShowCreateModal(false)} onClick={() => setShowCreateModal(false)}
className="flex-1 py-2 px-4 rounded-2xl border transition-colors" className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{ style={{
borderColor: 'var(--border-light)', borderColor: 'var(--border)',
color: 'var(--text-secondary-light)' color: 'var(--text-secondary)'
}} }}
> >
Cancel Cancel
@ -494,7 +544,11 @@ export default function Collections() {
<button <button
onClick={handleCreateCollection} onClick={handleCreateCollection}
disabled={!newCollection.name.trim()} disabled={!newCollection.name.trim()}
className="flex-1 action-btn-primary disabled:opacity-50" className="flex-1 py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md disabled:opacity-50"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
> >
Create Collection Create Collection
</button> </button>
@ -508,10 +562,10 @@ export default function Collections() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4 text-center"> <div className="card max-w-md w-full mx-4 text-center">
<div className="text-6xl mb-4">🎉</div> <div className="text-6xl mb-4">🎉</div>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary-light)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Collection Created! Collection Created!
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary-light)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Your collection "{createdCollection.name}" has been created successfully. Your collection "{createdCollection.name}" has been created successfully.
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
@ -520,16 +574,20 @@ export default function Collections() {
setShowSuccessModal(false); setShowSuccessModal(false);
router.push(`/collection/${createdCollection.id}`); router.push(`/collection/${createdCollection.id}`);
}} }}
className="w-full action-btn-primary" className="w-full py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
> >
View Collection View Collection
</button> </button>
<button <button
onClick={() => setShowSuccessModal(false)} onClick={() => setShowSuccessModal(false)}
className="w-full py-2 px-4 rounded-2xl border transition-colors" className="w-full py-2 px-4 rounded-xl border transition-colors"
style={{ style={{
borderColor: 'var(--border-light)', borderColor: 'var(--border)',
color: 'var(--text-secondary-light)' color: 'var(--text-secondary)'
}} }}
> >
Stay on Collections Page Stay on Collections Page
@ -543,68 +601,75 @@ export default function Collections() {
{editingCollection && ( {editingCollection && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="card max-w-md w-full mx-4"> <div className="card max-w-md w-full mx-4">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary-light)' }}> <h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Edit Collection Edit Collection
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary-light)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection Name Collection Name
</label> </label>
<input <input
type="text" type="text"
className="input-field" className="input-field w-full"
value={editingCollection.name} value={editingCollection.name}
onChange={(e) => setEditingCollection({...editingCollection, name: e.target.value})} onChange={(e) => setEditingCollection({...editingCollection, name: e.target.value})}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary-light)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Description Description
</label> </label>
<textarea <textarea
className="input-field" className="input-field w-full"
rows="3" rows="3"
value={editingCollection.description} value={editingCollection.description}
onChange={(e) => setEditingCollection({...editingCollection, description: e.target.value})} onChange={(e) => setEditingCollection({...editingCollection, description: e.target.value})}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary-light)' }}> <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Trading Card Game Hero Image
</label> </label>
<select
className="input-field"
value={editingCollection.tcg}
onChange={(e) => setEditingCollection({...editingCollection, tcg: e.target.value})}
>
{tcgOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="flex items-center">
<input <input
type="checkbox" type="url"
id="editIsPublic" className="input-field w-full"
checked={editingCollection.isPublic} value={editingCollection.image || ''}
onChange={(e) => setEditingCollection({...editingCollection, isPublic: e.target.checked})} onChange={(e) => setEditingCollection({...editingCollection, image: e.target.value})}
className="mr-2" placeholder="Enter image URL"
/> />
<label htmlFor="editIsPublic" className="text-sm" style={{ color: 'var(--text-primary-light)' }}> </div>
Make collection public <div className="flex items-center justify-between p-4 rounded-lg border" style={{ borderColor: 'var(--border)' }}>
</label> <div>
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
Public Collection
</label>
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
Make this collection discoverable by other users
</p>
</div>
<button
type="button"
onClick={() => setEditingCollection({...editingCollection, isPublic: !editingCollection.isPublic})}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
editingCollection.isPublic ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
editingCollection.isPublic ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div> </div>
</div> </div>
<div className="flex space-x-3 mt-6"> <div className="flex space-x-3 mt-6">
<button <button
onClick={() => setEditingCollection(null)} onClick={() => setEditingCollection(null)}
className="flex-1 py-2 px-4 rounded-2xl border transition-colors" className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{ style={{
borderColor: 'var(--border-light)', borderColor: 'var(--border)',
color: 'var(--text-secondary-light)' color: 'var(--text-secondary)'
}} }}
> >
Cancel Cancel
@ -612,7 +677,11 @@ export default function Collections() {
<button <button
onClick={handleUpdateCollection} onClick={handleUpdateCollection}
disabled={!editingCollection.name.trim()} disabled={!editingCollection.name.trim()}
className="flex-1 action-btn-primary disabled:opacity-50" className="flex-1 py-2 px-4 rounded-xl font-medium transition-all duration-200 hover:shadow-md disabled:opacity-50"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
> >
Update Collection Update Collection
</button> </button>

View file

@ -640,4 +640,16 @@ body {
0 0 35px rgba(244, 114, 182, 0.7), 0 0 35px rgba(244, 114, 182, 0.7),
0 0 70px rgba(244, 114, 182, 0.6), 0 0 70px rgba(244, 114, 182, 0.6),
0 0 105px rgba(244, 114, 182, 0.5); 0 0 105px rgba(244, 114, 182, 0.5);
}
/* Utility classes */
.flex-2 {
flex: 2;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
} }