feat(admin): add MediaLibrary with image reuse/delete and integrate into EditorShell
This commit is contained in:
parent
8f4fbb098f
commit
15b1ac4ac0
@ -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>
|
||||
);
|
||||
|
||||
79
apps/admin/src/components/MediaLibrary.tsx
Normal file
79
apps/admin/src/components/MediaLibrary.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user