import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { packsApi, isPacksApiError } from '@/api/packs' import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils' import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse, Question, } from '@/types/models' 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 { Checkbox } from '@/components/ui/checkbox' import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight, Upload, Download, FileJson } from 'lucide-react' import { GameQuestionsManager } from '@/components/GameQuestionsManager' import { GameQuestionEditorDialog } from '@/components/GameQuestionEditorDialog' import { PackImportDialog } from '@/components/PackImportDialog' export default function PacksPage() { const queryClient = useQueryClient() const [page, setPage] = useState(1) const [search, setSearch] = useState('') const [showDisabled, setShowDisabled] = useState(false) const [selectedPack, setSelectedPack] = useState(null) const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [packToDelete, setPackToDelete] = useState(null) // Import dialog state const [isImportDialogOpen, setIsImportDialogOpen] = useState(false) // Question editor state const [isQuestionEditorOpen, setIsQuestionEditorOpen] = useState(false) const [editingQuestion, setEditingQuestion] = useState<{ question: Question | null index: number } | null>(null) // Form state const [formData, setFormData] = useState({ name: '', description: '', category: '', isPublic: true, questions: [] as Question[], }) const limit = 20 // Fetch packs const { data, isLoading, error } = useQuery>({ queryKey: ['packs', page, search, showDisabled], queryFn: () => packsApi.getPacks({ page, limit, search, showDisabled }), retry: (failureCount, error) => { // Don't retry on client errors (4xx) if (isPacksApiError(error) && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) { return false } return failureCount < 2 }, }) // Mutations const createMutation = useMutation({ mutationFn: (pack: EditCardPackDto) => packsApi.upsertPack(pack), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packs'] }) toast.success('Pack created successfully') closeDialog() }, onError: (error: unknown) => { const errorMessage = isPacksApiError(error) ? error.message : getDetailedErrorMessage(error, 'create', 'pack') toast.error(errorMessage) console.error('Error creating pack:', error) }, }) const updateMutation = useMutation({ mutationFn: (pack: EditCardPackDto) => packsApi.upsertPack(pack), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packs'] }) toast.success('Pack updated successfully') closeDialog() }, onError: (error: unknown) => { const errorMessage = isPacksApiError(error) ? error.message : getDetailedErrorMessage(error, 'update', 'pack') toast.error(errorMessage) console.error('Error updating pack:', error) }, }) const deleteMutation = useMutation({ mutationFn: (packId: string) => packsApi.deletePack(packId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packs'] }) toast.success('Pack deleted successfully') setIsDeleteDialogOpen(false) setPackToDelete(null) }, onError: (error: unknown) => { const errorMessage = isPacksApiError(error) ? error.message : getDetailedErrorMessage(error, 'delete', 'pack') toast.error(errorMessage) console.error('Error deleting pack:', error) }, }) const openCreateDialog = () => { setSelectedPack(null) setFormData({ name: '', description: '', category: '', isPublic: true, questions: [], }) setIsDialogOpen(true) } const openEditDialog = async (pack: CardPackPreviewDto) => { try { const fullPack = await packsApi.getPack(pack.id) setSelectedPack(fullPack) // Convert questions from backend format (using 'text' field) const questions: Question[] = Array.isArray(fullPack.questions) ? fullPack.questions.map((q: any) => ({ text: q.text || '', answers: Array.isArray(q.answers) ? q.answers.map((a: any) => ({ text: a.text || '', points: typeof a.points === 'number' ? a.points : 0, })) : [], })) : [] setFormData({ name: fullPack.name || '', description: fullPack.description || '', category: fullPack.category || '', isPublic: fullPack.isPublic ?? true, questions, }) setIsDialogOpen(true) } catch (error) { const errorMessage = isPacksApiError(error) ? error.message : getDetailedErrorMessage(error, 'load', `pack "${pack.id}"`) toast.error(errorMessage) console.error('Error loading pack details:', error) } } const closeDialog = () => { setIsDialogOpen(false) setSelectedPack(null) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!formData.name.trim()) { toast.error('Name is required') return } if (!formData.description.trim()) { toast.error('Description is required') return } if (!formData.category.trim()) { toast.error('Category is required') return } // Questions are already in the correct format for backend const packData: EditCardPackDto = { // Only include id for updates, not for new packs ...(selectedPack && { id: selectedPack.id }), name: formData.name.trim(), description: formData.description.trim(), category: formData.category.trim(), isPublic: formData.isPublic, questions: formData.questions, } if (selectedPack) { updateMutation.mutate(packData) } else { createMutation.mutate(packData) } } // Question editor handlers const handleAddQuestion = () => { setEditingQuestion({ question: null, index: -1 }) setIsQuestionEditorOpen(true) } const handleEditQuestion = (question: Question, index: number) => { setEditingQuestion({ question, index }) setIsQuestionEditorOpen(true) } const handleSaveQuestion = (question: Question) => { if (editingQuestion) { const newQuestions = [...formData.questions] if (editingQuestion.index >= 0) { // Update existing question newQuestions[editingQuestion.index] = question } else { // Add new question newQuestions.push(question) } setFormData(prev => ({ ...prev, questions: newQuestions })) } setIsQuestionEditorOpen(false) setEditingQuestion(null) } const handleCloseQuestionEditor = () => { setIsQuestionEditorOpen(false) setEditingQuestion(null) } const handleQuestionsChange = (questions: Question[]) => { setFormData(prev => ({ ...prev, questions })) } const handleDelete = (pack: CardPackPreviewDto) => { setPackToDelete(pack) setIsDeleteDialogOpen(true) } const confirmDelete = () => { if (packToDelete) { deleteMutation.mutate(packToDelete.id) } } const handleSearch = (value: string) => { setSearch(value) setPage(1) // Reset to first page when searching } const downloadJson = (data: object, filename: string) => { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) } const handleDownloadTemplate = async () => { try { const template = await packsApi.getTemplate() downloadJson(template, 'pack-template.json') toast.success('Template downloaded') } catch (error) { const errorMessage = isPacksApiError(error) ? error.message : 'Failed to download template' toast.error(errorMessage) } } const handleExportPack = async (pack: CardPackPreviewDto) => { try { const exportedPack = await packsApi.exportPack(pack.id) const safeName = pack.title.replace(/[^a-z0-9]/gi, '_').toLowerCase() downloadJson(exportedPack, `pack-${safeName}.json`) toast.success('Pack exported successfully') } catch (error) { const errorMessage = isPacksApiError(error) ? error.message : 'Failed to export pack' toast.error(errorMessage) } } if (error) { const errorMessage = isPacksApiError(error) ? error.message : formatApiError(error) return (

Packs Management

Error loading packs

Failed to load packs

{errorMessage}

) } return (

Packs Management

View, create, edit and delete card packs

{/* Search and Filters */}
handleSearch(e.target.value)} className="max-w-sm" />
setShowDisabled(checked as boolean)} />
{/* Packs Table */} All Packs ({data?.total || 0}) Manage card packs and their contents {isLoading ? (
Loading packs...
) : ( <> ID Title Cards Enabled Actions {(data?.items || []).map((pack) => ( {pack.id}
{pack.title}
{pack.cards} {pack.enabled ? 'Enabled' : 'Disabled'}
))}
{/* Pagination */} {data && data.totalPages > 1 && (
Showing {((page - 1) * limit) + 1} to {Math.min(page * limit, data.total)} of {data.total} packs
Page {page} of {data.totalPages}
)} )}
{/* Create/Edit Dialog */} {selectedPack ? 'Edit Pack' : 'Create New Pack'} {selectedPack ? 'Update the pack information' : 'Add a new pack to the system'}
setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="Pack name" required />