🎨 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:
parent
a7ee884d02
commit
b95d972e95
3 changed files with 479 additions and 285 deletions
113
pages/api/collections/[id]/thumbnails.js
Normal file
113
pages/api/collections/[id]/thumbnails.js
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,137 +321,138 @@ 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>
|
||||||
|
{!searchQuery && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
className="action-btn-primary"
|
className="px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--accent-ember)',
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Create Collection
|
Create Collection
|
||||||
</button>
|
</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 className="flex items-center space-x-3">
|
|
||||||
<div
|
|
||||||
className={`w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold text-sm bg-${getTCGColor(tcg)}-500`}
|
|
||||||
>
|
|
||||||
{tcg}
|
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
{collections.map(collection => (
|
|
||||||
<div
|
<div
|
||||||
key={collection.id}
|
key={collection.id}
|
||||||
className="card hover:shadow-xl transition-all duration-300 cursor-pointer"
|
className="card hover:shadow-xl transition-all duration-300 cursor-pointer group"
|
||||||
onClick={() => router.push(`/collection/${collection.id}`)}
|
onClick={() => router.push(`/collection/${collection.id}`)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-4">
|
{/* Thumbnail Section */}
|
||||||
<div className="flex-1">
|
<CollectionThumbnail collection={collection} />
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-xl font-semibold" style={{ color: 'var(--text-primary-light)' }}>
|
{/* Collection Info */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Header with name and actions */}
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-lg font-semibold truncate" style={{ color: 'var(--text-primary)' }}>
|
||||||
{collection.name}
|
{collection.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
{collection.description && (
|
||||||
|
<p className="text-sm mt-1 line-clamp-2" style={{ color: 'var(--text-secondary)' }}>
|
||||||
|
{collection.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1 ml-2">
|
||||||
<PermissionIndicator
|
<PermissionIndicator
|
||||||
userRole={collection.userRole}
|
userRole={collection.userRole}
|
||||||
isPublic={collection.isPublic}
|
isPublic={collection.isPublic}
|
||||||
showTooltip={false}
|
showTooltip={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex space-x-1">
|
||||||
<p className="text-sm mb-3" style={{ color: 'var(--text-secondary-light)' }}>
|
|
||||||
{collection.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingCollection(collection)}
|
onClick={(e) => {
|
||||||
className="p-2 rounded-lg transition-colors"
|
e.stopPropagation();
|
||||||
|
setEditingCollection(collection);
|
||||||
|
}}
|
||||||
|
className="p-1.5 rounded-lg transition-colors hover:scale-105"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-tertiary-light)',
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
color: 'var(--text-secondary-light)'
|
color: 'var(--text-secondary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteCollection(collection.id)}
|
onClick={(e) => {
|
||||||
className="p-2 rounded-lg transition-colors"
|
e.stopPropagation();
|
||||||
|
handleDeleteCollection(collection.id);
|
||||||
|
}}
|
||||||
|
className="p-1.5 rounded-lg transition-colors hover:scale-105"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-tertiary-light)',
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
color: '#ef4444'
|
color: '#ef4444'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-xs" style={{ color: 'var(--text-secondary-light)' }}>
|
{/* Compact Stats */}
|
||||||
<span>Created {formatDate(collection.createdAt)}</span>
|
<div className="flex items-center justify-between text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<span>Viewed {formatDate(collection.lastViewed)}</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
{collection.tags.length > 0 && (
|
{/* Tags */}
|
||||||
<div className="flex flex-wrap gap-1 mt-3">
|
{collection.tags && collection.tags.length > 0 && (
|
||||||
{collection.tags.slice(0, 3).map(tag => (
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{collection.tags.slice(0, 2).map(tag => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="px-2 py-1 text-xs rounded-full"
|
className="px-2 py-1 text-xs rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-tertiary-light)',
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
color: 'var(--text-secondary-light)'
|
color: 'var(--text-secondary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{collection.tags.length > 3 && (
|
{collection.tags.length > 2 && (
|
||||||
<span className="px-2 py-1 text-xs rounded-full" style={{
|
<span className="px-2 py-1 text-xs rounded-full" style={{
|
||||||
backgroundColor: 'var(--bg-tertiary-light)',
|
backgroundColor: 'var(--bg-tertiary)',
|
||||||
color: 'var(--text-secondary-light)'
|
color: 'var(--text-secondary)'
|
||||||
}}>
|
}}>
|
||||||
+{collection.tags.length - 3}
|
+{collection.tags.length - 2}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</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)' }}>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
Public Collection
|
||||||
</label>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -641,3 +641,15 @@ body {
|
||||||
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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue