Files
flatrender/src/components/admin/RankingAdmin.tsx
T

96 lines
4.6 KiB
TypeScript
Raw Normal View History

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