Added debug logging to investigate why ownership indicators aren't showing: - Log current user in collections page - Log collections data with userRole and creator info - Log API response data to see what backend returns - Enhanced error handling in fetchCollections This will help identify if the issue is: - Frontend auth context not working properly - API not returning correct userRole values - Collections state not updating correctly - Permission indicator not receiving proper props Debug logs will show in browser console when testing Bob's login.
912 lines
No EOL
36 KiB
JavaScript
912 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';
|
||
|
||
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]);
|
||
|
||
// Debug logging
|
||
useEffect(() => {
|
||
console.log('🔍 Debug - Current user:', user);
|
||
console.log('🔍 Debug - Collections:', collections.map(c => ({
|
||
name: c.name,
|
||
userRole: c.userRole,
|
||
creator: c.creator
|
||
})));
|
||
}, [user, collections]);
|
||
|
||
const fetchCollections = async () => {
|
||
try {
|
||
const response = await fetch('/api/collections');
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
console.log('🔍 Debug - API response:', data.map(c => ({
|
||
name: c.name,
|
||
userRole: c.userRole,
|
||
creator: c.creator
|
||
})));
|
||
|
||
// 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`);
|
||
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 mb-2" style={{ color: 'var(--text-primary)' }}>
|
||
My Collections
|
||
</h1>
|
||
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
|
||
Organize and manage your card collections
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
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">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||
</svg>
|
||
<span>Create Collection</span>
|
||
</button>
|
||
</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>
|
||
);
|
||
}
|