From 889784e11fee34f1b8729e1959538b1a4c999f3b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 14 Dec 2025 22:21:36 +0300 Subject: [PATCH] stuff --- .../src/components/BulkCardEditor.tsx | 356 +++++++ .../src/components/BulkCardUpload.tsx | 248 +++++ mnemo_cards_admin/src/pages/CardsPage.tsx | 98 +- mnemo_cards_admin/src/pages/PacksPage.tsx | 43 +- mnemo_cards_admin/src/pages/UsersPage.tsx | 111 ++- mnemo_cards_admin/src/types/models.ts | 1 + mnemo_cards_backend/README.md | 877 +++++++++++++++++- .../lib/api/v2/admin_cards_api_v2.dart | 7 + mnemo_cards_web_v2/README.md | 479 ++++++++-- mnemo_cards_web_v2/lib/app.dart | 4 +- .../lib/domain/state/user_state_manager.dart | 2 +- .../presentation/pages/auth/auth_page.dart | 12 +- .../pages/auth/sign_in_with_telegram.dart | 15 +- 13 files changed, 2149 insertions(+), 104 deletions(-) create mode 100644 mnemo_cards_admin/src/components/BulkCardEditor.tsx create mode 100644 mnemo_cards_admin/src/components/BulkCardUpload.tsx diff --git a/mnemo_cards_admin/src/components/BulkCardEditor.tsx b/mnemo_cards_admin/src/components/BulkCardEditor.tsx new file mode 100644 index 0000000..1378511 --- /dev/null +++ b/mnemo_cards_admin/src/components/BulkCardEditor.tsx @@ -0,0 +1,356 @@ +import { useState, useEffect } from 'react' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { cardsApi } from '@/api/cards' +import type { GameCardDto } from '@/types/models' +import type { AxiosError } from 'axios' +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 { packsApi } from '@/api/packs' +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 + base64: string +} + +interface CardData { + imageId: string + packId: string + original: string + translation: string + mnemo: string + transcription: string + transcriptionMnemo: string + back: string + imageBack?: string + isSaved: boolean + cardId?: number +} + +interface BulkCardEditorProps { + images: UploadedImage[] + onComplete: () => void + onCancel: () => void +} + +export function BulkCardEditor({ images, onComplete, onCancel }: BulkCardEditorProps) { + const queryClient = useQueryClient() + const [currentIndex, setCurrentIndex] = useState(0) + const [cardsData, setCardsData] = useState>(new Map()) + const [packsData, setPacksData] = useState<{ id: string; title: string }[]>([]) + + // Load packs + useEffect(() => { + packsApi + .getPacks({ page: 1, limit: 1000, search: '' }) + .then((response) => { + setPacksData(response.items.map((pack) => ({ id: pack.id, title: pack.title }))) + }) + .catch(() => { + toast.error('Failed to load packs') + }) + }, []) + + // Initialize cards data + useEffect(() => { + const initialData = new Map() + images.forEach((image) => { + initialData.set(image.id, { + imageId: image.id, + packId: '', + original: '', + translation: '', + mnemo: '', + transcription: '', + transcriptionMnemo: '', + back: '', + imageBack: undefined, + isSaved: false, + }) + }) + setCardsData(initialData) + }, [images]) + + const currentImage = images[currentIndex] + const currentCard = cardsData.get(currentImage?.id || '') + + const updateMutation = useMutation({ + mutationFn: (card: GameCardDto) => cardsApi.upsertCard(card), + onSuccess: (response) => { + queryClient.invalidateQueries({ queryKey: ['cards'] }) + 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, + }) + } + return newData + }) + toast.success('Card saved successfully') + }, + onError: (error: unknown) => { + const axiosError = error as AxiosError<{ message?: string }> + toast.error(axiosError.response?.data?.message || 'Failed to save card') + }, + }) + + const handleFieldChange = (field: keyof CardData, value: string) => { + if (!currentCard) 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 || -1, + 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.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 + + if (!currentImage || !currentCard) { + return
Loading...
+ } + + return ( +
+
+
+

Fill Card Details

+

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

+
+
+ + {currentCard.isSaved ? 'Saved' : 'Not Saved'} + + +
+
+ +
+ {/* Image Preview */} + + + Image Preview + {currentImage.file.name} + + +
+ {currentImage.file.name} +
+
+
+ + {/* 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" + /> +
+
+ +
+ +