mnemo_cards/mnemo_cards_admin/src/components/CardEditorPreview.tsx
Dmitry 7aa12d0c59
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Mobile App CI / test (push) Waiting to run
Mobile App CI / build-android (push) Blocked by required conditions
Mobile App CI / build-ios (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App 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
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
minio
2025-12-19 03:56:58 +03:00

358 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { ImageUpload } from '@/components/ui/image-upload'
import { AudioUpload } from '@/components/ui/audio-upload'
import { CardVoicesManager } from '@/components/CardVoicesManager'
interface CardEditorPreviewProps {
formData: {
packId: string
original: string
translation: string
mnemo: string
transcription: string
transcriptionMnemo: string
back: string
image: string | undefined
imageBack: string | undefined
voice: string | undefined
voiceLanguage: string
}
onFormDataChange: (updates: Partial<CardEditorPreviewProps['formData']>) => void
cardId?: string
disabled?: boolean
packColor?: string
}
// Helper to get image source
// Note: For UUID (objectId), returns undefined - ImageUpload component will handle fetching presigned URL
function getImageSrc(image?: string): string | undefined {
if (!image) return undefined
// If it's already a data URL or http/https URL, return as is
if (image.startsWith('data:') || image.startsWith('http://') || image.startsWith('https://')) {
return image
}
// If it's a relative API path, prepend the base URL
if (image.startsWith('/api/')) {
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'https://api.mnemo-cards.online'
return `${baseUrl}${image}`
}
// If it's a UUID (objectId), return undefined - ImageUpload will fetch presigned URL
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(image)
if (isUuid) {
return undefined // Will be handled by ImageUpload component's useEffect
}
// Otherwise, assume it's base64 and add the data URL prefix
return `data:image/png;base64,${image}`
}
export function CardEditorPreview({
formData,
onFormDataChange,
cardId,
disabled = false,
packColor = '#6b7280', // Default gray border color
}: CardEditorPreviewProps) {
return (
<div className="flex flex-col items-center space-y-4">
{/* Cards Preview - Front and Back side by side */}
<div className="w-full max-w-5xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Front side */}
<div className="space-y-2">
<div className="text-sm font-medium text-center text-muted-foreground">Front Side</div>
<div
className="rounded-3xl shadow-lg"
style={{
borderColor: packColor,
backgroundColor: 'white',
borderWidth: '3px',
borderStyle: 'solid',
aspectRatio: '0.66',
minHeight: '400px',
maxHeight: '600px',
}}
>
<div className="h-full w-full rounded-3xl overflow-hidden flex flex-col">
{/* Top section: Original, Translation */}
<div className="w-full px-6 pt-6 pb-4 flex-shrink-0">
{/* Original - large, bold */}
<div className="mb-3">
<Input
value={formData.original}
onChange={(e) =>
onFormDataChange({ original: e.target.value })
}
placeholder="Original text"
className="text-center border-none shadow-none focus-visible:ring-2 focus-visible:ring-primary/20 p-0 h-auto bg-transparent"
disabled={disabled}
style={{
fontSize: '32px',
fontWeight: 700,
}}
/>
</div>
{/* Translation - smaller, gray */}
<div className="mb-3">
<Input
value={formData.translation}
onChange={(e) =>
onFormDataChange({ translation: e.target.value })
}
placeholder="Translation"
className="text-center border-none shadow-none focus-visible:ring-2 focus-visible:ring-primary/20 p-0 h-auto bg-transparent"
disabled={disabled}
style={{
fontSize: '22px',
fontWeight: 400,
color: formData.translation ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.3)',
}}
/>
</div>
</div>
{/* Image in the middle */}
<div className="flex-1 bg-gray-50 flex items-center justify-center relative min-h-0">
{getImageSrc(formData.image) ? (
<img
src={getImageSrc(formData.image)}
alt="Card"
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
}}
/>
) : (
<div className="text-gray-400">
<svg
className="w-24 h-24"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
)}
</div>
{/* Mnemo phrase at the bottom */}
<div className="w-full px-6 py-6 bg-white flex-shrink-0">
<div className="text-center min-h-[60px] flex items-center justify-center">
<Input
value={formData.mnemo}
onChange={(e) => onFormDataChange({ mnemo: e.target.value })}
placeholder="Mnemo phrase"
className="text-center border-none shadow-none focus-visible:ring-2 focus-visible:ring-primary/20 p-0 h-auto bg-transparent w-full"
disabled={disabled}
style={{
fontSize: '24px',
fontWeight: 600,
}}
/>
</div>
</div>
</div>
</div>
{/* Back side */}
<div
className="rounded-3xl shadow-lg"
style={{
borderColor: packColor,
backgroundColor: 'white',
borderWidth: '3px',
borderStyle: 'solid',
aspectRatio: '0.66',
minHeight: '400px',
maxHeight: '600px',
}}
>
<div className="h-full w-full rounded-3xl overflow-hidden flex flex-col">
{formData.imageBack ? (
<>
<div className="flex-1 flex items-center justify-center bg-gray-50 min-h-0">
<img
src={getImageSrc(formData.imageBack)}
alt="Back"
className="w-full h-full object-contain"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
}}
/>
</div>
<div className="p-6 bg-white flex-shrink-0">
<Textarea
value={formData.back || formData.original}
onChange={(e) =>
onFormDataChange({ back: e.target.value })
}
placeholder="Back side text"
className="text-center resize-none border-none shadow-none focus-visible:ring-2 focus-visible:ring-primary/20 p-0 min-h-[80px] w-full bg-transparent"
disabled={disabled}
style={{
fontSize: '28px',
fontWeight: 700,
lineHeight: '1.2',
}}
/>
</div>
</>
) : (
<div className="h-full flex items-center justify-center p-6">
<Textarea
value={formData.back || formData.original}
onChange={(e) =>
onFormDataChange({ back: e.target.value })
}
placeholder="Back side text"
className="text-center resize-none border-none shadow-none focus-visible:ring-2 focus-visible:ring-primary/20 p-0 min-h-[80px] w-full bg-transparent"
disabled={disabled}
style={{
fontSize: '28px',
fontWeight: 700,
lineHeight: '1.2',
}}
/>
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* Edit fields below the preview */}
<div className="w-full max-w-5xl space-y-4 p-4 border rounded-lg bg-muted/30">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Original *</label>
<Input
value={formData.original}
onChange={(e) =>
onFormDataChange({ original: e.target.value })
}
placeholder="e.g. cerdo"
disabled={disabled}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Translation *</label>
<Input
value={formData.translation}
onChange={(e) =>
onFormDataChange({ translation: e.target.value })
}
placeholder="e.g. pig"
disabled={disabled}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Mnemo *</label>
<Input
value={formData.mnemo}
onChange={(e) => onFormDataChange({ mnemo: e.target.value })}
placeholder="e.g. [pig] with heart"
disabled={disabled}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Back Side</label>
<Textarea
value={formData.back}
onChange={(e) => onFormDataChange({ back: e.target.value })}
placeholder="Additional information on the back of the card"
rows={3}
disabled={disabled}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<ImageUpload
label="Front Image"
value={formData.image}
onChange={(value) => onFormDataChange({ image: value })}
disabled={disabled}
uploadType="card-image"
/>
</div>
<div className="space-y-2">
<ImageUpload
label="Back Image"
value={formData.imageBack}
onChange={(value) => onFormDataChange({ imageBack: value })}
disabled={disabled}
uploadType="card-image"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Transcription</label>
<Input
value={formData.transcription}
onChange={(e) =>
onFormDataChange({ transcription: e.target.value })
}
placeholder="e.g. sɛrdo"
disabled={disabled}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Transcription Mnemo</label>
<Input
value={formData.transcriptionMnemo}
onChange={(e) =>
onFormDataChange({ transcriptionMnemo: e.target.value })
}
placeholder="e.g. pig with heart"
disabled={disabled}
/>
</div>
</div>
<div className="space-y-2">
<AudioUpload
label="Voice (will be added on Save)"
value={formData.voice}
onChange={(voice) => onFormDataChange({ voice })}
language={formData.voiceLanguage}
onLanguageChange={(voiceLanguage) =>
onFormDataChange({ voiceLanguage })
}
disabled={disabled}
/>
<p className="text-xs text-muted-foreground">
Выберите аудиофайл он будет загружен и привязан к карточке при
нажатии Create/Update.
</p>
</div>
{/* Voice Controls - only show if cardId exists */}
{cardId && (
<div className="space-y-2">
<CardVoicesManager cardId={cardId} disabled={disabled} />
</div>
)}
</div>
</div>
)
}