feat: media library with image picker for article forms, browse/upload from folder

This commit is contained in:
remco committed 2026-07-01 17:00:35 +02:00
1 parent d35c495539
commit 12576cc113
9 files changed
+392 -19

No files matched your search

+138
View File
@@ -0,0 +1,138 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { uploadMediaAndReturn } from "@/actions/admin-media";
type MediaFile = { name: string; url: string };
export function MediaPicker({
onSelect,
current,
}: {
onSelect: (url: string) => void;
current?: string;
}) {
const [open, setOpen] = useState(false);
const [files, setFiles] = useState<MediaFile[]>([]);
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const fileRef = useRef<HTMLInputElement>(null);
const load = useCallback(async () => {
setLoading(true);
try {
const res = await fetch("/api/media");
const data = await res.json();
setFiles(data.files ?? []);
} catch {
/* ignore */
}
setLoading(false);
}, []);
useEffect(() => {
if (open) load();
}, [open, load]);
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const fd = new FormData();
fd.set("file", file);
const url = await uploadMediaAndReturn(fd);
if (url) {
await load();
onSelect(url);
setOpen(false);
}
setUploading(false);
if (fileRef.current) fileRef.current.value = "";
}
return (
<>
<div style={{ display: "flex", gap: "0.4rem", alignItems: "center" }}>
<button type="button" onClick={() => setOpen(true)} className="btn btn-outline" style={{ fontSize: "0.82rem", padding: "0.3rem 0.7rem" }}>
Browse
</button>
{current && (
<span style={{ fontSize: "0.78rem", color: "var(--color-text-muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 200 }}>
{current.split("/").pop()}
</span>
)}
</div>
{open && (
<div
style={{
position: "fixed", inset: 0, zIndex: 9999,
display: "flex", alignItems: "center", justifyContent: "center",
backgroundColor: "rgba(0,0,0,0.5)",
}}
onClick={() => setOpen(false)}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: "var(--color-surface)", color: "var(--color-text)",
borderRadius: 14, width: "min(90vw, 680px)", maxHeight: "80vh",
display: "flex", flexDirection: "column", boxShadow: "0 16px 48px rgba(0,0,0,0.25)",
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0.85rem 1rem", borderBottom: "1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent)" }}>
<strong style={{ fontSize: "1rem" }}>Media Library</strong>
<div style={{ display: "flex", gap: "0.4rem", alignItems: "center" }}>
<label className="btn btn-primary" style={{ fontSize: "0.82rem", padding: "0.3rem 0.7rem", cursor: "pointer" }}>
{uploading ? "Uploading…" : "Upload"}
<input ref={fileRef} type="file" accept="image/png,image/jpeg,image/gif,image/webp" onChange={handleUpload} style={{ display: "none" }} />
</label>
<button type="button" onClick={() => setOpen(false)} className="btn" style={{ fontSize: "0.82rem", padding: "0.3rem 0.7rem" }}>
Close
</button>
</div>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "0.75rem" }}>
{loading ? (
<p className="muted" style={{ textAlign: "center", padding: "2rem" }}>Loading…</p>
) : files.length === 0 ? (
<p className="muted" style={{ textAlign: "center", padding: "2rem" }}>
No images yet. Upload one above.
</p>
) : (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: "0.65rem" }}>
{files.map((f) => (
<button
type="button"
key={f.name}
onClick={() => { onSelect(f.url); setOpen(false); }}
style={{
border: `2px solid ${f.url === current ? "var(--color-primary)" : "transparent"}`,
borderRadius: 10, overflow: "hidden", cursor: "pointer",
background: "var(--color-background)", padding: 0,
display: "flex", flexDirection: "column",
transition: "border-color 0.15s",
}}
onMouseEnter={(e) => { if (f.url !== current) e.currentTarget.style.borderColor = "color-mix(in srgb, var(--color-primary) 40%, transparent)"; }}
onMouseLeave={(e) => { if (f.url !== current) e.currentTarget.style.borderColor = "transparent"; }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={f.url}
alt={f.name}
style={{ width: "100%", height: 100, objectFit: "cover", display: "block" }}
/>
<span style={{ fontSize: "0.72rem", padding: "0.3rem 0.4rem", textAlign: "center", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{f.name}
</span>
</button>
))}
</div>
)}
</div>
</div>
</div>
)}
</>
);
}