Files
2026-06-03 09:01:42 +03:30

157 lines
4.9 KiB
TypeScript

"use client";
// Image association control used by media, music, categories, etc.
// Lets the user either (a) pick an existing image from the /images library
// (sets `image_id`) or (b) upload a new image file (sets `image` File). Shows a
// live thumbnail of the current choice.
import { useEffect, useMemo, useState } from "react";
import { useList } from "@/lib/useResource";
import { pickUrl, IMAGE_KEYS } from "@/lib/media";
import { Button, Modal, Spinner, EmptyState } from "./ui";
import { ImageIcon, CloseIcon } from "./icons";
interface LibImage {
id: number;
title?: string | null;
}
export function ImagePicker({
imageId,
onPickId,
file,
onPickFile,
existingUrl,
}: {
imageId: number | null;
onPickId: (id: number | null) => void;
file: File | null;
onPickFile: (f: File | null) => void;
// URL of the already-saved image (edit mode) when no new choice is made.
existingUrl?: string | null;
}) {
const [libOpen, setLibOpen] = useState(false);
const { data: images, loading } = useList<LibImage>(
libOpen ? "/images/all" : null,
);
// Object URL preview for a freshly uploaded file.
const [filePreview, setFilePreview] = useState<string | null>(null);
useEffect(() => {
if (!file) {
setFilePreview(null);
return;
}
const url = URL.createObjectURL(file);
setFilePreview(url);
return () => URL.revokeObjectURL(url);
}, [file]);
// Resolve the thumbnail for the currently chosen library image (if loaded).
const pickedLibUrl = useMemo(() => {
if (!imageId || !images.length) return null;
const found = images.find((i) => i.id === imageId);
return found ? pickUrl(found, IMAGE_KEYS) : null;
}, [imageId, images]);
const preview = filePreview ?? pickedLibUrl ?? existingUrl ?? null;
function clearAll() {
onPickId(null);
onPickFile(null);
}
return (
<div className="flex items-center gap-3">
<div className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-border bg-surface-muted text-muted">
{preview ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={preview} alt="" className="h-full w-full object-cover" />
) : (
<ImageIcon className="h-6 w-6" />
)}
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="secondary"
onClick={() => setLibOpen(true)}
>
انتخاب از کتابخانه
</Button>
<label className="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-border bg-surface-muted px-4 py-2 text-sm font-medium text-foreground transition hover:bg-surface-hover">
بارگذاری تصویر
<input
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0] ?? null;
onPickFile(f);
if (f) onPickId(null); // a fresh upload supersedes a library pick
}}
/>
</label>
{(preview || imageId || file) && (
<Button
type="button"
variant="ghost"
className="text-danger"
icon={<CloseIcon className="h-4 w-4" />}
onClick={clearAll}
>
حذف
</Button>
)}
</div>
<Modal
open={libOpen}
onClose={() => setLibOpen(false)}
title="انتخاب تصویر از کتابخانه"
>
{loading ? (
<div className="flex justify-center py-10">
<Spinner />
</div>
) : !images.length ? (
<EmptyState
title="تصویری موجود نیست"
message="ابتدا از بخش «کتابخانه تصاویر» تصویر اضافه کنید."
/>
) : (
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4">
{images.map((img) => {
const url = pickUrl(img, IMAGE_KEYS);
const selected = img.id === imageId;
return (
<button
key={img.id}
type="button"
onClick={() => {
onPickId(img.id);
onPickFile(null);
setLibOpen(false);
}}
className={`overflow-hidden rounded-xl border-2 transition ${
selected
? "border-primary"
: "border-transparent hover:border-border"
}`}
title={img.title ?? String(img.id)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={url ?? ""} alt={img.title ?? ""} className="aspect-square w-full object-cover" />
</button>
);
})}
</div>
)}
</Modal>
</div>
);
}