From 91b66ed81de2ff295d9b87bfb0245d2ab4eddf4c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 7 Jan 2026 00:25:06 +0300 Subject: [PATCH] admin cleanuo --- admin/src/App.tsx | 4 - admin/src/api/cards.ts | 131 ---- admin/src/api/cardsWithVoice.test.ts | 87 --- admin/src/api/cardsWithVoice.ts | 59 -- admin/src/api/client.test.ts | 4 +- admin/src/api/tests.ts | 131 ---- admin/src/api/voices.ts | 45 -- admin/src/components/BulkCardEditor.test.tsx | 95 --- admin/src/components/BulkCardEditor.tsx | 412 ----------- admin/src/components/CardEditorPreview.tsx | 358 --------- admin/src/components/CardVoicesManager.tsx | 311 -------- admin/src/components/PackCardsManager.tsx | 209 ------ admin/src/components/PackTestsManager.tsx | 214 ------ admin/src/components/layout/Layout.tsx | 6 +- admin/src/pages/CardsPage.tsx | 721 ------------------- admin/src/pages/DashboardPage.tsx | 4 +- admin/src/pages/LoginPage.tsx | 4 +- admin/src/pages/PacksPage.tsx | 66 -- admin/src/pages/TestsPage.tsx | 639 ---------------- admin/src/types/models.ts | 46 -- 20 files changed, 8 insertions(+), 3538 deletions(-) delete mode 100644 admin/src/api/cards.ts delete mode 100644 admin/src/api/cardsWithVoice.test.ts delete mode 100644 admin/src/api/cardsWithVoice.ts delete mode 100644 admin/src/api/tests.ts delete mode 100644 admin/src/api/voices.ts delete mode 100644 admin/src/components/BulkCardEditor.test.tsx delete mode 100644 admin/src/components/BulkCardEditor.tsx delete mode 100644 admin/src/components/CardEditorPreview.tsx delete mode 100644 admin/src/components/CardVoicesManager.tsx delete mode 100644 admin/src/components/PackCardsManager.tsx delete mode 100644 admin/src/components/PackTestsManager.tsx delete mode 100644 admin/src/pages/CardsPage.tsx delete mode 100644 admin/src/pages/TestsPage.tsx diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 6e3a86e..290e9bb 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -2,10 +2,8 @@ import { Routes, Route, Navigate } from 'react-router-dom' import { useAuthStore } from '@/stores/authStore' import LoginPage from '@/pages/LoginPage' import DashboardPage from '@/pages/DashboardPage' -import CardsPage from '@/pages/CardsPage' import PacksPage from '@/pages/PacksPage' import UsersPage from '@/pages/UsersPage' -import TestsPage from '@/pages/TestsPage' import Layout from '@/components/layout/Layout' import { TokenRefreshProvider } from '@/components/TokenRefreshProvider' @@ -27,10 +25,8 @@ function App() { } /> - } /> } /> } /> - } /> ) : ( diff --git a/admin/src/api/cards.ts b/admin/src/api/cards.ts deleted file mode 100644 index d4898e0..0000000 --- a/admin/src/api/cards.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { adminApiClient } from './client' -import type { GameCardDto, PaginatedResponse } from '@/types/models' -import type { AxiosError } from 'axios' - -export interface CardsApiError { - message: string - statusCode?: number - field?: string - originalError?: unknown - name: 'CardsApiError' -} - -export function createCardsApiError( - message: string, - statusCode?: number, - field?: string, - originalError?: unknown -): CardsApiError { - return { - message, - statusCode, - field, - originalError, - name: 'CardsApiError', - } -} - -export function isCardsApiError(error: unknown): error is CardsApiError { - return ( - typeof error === 'object' && - error !== null && - 'name' in error && - error.name === 'CardsApiError' - ) -} - -export const cardsApi = { - // Get all cards with pagination and search - getCards: async (params?: { - page?: number - limit?: number - search?: string - }): Promise> => { - try { - const response = await adminApiClient.get('/api/v2/admin/cards', { - params: { - page: params?.page || 1, - limit: params?.limit || 20, - search: params?.search, - }, - }) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string }> - throw createCardsApiError( - axiosError.response?.data?.message || - axiosError.response?.data?.error || - 'Failed to load cards', - axiosError.response?.status, - axiosError.response?.data?.field, - error - ) - } - }, - - // Get a specific card by ID - getCard: async (cardId: string): Promise => { - try { - const response = await adminApiClient.get(`/api/v2/admin/cards/${cardId}`) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string }> - throw createCardsApiError( - axiosError.response?.data?.message || - axiosError.response?.data?.error || - `Failed to load card ${cardId}`, - axiosError.response?.status, - undefined, - error - ) - } - }, - - // Create or update a card - upsertCard: async (card: GameCardDto): Promise<{ success: boolean; card: GameCardDto }> => { - try { - const response = await adminApiClient.post('/api/v2/admin/cards', card) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string; details?: string }> - 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 || - `Failed to ${operation}` - - let fullMessage = message - if (axiosError.response?.data?.details) { - fullMessage += `. ${axiosError.response.data.details}` - } - if (axiosError.response?.data?.field) { - fullMessage += ` (Field: ${axiosError.response.data.field})` - } - - throw createCardsApiError( - fullMessage, - axiosError.response?.status, - axiosError.response?.data?.field, - error - ) - } - }, - - // Delete a card by ID - deleteCard: async (cardId: string): Promise<{ success: boolean; message?: string }> => { - try { - const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string }> - throw createCardsApiError( - axiosError.response?.data?.message || - axiosError.response?.data?.error || - `Failed to delete card ${cardId}`, - axiosError.response?.status, - undefined, - error - ) - } - }, -} diff --git a/admin/src/api/cardsWithVoice.test.ts b/admin/src/api/cardsWithVoice.test.ts deleted file mode 100644 index bec9215..0000000 --- a/admin/src/api/cardsWithVoice.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it, vi, beforeEach } from 'vitest' -import type { GameCardDto } from '@/types/models' -import { cardsApi } from './cards' -import { voicesApi } from './voices' -import { upsertCardWithOptionalVoice } from './cardsWithVoice' - -describe('upsertCardWithOptionalVoice', () => { - beforeEach(() => { - vi.restoreAllMocks() - }) - - it('does not add voice when none provided', async () => { - vi.spyOn(cardsApi, 'upsertCard').mockResolvedValue({ - success: true, - card: { id: 'card-1' } satisfies GameCardDto, - }) - const addVoiceSpy = vi - .spyOn(voicesApi, 'addCardVoice') - .mockResolvedValue({ - success: true, - voice: { - id: 'voice-1', - cardId: 'card-1', - voiceUrl: 'BASE64', - language: 'en', - createdAt: new Date(0).toISOString(), - }, - }) - - await upsertCardWithOptionalVoice({ id: null } satisfies GameCardDto) - - expect(addVoiceSpy).not.toHaveBeenCalled() - }) - - it('adds voice after upsert when provided (uses response card id)', async () => { - vi.spyOn(cardsApi, 'upsertCard').mockResolvedValue({ - success: true, - card: { id: 'card-42' } satisfies GameCardDto, - }) - const addVoiceSpy = vi - .spyOn(voicesApi, 'addCardVoice') - .mockResolvedValue({ - success: true, - voice: { - id: 'voice-1', - cardId: 'card-42', - voiceUrl: 'BASE64', - language: 'en', - createdAt: new Date(0).toISOString(), - }, - }) - - await upsertCardWithOptionalVoice( - { id: null } satisfies GameCardDto, - { voiceUrl: 'BASE64', language: 'en' }, - ) - - expect(addVoiceSpy).toHaveBeenCalledWith('card-42', 'BASE64', 'en') - }) - - it('falls back to request card id when response card id is missing', async () => { - vi.spyOn(cardsApi, 'upsertCard').mockResolvedValue({ - success: true, - card: { id: null } satisfies GameCardDto, - }) - const addVoiceSpy = vi - .spyOn(voicesApi, 'addCardVoice') - .mockResolvedValue({ - success: true, - voice: { - id: 'voice-1', - cardId: 'card-req', - voiceUrl: 'BASE64', - language: 'es', - createdAt: new Date(0).toISOString(), - }, - }) - - await upsertCardWithOptionalVoice( - { id: 'card-req' } satisfies GameCardDto, - { voiceUrl: 'BASE64', language: 'es' }, - ) - - expect(addVoiceSpy).toHaveBeenCalledWith('card-req', 'BASE64', 'es') - }) -}) - diff --git a/admin/src/api/cardsWithVoice.ts b/admin/src/api/cardsWithVoice.ts deleted file mode 100644 index b512692..0000000 --- a/admin/src/api/cardsWithVoice.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { GameCardDto } from '@/types/models' -import { cardsApi } from './cards' -import { voicesApi } from './voices' - -export interface PendingVoiceUpload { - voiceUrl: string - language: string -} - -export async function upsertCardWithOptionalVoice( - card: GameCardDto, - voice?: PendingVoiceUpload, -): Promise<{ success: boolean; card: GameCardDto }> { - console.log('🔍 upsertCardWithOptionalVoice: Starting', { - cardId: card.id, - hasVoice: !!voice, - voiceUrl: voice?.voiceUrl, - language: voice?.language, - }) - - const response = await cardsApi.upsertCard(card) - - const responseCardId = response.card?.id != null ? String(response.card.id) : '' - const requestCardId = card.id != null ? String(card.id) : '' - const cardId = responseCardId || requestCardId - - console.log('🔍 upsertCardWithOptionalVoice: After upsertCard', { - responseCardId, - requestCardId, - cardId, - hasVoice: !!voice, - voiceUrl: voice?.voiceUrl, - }) - - if (!voice || !voice.voiceUrl || !cardId) { - console.log('⚠️ upsertCardWithOptionalVoice: Skipping voice link', { - hasVoice: !!voice, - voiceUrl: voice?.voiceUrl, - cardId, - }) - return response - } - - console.log('✅ upsertCardWithOptionalVoice: Calling addCardVoice', { - cardId, - voiceUrl: voice.voiceUrl, - language: voice.language, - }) - - try { - await voicesApi.addCardVoice(cardId, voice.voiceUrl, voice.language) - console.log('✅ upsertCardWithOptionalVoice: Successfully added voice') - } catch (error) { - console.error('❌ upsertCardWithOptionalVoice: Error adding voice', error) - throw error - } - - return response -} diff --git a/admin/src/api/client.test.ts b/admin/src/api/client.test.ts index 4d56656..d1c9506 100644 --- a/admin/src/api/client.test.ts +++ b/admin/src/api/client.test.ts @@ -11,7 +11,7 @@ describe('API Clients', () => { describe('apiClient', () => { it('should be defined and have correct base URL', () => { expect(apiClient).toBeDefined() - expect(apiClient.defaults.baseURL).toBe('https://api.mnemo-cards.online') + expect(apiClient.defaults.baseURL).toBe('https://api.party-games.online') }) it('should have interceptors configured', () => { @@ -24,7 +24,7 @@ describe('API Clients', () => { describe('adminApiClient', () => { it('should always use production API URL', () => { expect(adminApiClient).toBeDefined() - expect(adminApiClient.defaults.baseURL).toBe('https://api.mnemo-cards.online') + expect(adminApiClient.defaults.baseURL).toBe('https://api.party-games.online') }) it('should be a separate instance from apiClient', () => { diff --git a/admin/src/api/tests.ts b/admin/src/api/tests.ts deleted file mode 100644 index cf5f99e..0000000 --- a/admin/src/api/tests.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { adminApiClient } from './client' -import type { TestDto, PaginatedResponse } from '@/types/models' -import type { AxiosError } from 'axios' - -export interface TestsApiError { - message: string - statusCode?: number - field?: string - originalError?: unknown - name: 'TestsApiError' -} - -export function createTestsApiError( - message: string, - statusCode?: number, - field?: string, - originalError?: unknown -): TestsApiError { - return { - message, - statusCode, - field, - originalError, - name: 'TestsApiError', - } -} - -export function isTestsApiError(error: unknown): error is TestsApiError { - return ( - typeof error === 'object' && - error !== null && - 'name' in error && - error.name === 'TestsApiError' - ) -} - -export const testsApi = { - // Get all tests with pagination and search - getTests: async (params?: { - page?: number - limit?: number - search?: string - }): Promise> => { - try { - const response = await adminApiClient.get('/api/v2/admin/tests', { - params: { - page: params?.page || 1, - limit: params?.limit || 20, - search: params?.search, - }, - }) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string }> - throw createTestsApiError( - axiosError.response?.data?.message || - axiosError.response?.data?.error || - 'Failed to load tests', - axiosError.response?.status, - axiosError.response?.data?.field, - error - ) - } - }, - - // Get a specific test by ID - getTest: async (testId: string): Promise => { - try { - const response = await adminApiClient.get(`/api/v2/admin/tests/${testId}`) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string }> - throw createTestsApiError( - axiosError.response?.data?.message || - axiosError.response?.data?.error || - `Failed to load test ${testId}`, - axiosError.response?.status, - undefined, - error - ) - } - }, - - // Create or update a test - upsertTest: async (test: TestDto): Promise<{ success: boolean; test: TestDto }> => { - try { - const response = await adminApiClient.post('/api/v2/admin/tests', test) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string; field?: string; details?: string }> - const isUpdate = test.id && test.id.length > 0 - const operation = isUpdate ? 'update test' : 'create test' - const message = axiosError.response?.data?.message || - axiosError.response?.data?.error || - `Failed to ${operation}` - - let fullMessage = message - if (axiosError.response?.data?.details) { - fullMessage += `. ${axiosError.response.data.details}` - } - if (axiosError.response?.data?.field) { - fullMessage += ` (Field: ${axiosError.response.data.field})` - } - - throw createTestsApiError( - fullMessage, - axiosError.response?.status, - axiosError.response?.data?.field, - error - ) - } - }, - - // Delete a test by ID - deleteTest: async (testId: string): Promise<{ success: boolean; message: string }> => { - try { - const response = await adminApiClient.delete(`/api/v2/admin/tests/${testId}`) - return response.data - } catch (error) { - const axiosError = error as AxiosError<{ error?: string; message?: string }> - throw createTestsApiError( - axiosError.response?.data?.message || - axiosError.response?.data?.error || - `Failed to delete test ${testId}`, - axiosError.response?.status, - undefined, - error - ) - } - }, -} diff --git a/admin/src/api/voices.ts b/admin/src/api/voices.ts deleted file mode 100644 index e78e8a5..0000000 --- a/admin/src/api/voices.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { adminApiClient } from './client' - -export interface VoiceDto { - id: string - cardId: string - voiceUrl: string - language: string - createdAt: string -} - -export interface VoiceResponse { - items: VoiceDto[] -} - -export const voicesApi = { - // Get all voices for a card - 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: string, - voiceUrl: string, - language: string = 'en' - ): Promise<{ success: boolean; voice: VoiceDto }> => { - const response = await adminApiClient.post(`/api/v2/admin/cards/${cardId}/voices`, { - voiceUrl, - language, - }) - return response.data - }, - - // Remove a voice from a card - removeCardVoice: async ( - cardId: string, - voiceId: string - ): Promise<{ success: boolean; message?: string }> => { - const response = await adminApiClient.delete( - `/api/v2/admin/cards/${cardId}/voices/${voiceId}` - ) - return response.data - }, -} \ No newline at end of file diff --git a/admin/src/components/BulkCardEditor.test.tsx b/admin/src/components/BulkCardEditor.test.tsx deleted file mode 100644 index cbf46e0..0000000 --- a/admin/src/components/BulkCardEditor.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -import { BulkCardEditor } from '@/components/BulkCardEditor' - -vi.mock('@/api/packs', () => { - return { - packsApi: { - getPacks: vi.fn(), - }, - isPacksApiError: () => false, - } -}) - -import { packsApi } from '@/api/packs' - -function renderWithQuery(ui: React.ReactElement) { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }) - - return render( - {ui}, - ) -} - -describe('BulkCardEditor', () => { - it('loads packs and shows their titles in the pack select', async () => { - vi.mocked(packsApi.getPacks).mockResolvedValue({ - items: [ - { - id: 'pack-1', - title: 'Pack One', - cards: 10, - enabled: true, - order: 0, - }, - { - id: 'pack-empty-title', - title: ' ', - cards: 0, - enabled: true, - order: 1, - }, - ], - total: 2, - page: 1, - limit: 100, - totalPages: 1, - }) - - const images = [ - { - id: 'img-1', - file: new File(['x'], 'img-1.png', { type: 'image/png' }), - preview: 'data:image/png;base64,AA==', - objectId: '550e8400-e29b-41d4-a716-446655440000', - }, - ] - - renderWithQuery( - , - ) - - await waitFor(() => { - expect(packsApi.getPacks).toHaveBeenCalledWith({ - page: 1, - limit: 100, - search: '', - }) - }) - - const placeholder = screen.getByText('Select a pack (optional)') - const trigger = placeholder.closest('button') - expect(trigger).toBeTruthy() - - trigger!.focus() - fireEvent.keyDown(trigger!, { key: 'ArrowDown' }) - - await waitFor(() => { - expect(screen.getByText('None')).toBeInTheDocument() - expect(screen.getByText('Pack One')).toBeInTheDocument() - expect(screen.getByText('pack-empty-title')).toBeInTheDocument() - }) - }) -}) diff --git a/admin/src/components/BulkCardEditor.tsx b/admin/src/components/BulkCardEditor.tsx deleted file mode 100644 index 1aea1b7..0000000 --- a/admin/src/components/BulkCardEditor.tsx +++ /dev/null @@ -1,412 +0,0 @@ -import { useState, useEffect } from 'react' -import { useMutation, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' -import { cardsApi, isCardsApiError } from '@/api/cards' -import { packsApi, isPacksApiError } from '@/api/packs' -import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils' -import type { GameCardDto } from '@/types/models' -import { Button } from './ui/button' -import { Input } from './ui/input' -import { Label } from './ui/label' -import { Textarea } from './ui/textarea' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card' -import { Badge } from './ui/badge' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select' -import { ChevronLeft, ChevronRight, Save, X } from 'lucide-react' - -interface UploadedImage { - id: string - file: File - preview: string - objectId: string // Object ID in MinIO (UUID) - presignedUrl?: string // Presigned URL for preview -} - -interface CardData { - imageId: string - packId: string - original: string - translation: string - mnemo: string - transcription: string - transcriptionMnemo: string - back: string - imageBack?: string - isSaved: boolean - cardId?: string -} - -interface BulkCardEditorProps { - images: UploadedImage[] - defaultPackId?: string - onComplete: () => void - onCancel: () => void -} - -export function BulkCardEditor({ images, defaultPackId, onComplete, onCancel }: BulkCardEditorProps) { - const queryClient = useQueryClient() - const [currentIndex, setCurrentIndex] = useState(0) - const [cardsData, setCardsData] = useState>(() => { - const initialData = new Map() - if (images.length > 0) { - images.forEach((image) => { - initialData.set(image.id, { - imageId: image.id, - packId: defaultPackId || '', - original: '', - translation: '', - mnemo: '', - transcription: '', - transcriptionMnemo: '', - back: '', - imageBack: undefined, - isSaved: false, - }) - }) - } - return initialData - }) - const [packsData, setPacksData] = useState<{ id: string; title: string }[]>([]) - - // Load packs - useEffect(() => { - packsApi - .getPacks({ page: 1, limit: 100, search: '' }) - .then((response) => { - try { - if (response && response.items && Array.isArray(response.items)) { - const packs = response.items.map((pack) => { - const id = String(pack.id) - const title = pack.title?.trim() ? String(pack.title) : id - return { id, title } - }) - setPacksData(packs) - } else { - console.warn('Unexpected packs response format:', response) - setPacksData([]) - } - } catch (error) { - console.error('Error processing packs response:', error) - setPacksData([]) - } - }) - .catch((error) => { - const errorMessage = isPacksApiError(error) - ? error.message - : formatApiError(error) - console.error('Failed to load packs:', error) - toast.error(`Failed to load packs: ${errorMessage}`) - // Initialize with empty array so component doesn't crash - setPacksData([]) - }) - }, []) - - const currentImage = images[currentIndex] - const currentCard = currentImage ? cardsData.get(currentImage.id) : undefined - - const updateMutation = useMutation({ - mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card), - onSuccess: (response) => { - queryClient.invalidateQueries({ queryKey: ['cards'] }) - if (currentImage) { - const imageId = currentImage.id - setCardsData((prev) => { - const newData = new Map(prev) - const cardData = newData.get(imageId) - if (cardData && response.card) { - newData.set(imageId, { - ...cardData, - isSaved: true, - cardId: response.card.id || undefined, - }) - } - return newData - }) - } - toast.success('Card saved successfully') - }, - onError: (error: unknown) => { - const errorMessage = isCardsApiError(error) - ? error.message - : getDetailedErrorMessage(error, 'save', 'card') - toast.error(errorMessage) - console.error('Error saving card:', error) - }, - }) - - const handleFieldChange = (field: keyof CardData, value: string) => { - if (!currentCard || !currentImage) return - - setCardsData((prev) => { - const newData = new Map(prev) - const cardData = newData.get(currentImage.id) - if (cardData) { - newData.set(currentImage.id, { - ...cardData, - [field]: value, - }) - } - return newData - }) - } - - const handleSave = () => { - if (!currentCard) return - - if (!currentCard.original.trim() || !currentCard.translation.trim() || !currentCard.mnemo.trim()) { - toast.error('Original, translation and mnemo are required') - return - } - - const image = images.find((img) => img.id === currentCard.imageId) - if (!image) return - - const cardData: GameCardDto = { - id: currentCard.cardId || null, - packId: currentCard.packId || undefined, - original: currentCard.original.trim(), - translation: currentCard.translation.trim(), - mnemo: currentCard.mnemo.trim(), - transcription: currentCard.transcription.trim() || undefined, - transcriptionMnemo: currentCard.transcriptionMnemo.trim() || undefined, - back: currentCard.back.trim() || undefined, - image: image.objectId, // Use objectId instead of base64 - imageBack: currentCard.imageBack || undefined, - } - - updateMutation.mutate(cardData) - } - - const handleNext = () => { - if (currentIndex < images.length - 1) { - setCurrentIndex(currentIndex + 1) - } - } - - const handlePrevious = () => { - if (currentIndex > 0) { - setCurrentIndex(currentIndex - 1) - } - } - - const savedCount = Array.from(cardsData.values()).filter((card) => card.isSaved).length - const totalCount = images.length - - // Wait for images and cards data to be initialized - if (images.length === 0) { - return ( -
-

No images uploaded

-
- ) - } - - if (!currentImage || !currentCard) { - return ( -
-

Initializing card data...

-
- ) - } - - return ( -
-
-
-

Fill Card Details

-

- Card {currentIndex + 1} of {totalCount} • {savedCount} saved -

-
-
- - {currentCard.isSaved ? 'Saved' : 'Not Saved'} - - -
-
- -
- {/* Image Preview */} - - - Image Preview - {currentImage.file?.name || 'Unknown'} - - -
- {currentImage.presignedUrl || currentImage.preview ? ( - {currentImage.file?.name { - console.error('Failed to load image:', currentImage.presignedUrl || currentImage.preview) - e.currentTarget.style.display = 'none' - }} - /> - ) : ( -
- Image preview not available -
- )} -
-
-
- - {/* Form */} - - - Card Information - Fill in the details for this card - - -
- - -
- -
-
- - handleFieldChange('original', e.target.value)} - placeholder="e.g. cerdo" - /> -
-
- - handleFieldChange('translation', e.target.value)} - placeholder="e.g. pig" - /> -
-
- -
- - handleFieldChange('mnemo', e.target.value)} - placeholder="e.g. [pig] with heart" - /> -
- -
-
- - handleFieldChange('transcription', e.target.value)} - placeholder="e.g. sɛrdo" - /> -
-
- - handleFieldChange('transcriptionMnemo', e.target.value)} - placeholder="e.g. pig with heart" - /> -
-
- -
- -