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
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:
parent
af5ba195b3
commit
47cbd336d4
4 changed files with 105 additions and 46 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue