import { useState, useEffect } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { cardsApi, isCardsApiError } from '@/api/cards' import { packsApi, isPacksApiError } from '@/api/packs' import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils' import type { GameCardDto } from '@/types/models' import { Button } from './ui/button' import { Input } from './ui/input' import { Label } from './ui/label' import { Textarea } from './ui/textarea' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card' import { Badge } from './ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select' import { ChevronLeft, ChevronRight, Save, X } from 'lucide-react' interface UploadedImage { id: string file: File preview: string base64: string } interface CardData { imageId: string packId: string original: string translation: string mnemo: string transcription: string transcriptionMnemo: string back: string imageBack?: string isSaved: boolean cardId?: string } interface BulkCardEditorProps { images: UploadedImage[] defaultPackId?: string onComplete: () => void onCancel: () => void } export function BulkCardEditor({ images, defaultPackId, onComplete, onCancel }: BulkCardEditorProps) { const queryClient = useQueryClient() const [currentIndex, setCurrentIndex] = useState(0) const [cardsData, setCardsData] = useState>(new Map()) const [packsData, setPacksData] = useState<{ id: string; title: string }[]>([]) // Load packs useEffect(() => { packsApi .getPacks({ page: 1, limit: 100, search: '' }) .then((response) => { try { if (response && response.items && Array.isArray(response.items)) { const packs = response.items.map((pack: any) => ({ id: String(pack.id || ''), title: String(pack.title || pack.id || '') })) setPacksData(packs) } else { console.warn('Unexpected packs response format:', response) setPacksData([]) } } catch (error) { console.error('Error processing packs response:', error) setPacksData([]) } }) .catch((error) => { const errorMessage = isPacksApiError(error) ? error.message : formatApiError(error) console.error('Failed to load packs:', error) toast.error(`Failed to load packs: ${errorMessage}`) // Initialize with empty array so component doesn't crash setPacksData([]) }) }, []) // Initialize cards data useEffect(() => { if (images.length > 0) { const initialData = new Map() images.forEach((image) => { initialData.set(image.id, { imageId: image.id, packId: defaultPackId || '', original: '', translation: '', mnemo: '', transcription: '', transcriptionMnemo: '', back: '', imageBack: undefined, isSaved: false, }) }) setCardsData(initialData) } }, [images, defaultPackId]) const currentImage = images[currentIndex] const currentCard = currentImage ? cardsData.get(currentImage.id) : undefined const updateMutation = useMutation({ mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card), onSuccess: (response) => { queryClient.invalidateQueries({ queryKey: ['cards'] }) if (currentImage) { const imageId = currentImage.id setCardsData((prev) => { const newData = new Map(prev) const cardData = newData.get(imageId) if (cardData && response.card) { newData.set(imageId, { ...cardData, isSaved: true, cardId: response.card.id, }) } return newData }) } toast.success('Card saved successfully') }, onError: (error: unknown) => { const errorMessage = isCardsApiError(error) ? error.message : getDetailedErrorMessage(error, 'save', 'card') toast.error(errorMessage) console.error('Error saving card:', error) }, }) const handleFieldChange = (field: keyof CardData, value: string) => { if (!currentCard || !currentImage) return setCardsData((prev) => { const newData = new Map(prev) const cardData = newData.get(currentImage.id) if (cardData) { newData.set(currentImage.id, { ...cardData, [field]: value, }) } return newData }) } const handleSave = () => { if (!currentCard) return if (!currentCard.original.trim() || !currentCard.translation.trim() || !currentCard.mnemo.trim()) { toast.error('Original, translation and mnemo are required') return } const image = images.find((img) => img.id === currentCard.imageId) if (!image) return const cardData: GameCardDto = { id: currentCard.cardId || null, packId: currentCard.packId || undefined, original: currentCard.original.trim(), translation: currentCard.translation.trim(), mnemo: currentCard.mnemo.trim(), transcription: currentCard.transcription.trim() || undefined, transcriptionMnemo: currentCard.transcriptionMnemo.trim() || undefined, back: currentCard.back.trim() || undefined, image: image.base64, imageBack: currentCard.imageBack || undefined, } updateMutation.mutate(cardData) } const handleNext = () => { if (currentIndex < images.length - 1) { setCurrentIndex(currentIndex + 1) } } const handlePrevious = () => { if (currentIndex > 0) { setCurrentIndex(currentIndex - 1) } } const savedCount = Array.from(cardsData.values()).filter((card) => card.isSaved).length const totalCount = images.length // Wait for images and cards data to be initialized if (images.length === 0) { return (

No images uploaded

) } if (!currentImage || !currentCard) { return (

Initializing card data...

) } return (

Fill Card Details

Card {currentIndex + 1} of {totalCount} • {savedCount} saved

{currentCard.isSaved ? 'Saved' : 'Not Saved'}
{/* Image Preview */} Image Preview {currentImage.file?.name || 'Unknown'}
{currentImage.preview ? ( {currentImage.file?.name { console.error('Failed to load image:', currentImage.preview) e.currentTarget.style.display = 'none' }} /> ) : (
Image preview not available
)}
{/* Form */} Card Information Fill in the details for this card
handleFieldChange('original', e.target.value)} placeholder="e.g. cerdo" />
handleFieldChange('translation', e.target.value)} placeholder="e.g. pig" />
handleFieldChange('mnemo', e.target.value)} placeholder="e.g. [pig] with heart" />
handleFieldChange('transcription', e.target.value)} placeholder="e.g. sɛrdo" />
handleFieldChange('transcriptionMnemo', e.target.value)} placeholder="e.g. pig with heart" />