228 lines
7.7 KiB
TypeScript
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 = "";
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|