From 15b1ac4ac014c9bfbbd8273d3e97c4f1475e8c96 Mon Sep 17 00:00:00 2001 From: Ender Date: Fri, 24 Oct 2025 03:44:28 +0200 Subject: [PATCH] feat(admin): add MediaLibrary with image reuse/delete and integrate into EditorShell --- apps/admin/src/components/EditorShell.tsx | 5 ++ apps/admin/src/components/MediaLibrary.tsx | 79 ++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 apps/admin/src/components/MediaLibrary.tsx diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index 653dbff..6547246 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -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 }) { )} + { + // naive append an image block to current HTML + setDraft((prev) => `${prev || ''}

`); + }} /> ); diff --git a/apps/admin/src/components/MediaLibrary.tsx b/apps/admin/src/components/MediaLibrary.tsx new file mode 100644 index 0000000..8155dcc --- /dev/null +++ b/apps/admin/src/components/MediaLibrary.tsx @@ -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([]); + 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 ( + + + Media Library + + + {error && {error}} + + {items.map((it) => { + const url = `/api/media/obj?key=${encodeURIComponent(it.key)}`; + const name = it.key.split('/').slice(-1)[0]; + return ( + + + {name} + + {name} + + + + + + ); + })} + {items.length === 0 && !loading && ( + No images yet. + )} + + + ); +}