fix: upload image
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user