import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { cardsApi } from '@/api/cards' import type { GameCardDto, PaginatedResponse } from '@/types/models' import type { AxiosError } from 'axios' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { ImageUpload } from '@/components/ui/image-upload' import { CardVoicesManager } from '@/components/CardVoicesManager' import { BulkCardUpload } from '@/components/BulkCardUpload' import { BulkCardEditor } from '@/components/BulkCardEditor' import { packsApi } from '@/api/packs' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight, Upload } from 'lucide-react' export default function CardsPage() { const queryClient = useQueryClient() const [page, setPage] = useState(1) const [search, setSearch] = useState('') const [selectedCard, setSelectedCard] = useState(null) const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [cardToDelete, setCardToDelete] = useState(null) const [isBulkUploadOpen, setIsBulkUploadOpen] = useState(false) const [isBulkEditorOpen, setIsBulkEditorOpen] = useState(false) const [uploadedImages, setUploadedImages] = useState>([]) // Form state const [formData, setFormData] = useState({ packId: '', original: '', translation: '', mnemo: '', transcription: '', transcriptionMnemo: '', back: '', image: undefined as string | undefined, imageBack: undefined as string | undefined, }) const limit = 20 // Fetch cards const { data, isLoading, error } = useQuery>({ queryKey: ['cards', page, search], queryFn: () => cardsApi.getCards({ page, limit, search }), }) // Mutations const createMutation = useMutation({ mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cards'] }) toast.success('Card created successfully') closeDialog() }, onError: (error: unknown) => { const axiosError = error as AxiosError<{ message?: string }> toast.error(axiosError.response?.data?.message || 'Failed to create card') }, }) const updateMutation = useMutation({ mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cards'] }) toast.success('Card updated successfully') closeDialog() }, onError: (error: unknown) => { const axiosError = error as AxiosError<{ message?: string }> toast.error(axiosError.response?.data?.message || 'Failed to update card') }, }) const deleteMutation = useMutation({ mutationFn: (cardId: number) => cardsApi.deleteCard(cardId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cards'] }) toast.success('Card deleted successfully') setIsDeleteDialogOpen(false) setCardToDelete(null) }, onError: (error: unknown) => { const axiosError = error as AxiosError<{ message?: string }> toast.error(axiosError.response?.data?.message || 'Failed to delete card') }, }) // Fetch packs for packId selection const { data: packsData } = useQuery({ queryKey: ['packs', 1, 1000, ''], queryFn: () => packsApi.getPacks({ page: 1, limit: 1000, search: '' }), }) const openCreateDialog = () => { setSelectedCard(null) setFormData({ packId: '', original: '', translation: '', mnemo: '', transcription: '', transcriptionMnemo: '', back: '', image: undefined, imageBack: undefined, }) setIsDialogOpen(true) } const openEditDialog = (card: GameCardDto) => { setSelectedCard(card) setFormData({ packId: (card as any).packId || '', original: card.original || '', translation: card.translation || '', mnemo: card.mnemo || '', transcription: card.transcription || '', transcriptionMnemo: card.transcriptionMnemo || '', back: card.back || '', image: card.image, imageBack: card.imageBack, }) setIsDialogOpen(true) } const closeDialog = () => { setIsDialogOpen(false) setSelectedCard(null) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!formData.original.trim() || !formData.translation.trim() || !formData.mnemo.trim()) { toast.error('Original, translation and mnemo are required') return } const cardData: GameCardDto = { id: selectedCard?.id || -1, packId: formData.packId.trim() || undefined, original: formData.original.trim(), translation: formData.translation.trim(), mnemo: formData.mnemo.trim(), transcription: formData.transcription.trim() || undefined, transcriptionMnemo: formData.transcriptionMnemo.trim() || undefined, back: formData.back.trim() || undefined, image: formData.image || undefined, imageBack: formData.imageBack || undefined, } if (selectedCard) { updateMutation.mutate(cardData) } else { createMutation.mutate(cardData) } } const handleDelete = (card: GameCardDto) => { setCardToDelete(card) setIsDeleteDialogOpen(true) } const confirmDelete = () => { if (cardToDelete) { deleteMutation.mutate(cardToDelete.id) } } const handleSearch = (value: string) => { setSearch(value) setPage(1) // Reset to first page when searching } if (error) { return (

Cards Management

Error loading cards

Failed to load cards. Please try again later.

) } return (

Cards Management

View, create, edit and delete game cards

{/* Search */}
handleSearch(e.target.value)} className="max-w-sm" />
{/* Cards Table */} All Cards ({data?.total || 0}) Manage game cards in the system {isLoading ? (
Loading cards...
) : ( <> ID Original Translation Mnemo Actions {data?.items.map((card) => ( {card.id} {card.original} {card.translation} {card.mnemo}
))}
{/* Pagination */} {data && data.totalPages > 1 && (
Showing {((page - 1) * limit) + 1} to {Math.min(page * limit, data.total)} of {data.total} cards
Page {page} of {data.totalPages}
)} )}
{/* Create/Edit Dialog */} {selectedCard ? 'Edit Card' : 'Create New Card'} {selectedCard ? 'Update the card information' : 'Add a new card to the system'}
setFormData(prev => ({ ...prev, original: e.target.value }))} placeholder="e.g. cerdo" required />
setFormData(prev => ({ ...prev, translation: e.target.value }))} placeholder="e.g. pig" required />
setFormData(prev => ({ ...prev, mnemo: e.target.value }))} placeholder="e.g. [pig] with heart" required />
setFormData(prev => ({ ...prev, transcription: e.target.value }))} placeholder="e.g. sɛrdo" />
setFormData(prev => ({ ...prev, transcriptionMnemo: e.target.value }))} placeholder="e.g. pig with heart" />