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 } 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 { ImageUpload } from '@/components/ui/image-upload' import { ColorPaletteInput } from '@/components/ui/color-palette-input' import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight } from 'lucide-react' 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) // Form state const [formData, setFormData] = useState({ title: '', subtitle: '', description: '', color: '', enabled: true, googlePlayId: '', rustoreId: '', appStoreId: '', price: '', order: 0, version: '', size: 0, cover: undefined as string | undefined, }) 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({ title: '', subtitle: '', description: '', color: '', enabled: true, googlePlayId: '', rustoreId: '', appStoreId: '', price: '', order: 0, version: '', size: 0, cover: undefined, }) setIsDialogOpen(true) } const openEditDialog = async (pack: CardPackPreviewDto) => { try { const fullPack = await packsApi.getPack(pack.id) setSelectedPack(fullPack) setFormData({ title: fullPack.title || '', subtitle: fullPack.subtitle || '', description: fullPack.description || '', color: fullPack.color || '', enabled: fullPack.enabled ?? true, googlePlayId: fullPack.googlePlayId || '', rustoreId: fullPack.rustoreId || '', appStoreId: fullPack.appStoreId || '', price: fullPack.price || '', order: fullPack.order || 0, version: fullPack.version || '', size: fullPack.size || 0, cover: fullPack.cover, }) 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.title.trim()) { toast.error('Title is required') return } const packData: EditCardPackDto = { // Only include id for updates, not for new packs ...(selectedPack && { id: selectedPack.id }), title: formData.title.trim(), subtitle: formData.subtitle.trim() || undefined, description: formData.description.trim() || undefined, color: formData.color.trim() || undefined, enabled: formData.enabled, googlePlayId: formData.googlePlayId.trim() || undefined, rustoreId: formData.rustoreId.trim() || undefined, appStoreId: formData.appStoreId.trim() || undefined, price: formData.price.trim() || undefined, order: formData.order, version: formData.version.trim() || undefined, size: formData.size > 0 ? formData.size : undefined, cover: formData.cover || undefined, } if (selectedPack) { updateMutation.mutate(packData) } else { createMutation.mutate(packData) } } 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 } 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 Order Actions {data?.items.map((pack) => ( {pack.id}
{pack.title}
{pack.subtitle && (
{pack.subtitle}
)}
{pack.cards} {pack.enabled ? 'Enabled' : 'Disabled'} {pack.order ?? 0}
))}
{/* 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, title: e.target.value }))} placeholder="Pack title" required />
setFormData(prev => ({ ...prev, subtitle: e.target.value }))} placeholder="Pack subtitle" />