157 lines
4.9 KiB
TypeScript
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-[#e2e6f2]">
|
|
بارگذاری تصویر
|
|
<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>
|
|
);
|
|
}
|