From 7d1300a7a42184279251253a7766b645ddde3502 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 17 Dec 2025 02:08:37 +0300 Subject: [PATCH] image --- mnemo_cards_admin/src/api/cards.ts | 6 +-- mnemo_cards_admin/src/api/voices.ts | 6 +-- .../src/components/BulkCardEditor.tsx | 11 +++-- .../src/components/BulkCardUpload.tsx | 46 +++++++++++++++++-- .../src/components/CardVoicesManager.tsx | 2 +- .../src/components/PackCardsManager.tsx | 6 +-- mnemo_cards_admin/src/pages/CardsPage.tsx | 37 +++++++++++---- mnemo_cards_admin/src/types/models.ts | 2 +- .../lib/api/v2/admin_cards_api_v2.dart | 5 +- 9 files changed, 91 insertions(+), 30 deletions(-) diff --git a/mnemo_cards_admin/src/api/cards.ts b/mnemo_cards_admin/src/api/cards.ts index 4f55223..cf993eb 100644 --- a/mnemo_cards_admin/src/api/cards.ts +++ b/mnemo_cards_admin/src/api/cards.ts @@ -64,7 +64,7 @@ export const cardsApi = { }, // Get a specific card by ID - getCard: async (cardId: number): Promise => { + getCard: async (cardId: string): Promise => { try { const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}`) return response.data @@ -88,7 +88,7 @@ export const cardsApi = { return response.data } catch (error) { 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 message = axiosError.response?.data?.message || axiosError.response?.data?.error || @@ -112,7 +112,7 @@ export const cardsApi = { }, // Delete a card by ID - deleteCard: async (cardId: number): Promise<{ success: boolean; message: string }> => { + deleteCard: async (cardId: string): Promise<{ success: boolean; message: string }> => { try { const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`) return response.data diff --git a/mnemo_cards_admin/src/api/voices.ts b/mnemo_cards_admin/src/api/voices.ts index aebb423..e215331 100644 --- a/mnemo_cards_admin/src/api/voices.ts +++ b/mnemo_cards_admin/src/api/voices.ts @@ -14,14 +14,14 @@ export interface VoiceResponse { export const voicesApi = { // Get all voices for a card - getCardVoices: async (cardId: number | string): Promise => { + getCardVoices: async (cardId: string): Promise => { const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}/voices`) return response.data }, // Add a voice to a card addCardVoice: async ( - cardId: number | string, + cardId: string, voiceUrl: string, language: string = 'en' ): Promise<{ success: boolean; voice: VoiceDto }> => { @@ -34,7 +34,7 @@ export const voicesApi = { // Remove a voice from a card removeCardVoice: async ( - cardId: number | string, + cardId: string, voiceId: string ): Promise<{ success: boolean }> => { const response = await adminApiClient.delete( diff --git a/mnemo_cards_admin/src/components/BulkCardEditor.tsx b/mnemo_cards_admin/src/components/BulkCardEditor.tsx index 76b5be6..6a6ca32 100644 --- a/mnemo_cards_admin/src/components/BulkCardEditor.tsx +++ b/mnemo_cards_admin/src/components/BulkCardEditor.tsx @@ -32,16 +32,17 @@ interface CardData { back: string imageBack?: string isSaved: boolean - cardId?: number + cardId?: string } interface BulkCardEditorProps { images: UploadedImage[] + defaultPackId?: string onComplete: () => void onCancel: () => void } -export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorProps) { +export function BulkCardEditor({ images, defaultPackId, onComplete, onCancel }: BulkCardEditorProps) { const queryClient = useQueryClient() const [currentIndex, setCurrentIndex] = useState(0) const [cardsData, setCardsData] = useState>(new Map()) @@ -86,7 +87,7 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP images.forEach((image) => { initialData.set(image.id, { imageId: image.id, - packId: '', + packId: defaultPackId || '', original: '', translation: '', mnemo: '', @@ -99,7 +100,7 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP }) setCardsData(initialData) } - }, [images]) + }, [images, defaultPackId]) const currentImage = images[currentIndex] const currentCard = currentImage ? cardsData.get(currentImage.id) : undefined @@ -162,7 +163,7 @@ export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorP if (!image) return const cardData: GameCardDto = { - id: currentCard.cardId || -1, + id: currentCard.cardId || null, packId: currentCard.packId || undefined, original: currentCard.original.trim(), translation: currentCard.translation.trim(), diff --git a/mnemo_cards_admin/src/components/BulkCardUpload.tsx b/mnemo_cards_admin/src/components/BulkCardUpload.tsx index 9fd6ae2..e2fa1a2 100644 --- a/mnemo_cards_admin/src/components/BulkCardUpload.tsx +++ b/mnemo_cards_admin/src/components/BulkCardUpload.tsx @@ -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 { 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 { packsApi } from '@/api/packs' interface UploadedImage { id: string @@ -11,7 +15,7 @@ interface UploadedImage { } interface BulkCardUploadProps { - onImagesUploaded: (images: UploadedImage[]) => void + onImagesUploaded: (images: UploadedImage[], packId?: string) => void onClose: () => void } @@ -19,6 +23,13 @@ export function BulkCardUpload({ onImagesUploaded, onClose }: BulkCardUploadProp const fileInputRef = useRef(null) const [uploadedImages, setUploadedImages] = useState([]) const [isDragging, setIsDragging] = useState(false) + const [selectedPackId, setSelectedPackId] = useState('') + + // 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 newImages: UploadedImage[] = [] @@ -106,7 +117,7 @@ export function BulkCardUpload({ onImagesUploaded, onClose }: BulkCardUploadProp const handleContinue = () => { if (uploadedImages.length > 0) { - onImagesUploaded(uploadedImages) + onImagesUploaded(uploadedImages, selectedPackId || undefined) } } @@ -133,6 +144,35 @@ export function BulkCardUpload({ onImagesUploaded, onClose }: BulkCardUploadProp +
+
+ + +

