import { useState, useEffect } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { X, Tag, Plus, Loader2 } from 'lucide-react'; import { api } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { useToast } from '../contexts/ToastContext'; interface BatchTagModalProps { selectedIds: number[]; existingTags: string[]; onClose: () => void; } export function BatchTagModal({ selectedIds, existingTags, onClose }: BatchTagModalProps) { const queryClient = useQueryClient(); const { showToast } = useToast(); const [newTag, setNewTag] = useState(''); const [selectedTags, setSelectedTags] = useState>(new Set()); const [mode, setMode] = useState<'add' | 'remove'>('add'); // Close on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); const batchTagMutation = useMutation({ mutationFn: async () => { const tagsArray = Array.from(selectedTags); let successCount = 0; // Process sequentially to avoid SQLite database locks for (const id of selectedIds) { try { const archive = await api.getArchive(id); const currentTags = archive.tags ? archive.tags.split(',').map(t => t.trim()).filter(Boolean) : []; let newTags: string[]; if (mode === 'add') { // Add tags that aren't already present newTags = [...new Set([...currentTags, ...tagsArray])]; } else { // Remove selected tags newTags = currentTags.filter(t => !selectedTags.has(t)); } await api.updateArchive(id, { tags: newTags.join(', ') }); successCount++; } catch (err) { console.error(`Failed to update archive ${id}:`, err); throw new Error(`Failed on archive ${id}: ${err instanceof Error ? err.message : 'Unknown error'}`); } } return { count: successCount, mode, tags: tagsArray }; }, onSuccess: ({ count, mode, tags }) => { queryClient.invalidateQueries({ queryKey: ['archives'] }); showToast(`${mode === 'add' ? 'Added' : 'Removed'} ${tags.length} tag${tags.length !== 1 ? 's' : ''} ${mode === 'add' ? 'to' : 'from'} ${count} archive${count !== 1 ? 's' : ''}`); onClose(); }, onError: (error: Error) => { showToast(error.message || 'Failed to update tags', 'error'); }, }); const toggleTag = (tag: string) => { setSelectedTags((prev) => { const next = new Set(prev); if (next.has(tag)) { next.delete(tag); } else { next.add(tag); } return next; }); }; const addNewTag = () => { if (newTag.trim() && !selectedTags.has(newTag.trim())) { setSelectedTags((prev) => new Set([...prev, newTag.trim()])); setNewTag(''); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); addNewTag(); } }; return (
{/* Header */}

{mode === 'add' ? 'Add Tags' : 'Remove Tags'}

{/* Content */}

{mode === 'add' ? 'Add' : 'Remove'} tags for {selectedIds.length} selected archive{selectedIds.length !== 1 ? 's' : ''}

{/* Mode toggle */}
{/* New tag input (only for add mode) */} {mode === 'add' && (
setNewTag(e.target.value)} onKeyDown={handleKeyDown} />
)} {/* Existing tags */} {existingTags.length > 0 && (

Existing tags:

{existingTags.map((tag) => ( ))}
)} {/* Selected tags preview */} {selectedTags.size > 0 && (

Tags to {mode === 'add' ? 'add' : 'remove'}:

{Array.from(selectedTags).map((tag) => ( {tag} ))}
)}
{/* Footer */}
); }