admin
Some checks are pending
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2025-12-15 01:31:58 +03:00
parent 5a8c55c1ef
commit 2adffbf235

View file

@ -41,12 +41,15 @@ import { BulkCardUpload } from '@/components/BulkCardUpload'
import { BulkCardEditor } from '@/components/BulkCardEditor' import { BulkCardEditor } from '@/components/BulkCardEditor'
import { packsApi } from '@/api/packs' import { packsApi } from '@/api/packs'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight, Upload } from 'lucide-react' import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight, Upload, List, Grid } from 'lucide-react'
type ViewMode = 'list' | 'grid'
export default function CardsPage() { export default function CardsPage() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [viewMode, setViewMode] = useState<ViewMode>('list')
const [selectedCard, setSelectedCard] = useState<GameCardDto | null>(null) const [selectedCard, setSelectedCard] = useState<GameCardDto | null>(null)
const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
@ -225,6 +228,25 @@ export default function CardsPage() {
setPage(1) // Reset to first page when searching setPage(1) // Reset to first page when searching
} }
// Group cards by pack
const groupedCards = data?.items.reduce((acc, card) => {
const packId = card.packId || '__no_pack__'
if (!acc[packId]) {
acc[packId] = []
}
acc[packId].push(card)
return acc
}, {} as Record<string, GameCardDto[]>) || {}
// Get pack name by ID
const getPackName = (packId: string): string => {
if (packId === '__no_pack__') {
return 'No Pack'
}
const pack = packsData?.items.find(p => p.id === packId)
return pack?.title || packId
}
if (error) { if (error) {
const errorMessage = isCardsApiError(error) const errorMessage = isCardsApiError(error)
? error.message ? error.message
@ -294,55 +316,159 @@ export default function CardsPage() {
{/* Cards Table */} {/* Cards Table */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>All Cards ({data?.total || 0})</CardTitle> <div className="flex items-center justify-between">
<CardDescription> <div>
Manage game cards in the system <CardTitle>All Cards ({data?.total || 0})</CardTitle>
</CardDescription> <CardDescription>
Manage game cards in the system
</CardDescription>
</div>
<div className="flex items-center space-x-2">
<Button
variant={viewMode === 'list' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('list')}
>
<List className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'grid' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('grid')}
>
<Grid className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<div className="text-center py-8">Loading cards...</div> <div className="text-center py-8">Loading cards...</div>
) : ( ) : (
<> <>
<Table> {viewMode === 'list' ? (
<TableHeader> <>
<TableRow> {Object.entries(groupedCards).map(([packId, cards]) => (
<TableHead>ID</TableHead> <div key={packId} className="mb-8">
<TableHead>Original</TableHead> <h3 className="text-lg font-semibold mb-4 pb-2 border-b">
<TableHead>Translation</TableHead> {getPackName(packId)} ({cards.length})
<TableHead>Mnemo</TableHead> </h3>
<TableHead>Actions</TableHead> <Table>
</TableRow> <TableHeader>
</TableHeader> <TableRow>
<TableBody> <TableHead>ID</TableHead>
{data?.items.map((card) => ( <TableHead>Original</TableHead>
<TableRow key={card.id}> <TableHead>Translation</TableHead>
<TableCell>{card.id}</TableCell> <TableHead>Mnemo</TableHead>
<TableCell className="font-medium">{card.original}</TableCell> <TableHead>Actions</TableHead>
<TableCell>{card.translation}</TableCell> </TableRow>
<TableCell className="max-w-xs truncate">{card.mnemo}</TableCell> </TableHeader>
<TableCell> <TableBody>
<div className="flex items-center space-x-2"> {cards.map((card) => (
<Button <TableRow key={card.id}>
variant="ghost" <TableCell>{card.id}</TableCell>
size="sm" <TableCell className="font-medium">{card.original}</TableCell>
<TableCell>{card.translation}</TableCell>
<TableCell className="max-w-xs truncate">{card.mnemo}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => openEditDialog(card)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(card)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
))}
</>
) : (
<>
{Object.entries(groupedCards).map(([packId, cards]) => (
<div key={packId} className="mb-8">
<h3 className="text-lg font-semibold mb-4 pb-2 border-b">
{getPackName(packId)} ({cards.length})
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
{cards.map((card) => (
<div
key={card.id}
className="border rounded-lg overflow-hidden hover:shadow-md transition-shadow cursor-pointer group"
onClick={() => openEditDialog(card)} onClick={() => openEditDialog(card)}
> >
<Edit className="h-4 w-4" /> <div className="aspect-square bg-muted relative">
</Button> {card.image ? (
<Button <img
variant="ghost" src={card.image}
size="sm" alt={card.original}
onClick={() => handleDelete(card)} className="w-full h-full object-cover"
> onError={(e) => {
<Trash2 className="h-4 w-4" /> const target = e.target as HTMLImageElement
</Button> target.style.display = 'none'
</div> }}
</TableCell> />
</TableRow> ) : (
<div className="w-full h-full flex items-center justify-center text-muted-foreground">
No Image
</div>
)}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex space-x-1">
<Button
variant="secondary"
size="icon"
className="h-7 w-7"
onClick={(e) => {
e.stopPropagation()
openEditDialog(card)
}}
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="destructive"
size="icon"
className="h-7 w-7"
onClick={(e) => {
e.stopPropagation()
handleDelete(card)
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
</div>
<div className="p-2">
<p className="font-medium text-sm truncate" title={card.original}>
{card.original}
</p>
{card.translation && (
<p className="text-xs text-muted-foreground truncate" title={card.translation}>
{card.translation}
</p>
)}
</div>
</div>
))}
</div>
</div>
))} ))}
</TableBody> </>
</Table> )}
{/* Pagination */} {/* Pagination */}
{data && data.totalPages > 1 && ( {data && data.totalPages > 1 && (