✨ 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 [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
const [createdCollection, setCreatedCollection] = useState(null);
|
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
|
// Redirect to login if not authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -132,6 +134,7 @@ export default function Collections() {
|
||||||
image: '',
|
image: '',
|
||||||
tags: []
|
tags: []
|
||||||
});
|
});
|
||||||
|
setTagInput(''); // Clear tag input
|
||||||
|
|
||||||
// Refresh collections list
|
// Refresh collections list
|
||||||
fetchCollections();
|
fetchCollections();
|
||||||
|
|
@ -145,16 +148,79 @@ export default function Collections() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateCollection = () => {
|
const handleUpdateCollection = async () => {
|
||||||
setCollections(collections.map(c =>
|
try {
|
||||||
c.id === editingCollection.id ? editingCollection : c
|
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);
|
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) => {
|
const handleDeleteCollection = async (collectionId) => {
|
||||||
if (confirm('Are you sure you want to delete this collection?')) {
|
if (confirm('Are you sure you want to delete this collection? This action cannot be undone.')) {
|
||||||
setCollections(collections.filter(c => c.id !== id));
|
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
|
Add a custom thumbnail image, or we'll use your rarest cards
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 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)' }}>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
|
@ -544,7 +669,10 @@ export default function Collections() {
|
||||||
</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);
|
||||||
|
setTagInput(''); // Clear tag input when closing
|
||||||
|
}}
|
||||||
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
|
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
|
|
@ -651,6 +779,62 @@ export default function Collections() {
|
||||||
placeholder="Enter image URL"
|
placeholder="Enter image URL"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 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)' }}>
|
<label className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
|
@ -677,7 +861,10 @@ export default function Collections() {
|
||||||
</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);
|
||||||
|
setEditTagInput(''); // Clear tag input when closing
|
||||||
|
}}
|
||||||
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
|
className="flex-1 py-2 px-4 rounded-xl border transition-colors"
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--border)',
|
borderColor: 'var(--border)',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue