✨ Add Tag Management to Collection Modals
🏷️ Tag Functionality Added: - Tag input field in Create Collection modal - Tag editing in Edit Collection modal - Add tags with Enter key or Add button - Remove tags with × button - Visual tag display with styling 🎨 Tag Features: - Real-time tag addition/removal - Duplicate tag prevention - Tag input clearing on modal close - Proper tag persistence to database - Clean tag display with hover effects 🔧 Technical Improvements: - Added tagInput and editTagInput state management - Created reusable tag handling functions - Updated API calls to include tags in create/update - Enhanced modal UX with tag management - Proper form cleanup on modal close 🎯 User Experience: - Users can now organize collections with tags - Tags display in collection grid view - Easy tag management in both create and edit flows - Consistent tag styling across the app Tags are now fully functional for collection organization! 🚀
This commit is contained in:
parent
e4ea9b4e73
commit
dc867f2a2b
1 changed files with 197 additions and 10 deletions
|
|
@ -24,6 +24,8 @@ export default function Collections() {
|
|||
});
|
||||
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(() => {
|
||||
|
|
@ -132,6 +134,7 @@ export default function Collections() {
|
|||
image: '',
|
||||
tags: []
|
||||
});
|
||||
setTagInput(''); // Clear tag input
|
||||
|
||||
// Refresh collections list
|
||||
fetchCollections();
|
||||
|
|
@ -145,16 +148,79 @@ export default function Collections() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleUpdateCollection = () => {
|
||||
setCollections(collections.map(c =>
|
||||
c.id === editingCollection.id ? editingCollection : c
|
||||
));
|
||||
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 = (id) => {
|
||||
if (confirm('Are you sure you want to delete this collection?')) {
|
||||
setCollections(collections.filter(c => c.id !== id));
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -518,6 +584,65 @@ export default function Collections() {
|
|||
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)' }}>
|
||||
|
|
@ -544,7 +669,10 @@ export default function Collections() {
|
|||
</div>
|
||||
<div className="flex space-x-3 mt-6">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
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)',
|
||||
|
|
@ -651,6 +779,62 @@ export default function Collections() {
|
|||
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)' }}>
|
||||
|
|
@ -677,7 +861,10 @@ export default function Collections() {
|
|||
</div>
|
||||
<div className="flex space-x-3 mt-6">
|
||||
<button
|
||||
onClick={() => setEditingCollection(null)}
|
||||
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)',
|
||||
|
|
|
|||
Loading…
Reference in a new issue