deckhearth/pages/collection/[id].js

672 lines
28 KiB
JavaScript
Raw Normal View History

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
2025-07-25 09:34:28 -04:00
import CollaborationManager from '../../components/CollaborationManager';
import Layout from '../../components/Layout';
export default function CollectionView() {
const router = useRouter();
const { id } = router.query;
// Get user from auth context - for now using admin user
const user = {
email: 'admin@tcgvault.com',
role: 'admin'
};
const [collection, setCollection] = useState(null);
const [cards, setCards] = useState([]);
const [loading, setLoading] = useState(true);
const [isFavorited, setIsFavorited] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
// Filter states
const [searchQuery, setSearchQuery] = useState('');
const [selectedRarity, setSelectedRarity] = useState('all');
const [selectedType, setSelectedType] = useState('all');
const [sortBy, setSortBy] = useState('name');
const [viewMode, setViewMode] = useState('grid'); // grid or list
const [searchCards, setSearchCards] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [showSearchResults, setShowSearchResults] = useState(false);
useEffect(() => {
if (id) {
fetchCollectionData();
}
}, [id]);
const fetchCollectionData = async () => {
try {
const response = await fetch(`/api/collections/${id}`);
if (response.ok) {
const data = await response.json();
setCollection(data.collection);
setCards(data.cards || []);
} else {
console.error('Failed to fetch collection');
setCollection(null);
setCards([]);
}
} catch (error) {
console.error('Error fetching collection:', error);
setCollection(null);
setCards([]);
} finally {
setLoading(false);
}
};
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy link:', err);
}
};
const handleFavorite = () => {
setIsFavorited(!isFavorited);
// Here you would typically make an API call
};
const handleSaveCopy = () => {
// Logic to save a copy to user's collections
console.log('Saving copy of collection');
};
const handleSearchCards = async (query) => {
if (!query.trim()) {
setSearchResults([]);
setShowSearchResults(false);
return;
}
try {
const response = await fetch(`/api/cards/search?q=${encodeURIComponent(query)}&limit=10`);
if (response.ok) {
const results = await response.json();
setSearchResults(results);
setShowSearchResults(true);
}
} catch (error) {
console.error('Error searching cards:', error);
}
};
const handleAddCard = async (card, quantity = 1) => {
try {
const response = await fetch(`/api/collections/${id}/cards`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cardId: card.id,
quantity
})
});
if (response.ok) {
// Refresh collection data
fetchCollectionData();
setSearchCards('');
setSearchResults([]);
setShowSearchResults(false);
} else {
const error = await response.json();
alert(error.error || 'Failed to add card');
}
} catch (error) {
console.error('Error adding card:', error);
alert('Network error. Please try again.');
}
};
const filteredCards = cards.filter(card => {
const matchesSearch = card.name.toLowerCase().includes(searchQuery.toLowerCase());
const matchesRarity = selectedRarity === 'all' || card.rarity === selectedRarity;
const matchesType = selectedType === 'all' || card.card_type === selectedType;
return matchesSearch && matchesRarity && matchesType;
});
const sortedCards = [...filteredCards].sort((a, b) => {
switch (sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'cost':
return b.market_price - a.market_price;
case 'rarity':
return a.rarity.localeCompare(b.rarity);
case 'type':
return a.card_type.localeCompare(b.card_type);
default:
return 0;
}
});
const totalValue = cards.reduce((sum, card) => sum + (card.market_price * card.quantity), 0);
const totalCards = cards.reduce((sum, card) => sum + card.quantity, 0);
if (loading) {
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" style={{ borderColor: 'var(--text-accent)' }}></div>
</div>
</Layout>
);
}
if (!collection) {
return (
<Layout user={user}>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Collection not found
</h2>
<button
onClick={() => router.push('/collections')}
className="px-6 py-3 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
>
Back to Collections
</button>
</div>
</div>
</Layout>
);
}
return (
<Layout user={user}>
{/* Hero Section with Collection Info */}
<div
className="relative h-64 bg-gradient-to-r from-purple-600 to-blue-600 flex items-end"
style={{
backgroundImage: collection.image
? `linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.6)), url("${collection.image}")`
: 'linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.6)), url("https://images.unsplash.com/photo-1606092195730-5d7b9af1efc5?w=1200")',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<div className="container mx-auto px-6 pb-8">
<div className="flex items-center space-x-4 mb-4">
<div className="p-3 rounded-xl bg-white bg-opacity-20 backdrop-blur-sm">
<div className="text-2xl">🃏</div>
</div>
<div>
<h1 className="text-4xl font-bold text-white mb-2">
{collection.name}
</h1>
<div className="flex items-center space-x-4 text-white text-opacity-90">
<div className="flex items-center space-x-2">
<div className="w-6 h-6 rounded-full bg-white bg-opacity-20 flex items-center justify-center">
<span className="text-sm">👤</span>
</div>
<span>Created by {collection.creator_email}</span>
</div>
<span></span>
<span>TCG: {collection.tcg}</span>
<span></span>
<span>Value: ${totalValue.toFixed(2)}</span>
<span></span>
<span>Cards: {totalCards}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2 text-white text-opacity-75 text-sm">
<span>Created {new Date(collection.created_at).toLocaleDateString()}</span>
<span></span>
<span>Last updated {new Date(collection.updated_at).toLocaleDateString()}</span>
{collection.is_public && (
<>
<span></span>
<span className="px-2 py-1 rounded-full bg-white bg-opacity-20 text-xs">
🌍 Public
</span>
</>
)}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="border-b" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}>
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<button
onClick={handleCopyLink}
className="flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--bg-primary)',
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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span>{copySuccess ? 'Copied!' : 'Copy link'}</span>
</button>
<button
onClick={() => setShowShareModal(true)}
className="flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--bg-primary)',
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="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
</svg>
<span>Copy list</span>
</button>
<button
className="flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--bg-primary)',
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="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H9.414a1 1 0 01-.707-.293l-2-2A1 1 0 006 6H4a2 2 0 00-2 2v11a2 2 0 002 2h4a2 2 0 002-2v-1" />
</svg>
<span>Print proxies</span>
</button>
</div>
<button
onClick={handleSaveCopy}
className="flex items-center space-x-2 px-6 py-2 rounded-lg font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12" />
</svg>
<span>Save deck</span>
</button>
</div>
</div>
</div>
{/* Stats and Filters */}
<div className="container mx-auto px-6 py-6">
{/* Collection Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div className="text-center p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="text-2xl font-bold gradient-text-blue">{totalCards}</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Total Cards</div>
</div>
<div className="text-center p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="text-2xl font-bold gradient-text-purple">${totalValue.toFixed(2)}</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Total Value</div>
</div>
<div className="text-center p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="text-2xl font-bold gradient-text-green">{collection.tcg}</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Game</div>
</div>
<div className="text-center p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="text-2xl font-bold gradient-text-orange">{collection.is_public ? 'Public' : 'Private'}</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>Visibility</div>
</div>
</div>
{/* Collaboration Manager */}
<CollaborationManager
collectionId={id}
isOwner={collection.creator_email === user.email}
/>
{/* Filters */}
<div className="p-6 rounded-xl mb-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1">
<input
type="text"
placeholder="Search cards..."
className="w-full px-4 py-3 rounded-lg border transition-all duration-200 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex flex-wrap gap-3">
<select
value={selectedRarity}
onChange={(e) => setSelectedRarity(e.target.value)}
className="px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="all">All Rarities</option>
<option value="Common">Common</option>
<option value="Uncommon">Uncommon</option>
<option value="Rare">Rare</option>
<option value="Ultra Rare">Ultra Rare</option>
<option value="Rainbow Rare">Rainbow Rare</option>
</select>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
className="px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="all">All Types</option>
<option value="Electric">Electric</option>
<option value="Trainer">Trainer</option>
<option value="Energy">Energy</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="name">Sort by Name</option>
<option value="cost">Sort by Price</option>
<option value="rarity">Sort by Rarity</option>
<option value="type">Sort by Type</option>
</select>
<div className="flex rounded-lg border" style={{ borderColor: 'var(--border)' }}>
<button
onClick={() => setViewMode('grid')}
className={`p-3 rounded-l-lg transition-all duration-200 ${
viewMode === 'grid'
? 'gradient-bg-purple text-white'
: 'bg-transparent'
}`}
style={viewMode !== 'grid' ? { color: 'var(--text-secondary)' } : {}}
>
<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" />
</svg>
</button>
<button
onClick={() => setViewMode('list')}
className={`p-3 rounded-r-lg transition-all duration-200 ${
viewMode === 'list'
? 'gradient-bg-purple text-white'
: 'bg-transparent'
}`}
style={viewMode !== 'list' ? { color: 'var(--text-secondary)' } : {}}
>
<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" />
</svg>
</button>
</div>
</div>
</div>
</div>
{/* Cards Display */}
<div className="mb-4 flex items-center justify-between">
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
Cards ({sortedCards.length})
</h2>
</div>
{viewMode === 'grid' ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
{sortedCards.map(card => (
<div
key={card.id}
className="group cursor-pointer transition-all duration-300 hover:scale-105"
onClick={() => router.push(`/card/${card.id}`)}
>
<div className="relative rounded-xl overflow-hidden shadow-lg group-hover:shadow-xl transition-all duration-300">
<img
src={card.image_url}
alt={card.name}
className="w-full h-auto object-cover"
onError={(e) => {
e.target.src = 'https://via.placeholder.com/250x350/6366f1/ffffff?text=No+Image';
}}
/>
{card.quantity > 1 && (
<div className="absolute top-2 right-2 bg-black bg-opacity-75 text-white text-xs px-2 py-1 rounded-full">
{card.quantity}x
</div>
)}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-3">
<div className="text-white text-sm font-medium truncate">
{card.name}
</div>
<div className="text-white text-opacity-75 text-xs">
${card.market_price}
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="space-y-2">
{sortedCards.map(card => (
<div
key={card.id}
className="flex items-center p-4 rounded-xl border transition-all duration-200 hover:shadow-md cursor-pointer"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)'
}}
onClick={() => router.push(`/card/${card.id}`)}
>
<img
src={card.image_url}
alt={card.name}
className="w-16 h-22 object-cover rounded-lg mr-4"
onError={(e) => {
e.target.src = 'https://via.placeholder.com/64x88/6366f1/ffffff?text=No+Image';
}}
/>
<div className="flex-1">
<h3 className="font-semibold" style={{ color: 'var(--text-primary)' }}>
{card.name}
</h3>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{card.set_name} {card.rarity} {card.card_type}
</div>
</div>
<div className="text-right">
<div className="font-semibold" style={{ color: 'var(--text-primary)' }}>
${card.market_price}
</div>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Qty: {card.quantity}
</div>
</div>
</div>
))}
</div>
)}
{cards.length === 0 ? (
// Empty collection state
<div className="text-center py-16">
<div className="text-8xl mb-6">🃏</div>
<h3 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Start Building Your Collection
</h3>
<p className="text-lg mb-8 max-w-md mx-auto" style={{ color: 'var(--text-secondary)' }}>
This collection is empty. Add your first cards to get started!
</p>
<div className="space-y-4">
<button
onClick={() => router.push('/cards')}
className="inline-flex items-center space-x-2 px-8 py-4 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transform hover:scale-105 transition-all duration-200"
>
<svg className="w-5 h-5" 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>Browse Cards to Add</span>
</button>
<div className="text-sm" style={{ color: 'var(--text-secondary)' }}>
Or search for specific cards to add to your collection
</div>
</div>
{/* Quick add section */}
<div className="mt-12 p-6 rounded-2xl max-w-md mx-auto relative" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h4 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Quick Add
</h4>
<div className="space-y-3">
<input
type="text"
placeholder="Search for a card to add..."
value={searchCards}
onChange={(e) => {
setSearchCards(e.target.value);
handleSearchCards(e.target.value);
}}
className="w-full px-4 py-3 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
/>
{/* Search Results */}
{showSearchResults && searchResults.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white border rounded-lg shadow-lg z-10 max-h-60 overflow-y-auto">
{searchResults.map((card) => (
<div
key={card.id}
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
onClick={() => handleAddCard(card)}
>
<img
src={card.image_url}
alt={card.name}
className="w-10 h-14 object-cover rounded mr-3"
onError={(e) => {
e.target.src = 'https://via.placeholder.com/40x56/6366f1/ffffff?text=?';
}}
/>
<div className="flex-1">
<div className="font-medium text-gray-900">{card.name}</div>
<div className="text-sm text-gray-500">{card.set_name} {card.rarity}</div>
</div>
<div className="text-right">
<div className="font-medium text-gray-900">${card.market_price}</div>
<div className="text-xs text-gray-500">{card.game}</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
) : sortedCards.length === 0 ? (
// No results for current filters
<div className="text-center py-12">
<div className="text-6xl mb-4">🔍</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
No cards match your filters
</h3>
<p style={{ color: 'var(--text-secondary)' }}>
Try adjusting your search or filter criteria
</p>
<button
onClick={() => {
setSearchQuery('');
setSelectedRarity('all');
setSelectedType('all');
}}
className="mt-4 px-4 py-2 rounded-lg border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
Clear All Filters
</button>
</div>
) : null}
</div>
{/* Share Modal */}
{showShareModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="p-6 rounded-2xl shadow-lg max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Share Collection
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Collection URL
</label>
<div className="flex">
<input
type="text"
value={window.location.href}
readOnly
className="flex-1 px-4 py-2 rounded-l-lg border"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
/>
<button
onClick={handleCopyLink}
className="px-4 py-2 rounded-r-lg gradient-bg-purple text-white hover:shadow-lg transition-all duration-200"
>
Copy
</button>
</div>
</div>
</div>
<div className="flex space-x-3 mt-6">
<button
onClick={() => setShowShareModal(false)}
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Close
</button>
</div>
</div>
</div>
)}
</Layout>
);
}