9be692974e
Pagina-editor, hoofdmenu, SEO en media onder Publieke Website → Pagina's (/website), los van het CMS in rikx-org: eigen tabellen in de beheerdatabase. Blokken (hero, tekstblok, testimonial, voordelenlijst, call-to-action) met Quill, NL/EN/DE met terugval op NL, en afbeeldingsupload naar MEDIA_DIR met een openbare /media-route (CSP-sandbox). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { useId, useRef, useState } from "react";
|
|
import { uploadMediaAction } from "./actions";
|
|
import { errorClass, inputClass, labelClass, secondaryButton } from "./ui";
|
|
|
|
// Kies een eerder geüploade afbeelding of upload een nieuwe (max 10 MB)
|
|
export function ImageField({
|
|
label,
|
|
value,
|
|
onChange,
|
|
mediaImages,
|
|
emptyLabel = "Geen afbeelding",
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChange: (path: string) => void;
|
|
mediaImages: string[];
|
|
emptyLabel?: string;
|
|
}) {
|
|
const [uploaded, setUploaded] = useState<string[]>([]);
|
|
const [uploading, setUploading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
const fieldId = useId();
|
|
|
|
async function handleUpload() {
|
|
const file = fileInputRef.current?.files?.[0];
|
|
if (!file) return;
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
setUploading(true);
|
|
setError(null);
|
|
try {
|
|
const result = await uploadMediaAction(formData);
|
|
if (result.error) setError(result.error);
|
|
else if (result.path) {
|
|
setUploaded((u) => [result.path!, ...u]);
|
|
onChange(result.path);
|
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
}
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
}
|
|
|
|
// Ook een gekozen afbeelding die niet (meer) in de lijst staat blijft zichtbaar als optie
|
|
const options = [...new Set([...(value ? [value] : []), ...uploaded, ...mediaImages])];
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<label className={labelClass} htmlFor={fieldId}>
|
|
{label}
|
|
</label>
|
|
<select id={fieldId} value={value} onChange={(e) => onChange(e.target.value)} className={inputClass}>
|
|
<option value="">{emptyLabel}</option>
|
|
{options.map((img) => (
|
|
<option key={img} value={img}>
|
|
{img.split("/").pop()}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{value && (
|
|
<img src={value} alt="" className="max-w-[200px] max-h-[120px] object-cover rounded-lg border border-zinc-200 dark:border-zinc-700" />
|
|
)}
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<input ref={fileInputRef} type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml" className="text-sm text-zinc-600 dark:text-zinc-400" />
|
|
<button type="button" className={secondaryButton} onClick={handleUpload} disabled={uploading}>
|
|
{uploading ? "Bezig..." : "Uploaden"}
|
|
</button>
|
|
</div>
|
|
{error && <p className={errorClass}>{error}</p>}
|
|
</div>
|
|
);
|
|
}
|