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
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:
parent
5a8c55c1ef
commit
2adffbf235
1 changed files with 167 additions and 41 deletions
|
|
@ -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 && (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue