Files
admin-panel-hokm/app/shop/page.tsx
T
2026-08-07 09:40:16 +03:30

228 lines
7.7 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { Upload } from "lucide-react";
import Shell from "@/components/Shell";
import EditableTable, { Col } from "@/components/EditableTable";
import { TableSkeleton } from "@/components/Skeleton";
import Thumb from "@/components/Thumb";
import { api, getAuth, ASSET_BASE } from "@/lib/api";
import { toastError } from "@/lib/toast";
type Catalog = Record<string, Record<string, unknown>[]>;
const TABS: { key: string; label: string; cols: Col[]; tpl: Record<string, unknown> }[] = [
{
key: "coin",
label: "سکه",
cols: [
{ key: "id", label: "شناسه" },
{ key: "title", label: "عنوان" },
{ key: "coins", label: "سکه", type: "number" },
{ key: "vip_days", label: "VIP روز", type: "number" },
{ key: "price_toman", label: "قیمت", type: "number" },
{ key: "bonus_pct", label: "٪هدیه", type: "number" },
{ key: "sku", label: "SKU", readonly: true },
{ key: "sort", label: "ترتیب", type: "number" },
{ key: "enabled", label: "فعال", type: "bool" },
],
tpl: { id: "", title: "", coins: 0, vip_days: 0, price_toman: 0, bonus_pct: 0, sort: 0, enabled: true },
},
{
key: "ticket",
label: "بلیط",
cols: [
{ key: "id", label: "شناسه" },
{ key: "title", label: "عنوان" },
{ key: "tickets", label: "بلیط", type: "number" },
{ key: "price_toman", label: "قیمت", type: "number" },
{ key: "sku", label: "SKU", readonly: true },
{ key: "sort", label: "ترتیب", type: "number" },
{ key: "enabled", label: "فعال", type: "bool" },
],
tpl: { id: "", title: "", tickets: 0, price_toman: 0, sort: 0, enabled: true },
},
{
key: "card",
label: "کارت",
cols: [
{ key: "id", label: "شناسه" },
{ key: "title", label: "عنوان" },
{ key: "price_coins", label: "قیمت (سکه)", type: "number" },
{ key: "sort", label: "ترتیب", type: "number" },
{ key: "enabled", label: "فعال", type: "bool" },
],
tpl: { id: "", title: "", price_coins: 0, sort: 0, enabled: true },
},
{
key: "booster",
label: "تجهیزات",
cols: [
{ key: "id", label: "شناسه" },
{ key: "title", label: "عنوان" },
{ key: "multiplier", label: "ضریب", type: "number" },
{ key: "hours", label: "ساعت", type: "number" },
{ key: "price_toman", label: "قیمت", type: "number" },
{ key: "sku", label: "SKU", readonly: true },
{ key: "sort", label: "ترتیب", type: "number" },
{ key: "enabled", label: "فعال", type: "bool" },
],
tpl: { id: "", title: "", multiplier: 2, hours: 24, price_toman: 0, sort: 0, enabled: true },
},
{
key: "vip",
label: "VIP",
cols: [
{ key: "id", label: "شناسه" },
{ key: "title", label: "عنوان" },
{ key: "months", label: "ماه", type: "number" },
{ key: "price_toman", label: "قیمت", type: "number" },
{ key: "sku", label: "SKU", readonly: true },
{ key: "sort", label: "ترتیب", type: "number" },
{ key: "enabled", label: "فعال", type: "bool" },
],
tpl: { id: "", title: "", months: 1, price_toman: 0, sort: 0, enabled: true },
},
{
key: "tier",
label: "میزها",
cols: [
{ key: "id", label: "شناسه" },
{ key: "title", label: "عنوان" },
{ key: "hands", label: "دست", type: "number" },
{ key: "entry", label: "ورودی", type: "number" },
{ key: "prize", label: "جایزه", type: "number" },
{ key: "xp", label: "XP", type: "number" },
{ key: "trophy", label: "جام", type: "number" },
{ key: "rank_reward", label: "رتبه", type: "number" },
{ key: "sort", label: "ترتیب", type: "number" },
{ key: "enabled", label: "فعال", type: "bool" },
],
tpl: { id: "", title: "", hands: 7, entry: 0, prize: 0, xp: 0, trophy: 0, rank_reward: 0, sort: 0, enabled: true },
},
];
export default function ShopPage() {
const [cat, setCat] = useState<Catalog>({});
const [tab, setTab] = useState("coin");
const [ver, setVer] = useState(1);
const [loading, setLoading] = useState(true);
async function load() {
setLoading(true);
try {
setCat(await api.get<Catalog>("/catalog"));
setVer((v) => v + 1);
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
const active = TABS.find((t) => t.key === tab)!;
const isCard = tab === "card";
return (
<Shell title="فروشگاه" subtitle="کاتالوگ سکه، بلیط، کارت، تجهیزات، VIP و میزها">
<div className="flex gap-2 mb-4 flex-wrap">
{TABS.map((t) => (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={`btn ${tab === t.key ? "btn-gold" : "btn-ghost"}`}
>
{t.label}
</button>
))}
</div>
{loading ? (
<TableSkeleton rows={5} cols={active.cols.length} />
) : (
<EditableTable
key={tab}
cols={active.cols}
rows={cat[tab] || []}
newTemplate={active.tpl}
onSave={async (row) => {
await api.post(`/catalog/${tab}`, row);
await load();
}}
onDelete={async (id) => {
if (!confirm("حذف آیتم؟")) return;
await api.del(`/catalog/${tab}?id=${encodeURIComponent(id)}`);
await load();
}}
preview={
isCard
? (row) =>
String(row.id) === "simple" ? (
<span className="text-[#8aa] text-xs">پیش‌فرض</span>
) : (
<Thumb src={`${ASSET_BASE}/cards/${row.id}/AS.png?v=${ver}`} size={44} rounded={6} />
)
: undefined
}
extra={isCard ? (row) => <DeckUpload id={String(row.id)} onDone={load} /> : undefined}
/>
)}
<p className="text-xs text-[#8aa] mt-3">
{isCard
? "برای هر اسکینِ کارت، یک فایلِ zip از تصاویرِ کارت‌ها (مثلِ AS.png، KH.png، back.jpg) آپلود کنید."
: "SKU برای سکه/بلیط/تجهیزات/VIP هنگام ساخت خودکار تولید می‌شود؛ همان را در پنل مایکت/کافه‌بازار ثبت کنید."}
</p>
</Shell>
);
}
// آپلودِ zipِ اسکینِ کارت (به /catalog/card/{id}/upload، فیلد "deck").
function DeckUpload({ id, onDone }: { id: string; onDone: () => void }) {
const ref = useRef<HTMLInputElement>(null);
const [busy, setBusy] = useState(false);
async function upload(file: File) {
setBusy(true);
try {
const fd = new FormData();
fd.append("deck", file);
const auth = getAuth();
const res = await fetch(`${ASSET_BASE}/admin/api/catalog/card/${encodeURIComponent(id)}/upload`, {
method: "POST",
headers: auth ? { Authorization: `Basic ${auth}` } : {},
body: fd,
});
if (!res.ok) throw new Error("آپلودِ zip ناموفق بود");
onDone();
} catch (e) {
toastError((e as Error).message);
} finally {
setBusy(false);
}
}
return (
<>
<button
className="btn btn-ghost"
disabled={busy}
onClick={() => ref.current?.click()}
title="آپلود zip کارت‌ها"
>
<Upload size={14} />
</button>
<input
ref={ref}
type="file"
accept=".zip"
hidden
onChange={(e) => {
const f = e.target.files?.[0];
if (f) upload(f);
e.target.value = "";
}}
/>
</>
);
}