+ Selected pack will be applied to all uploaded cards +

+
+
) : ( filteredCards.map((card) => { - const cardIdStr = String(card.id) + const cardIdStr = card.id ? String(card.id) : `temp-${card.original}` const selected = isCardSelected(cardIdStr) const inCurrentPack = isCardInCurrentPack(cardIdStr) const newlyRemoved = removedCards.has(cardIdStr) return ( handleToggleCard(cardIdStr)} > @@ -181,7 +181,7 @@ export function PackCardsManager({
)} - {card.id} + {card.id || '-'} {card.original} {card.translation} {card.mnemo} diff --git a/mnemo_cards_admin/src/pages/CardsPage.tsx b/mnemo_cards_admin/src/pages/CardsPage.tsx index 03fda69..30d31b3 100644 --- a/mnemo_cards_admin/src/pages/CardsPage.tsx +++ b/mnemo_cards_admin/src/pages/CardsPage.tsx @@ -62,6 +62,7 @@ export default function CardsPage() { preview: string base64: string }>>([]) + const [bulkUploadPackId, setBulkUploadPackId] = useState(undefined) // Form state const [formData, setFormData] = useState({ @@ -94,10 +95,14 @@ export default function CardsPage() { // Mutations const createMutation = useMutation({ mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card), - onSuccess: () => { + onSuccess: (response) => { queryClient.invalidateQueries({ queryKey: ['cards'] }) toast.success('Card created successfully') closeDialog() + // Update selectedCard with the new ID from response + if (response?.card?.id) { + setSelectedCard(response.card) + } }, onError: (error: unknown) => { const errorMessage = isCardsApiError(error) @@ -110,10 +115,14 @@ export default function CardsPage() { const updateMutation = useMutation({ mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card), - onSuccess: () => { + onSuccess: (response) => { queryClient.invalidateQueries({ queryKey: ['cards'] }) toast.success('Card updated successfully') closeDialog() + // Update selectedCard with the updated card from response + if (response?.card) { + setSelectedCard(response.card) + } }, onError: (error: unknown) => { const errorMessage = isCardsApiError(error) @@ -125,7 +134,7 @@ export default function CardsPage() { }) const deleteMutation = useMutation({ - mutationFn: (cardId: number) => cardsApi.deleteCard(cardId), + mutationFn: (cardId: string) => cardsApi.deleteCard(cardId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cards'] }) toast.success('Card deleted successfully') @@ -192,8 +201,11 @@ export default function CardsPage() { return } + // Determine if this is an update or create + const isUpdate = selectedCard !== null && selectedCard.id !== undefined && selectedCard.id !== null && selectedCard.id !== '' + const cardData: GameCardDto = { - id: selectedCard?.id || -1, + id: isUpdate ? selectedCard.id : null, packId: formData.packId.trim() || undefined, original: formData.original.trim(), translation: formData.translation.trim(), @@ -205,7 +217,7 @@ export default function CardsPage() { imageBack: formData.imageBack || undefined, } - if (selectedCard) { + if (isUpdate) { updateMutation.mutate(cardData) } else { createMutation.mutate(cardData) @@ -384,8 +396,8 @@ export default function CardsPage() { {cards.map((card) => ( - - {card.id} + + {card.id || '-'} {card.original} {card.translation} {card.mnemo} @@ -429,7 +441,7 @@ export default function CardsPage() {
{cards.map((card) => (
openEditDialog(card)} > @@ -654,7 +666,7 @@ export default function CardsPage() {
- {selectedCard && ( + {selectedCard && selectedCard.id && (
{ + onImagesUploaded={(images, packId) => { setUploadedImages(images) + setBulkUploadPackId(packId) setIsBulkUploadOpen(false) setIsBulkEditorOpen(true) }} onClose={() => { setIsBulkUploadOpen(false) setUploadedImages([]) + setBulkUploadPackId(undefined) }} /> @@ -722,15 +736,18 @@ export default function CardsPage() { { setIsBulkEditorOpen(false) setUploadedImages([]) + setBulkUploadPackId(undefined) queryClient.invalidateQueries({ queryKey: ['cards'] }) toast.success('All cards saved successfully!') }} onCancel={() => { setIsBulkEditorOpen(false) setUploadedImages([]) + setBulkUploadPackId(undefined) }} /> diff --git a/mnemo_cards_admin/src/types/models.ts b/mnemo_cards_admin/src/types/models.ts index 4a2fdfa..c1cd291 100644 --- a/mnemo_cards_admin/src/types/models.ts +++ b/mnemo_cards_admin/src/types/models.ts @@ -1,7 +1,7 @@ // Types based on backend DTOs export interface GameCardDto { - id: number + id: string | null packId?: string image?: string mnemo?: string diff --git a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart index 6bfe9aa..a05629d 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart @@ -198,7 +198,10 @@ class AdminCardsApiV2 { // Validate required fields for new cards 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) { // Validate required fields for new cards