admin
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:
Dmitry 2025-12-17 04:59:35 +03:00
parent af5ba195b3
commit 47cbd336d4
4 changed files with 105 additions and 46 deletions

View file

@ -5,7 +5,7 @@ import { authApi } from './auth'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.mnemo-cards.online'
// Admin API always uses production backend (admin auth requires production server)
const ADMIN_API_BASE_URL = 'https://api.mnemo-cards.online'
export const ADMIN_API_BASE_URL = 'https://api.mnemo-cards.online'
// Regular API client for non-admin endpoints
export const apiClient = axios.create({

View file

@ -89,6 +89,12 @@ function getImageSrc(image?: string): string | undefined {
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}`
}
return `data:image/png;base64,${image}`
}
@ -193,43 +199,17 @@ export function CardEditorPreview({
{/* 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">
{formData.mnemo ? (
<div className="w-full">
<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 mb-2"
disabled={disabled}
style={{
fontSize: '24px',
fontWeight: 600,
}}
/>
{/* Preview of formatted mnemo text */}
<div
className="text-center"
style={{
fontSize: '24px',
fontWeight: 600,
}}
>
{formatMnemoText(formData.mnemo)}
</div>
</div>
) : (
<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,
}}
/>
)}
<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>

View file

@ -282,6 +282,12 @@ export default function CardsPage() {
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}`
}
// Otherwise, assume it's base64 and add the data URL prefix
// Try to detect image type from base64 or default to png
return `data:image/png;base64,${image}`

View file

@ -29,6 +29,79 @@ class AdminCardsApiV2 {
}
}
// Helper function to check if string is base64 encoded
bool _isBase64(String value) {
if (value.isEmpty) return false;
// Base64 strings are typically long and contain only base64 characters
// Check length (base64 images are usually > 100 chars) and character set
if (value.length < 50) return false;
final base64Regex = RegExp(r'^[A-Za-z0-9+/]*={0,2}$');
return base64Regex.hasMatch(value) && value.length > 100;
}
// Helper function to convert image value to URL
// If image is base64 and we have packId and cardId, convert to URL
// Otherwise return as is
String? _convertImageToUrl(String? imageValue, String? packId, String cardId) {
if (imageValue == null || imageValue.isEmpty) return imageValue;
// If it's already a proper URL, return as is
if (imageValue.startsWith('http://') ||
imageValue.startsWith('https://') ||
(imageValue.startsWith('/api/') && imageValue.contains('/cards/') &&
(imageValue.endsWith('/image') || imageValue.endsWith('/imageBack')))) {
return imageValue;
}
// If packId is null, we can't convert to URL, return as is
if (packId == null) return imageValue;
// Check if it's base64 encoded image
if (_isBase64(imageValue)) {
// Convert base64 to URL using card ID
// The endpoint will decode base64 from card.image field
return '/api/v2/packs/$packId/cards/$cardId/image';
}
// If it looks like a UUID (card ID), convert to URL
if (RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false).hasMatch(imageValue)) {
return '/api/v2/packs/$packId/cards/$imageValue/image';
}
// Otherwise, assume it's already a card ID and convert
return '/api/v2/packs/$packId/cards/$imageValue/image';
}
// Helper function to convert imageBack value to URL
String? _convertImageBackToUrl(String? imageValue, String? packId, String cardId) {
if (imageValue == null || imageValue.isEmpty) return imageValue;
// If it's already a proper URL, return as is
if (imageValue.startsWith('http://') ||
imageValue.startsWith('https://') ||
(imageValue.startsWith('/api/') && imageValue.contains('/cards/') &&
(imageValue.endsWith('/image') || imageValue.endsWith('/imageBack')))) {
return imageValue;
}
// If packId is null, we can't convert to URL, return as is
if (packId == null) return imageValue;
// Check if it's base64 encoded image
if (_isBase64(imageValue)) {
// Convert base64 to URL using card ID
return '/api/v2/packs/$packId/cards/$cardId/imageBack';
}
// If it looks like a UUID (card ID), convert to URL
if (RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false).hasMatch(imageValue)) {
return '/api/v2/packs/$packId/cards/$imageValue/imageBack';
}
// Otherwise, assume it's already a card ID and convert
return '/api/v2/packs/$packId/cards/$imageValue/imageBack';
}
/// GET /api/v2/admin/cards
/// Get all cards with pagination and search
@Route.get('/admin/cards')
@ -93,11 +166,11 @@ class AdminCardsApiV2 {
'original': card.original,
'translation': card.translation,
'mnemo': card.mnemo,
'image': card.image,
'image': _convertImageToUrl(card.image, packId, card.id),
'back': card.back,
'transcription': card.transcription,
'transcriptionMnemo': card.transcriptionMnemo,
'imageBack': card.imageBack,
'imageBack': _convertImageBackToUrl(card.imageBack, packId, card.id),
});
}
@ -164,8 +237,8 @@ class AdminCardsApiV2 {
'original': card.original,
'translation': card.translation,
'mnemo': card.mnemo,
'image': card.image,
'imageBack': card.imageBack,
'image': _convertImageToUrl(card.image, packId, card.id),
'imageBack': _convertImageBackToUrl(card.imageBack, packId, card.id),
'back': card.back,
'transcription': card.transcription,
'transcriptionMnemo': card.transcriptionMnemo,
@ -269,8 +342,8 @@ class AdminCardsApiV2 {
'original': updated.original,
'translation': updated.translation,
'mnemo': updated.mnemo,
'image': updated.image,
'imageBack': updated.imageBack,
'image': _convertImageToUrl(updated.image, packId, updated.id),
'imageBack': _convertImageBackToUrl(updated.imageBack, packId, updated.id),
'back': updated.back,
'transcription': updated.transcription,
'transcriptionMnemo': updated.transcriptionMnemo,
@ -326,8 +399,8 @@ class AdminCardsApiV2 {
'original': created.original,
'translation': created.translation,
'mnemo': created.mnemo,
'image': created.image,
'imageBack': created.imageBack,
'image': _convertImageToUrl(created.image, packId, created.id),
'imageBack': _convertImageBackToUrl(created.imageBack, packId, created.id),
'back': created.back,
'transcription': created.transcription,
'transcriptionMnemo': created.transcriptionMnemo,