diff --git a/app/dashboard/announcements/page.tsx b/app/dashboard/announcements/page.tsx new file mode 100644 index 0000000..ff78c2a --- /dev/null +++ b/app/dashboard/announcements/page.tsx @@ -0,0 +1,316 @@ +"use client"; + +import { useState } from "react"; +import { apiFetch, ApiError, toFormData } from "@/lib/api"; +import { useList } from "@/lib/useResource"; +import { useToast } from "@/components/toast"; +import { DataTable, type Column } from "@/components/DataTable"; +import { + Button, + ConfirmDialog, + Field, + Input, + Modal, + PageHeader, + Textarea, +} from "@/components/ui"; +import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; +import { toFa } from "@/lib/utils"; +import { MediaPreview } from "@/components/MediaPreview"; +import { ImagePicker } from "@/components/ImagePicker"; +import { pickUrl, IMAGE_KEYS } from "@/lib/media"; + +interface Announcement { + id: number; + title?: string; + description?: string; + link?: string; + button_text?: string; + start_date?: string | null; + end_date?: string | null; + image_id?: number | null; + image?: unknown; +} + +// Datetime ISO string -> "YYYY-MM-DD" for a . +function toDateInput(value?: string | null): string { + return value ? value.slice(0, 10) : ""; +} + +// "YYYY-MM-DD" Gregorian -> Persian display, or "—". +function toFaDate(value?: string | null): string { + if (!value) return "—"; + const d = value.slice(0, 10); + try { + return new Intl.DateTimeFormat("fa-IR").format(new Date(d)); + } catch { + return d; + } +} + +export default function AnnouncementsPage() { + const { data, loading, error, reload } = + useList("/announcements"); + const toast = useToast(); + + const [editing, setEditing] = useState(null); + const [open, setOpen] = useState(false); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [link, setLink] = useState(""); + const [buttonText, setButtonText] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [imageId, setImageId] = useState(null); + const [imageFile, setImageFile] = useState(null); + const [saving, setSaving] = useState(false); + + const [deleting, setDeleting] = useState(null); + const [removing, setRemoving] = useState(false); + + function openCreate() { + setEditing(null); + setTitle(""); + setDescription(""); + setLink(""); + setButtonText(""); + setStartDate(""); + setEndDate(""); + setImageId(null); + setImageFile(null); + setOpen(true); + } + function openEdit(row: Announcement) { + setEditing(row); + setTitle(row.title ?? ""); + setDescription(row.description ?? ""); + setLink(row.link ?? ""); + setButtonText(row.button_text ?? ""); + setStartDate(toDateInput(row.start_date)); + setEndDate(toDateInput(row.end_date)); + setImageId(row.image_id ?? null); + setImageFile(null); + setOpen(true); + } + + async function save(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + try { + // Multipart (the image is a file). Edit posts to /announcements/:id, + // which the backend also accepts as a multipart update. + const body = toFormData({ + title, + description, + link, + button_text: buttonText, + start_date: startDate, + end_date: endDate, + image_id: imageId, + image: imageFile, + }); + if (editing) { + await apiFetch(`/announcements/${editing.id}`, { method: "POST", body }); + toast.success("اعلان ویرایش شد."); + } else { + await apiFetch("/announcements", { method: "POST", body }); + toast.success("اعلان افزوده شد."); + } + setOpen(false); + reload(); + } catch (err) { + toast.error(err instanceof ApiError ? err.message : "خطا در ذخیره‌سازی"); + } finally { + setSaving(false); + } + } + + async function confirmDelete() { + if (!deleting) return; + setRemoving(true); + try { + await apiFetch(`/announcements/${deleting.id}`, { method: "DELETE" }); + toast.success("اعلان حذف شد."); + setDeleting(null); + reload(); + } catch (err) { + toast.error(err instanceof ApiError ? err.message : "خطا در حذف"); + } finally { + setRemoving(false); + } + } + + const columns: Column[] = [ + { key: "id", header: "شناسه", render: (r) => toFa(r.id), className: "w-20" }, + { + key: "thumbnail", + header: "تصویر", + className: "w-20", + render: (r) => ( + + ), + }, + { key: "title", header: "عنوان", render: (r) => r.title ?? "—" }, + { + key: "link", + header: "لینک", + render: (r) => + r.link ? ( + + {r.link} + + ) : ( + "—" + ), + }, + { + key: "start_date", + header: "تاریخ شروع", + render: (r) => toFaDate(r.start_date), + className: "w-32", + }, + { + key: "end_date", + header: "تاریخ پایان", + render: (r) => toFaDate(r.end_date), + className: "w-32", + }, + ]; + + return ( +
+ } onClick={openCreate}> + اعلان جدید + + } + /> + + } onClick={openCreate}> + اعلان جدید + + } + actions={(row) => ( + <> + + + + )} + /> + + setOpen(false)} + title={editing ? "ویرایش اعلان" : "اعلان جدید"} + footer={ + <> + + + + } + > +
+ + setTitle(e.target.value)} + placeholder="مثلاً تخفیف ویژه نوروز" + required + autoFocus + /> + + + +