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:
Randall Stillwell 2025-07-27 12:33:14 -05:00
parent e4ea9b4e73
commit dc867f2a2b

View file

@ -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
));
setEditingCollection(null);
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)',