admin: Slice 3f homepage/merchandising UI
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
b4faa9d180
commit
ef7804df0e
@ -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<HomeBannerRow>("homepage/banners", params);
|
||||||
|
export const useHomeCategories = (params: AdminListParams) =>
|
||||||
|
useAdminList<HomeCategoryRow>("homepage/categories", params);
|
||||||
|
export const useHomeSellerTiles = (params: AdminListParams) =>
|
||||||
|
useAdminList<HomeSellerTileRow>("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<string, unknown> }) =>
|
||||||
|
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) ---------------------- */
|
/* ---------------------- AI pipelines health (3c) ---------------------- */
|
||||||
|
|
||||||
interface FailureRow {
|
interface FailureRow {
|
||||||
|
|||||||
219
app/routes/admin.homepage.tsx
Normal file
219
app/routes/admin.homepage.tsx
Normal file
@ -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 (
|
||||||
|
<div className="h-11 w-11 rounded-lg border border-WHITE3 bg-WHITE2 grid place-items-center text-GRAY">
|
||||||
|
<ImageOff size={16} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return <img src={url} alt="" className="h-11 w-11 rounded-lg object-cover border border-WHITE3" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toggle({ on, onClick }: { on: boolean; onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
"relative h-6 w-11 rounded-full transition-colors",
|
||||||
|
on ? "bg-GREEN" : "bg-GRAY2"
|
||||||
|
)}
|
||||||
|
aria-pressed={on}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute top-0.5 h-5 w-5 rounded-full bg-white transition-all",
|
||||||
|
on ? "left-0.5" : "right-0.5"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteBtn({ onClick }: { onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="grid h-8 w-8 place-items-center rounded-lg border border-WHITE3 text-RED hover:bg-RED/5"
|
||||||
|
aria-label="حذف"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<AdminPageTitle>صفحه اصلی</AdminPageTitle>
|
||||||
|
<p className="text-GRAY text-[12px] -mt-2 mb-4">
|
||||||
|
مدیریت آیتمهای موجود (فعال/غیرفعال، ترتیب، حذف). افزودن آیتم جدید از طریق آپلود تصویر انجام میشود.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-1 mb-5 border-b border-WHITE3">
|
||||||
|
{TABS.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.k}
|
||||||
|
onClick={() => setTab(t.k)}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-2 text-[13.5px] font-semibold border-b-2 -mb-px transition-colors",
|
||||||
|
tab === t.k ? "border-BLACK text-BLACK" : "border-transparent text-GRAY hover:text-BLACK2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{tab === "banners" && <BannersTab />}
|
||||||
|
{tab === "categories" && <CategoriesTab />}
|
||||||
|
{tab === "tiles" && <TilesTab />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BannersTab() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useHomeBanners({ page, page_size: PAGE_SIZE });
|
||||||
|
const { update, remove } = useHomeBannerMutations();
|
||||||
|
|
||||||
|
const columns: Column<HomeBannerRow>[] = [
|
||||||
|
{ header: "", render: (r) => <Thumb url={r.imageUrl} /> },
|
||||||
|
{
|
||||||
|
header: "بنر",
|
||||||
|
render: (r) => (
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-[13px]">{r.title || "—"}</p>
|
||||||
|
<p className="text-GRAY text-[11px] tabular-nums" dir="ltr">
|
||||||
|
{r.linkType}:{r.linkId}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "بازه",
|
||||||
|
render: (r) => (
|
||||||
|
<span className="text-GRAY text-[11px] whitespace-nowrap">
|
||||||
|
{faDate(r.startDate)} — {faDate(r.endDate)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "نمایش",
|
||||||
|
render: (r) =>
|
||||||
|
r.isVisible ? <StatusBadge label="در حال نمایش" tone="green" /> : <StatusBadge label="غیرفعال" tone="gray" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "فعال",
|
||||||
|
render: (r) => (
|
||||||
|
<Toggle on={r.isActive} onClick={() => update.mutate({ id: r.id, data: { isActive: !r.isActive } })} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "",
|
||||||
|
render: (r) => (
|
||||||
|
<DeleteBtn onClick={() => confirm("حذف این بنر؟") && remove.mutate(r.id)} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AdminTable columns={columns} rows={data?.results || []} isLoading={isLoading} getRowKey={(r) => String(r.id)} emptyText="بنری ثبت نشده است" />
|
||||||
|
<AdminPagination page={page} pageSize={PAGE_SIZE} count={data?.count || 0} onPageChange={setPage} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CategoriesTab() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useHomeCategories({ page, page_size: PAGE_SIZE });
|
||||||
|
const { update, remove } = useHomeCategoryMutations();
|
||||||
|
|
||||||
|
const columns: Column<HomeCategoryRow>[] = [
|
||||||
|
{ header: "", render: (r) => <Thumb url={r.imageUrl} /> },
|
||||||
|
{ header: "دسته", render: (r) => <span className="font-semibold text-[13px]">{r.categoryName || "—"}</span> },
|
||||||
|
{
|
||||||
|
header: "فعال",
|
||||||
|
render: (r) => (
|
||||||
|
<Toggle on={r.isActive} onClick={() => update.mutate({ id: r.id, data: { isActive: !r.isActive } })} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ header: "", render: (r) => <DeleteBtn onClick={() => confirm("حذف این دسته از صفحه اصلی؟") && remove.mutate(r.id)} /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AdminTable columns={columns} rows={data?.results || []} isLoading={isLoading} getRowKey={(r) => r.id} emptyText="دستهای ثبت نشده است" />
|
||||||
|
<AdminPagination page={page} pageSize={PAGE_SIZE} count={data?.count || 0} onPageChange={setPage} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TilesTab() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useHomeSellerTiles({ page, page_size: PAGE_SIZE });
|
||||||
|
const { update, remove } = useHomeSellerTileMutations();
|
||||||
|
|
||||||
|
const columns: Column<HomeSellerTileRow>[] = [
|
||||||
|
{ header: "", render: (r) => <Thumb url={r.imageUrl} /> },
|
||||||
|
{ header: "فروشگاه", render: (r) => <span className="font-semibold text-[13px]">{r.sellerName || "—"}</span> },
|
||||||
|
{
|
||||||
|
header: "ترتیب",
|
||||||
|
render: (r) => (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
defaultValue={r.order}
|
||||||
|
onBlur={(e) => {
|
||||||
|
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) => (
|
||||||
|
<Toggle on={r.isActive} onClick={() => update.mutate({ id: r.id, data: { isActive: !r.isActive } })} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ header: "", render: (r) => <DeleteBtn onClick={() => confirm("حذف این کاشی فروشگاه؟") && remove.mutate(r.id)} /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AdminTable columns={columns} rows={data?.results || []} isLoading={isLoading} getRowKey={(r) => r.id} emptyText="کاشی فروشگاهی ثبت نشده است" />
|
||||||
|
<AdminPagination page={page} pageSize={PAGE_SIZE} count={data?.count || 0} onPageChange={setPage} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
Ticket,
|
Ticket,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Landmark,
|
Landmark,
|
||||||
|
LayoutTemplate,
|
||||||
Bell,
|
Bell,
|
||||||
Activity,
|
Activity,
|
||||||
MessageSquareWarning,
|
MessageSquareWarning,
|
||||||
@ -36,6 +37,7 @@ const NAV: NavItem[] = [
|
|||||||
{ label: "تخفیفها", to: "/admin/discounts", icon: Ticket },
|
{ label: "تخفیفها", to: "/admin/discounts", icon: Ticket },
|
||||||
{ label: "مجموعهها", to: "/admin/collections", icon: LayoutGrid },
|
{ label: "مجموعهها", to: "/admin/collections", icon: LayoutGrid },
|
||||||
{ label: "دفتر مالی", to: "/admin/ledger", icon: Landmark },
|
{ label: "دفتر مالی", to: "/admin/ledger", icon: Landmark },
|
||||||
|
{ label: "صفحه اصلی", to: "/admin/homepage", icon: LayoutTemplate },
|
||||||
{ label: "اعلانها", to: "/admin/notifications", icon: Bell },
|
{ label: "اعلانها", to: "/admin/notifications", icon: Bell },
|
||||||
{ label: "سلامت AI", to: "/admin/ai-health", icon: Activity },
|
{ label: "سلامت AI", to: "/admin/ai-health", icon: Activity },
|
||||||
{ label: "مدیریت محتوا", to: "/admin/moderation", icon: MessageSquareWarning },
|
{ label: "مدیریت محتوا", to: "/admin/moderation", icon: MessageSquareWarning },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user