96 lines
4.6 KiB
TypeScript
96 lines
4.6 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import { useCallback, useEffect, useState } from "react";
|
|||
|
|
|
|||
|
|
interface Tpl {
|
|||
|
|
id: string; name: string; slug: string;
|
|||
|
|
view_count?: number; use_count?: number; rate_avg?: number; rate_count?: number; sort?: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const card = "rounded-xl border border-[#1e2235] bg-[#0f1120]";
|
|||
|
|
const inp = "rounded-lg border border-[#262b40] bg-[#0c0e1a] px-2 py-1 text-sm text-gray-100 outline-none focus:border-indigo-500";
|
|||
|
|
const ghost = "rounded-lg border border-[#262b40] px-2.5 py-1 text-xs text-gray-300 hover:bg-[#161a2e] disabled:opacity-50";
|
|||
|
|
|
|||
|
|
const SORTS = [
|
|||
|
|
{ key: "use_count_desc", label: "محبوبترین (استفاده)" },
|
|||
|
|
{ key: "view_count_desc", label: "پربازدیدترین" },
|
|||
|
|
{ key: "rating_desc", label: "بالاترین امتیاز" },
|
|||
|
|
{ key: "sort_asc", label: "ترتیب دستی" },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export function RankingAdmin() {
|
|||
|
|
const [rows, setRows] = useState<Tpl[]>([]);
|
|||
|
|
const [sort, setSort] = useState("use_count_desc");
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [draft, setDraft] = useState<Record<string, string>>({});
|
|||
|
|
const [msg, setMsg] = useState<string | null>(null);
|
|||
|
|
|
|||
|
|
const load = useCallback(async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
const r = await fetch(`/api/admin/resource/templates?sort=${sort}&pageSize=100&isPublished=true`, { cache: "no-store" })
|
|||
|
|
.then((x) => x.json()).catch(() => null);
|
|||
|
|
const list: Tpl[] = r?.data ?? (Array.isArray(r) ? r : []);
|
|||
|
|
setRows(list);
|
|||
|
|
setDraft(Object.fromEntries(list.map((t) => [t.id, String(t.sort ?? 0)])));
|
|||
|
|
setLoading(false);
|
|||
|
|
}, [sort]);
|
|||
|
|
useEffect(() => { load(); }, [load]);
|
|||
|
|
|
|||
|
|
const saveSort = async (id: string) => {
|
|||
|
|
const res = await fetch(`/api/admin/resource/templates/${id}/sort`, {
|
|||
|
|
method: "PATCH", headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ sort: Number(draft[id]) || 0 }),
|
|||
|
|
});
|
|||
|
|
setMsg(res.ok ? "ترتیب ذخیره شد ✓" : "خطا");
|
|||
|
|
setTimeout(() => setMsg(null), 2000);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-5" dir="rtl">
|
|||
|
|
<div className="flex flex-wrap items-end justify-between gap-3">
|
|||
|
|
<div>
|
|||
|
|
<h1 className="text-xl font-semibold text-white">رتبهبندی قالبها</h1>
|
|||
|
|
<p className="mt-1 text-sm text-gray-400">قالبها را بر اساس محبوبیت ببینید و وزن ترتیب دستی را برای «ویژه/پین» تنظیم کنید.</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{msg && <span className="text-xs text-gray-400">{msg}</span>}
|
|||
|
|
<select className={inp} value={sort} onChange={(e) => setSort(e.target.value)}>
|
|||
|
|
{SORTS.map((s) => <option key={s.key} value={s.key}>{s.label}</option>)}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className={`${card} overflow-hidden`}>
|
|||
|
|
<table className="w-full text-sm">
|
|||
|
|
<thead><tr className="border-b border-[#1e2235] text-right text-xs text-gray-500">
|
|||
|
|
<th className="px-4 py-3">#</th><th className="px-4 py-3">قالب</th>
|
|||
|
|
<th className="px-4 py-3">بازدید</th><th className="px-4 py-3">استفاده</th><th className="px-4 py-3">امتیاز</th>
|
|||
|
|
<th className="px-4 py-3">ترتیب دستی</th>
|
|||
|
|
</tr></thead>
|
|||
|
|
<tbody>
|
|||
|
|
{loading ? (
|
|||
|
|
<tr><td colSpan={6} className="px-4 py-8 text-center text-gray-500">در حال بارگذاری…</td></tr>
|
|||
|
|
) : rows.length === 0 ? (
|
|||
|
|
<tr><td colSpan={6} className="px-4 py-8 text-center text-gray-500">قالبی یافت نشد.</td></tr>
|
|||
|
|
) : rows.map((t, i) => (
|
|||
|
|
<tr key={t.id} className="border-b border-[#161a2e] hover:bg-[#12152a]">
|
|||
|
|
<td className="px-4 py-3 font-mono text-gray-500">{i + 1}</td>
|
|||
|
|
<td className="px-4 py-3 text-gray-200">{t.name}</td>
|
|||
|
|
<td className="px-4 py-3 text-gray-400">{(t.view_count ?? 0).toLocaleString("fa-IR")}</td>
|
|||
|
|
<td className="px-4 py-3 text-emerald-300">{(t.use_count ?? 0).toLocaleString("fa-IR")}</td>
|
|||
|
|
<td className="px-4 py-3 text-amber-300">{(t.rate_avg ?? 0).toFixed(1)} ({t.rate_count ?? 0})</td>
|
|||
|
|
<td className="px-4 py-3">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<input className={`${inp} w-20`} type="number" value={draft[t.id] ?? "0"} onChange={(e) => setDraft({ ...draft, [t.id]: e.target.value })} />
|
|||
|
|
<button className={ghost} onClick={() => saveSort(t.id)}>ذخیره</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
))}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|