image
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
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
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
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
498f542a7e
commit
7d1300a7a4
9 changed files with 91 additions and 30 deletions
|
|
@ -64,7 +64,7 @@ export const cardsApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get a specific card by ID
|
// Get a specific card by ID
|
||||||
getCard: async (cardId: number): Promise<GameCardDto> => {
|
getCard: async (cardId: string): Promise<GameCardDto> => {
|
||||||
try {
|
try {
|
||||||
const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}`)
|
const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}`)
|
||||||
return response.data
|
return response.data
|
||||||
|
|
@ -88,7 +88,7 @@ export const cardsApi = {
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string; details?: string }>
|
const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string; details?: string }>
|
||||||
const isUpdate = card.id && card.id > 0
|
const isUpdate = card.id !== null && card.id !== undefined && card.id !== ''
|
||||||
const operation = isUpdate ? 'update card' : 'create card'
|
const operation = isUpdate ? 'update card' : 'create card'
|
||||||
const message = axiosError.response?.data?.message ||
|
const message = axiosError.response?.data?.message ||
|
||||||
axiosError.response?.data?.error ||
|
axiosError.response?.data?.error ||
|
||||||
|
|
@ -112,7 +112,7 @@ export const cardsApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Delete a card by ID
|
// Delete a card by ID
|
||||||
deleteCard: async (cardId: number): Promise<{ success: boolean; message: string }> => {
|
deleteCard: async (cardId: string): Promise<{ success: boolean; message: string }> => {
|
||||||
try {
|
try {
|
||||||
const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`)
|
const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`)
|
||||||
return response.data
|
return response.data
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ export interface VoiceResponse {
|
||||||
|
|
||||||
export const voicesApi = {
|
export const voicesApi = {
|
||||||
// Get all voices for a card
|
// Get all voices for a card
|
||||||
getCardVoices: async (cardId: number | string): Promise<VoiceResponse> => {
|
getCardVoices: async (cardId: string): Promise<VoiceResponse> => {
|
||||||
const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}/voices`)
|
const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}/voices`)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
// Add a voice to a card
|
// Add a voice to a card
|
||||||
addCardVoice: async (
|
addCardVoice: async (
|
||||||
cardId: number | string,
|
cardId: string,
|
||||||
voiceUrl: string,
|
voiceUrl: string,
|
||||||
language: string = 'en'
|
language: string = 'en'
|
||||||
): Promise<{ success: boolean; voice: VoiceDto }> => {
|
): Promise<{ success: boolean; voice: VoiceDto }> => {
|
||||||
|
|
@ -34,7 +34,7 @@ export const voicesApi = {
|
||||||
|
|
||||||
// Remove a voice from a card
|
// Remove a voice from a card
|
||||||
removeCardVoice: async (
|
removeCardVoice: async (
|
||||||
cardId: number | string,
|
cardId: string,
|
||||||
voiceId: string
|
voiceId: string
|
||||||
): Promise<{ success: boolean }> => {
|
): Promise<{ success: boolean }> => {
|
||||||
const response = await adminApiClient.delete(
|
const response = await adminApiClient.delete(
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,17 @@ interface CardData {
|
||||||
back: string
|
back: string
|
||||||
imageBack?: string
|
imageBack?: string
|
||||||
isSaved: boolean
|
isSaved: boolean
|
||||||
cardId?: number
|
cardId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BulkCardEditorProps {
|
interface BulkCardEditorProps {
|
||||||
images: UploadedImage[]
|
images: UploadedImage[]
|
||||||
|
defaultPackId?: string
|
||||||
onComplete: () => void
|
onComplete: () => void
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorProps) {
|
export function BulkCardEditor({ images, defaultPackId, onComplete, onCancel }: BulkCardEditorProps) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [currentIndex, setCurrentIndex] = useState(0)
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
const [cardsData, setCardsData] = useState<Map<string, CardData>>(new Map())
|
const [cardsData, setCardsData] = useState<Map<string, CardData>>(new Map())
|
||||||
|
|
@ -86,7 +87,7 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP
|
||||||
images.forEach((image) => {
|
images.forEach((image) => {
|
||||||
initialData.set(image.id, {
|
initialData.set(image.id, {
|
||||||
imageId: image.id,
|
imageId: image.id,
|
||||||
packId: '',
|
packId: defaultPackId || '',
|
||||||
original: '',
|
original: '',
|
||||||
translation: '',
|
translation: '',
|
||||||
mnemo: '',
|
mnemo: '',
|
||||||
|
|
@ -99,7 +100,7 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP
|
||||||
})
|
})
|
||||||
setCardsData(initialData)
|
setCardsData(initialData)
|
||||||
}
|
}
|
||||||
}, [images])
|
}, [images, defaultPackId])
|
||||||
|
|
||||||
const currentImage = images[currentIndex]
|
const currentImage = images[currentIndex]
|
||||||
const currentCard = currentImage ? cardsData.get(currentImage.id) : undefined
|
const currentCard = currentImage ? cardsData.get(currentImage.id) : undefined
|
||||||
|
|
@ -162,7 +163,7 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP
|
||||||
if (!image) return
|
if (!image) return
|
||||||
|
|
||||||
const cardData: GameCardDto = {
|
const cardData: GameCardDto = {
|
||||||
id: currentCard.cardId || -1,
|
id: currentCard.cardId || null,
|
||||||
packId: currentCard.packId || undefined,
|
packId: currentCard.packId || undefined,
|
||||||
original: currentCard.original.trim(),
|
original: currentCard.original.trim(),
|
||||||
translation: currentCard.translation.trim(),
|
translation: currentCard.translation.trim(),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { useState, useRef } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Button } from './ui/button'
|
import { Button } from './ui/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'
|
||||||
|
import { Label } from './ui/label'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
|
||||||
import { Upload, X, Check } from 'lucide-react'
|
import { Upload, X, Check } from 'lucide-react'
|
||||||
|
import { packsApi } from '@/api/packs'
|
||||||
|
|
||||||
interface UploadedImage {
|
interface UploadedImage {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -11,7 +15,7 @@ interface UploadedImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BulkCardUploadProps {
|
interface BulkCardUploadProps {
|
||||||
onImagesUploaded: (images: UploadedImage[]) => void
|
onImagesUploaded: (images: UploadedImage[], packId?: string) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +23,13 @@ export function BulkCardUpload({ onImagesUploaded, onClose }: BulkCardUploadProp
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([])
|
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([])
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const [selectedPackId, setSelectedPackId] = useState<string>('')
|
||||||
|
|
||||||
|
// Fetch packs for pack selection
|
||||||
|
const { data: packsData } = useQuery({
|
||||||
|
queryKey: ['packs', 1, 100, ''],
|
||||||
|
queryFn: () => packsApi.getPacks({ page: 1, limit: 100, search: '' }),
|
||||||
|
})
|
||||||
|
|
||||||
const handleFileSelect = async (files: FileList) => {
|
const handleFileSelect = async (files: FileList) => {
|
||||||
const newImages: UploadedImage[] = []
|
const newImages: UploadedImage[] = []
|
||||||
|
|
@ -106,7 +117,7 @@ export function BulkCardUpload({ onImagesUploaded, onClose }: BulkCardUploadProp
|
||||||
|
|
||||||
const handleContinue = () => {
|
const handleContinue = () => {
|
||||||
if (uploadedImages.length > 0) {
|
if (uploadedImages.length > 0) {
|
||||||
onImagesUploaded(uploadedImages)
|
onImagesUploaded(uploadedImages, selectedPackId || undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +144,35 @@ export function BulkCardUpload({ onImagesUploaded, onClose }: BulkCardUploadProp
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<div className="space-y-4 mb-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="packId">Pack (optional)</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedPackId || undefined}
|
||||||
|
onValueChange={(value: string) => {
|
||||||
|
const packId = value === '__none__' ? '' : value
|
||||||
|
setSelectedPackId(packId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a pack (optional)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">None</SelectItem>
|
||||||
|
{packsData?.items && packsData.items.length > 0 ? (
|
||||||
|
packsData.items.map((pack) => (
|
||||||
|
<SelectItem key={pack.id} value={pack.id}>
|
||||||
|
{pack.title || pack.id}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : null}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Selected pack will be applied to all uploaded cards
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
|
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
|
||||||
isDragging
|
isDragging
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { X, Plus, Music, Play, Pause } from 'lucide-react'
|
||||||
import type { AxiosError } from 'axios'
|
import type { AxiosError } from 'axios'
|
||||||
|
|
||||||
interface CardVoicesManagerProps {
|
interface CardVoicesManagerProps {
|
||||||
cardId: number
|
cardId: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,14 +163,14 @@ export function PackCardsManager({
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
filteredCards.map((card) => {
|
filteredCards.map((card) => {
|
||||||
const cardIdStr = String(card.id)
|
const cardIdStr = card.id ? String(card.id) : `temp-${card.original}`
|
||||||
const selected = isCardSelected(cardIdStr)
|
const selected = isCardSelected(cardIdStr)
|
||||||
const inCurrentPack = isCardInCurrentPack(cardIdStr)
|
const inCurrentPack = isCardInCurrentPack(cardIdStr)
|
||||||
const newlyRemoved = removedCards.has(cardIdStr)
|
const newlyRemoved = removedCards.has(cardIdStr)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={card.id}
|
key={card.id || `temp-${card.original}`}
|
||||||
className={selected ? 'bg-muted/50' : ''}
|
className={selected ? 'bg-muted/50' : ''}
|
||||||
onClick={() => handleToggleCard(cardIdStr)}
|
onClick={() => handleToggleCard(cardIdStr)}
|
||||||
>
|
>
|
||||||
|
|
@ -181,7 +181,7 @@ export function PackCardsManager({
|
||||||
<div className="h-4 w-4 border rounded" />
|
<div className="h-4 w-4 border rounded" />
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{card.id}</TableCell>
|
<TableCell className="font-mono text-sm">{card.id || '-'}</TableCell>
|
||||||
<TableCell className="font-medium">{card.original}</TableCell>
|
<TableCell className="font-medium">{card.original}</TableCell>
|
||||||
<TableCell>{card.translation}</TableCell>
|
<TableCell>{card.translation}</TableCell>
|
||||||
<TableCell className="max-w-xs truncate">{card.mnemo}</TableCell>
|
<TableCell className="max-w-xs truncate">{card.mnemo}</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export default function CardsPage() {
|
||||||
preview: string
|
preview: string
|
||||||
base64: string
|
base64: string
|
||||||
}>>([])
|
}>>([])
|
||||||
|
const [bulkUploadPackId, setBulkUploadPackId] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
|
@ -94,10 +95,14 @@ export default function CardsPage() {
|
||||||
// Mutations
|
// Mutations
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card),
|
mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card),
|
||||||
onSuccess: () => {
|
onSuccess: (response) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
||||||
toast.success('Card created successfully')
|
toast.success('Card created successfully')
|
||||||
closeDialog()
|
closeDialog()
|
||||||
|
// Update selectedCard with the new ID from response
|
||||||
|
if (response?.card?.id) {
|
||||||
|
setSelectedCard(response.card)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
const errorMessage = isCardsApiError(error)
|
const errorMessage = isCardsApiError(error)
|
||||||
|
|
@ -110,10 +115,14 @@ export default function CardsPage() {
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card),
|
mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card),
|
||||||
onSuccess: () => {
|
onSuccess: (response) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
||||||
toast.success('Card updated successfully')
|
toast.success('Card updated successfully')
|
||||||
closeDialog()
|
closeDialog()
|
||||||
|
// Update selectedCard with the updated card from response
|
||||||
|
if (response?.card) {
|
||||||
|
setSelectedCard(response.card)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
const errorMessage = isCardsApiError(error)
|
const errorMessage = isCardsApiError(error)
|
||||||
|
|
@ -125,7 +134,7 @@ export default function CardsPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (cardId: number) => cardsApi.deleteCard(cardId),
|
mutationFn: (cardId: string) => cardsApi.deleteCard(cardId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
||||||
toast.success('Card deleted successfully')
|
toast.success('Card deleted successfully')
|
||||||
|
|
@ -192,8 +201,11 @@ export default function CardsPage() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if this is an update or create
|
||||||
|
const isUpdate = selectedCard !== null && selectedCard.id !== undefined && selectedCard.id !== null && selectedCard.id !== ''
|
||||||
|
|
||||||
const cardData: GameCardDto = {
|
const cardData: GameCardDto = {
|
||||||
id: selectedCard?.id || -1,
|
id: isUpdate ? selectedCard.id : null,
|
||||||
packId: formData.packId.trim() || undefined,
|
packId: formData.packId.trim() || undefined,
|
||||||
original: formData.original.trim(),
|
original: formData.original.trim(),
|
||||||
translation: formData.translation.trim(),
|
translation: formData.translation.trim(),
|
||||||
|
|
@ -205,7 +217,7 @@ export default function CardsPage() {
|
||||||
imageBack: formData.imageBack || undefined,
|
imageBack: formData.imageBack || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCard) {
|
if (isUpdate) {
|
||||||
updateMutation.mutate(cardData)
|
updateMutation.mutate(cardData)
|
||||||
} else {
|
} else {
|
||||||
createMutation.mutate(cardData)
|
createMutation.mutate(cardData)
|
||||||
|
|
@ -384,8 +396,8 @@ export default function CardsPage() {
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<TableRow key={card.id}>
|
<TableRow key={card.id || `temp-${card.original}`}>
|
||||||
<TableCell>{card.id}</TableCell>
|
<TableCell>{card.id || '-'}</TableCell>
|
||||||
<TableCell className="font-medium">{card.original}</TableCell>
|
<TableCell className="font-medium">{card.original}</TableCell>
|
||||||
<TableCell>{card.translation}</TableCell>
|
<TableCell>{card.translation}</TableCell>
|
||||||
<TableCell className="max-w-xs truncate">{card.mnemo}</TableCell>
|
<TableCell className="max-w-xs truncate">{card.mnemo}</TableCell>
|
||||||
|
|
@ -429,7 +441,7 @@ export default function CardsPage() {
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
<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) => (
|
{cards.map((card) => (
|
||||||
<div
|
<div
|
||||||
key={card.id}
|
key={card.id || `temp-${card.original}`}
|
||||||
className="border rounded-lg overflow-hidden hover:shadow-md transition-shadow cursor-pointer group"
|
className="border rounded-lg overflow-hidden hover:shadow-md transition-shadow cursor-pointer group"
|
||||||
onClick={() => openEditDialog(card)}
|
onClick={() => openEditDialog(card)}
|
||||||
>
|
>
|
||||||
|
|
@ -654,7 +666,7 @@ export default function CardsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedCard && (
|
{selectedCard && selectedCard.id && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<CardVoicesManager
|
<CardVoicesManager
|
||||||
cardId={selectedCard.id}
|
cardId={selectedCard.id}
|
||||||
|
|
@ -702,14 +714,16 @@ export default function CardsPage() {
|
||||||
<Dialog open={isBulkUploadOpen} onOpenChange={setIsBulkUploadOpen}>
|
<Dialog open={isBulkUploadOpen} onOpenChange={setIsBulkUploadOpen}>
|
||||||
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
||||||
<BulkCardUpload
|
<BulkCardUpload
|
||||||
onImagesUploaded={(images) => {
|
onImagesUploaded={(images, packId) => {
|
||||||
setUploadedImages(images)
|
setUploadedImages(images)
|
||||||
|
setBulkUploadPackId(packId)
|
||||||
setIsBulkUploadOpen(false)
|
setIsBulkUploadOpen(false)
|
||||||
setIsBulkEditorOpen(true)
|
setIsBulkEditorOpen(true)
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsBulkUploadOpen(false)
|
setIsBulkUploadOpen(false)
|
||||||
setUploadedImages([])
|
setUploadedImages([])
|
||||||
|
setBulkUploadPackId(undefined)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
@ -722,15 +736,18 @@ export default function CardsPage() {
|
||||||
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] overflow-y-auto">
|
||||||
<BulkCardEditor
|
<BulkCardEditor
|
||||||
images={uploadedImages}
|
images={uploadedImages}
|
||||||
|
defaultPackId={bulkUploadPackId}
|
||||||
onComplete={() => {
|
onComplete={() => {
|
||||||
setIsBulkEditorOpen(false)
|
setIsBulkEditorOpen(false)
|
||||||
setUploadedImages([])
|
setUploadedImages([])
|
||||||
|
setBulkUploadPackId(undefined)
|
||||||
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
queryClient.invalidateQueries({ queryKey: ['cards'] })
|
||||||
toast.success('All cards saved successfully!')
|
toast.success('All cards saved successfully!')
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setIsBulkEditorOpen(false)
|
setIsBulkEditorOpen(false)
|
||||||
setUploadedImages([])
|
setUploadedImages([])
|
||||||
|
setBulkUploadPackId(undefined)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Types based on backend DTOs
|
// Types based on backend DTOs
|
||||||
|
|
||||||
export interface GameCardDto {
|
export interface GameCardDto {
|
||||||
id: number
|
id: string | null
|
||||||
packId?: string
|
packId?: string
|
||||||
image?: string
|
image?: string
|
||||||
mnemo?: string
|
mnemo?: string
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,10 @@ class AdminCardsApiV2 {
|
||||||
|
|
||||||
// Validate required fields for new cards
|
// Validate required fields for new cards
|
||||||
final cardIdParam = data['id'];
|
final cardIdParam = data['id'];
|
||||||
final isUpdate = cardIdParam != null && cardIdParam != -1;
|
// Check if this is an update: id must be present, not null, not empty string, and not -1
|
||||||
|
// Handle both null and string 'null' cases
|
||||||
|
final isUpdate = cardIdParam != null
|
||||||
|
&& cardIdParam.toString().trim().isNotEmpty;
|
||||||
|
|
||||||
if (!isUpdate) {
|
if (!isUpdate) {
|
||||||
// Validate required fields for new cards
|
// Validate required fields for new cards
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue