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

+78
View File
@@ -0,0 +1,78 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { uploadMedia } from "@/actions/admin-media";
type MediaFile = { name: string; url: string };
export function AdminMediaGrid() {
const [files, setFiles] = useState<MediaFile[]>([]);
const [loading, setLoading] = useState(true);
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(() => { load(); }, [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);
try {
await uploadMedia(fd);
await load();
} catch { /* ignore */ }
setUploading(false);
if (fileRef.current) fileRef.current.value = "";
}
function copyUrl(url: string) {
navigator.clipboard.writeText(window.location.origin + url).catch(() => {});
}
return (
<div className="card" style={{ marginTop: "1rem" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.75rem" }}>
<strong>{files.length} image{files.length !== 1 ? "s" : ""}</strong>
<label className="btn btn-primary" style={{ cursor: "pointer", fontSize: "0.88rem" }}>
{uploading ? "Uploading…" : "Upload image"}
<input ref={fileRef} type="file" accept="image/png,image/jpeg,image/gif,image/webp" onChange={handleUpload} style={{ display: "none" }} />
</label>
</div>
{loading ? (
<p className="muted">Loading…</p>
) : files.length === 0 ? (
<p className="muted">No images yet. Upload one above.</p>
) : (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: "0.75rem" }}>
{files.map((f) => (
<div key={f.name} style={{ border: "1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent)", borderRadius: 10, overflow: "hidden", background: "var(--color-background)" }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={f.url} alt={f.name} style={{ width: "100%", height: 120, objectFit: "cover", display: "block" }} />
<div style={{ padding: "0.4rem" }}>
<p style={{ fontSize: "0.72rem", margin: "0 0 0.3rem", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--color-text-muted)" }}>
{f.name}
</p>
<button type="button" onClick={() => copyUrl(f.url)} className="btn btn-outline" style={{ fontSize: "0.72rem", padding: "0.2rem 0.5rem", width: "100%" }}>
Copy URL
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}