2026-01-06 20:12:36 +00:00
|
|
|
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'
|
2026-01-07 14:10:14 +00:00
|
|
|
import type {
|
|
|
|
|
EditCardPackDto,
|
|
|
|
|
CardPackPreviewDto,
|
|
|
|
|
PaginatedResponse,
|
|
|
|
|
Question,
|
|
|
|
|
} from '@/types/models'
|
2026-01-06 20:12:36 +00:00
|
|
|
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 } from 'lucide-react'
|
2026-01-07 14:10:14 +00:00
|
|
|
import { GameQuestionsManager } from '@/components/GameQuestionsManager'
|
|
|
|
|
import { GameQuestionEditorDialog } from '@/components/GameQuestionEditorDialog'
|
2026-01-06 20:12:36 +00:00
|
|
|
|
|
|
|
|
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<EditCardPackDto | null>(null)
|
|
|
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
|
|
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
|
|
|
|
const [packToDelete, setPackToDelete] = useState<CardPackPreviewDto | null>(null)
|
|
|
|
|
|
2026-01-07 13:59:18 +00:00
|
|
|
// Question editor state
|
|
|
|
|
const [isQuestionEditorOpen, setIsQuestionEditorOpen] = useState(false)
|
|
|
|
|
const [editingQuestion, setEditingQuestion] = useState<{
|
|
|
|
|
question: Question | null
|
|
|
|
|
index: number
|
|
|
|
|
} | null>(null)
|
|
|
|
|
|
2026-01-06 20:12:36 +00:00
|
|
|
// Form state
|
|
|
|
|
const [formData, setFormData] = useState({
|
2026-01-07 13:35:50 +00:00
|
|
|
name: '',
|
2026-01-06 20:12:36 +00:00
|
|
|
description: '',
|
2026-01-07 13:35:50 +00:00
|
|
|
category: '',
|
|
|
|
|
isPublic: true,
|
|
|
|
|
questions: [] as Question[],
|
2026-01-06 20:12:36 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const limit = 20
|
|
|
|
|
|
|
|
|
|
// Fetch packs
|
|
|
|
|
const { data, isLoading, error } = useQuery<PaginatedResponse<CardPackPreviewDto>>({
|
|
|
|
|
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({
|
2026-01-07 13:35:50 +00:00
|
|
|
name: '',
|
2026-01-06 20:12:36 +00:00
|
|
|
description: '',
|
2026-01-07 13:35:50 +00:00
|
|
|
category: '',
|
|
|
|
|
isPublic: true,
|
|
|
|
|
questions: [],
|
2026-01-06 20:12:36 +00:00
|
|
|
})
|
|
|
|
|
setIsDialogOpen(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const openEditDialog = async (pack: CardPackPreviewDto) => {
|
|
|
|
|
try {
|
|
|
|
|
const fullPack = await packsApi.getPack(pack.id)
|
|
|
|
|
setSelectedPack(fullPack)
|
2026-01-07 13:59:18 +00:00
|
|
|
|
2026-01-07 14:10:14 +00:00
|
|
|
// Convert questions from backend format (supports both 'question' and 'text' fields)
|
2026-01-07 13:59:18 +00:00
|
|
|
const questions: Question[] = Array.isArray(fullPack.questions)
|
2026-01-07 14:10:14 +00:00
|
|
|
? fullPack.questions.map((q: any) => ({
|
|
|
|
|
question: q.question || q.text || '',
|
|
|
|
|
answers: Array.isArray(q.answers)
|
|
|
|
|
? q.answers.map((a: any) => ({
|
|
|
|
|
text: a.text || '',
|
|
|
|
|
points: typeof a.points === 'number' ? a.points : 0,
|
|
|
|
|
}))
|
|
|
|
|
: [],
|
|
|
|
|
}))
|
2026-01-07 13:59:18 +00:00
|
|
|
: []
|
|
|
|
|
|
2026-01-06 20:12:36 +00:00
|
|
|
setFormData({
|
2026-01-07 13:35:50 +00:00
|
|
|
name: fullPack.name || '',
|
2026-01-06 20:12:36 +00:00
|
|
|
description: fullPack.description || '',
|
2026-01-07 13:35:50 +00:00
|
|
|
category: fullPack.category || '',
|
|
|
|
|
isPublic: fullPack.isPublic ?? true,
|
2026-01-07 13:59:18 +00:00
|
|
|
questions,
|
2026-01-06 20:12:36 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2026-01-07 13:35:50 +00:00
|
|
|
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')
|
2026-01-06 20:12:36 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 14:10:14 +00:00
|
|
|
// Questions are already in the correct format for backend
|
2026-01-06 20:12:36 +00:00
|
|
|
const packData: EditCardPackDto = {
|
|
|
|
|
// Only include id for updates, not for new packs
|
|
|
|
|
...(selectedPack && { id: selectedPack.id }),
|
2026-01-07 13:35:50 +00:00
|
|
|
name: formData.name.trim(),
|
|
|
|
|
description: formData.description.trim(),
|
|
|
|
|
category: formData.category.trim(),
|
|
|
|
|
isPublic: formData.isPublic,
|
2026-01-07 14:10:14 +00:00
|
|
|
questions: formData.questions,
|
2026-01-06 20:12:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedPack) {
|
|
|
|
|
updateMutation.mutate(packData)
|
|
|
|
|
} else {
|
|
|
|
|
createMutation.mutate(packData)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 13:59:18 +00:00
|
|
|
// 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 }))
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 20:12:36 +00:00
|
|
|
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 (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-bold tracking-tight">Packs Management</h1>
|
|
|
|
|
<p className="text-muted-foreground">Error loading packs</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<p className="text-red-500 font-medium">Failed to load packs</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">{errorMessage}</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => queryClient.invalidateQueries({ queryKey: ['packs'] })}
|
|
|
|
|
className="mt-2"
|
|
|
|
|
>
|
|
|
|
|
Retry
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-bold tracking-tight">Packs Management</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
View, create, edit and delete card packs
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button onClick={openCreateDialog}>
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
|
|
|
Add Pack
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search and Filters */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Search className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Search packs..."
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={(e) => handleSearch(e.target.value)}
|
|
|
|
|
className="max-w-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="showDisabled"
|
|
|
|
|
checked={showDisabled}
|
|
|
|
|
onCheckedChange={(checked) => setShowDisabled(checked as boolean)}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="showDisabled">Show disabled packs</Label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Packs Table */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle>All Packs ({data?.total || 0})</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Manage card packs and their contents
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="text-center py-8">Loading packs...</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>ID</TableHead>
|
|
|
|
|
<TableHead>Title</TableHead>
|
|
|
|
|
<TableHead>Cards</TableHead>
|
|
|
|
|
<TableHead>Enabled</TableHead>
|
|
|
|
|
<TableHead>Actions</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
2026-01-09 16:44:33 +00:00
|
|
|
{(data?.items || []).map((pack) => (
|
2026-01-06 20:12:36 +00:00
|
|
|
<TableRow key={pack.id}>
|
|
|
|
|
<TableCell className="font-mono text-sm">{pack.id}</TableCell>
|
|
|
|
|
<TableCell>
|
2026-01-07 13:35:50 +00:00
|
|
|
<div className="font-medium">{pack.title}</div>
|
2026-01-06 20:12:36 +00:00
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>{pack.cards}</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
|
|
|
|
pack.enabled
|
|
|
|
|
? 'bg-green-100 text-green-800'
|
|
|
|
|
: 'bg-red-100 text-red-800'
|
|
|
|
|
}`}>
|
|
|
|
|
{pack.enabled ? 'Enabled' : 'Disabled'}
|
|
|
|
|
</span>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => openEditDialog(pack)}
|
|
|
|
|
>
|
|
|
|
|
<Edit className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => handleDelete(pack)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
|
|
|
|
|
{/* Pagination */}
|
|
|
|
|
{data && data.totalPages > 1 && (
|
|
|
|
|
<div className="flex items-center justify-between mt-4">
|
|
|
|
|
<div className="text-sm text-muted-foreground">
|
|
|
|
|
Showing {((page - 1) * limit) + 1} to {Math.min(page * limit, data.total)} of {data.total} packs
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setPage(page - 1)}
|
|
|
|
|
disabled={page <= 1}
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeft className="h-4 w-4" />
|
|
|
|
|
Previous
|
|
|
|
|
</Button>
|
|
|
|
|
<span className="text-sm">
|
|
|
|
|
Page {page} of {data.totalPages}
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setPage(page + 1)}
|
|
|
|
|
disabled={page >= data.totalPages}
|
|
|
|
|
>
|
|
|
|
|
Next
|
|
|
|
|
<ChevronRight className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Create/Edit Dialog */}
|
|
|
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
2026-01-07 13:59:18 +00:00
|
|
|
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
|
2026-01-06 20:12:36 +00:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>
|
|
|
|
|
{selectedPack ? 'Edit Pack' : 'Create New Pack'}
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
{selectedPack ? 'Update the pack information' : 'Add a new pack to the system'}
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
|
|
|
<div className="grid gap-4 py-4">
|
2026-01-07 13:35:50 +00:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="name">Name *</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="name"
|
|
|
|
|
value={formData.name}
|
|
|
|
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
|
|
|
placeholder="Pack name"
|
|
|
|
|
required
|
|
|
|
|
/>
|
2026-01-06 20:12:36 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
2026-01-07 13:35:50 +00:00
|
|
|
<Label htmlFor="description">Description *</Label>
|
2026-01-06 20:12:36 +00:00
|
|
|
<Textarea
|
|
|
|
|
id="description"
|
|
|
|
|
value={formData.description}
|
|
|
|
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
|
|
|
|
placeholder="Pack description"
|
|
|
|
|
rows={3}
|
2026-01-07 13:35:50 +00:00
|
|
|
required
|
2026-01-06 20:12:36 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-07 13:35:50 +00:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="category">Category *</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="category"
|
|
|
|
|
value={formData.category}
|
|
|
|
|
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))}
|
|
|
|
|
placeholder="Pack category"
|
|
|
|
|
required
|
|
|
|
|
/>
|
2026-01-06 20:12:36 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
2026-01-07 13:35:50 +00:00
|
|
|
id="isPublic"
|
|
|
|
|
checked={formData.isPublic}
|
|
|
|
|
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isPublic: checked as boolean }))}
|
2026-01-06 20:12:36 +00:00
|
|
|
/>
|
2026-01-07 13:35:50 +00:00
|
|
|
<Label htmlFor="isPublic">Public</Label>
|
2026-01-06 20:12:36 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
2026-01-07 13:59:18 +00:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Label>Questions</Label>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleAddQuestion}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
|
|
|
Add Question
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-01-07 14:10:14 +00:00
|
|
|
<GameQuestionsManager
|
2026-01-07 13:59:18 +00:00
|
|
|
questions={formData.questions}
|
|
|
|
|
onChange={handleQuestionsChange}
|
|
|
|
|
onEdit={handleEditQuestion}
|
|
|
|
|
/>
|
2026-01-06 20:12:36 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button type="button" variant="outline" onClick={closeDialog}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
|
|
|
|
{createMutation.isPending || updateMutation.isPending ? 'Saving...' : (selectedPack ? 'Update' : 'Create')}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
2026-01-07 13:59:18 +00:00
|
|
|
{/* Question Editor Dialog */}
|
2026-01-07 14:10:14 +00:00
|
|
|
<GameQuestionEditorDialog
|
2026-01-07 13:59:18 +00:00
|
|
|
open={isQuestionEditorOpen}
|
|
|
|
|
question={editingQuestion?.question || null}
|
|
|
|
|
onSave={handleSaveQuestion}
|
|
|
|
|
onClose={handleCloseQuestionEditor}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-01-06 20:12:36 +00:00
|
|
|
{/* Delete Confirmation Dialog */}
|
|
|
|
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Delete Pack</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
Are you sure you want to delete the pack "{packToDelete?.title}"? This action cannot be undone and will remove all associated cards.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={confirmDelete}
|
|
|
|
|
className="bg-red-600 hover:bg-red-700"
|
|
|
|
|
disabled={deleteMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|