From ef7804df0e7a04cd2306270591baa5d236018cf4 Mon Sep 17 00:00:00 2001 From: Arda Samadi Date: Sat, 25 Jul 2026 15:55:22 +0330 Subject: [PATCH] admin: Slice 3f homepage/merchandising UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hooks: banners/categories/seller-tiles list + shared update/delete mutations; row types. - /admin/homepage: tabbed بنرها/دسته‌ها/فروشگاه‌ها with image thumbnails, active toggle, seller-tile reorder, delete. - sidebar "صفحه اصلی" nav link. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/requestHandler/use-admin-hooks.ts | 61 +++++++ app/routes/admin.homepage.tsx | 219 ++++++++++++++++++++++++++ app/routes/admin.tsx | 2 + 3 files changed, 282 insertions(+) create mode 100644 app/routes/admin.homepage.tsx diff --git a/app/requestHandler/use-admin-hooks.ts b/app/requestHandler/use-admin-hooks.ts index e020caf..51465f6 100644 --- a/app/requestHandler/use-admin-hooks.ts +++ b/app/requestHandler/use-admin-hooks.ts @@ -517,6 +517,67 @@ export function useSmsBlacklistDelete() { }); } +/* --------------------- homepage / merchandising (3f) --------------------- */ + +export interface HomeBannerRow { + id: number; + title: string; + linkType: string; + linkId: number; + isActive: boolean; + isVisible: boolean; + imageUrl: string | null; + startDate: string; + endDate: string; + createdAt: string; +} + +export interface HomeCategoryRow { + id: string; + categoryId: string; + categoryName: string | null; + imageUrl: string | null; + isActive: boolean; +} + +export interface HomeSellerTileRow { + id: string; + sellerId: string; + sellerName: string | null; + imageUrl: string | null; + isActive: boolean; + order: number; + createdAt: string; +} + +export const useHomeBanners = (params: AdminListParams) => + useAdminList("homepage/banners", params); +export const useHomeCategories = (params: AdminListParams) => + useAdminList("homepage/categories", params); +export const useHomeSellerTiles = (params: AdminListParams) => + useAdminList("homepage/tiles", params); + +/** PATCH/DELETE a homepage entry. `kind` maps to the URL segment. */ +function useHomeMutations(kind: "banners" | "categories" | "tiles") { + const token = useAuthToken(); + const qc = useQueryClient(); + const invalidate = () => qc.invalidateQueries({ queryKey: ["admin", `homepage/${kind}`] }); + const update = useMutation({ + mutationFn: ({ id, data }: { id: string | number; data: Record }) => + adminPatch(`homepage/${kind}/${id}/`, data, token as string), + onSuccess: invalidate, + }); + const remove = useMutation({ + mutationFn: (id: string | number) => adminDelete(`homepage/${kind}/${id}/`, token as string), + onSuccess: invalidate, + }); + return { update, remove }; +} + +export const useHomeBannerMutations = () => useHomeMutations("banners"); +export const useHomeCategoryMutations = () => useHomeMutations("categories"); +export const useHomeSellerTileMutations = () => useHomeMutations("tiles"); + /* ---------------------- AI pipelines health (3c) ---------------------- */ interface FailureRow { diff --git a/app/routes/admin.homepage.tsx b/app/routes/admin.homepage.tsx new file mode 100644 index 0000000..060838d --- /dev/null +++ b/app/routes/admin.homepage.tsx @@ -0,0 +1,219 @@ +import { useState } from "react"; +import { Trash2, ImageOff } from "lucide-react"; +import { + useHomeBanners, + useHomeCategories, + useHomeSellerTiles, + useHomeBannerMutations, + useHomeCategoryMutations, + useHomeSellerTileMutations, + type HomeBannerRow, + type HomeCategoryRow, + type HomeSellerTileRow, +} from "~/requestHandler/use-admin-hooks"; +import { + AdminTable, + AdminPagination, + AdminPageTitle, + StatusBadge, + faNum, + faDate, + type Column, +} from "~/components/admin/DataTable"; +import { cn } from "~/lib/utils"; + +const PAGE_SIZE = 25; + +function Thumb({ url }: { url: string | null }) { + if (!url) + return ( +
+ +
+ ); + return ; +} + +function Toggle({ on, onClick }: { on: boolean; onClick: () => void }) { + return ( + + ); +} + +function DeleteBtn({ onClick }: { onClick: () => void }) { + return ( + + ); +} + +export default function AdminHomepage() { + const [tab, setTab] = useState<"banners" | "categories" | "tiles">("banners"); + const TABS = [ + { k: "banners", label: "بنرها" }, + { k: "categories", label: "دسته‌ها" }, + { k: "tiles", label: "فروشگاه‌ها" }, + ] as const; + return ( +
+ صفحه اصلی +

+ مدیریت آیتم‌های موجود (فعال/غیرفعال، ترتیب، حذف). افزودن آیتم جدید از طریق آپلود تصویر انجام می‌شود. +

+
+ {TABS.map((t) => ( + + ))} +
+ {tab === "banners" && } + {tab === "categories" && } + {tab === "tiles" && } +
+ ); +} + +function BannersTab() { + const [page, setPage] = useState(1); + const { data, isLoading } = useHomeBanners({ page, page_size: PAGE_SIZE }); + const { update, remove } = useHomeBannerMutations(); + + const columns: Column[] = [ + { header: "", render: (r) => }, + { + header: "بنر", + render: (r) => ( +
+

{r.title || "—"}

+

+ {r.linkType}:{r.linkId} +

+
+ ), + }, + { + header: "بازه", + render: (r) => ( + + {faDate(r.startDate)} — {faDate(r.endDate)} + + ), + }, + { + header: "نمایش", + render: (r) => + r.isVisible ? : , + }, + { + header: "فعال", + render: (r) => ( + update.mutate({ id: r.id, data: { isActive: !r.isActive } })} /> + ), + }, + { + header: "", + render: (r) => ( + confirm("حذف این بنر؟") && remove.mutate(r.id)} /> + ), + }, + ]; + + return ( +
+ String(r.id)} emptyText="بنری ثبت نشده است" /> + +
+ ); +} + +function CategoriesTab() { + const [page, setPage] = useState(1); + const { data, isLoading } = useHomeCategories({ page, page_size: PAGE_SIZE }); + const { update, remove } = useHomeCategoryMutations(); + + const columns: Column[] = [ + { header: "", render: (r) => }, + { header: "دسته", render: (r) => {r.categoryName || "—"} }, + { + header: "فعال", + render: (r) => ( + update.mutate({ id: r.id, data: { isActive: !r.isActive } })} /> + ), + }, + { header: "", render: (r) => confirm("حذف این دسته از صفحه اصلی؟") && remove.mutate(r.id)} /> }, + ]; + + return ( +
+ r.id} emptyText="دسته‌ای ثبت نشده است" /> + +
+ ); +} + +function TilesTab() { + const [page, setPage] = useState(1); + const { data, isLoading } = useHomeSellerTiles({ page, page_size: PAGE_SIZE }); + const { update, remove } = useHomeSellerTileMutations(); + + const columns: Column[] = [ + { header: "", render: (r) => }, + { header: "فروشگاه", render: (r) => {r.sellerName || "—"} }, + { + header: "ترتیب", + render: (r) => ( + { + const v = Number(e.target.value); + if (v !== r.order) update.mutate({ id: r.id, data: { order: v } }); + }} + className="w-16 rounded-lg border border-WHITE3 px-2 py-1 text-[13px] tabular-nums text-center" + dir="ltr" + /> + ), + }, + { + header: "فعال", + render: (r) => ( + update.mutate({ id: r.id, data: { isActive: !r.isActive } })} /> + ), + }, + { header: "", render: (r) => confirm("حذف این کاشی فروشگاه؟") && remove.mutate(r.id)} /> }, + ]; + + return ( +
+ r.id} emptyText="کاشی فروشگاهی ثبت نشده است" /> + +
+ ); +} diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx index 7b36e0f..abaf67a 100644 --- a/app/routes/admin.tsx +++ b/app/routes/admin.tsx @@ -8,6 +8,7 @@ import { Ticket, LayoutGrid, Landmark, + LayoutTemplate, Bell, Activity, MessageSquareWarning, @@ -36,6 +37,7 @@ const NAV: NavItem[] = [ { label: "تخفیف‌ها", to: "/admin/discounts", icon: Ticket }, { label: "مجموعه‌ها", to: "/admin/collections", icon: LayoutGrid }, { label: "دفتر مالی", to: "/admin/ledger", icon: Landmark }, + { label: "صفحه اصلی", to: "/admin/homepage", icon: LayoutTemplate }, { label: "اعلان‌ها", to: "/admin/notifications", icon: Bell }, { label: "سلامت AI", to: "/admin/ai-health", icon: Activity }, { label: "مدیریت محتوا", to: "/admin/moderation", icon: MessageSquareWarning },