Vitron-Front/app/routes/admin.homepage.tsx
Arda Samadi ef7804df0e 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>
2026-07-25 15:55:22 +03:30

220 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}