✨ Collection Organization Restructure: - /collections now shows only user's own collections, collaborations, and shared collections - /community/collections shows all public collections for discovery - Updated navigation to include 'Community Collections' link - Added 'Discover Community' button on My Collections page 🔧 API Changes: - Modified /api/collections to exclude public collections from other users - Created /api/community/collections for public collection discovery - Proper authentication and permission handling for both endpoints 🎯 User Experience Improvements: - Clear separation between personal and community spaces - 'My Collection' sidebar item now accurately reflects content - Community discovery is intentional and separate - Better organization matches user mental models 📱 UI Enhancements: - Updated page titles and descriptions - Added community discovery button with globe icon - Consistent styling across both collection views - Same thumbnail and layout system for both pages This properly separates personal collection management from community discovery! 🚀
917 lines
No EOL
36 KiB
JavaScript
917 lines
No EOL
36 KiB
JavaScript
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/router';
|
||
import Layout from '../components/Layout';
|
||
import PermissionIndicator from '../components/PermissionIndicator';
|
||
import { useAuth } from '../lib/use-auth';
|
||
import Link from 'next/link';
|
||
|
||
export default function Collections() {
|
||
const router = useRouter();
|
||
const { user, loading: authLoading } = useAuth();
|
||
|
||
const [collections, setCollections] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [editingCollection, setEditingCollection] = useState(null);
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [sortBy, setSortBy] = useState('name'); // name, value, cardCount, createdAt
|
||
|
||
const [newCollection, setNewCollection] = useState({
|
||
name: '',
|
||
description: '',
|
||
isPublic: false,
|
||
image: '',
|
||
tags: []
|
||
});
|
||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||
const [createdCollection, setCreatedCollection] = useState(null);
|
||
const [tagInput, setTagInput] = useState(''); // For creating new collections
|
||
const [editTagInput, setEditTagInput] = useState(''); // For editing collections
|
||
|
||
// Redirect to login if not authenticated
|
||
useEffect(() => {
|
||
if (!authLoading && !user) {
|
||
router.push('/login');
|
||
}
|
||
}, [authLoading, user, router]);
|
||
|
||
useEffect(() => {
|
||
if (user) {
|
||
fetchCollections();
|
||
}
|
||
}, [user]);
|
||
|
||
const fetchCollections = async () => {
|
||
try {
|
||
const token = localStorage.getItem('auth_token');
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
};
|
||
|
||
if (token) {
|
||
headers.Authorization = `Bearer ${token}`;
|
||
}
|
||
|
||
const response = await fetch('/api/collections', { headers });
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
|
||
// Fetch thumbnails for each collection
|
||
const collectionsWithThumbnails = await Promise.all(
|
||
data.map(async (collection) => {
|
||
try {
|
||
const identifier = collection.slug || collection.id;
|
||
const thumbnailResponse = await fetch(`/api/collections/${identifier}/thumbnails`, { headers });
|
||
if (thumbnailResponse.ok) {
|
||
const thumbnailData = await thumbnailResponse.json();
|
||
return { ...collection, thumbnails: thumbnailData.thumbnails };
|
||
}
|
||
return { ...collection, thumbnails: [] };
|
||
} catch (error) {
|
||
console.error(`Error fetching thumbnails for collection ${collection.id}:`, error);
|
||
return { ...collection, thumbnails: [] };
|
||
}
|
||
})
|
||
);
|
||
|
||
setCollections(collectionsWithThumbnails);
|
||
} else {
|
||
console.error('Failed to fetch collections');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching collections:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const sortOptions = [
|
||
{ value: 'name', label: 'Name (A-Z)' },
|
||
{ value: 'value', label: 'Value (High to Low)' },
|
||
{ value: 'cardCount', label: 'Card Count (High to Low)' },
|
||
{ value: 'createdAt', label: 'Date Created (Newest)' }
|
||
];
|
||
|
||
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 matchesSearch = collection.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
collection.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||
return matchesSearch;
|
||
});
|
||
|
||
const sortedCollections = sortCollections(filteredCollections, sortBy);
|
||
|
||
const handleCreateCollection = async () => {
|
||
try {
|
||
const response = await fetch('/api/collections', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
name: newCollection.name,
|
||
description: newCollection.description,
|
||
isPublic: newCollection.isPublic,
|
||
image: newCollection.image,
|
||
tags: newCollection.tags
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const createdCollection = await response.json();
|
||
setCreatedCollection(createdCollection);
|
||
setShowCreateModal(false);
|
||
setShowSuccessModal(true);
|
||
|
||
// Reset form
|
||
setNewCollection({
|
||
name: '',
|
||
description: '',
|
||
isPublic: false,
|
||
image: '',
|
||
tags: []
|
||
});
|
||
setTagInput(''); // Clear tag input
|
||
|
||
// Refresh collections list
|
||
fetchCollections();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || 'Failed to create collection');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating collection:', error);
|
||
alert('Network error. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleUpdateCollection = async () => {
|
||
try {
|
||
const response = await fetch(`/api/collections/${editingCollection.slug || editingCollection.id}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
name: editingCollection.name,
|
||
description: editingCollection.description,
|
||
isPublic: editingCollection.isPublic,
|
||
image: editingCollection.image,
|
||
tags: editingCollection.tags
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Refresh collections after update
|
||
fetchCollections();
|
||
setEditingCollection(null);
|
||
setEditTagInput(''); // Clear the tag input
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || 'Failed to update collection');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating collection:', error);
|
||
alert('Network error. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleDeleteCollection = async (collectionId) => {
|
||
if (confirm('Are you sure you want to delete this collection? This action cannot be undone.')) {
|
||
try {
|
||
const response = await fetch(`/api/collections/${collectionId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (response.ok) {
|
||
fetchCollections(); // Refresh the list
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || 'Failed to delete collection');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting collection:', error);
|
||
alert('Network error. Please try again.');
|
||
}
|
||
}
|
||
};
|
||
|
||
// Tag handling functions
|
||
const handleAddTag = (tagInput, setTagInput, collection, setCollection) => {
|
||
if (tagInput.trim() && !collection.tags.includes(tagInput.trim())) {
|
||
setCollection({
|
||
...collection,
|
||
tags: [...collection.tags, tagInput.trim()]
|
||
});
|
||
setTagInput('');
|
||
}
|
||
};
|
||
|
||
const handleRemoveTag = (tagToRemove, collection, setCollection) => {
|
||
setCollection({
|
||
...collection,
|
||
tags: collection.tags.filter(tag => tag !== tagToRemove)
|
||
});
|
||
};
|
||
|
||
const handleTagKeyPress = (e, tagInput, setTagInput, collection, setCollection) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
handleAddTag(tagInput, setTagInput, collection, setCollection);
|
||
}
|
||
};
|
||
|
||
const formatCurrency = (amount) => {
|
||
return new Intl.NumberFormat('en-US', {
|
||
style: 'currency',
|
||
currency: 'USD'
|
||
}).format(amount);
|
||
};
|
||
|
||
const formatDate = (dateString) => {
|
||
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>
|
||
);
|
||
};
|
||
|
||
// Show loading spinner while auth is loading or data is loading
|
||
if (authLoading || 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(--accent-ember)' }}></div>
|
||
</div>
|
||
</Layout>
|
||
);
|
||
}
|
||
|
||
// Redirect to login if not authenticated (handled by useEffect, but this is a fallback)
|
||
if (!user) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<Layout user={user}>
|
||
{/* Header */}
|
||
<div className="p-6" style={{ backgroundColor: 'var(--bg-secondary)', borderBottom: '1px solid var(--border)' }}>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||
My Collections
|
||
</h1>
|
||
<p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
|
||
Collections you own, collaborate on, or have been shared with you
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Link href="/community/collections">
|
||
<button className="px-4 py-2 rounded-xl border transition-colors hover:shadow-md"
|
||
style={{
|
||
borderColor: 'var(--border)',
|
||
color: 'var(--text-secondary)',
|
||
backgroundColor: 'transparent'
|
||
}}
|
||
>
|
||
🌍 Discover Community
|
||
</button>
|
||
</Link>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className="px-6 py-2 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
|
||
style={{
|
||
backgroundColor: 'var(--accent-ember)',
|
||
color: 'white'
|
||
}}
|
||
>
|
||
+ Create Collection
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Filters and Search */}
|
||
<div className="p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
||
<div className="flex flex-col md:flex-row gap-4">
|
||
<div className="flex-1">
|
||
<input
|
||
type="text"
|
||
placeholder="Search collections..."
|
||
className="input-field w-full"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="flex gap-4">
|
||
<select
|
||
value={sortBy}
|
||
onChange={(e) => setSortBy(e.target.value)}
|
||
className="input-field w-48"
|
||
>
|
||
{sortOptions.map(option => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Collections Grid */}
|
||
<div className="p-6">
|
||
{sortedCollections.length === 0 ? (
|
||
<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)' }}>
|
||
{searchQuery ? 'No collections found' : 'No collections yet'}
|
||
</h3>
|
||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||
{searchQuery
|
||
? 'Try adjusting your search terms'
|
||
: 'Create your first collection to get started'
|
||
}
|
||
</p>
|
||
{!searchQuery && (
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
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
|
||
</button>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||
{sortedCollections.map(collection => (
|
||
<div
|
||
key={collection.id}
|
||
className="card hover:shadow-xl transition-all duration-300 cursor-pointer group"
|
||
onClick={() => router.push(`/collection/${collection.slug || collection.id}`)}
|
||
>
|
||
{/* Thumbnail Section */}
|
||
<CollectionThumbnail collection={collection} />
|
||
|
||
{/* 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}
|
||
</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
|
||
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>
|
||
|
||
{/* Create Collection Modal */}
|
||
{showCreateModal && (
|
||
<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">
|
||
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||
Create New Collection
|
||
</h2>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Collection Name
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="input-field w-full"
|
||
value={newCollection.name}
|
||
onChange={(e) => setNewCollection({...newCollection, name: e.target.value})}
|
||
placeholder="Enter collection name"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Description
|
||
</label>
|
||
<textarea
|
||
className="input-field w-full"
|
||
rows="3"
|
||
value={newCollection.description}
|
||
onChange={(e) => setNewCollection({...newCollection, description: e.target.value})}
|
||
placeholder="Describe your collection"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Hero Image (Optional)
|
||
</label>
|
||
<input
|
||
type="url"
|
||
className="input-field w-full"
|
||
value={newCollection.image}
|
||
onChange={(e) => setNewCollection({...newCollection, image: e.target.value})}
|
||
placeholder="Enter image URL or upload later"
|
||
/>
|
||
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
||
Add a custom thumbnail image, or we'll use your rarest cards
|
||
</p>
|
||
</div>
|
||
|
||
{/* Tags Section */}
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Tags (Optional)
|
||
</label>
|
||
<div className="space-y-2">
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
className="input-field flex-1"
|
||
value={tagInput}
|
||
onChange={(e) => setTagInput(e.target.value)}
|
||
onKeyPress={(e) => handleTagKeyPress(e, tagInput, setTagInput, newCollection, setNewCollection)}
|
||
placeholder="Add tags (press Enter)"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleAddTag(tagInput, setTagInput, newCollection, setNewCollection)}
|
||
className="px-3 py-2 rounded-lg text-sm font-medium transition-colors"
|
||
style={{
|
||
backgroundColor: 'var(--accent-ember)',
|
||
color: 'white'
|
||
}}
|
||
disabled={!tagInput.trim()}
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
{newCollection.tags.length > 0 && (
|
||
<div className="flex flex-wrap gap-2">
|
||
{newCollection.tags.map((tag, index) => (
|
||
<span
|
||
key={index}
|
||
className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium"
|
||
style={{
|
||
backgroundColor: 'var(--bg-tertiary)',
|
||
color: 'var(--text-primary)',
|
||
border: '1px solid var(--border)'
|
||
}}
|
||
>
|
||
{tag}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRemoveTag(tag, newCollection, setNewCollection)}
|
||
className="ml-1 hover:text-red-500 transition-colors"
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||
Add tags to help organize and categorize your collection
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
||
Make this collection discoverable by other users
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setNewCollection({...newCollection, isPublic: !newCollection.isPublic})}
|
||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||
newCollection.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 ${
|
||
newCollection.isPublic ? 'translate-x-6' : 'translate-x-1'
|
||
}`}
|
||
/>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="flex space-x-3 mt-6">
|
||
<button
|
||
onClick={() => {
|
||
setShowCreateModal(false);
|
||
setTagInput(''); // Clear tag input when closing
|
||
}}
|
||
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
|
||
style={{
|
||
borderColor: 'var(--border)',
|
||
color: 'var(--text-secondary)'
|
||
}}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleCreateCollection}
|
||
disabled={!newCollection.name.trim()}
|
||
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
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Success Modal */}
|
||
{showSuccessModal && createdCollection && (
|
||
<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="text-6xl mb-4">🎉</div>
|
||
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||
Collection Created!
|
||
</h2>
|
||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||
Your collection "{createdCollection.name}" has been created successfully.
|
||
</p>
|
||
<div className="space-y-3">
|
||
<button
|
||
onClick={() => {
|
||
setShowSuccessModal(false);
|
||
router.push(`/collection/${createdCollection.slug || createdCollection.id}`);
|
||
}}
|
||
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
|
||
</button>
|
||
<button
|
||
onClick={() => setShowSuccessModal(false)}
|
||
className="w-full py-2 px-4 rounded-xl border transition-colors"
|
||
style={{
|
||
borderColor: 'var(--border)',
|
||
color: 'var(--text-secondary)'
|
||
}}
|
||
>
|
||
Stay on Collections Page
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Collection Modal */}
|
||
{editingCollection && (
|
||
<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">
|
||
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
||
Edit Collection
|
||
</h2>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Collection Name
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="input-field w-full"
|
||
value={editingCollection.name}
|
||
onChange={(e) => setEditingCollection({...editingCollection, name: e.target.value})}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Description
|
||
</label>
|
||
<textarea
|
||
className="input-field w-full"
|
||
rows="3"
|
||
value={editingCollection.description}
|
||
onChange={(e) => setEditingCollection({...editingCollection, description: e.target.value})}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Hero Image
|
||
</label>
|
||
<input
|
||
type="url"
|
||
className="input-field w-full"
|
||
value={editingCollection.image || ''}
|
||
onChange={(e) => setEditingCollection({...editingCollection, image: e.target.value})}
|
||
placeholder="Enter image URL"
|
||
/>
|
||
</div>
|
||
|
||
{/* Tags Section */}
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
Tags
|
||
</label>
|
||
<div className="space-y-2">
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
className="input-field flex-1"
|
||
value={editTagInput}
|
||
onChange={(e) => setEditTagInput(e.target.value)}
|
||
onKeyPress={(e) => handleTagKeyPress(e, editTagInput, setEditTagInput, editingCollection, setEditingCollection)}
|
||
placeholder="Add tags (press Enter)"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleAddTag(editTagInput, setEditTagInput, editingCollection, setEditingCollection)}
|
||
className="px-3 py-2 rounded-lg text-sm font-medium transition-colors"
|
||
style={{
|
||
backgroundColor: 'var(--accent-ember)',
|
||
color: 'white'
|
||
}}
|
||
disabled={!editTagInput.trim()}
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
{editingCollection.tags && editingCollection.tags.length > 0 && (
|
||
<div className="flex flex-wrap gap-2">
|
||
{editingCollection.tags.map((tag, index) => (
|
||
<span
|
||
key={index}
|
||
className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium"
|
||
style={{
|
||
backgroundColor: 'var(--bg-tertiary)',
|
||
color: 'var(--text-primary)',
|
||
border: '1px solid var(--border)'
|
||
}}
|
||
>
|
||
{tag}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleRemoveTag(tag, editingCollection, setEditingCollection)}
|
||
className="ml-1 hover:text-red-500 transition-colors"
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
<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 className="flex space-x-3 mt-6">
|
||
<button
|
||
onClick={() => {
|
||
setEditingCollection(null);
|
||
setEditTagInput(''); // Clear tag input when closing
|
||
}}
|
||
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
|
||
style={{
|
||
borderColor: 'var(--border)',
|
||
color: 'var(--text-secondary)'
|
||
}}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleUpdateCollection}
|
||
disabled={!editingCollection.name.trim()}
|
||
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
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Layout>
|
||
);
|
||
}
|