feat(admin): add MediaLibrary with image reuse/delete and integrate into EditorShell

This commit is contained in:
Ender 2025-10-24 03:44:28 +02:00
parent 8f4fbb098f
commit 15b1ac4ac0
2 changed files with 84 additions and 0 deletions

View File

@ -2,6 +2,7 @@ import { Box, Button, Stack, Typography } from '@mui/material';
import AdminLayout from '../layout/AdminLayout';
import Recorder from '../features/recorder/Recorder';
import RichEditor from './RichEditor';
import MediaLibrary from './MediaLibrary';
import { useEffect, useState } from 'react';
export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
@ -97,6 +98,10 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
)}
</Stack>
</Box>
<MediaLibrary onInsert={(url) => {
// naive append an image block to current HTML
setDraft((prev) => `${prev || ''}<p><img src="${url}" alt="" /></p>`);
}} />
</Box>
</AdminLayout>
);

View File

@ -0,0 +1,79 @@
import { useEffect, useState } from 'react';
import { Box, Button, Stack, Typography } from '@mui/material';
type MediaItem = {
key: string;
size: number;
lastModified: string | null;
};
export default function MediaLibrary({ onInsert }: { onInsert: (url: string) => void }) {
const [items, setItems] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const load = async () => {
try {
setLoading(true);
setError('');
const res = await fetch('/api/media/list?prefix=images/');
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
setItems(Array.isArray(data.items) ? data.items : []);
} catch (e: any) {
setError(e?.message || 'Failed to load media');
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
const del = async (key: string) => {
try {
setError('');
const res = await fetch('/api/media/obj', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key }),
});
if (!res.ok) throw new Error(await res.text());
await load();
} catch (e: any) {
setError(e?.message || 'Delete failed');
}
};
return (
<Box>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
<Typography variant="subtitle1">Media Library</Typography>
<Button size="small" onClick={load} disabled={loading}>Refresh</Button>
</Stack>
{error && <Typography color="error" sx={{ mb: 1 }}>{error}</Typography>}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 1 }}>
{items.map((it) => {
const url = `/api/media/obj?key=${encodeURIComponent(it.key)}`;
const name = it.key.split('/').slice(-1)[0];
return (
<Box key={it.key} sx={{ border: '1px solid #eee', borderRadius: 1, p: 1 }}>
<Box sx={{ width: '100%', height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 1, overflow: 'hidden', background: '#fafafa' }}>
<img src={url} alt={name} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
</Box>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }} title={name}>{name}</Typography>
<Stack direction="row" spacing={1}>
<Button size="small" variant="outlined" onClick={() => onInsert(url)}>Insert</Button>
<Button size="small" color="error" onClick={() => del(it.key)}>Delete</Button>
</Stack>
</Box>
);
})}
{items.length === 0 && !loading && (
<Typography variant="body2">No images yet.</Typography>
)}
</Box>
</Box>
);
}