134 lines
5.1 KiB
TypeScript
134 lines
5.1 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import { useState } from "react";
|
|||
|
|
import { fetchLinks, FALLBACK_LINKS, type SiteLinks } from "@/lib/links";
|
|||
|
|
import { API_URL } from "@/lib/site";
|
|||
|
|
|
|||
|
|
type Field = { key: keyof SiteLinks; label: string; type: "text" | "bool" };
|
|||
|
|
|
|||
|
|
const FIELDS: Field[] = [
|
|||
|
|
{ key: "bazaarUrl", label: "لینک کافهبازار", type: "text" },
|
|||
|
|
{ key: "bazaarEnabled", label: "نمایش دکمهٔ کافهبازار", type: "bool" },
|
|||
|
|
{ key: "myketUrl", label: "لینک مایکت", type: "text" },
|
|||
|
|
{ key: "myketEnabled", label: "نمایش دکمهٔ مایکت", type: "bool" },
|
|||
|
|
{ key: "directApkUrl", label: "لینک دانلود مستقیم APK", type: "text" },
|
|||
|
|
{ key: "directApkEnabled", label: "نمایش دانلود مستقیم", type: "bool" },
|
|||
|
|
{ key: "webPlayUrl", label: "آدرس بازی (وب)", type: "text" },
|
|||
|
|
{ key: "iosPwaEnabled", label: "نمایش نصب iOS/PWA", type: "bool" },
|
|||
|
|
{ key: "instagram", label: "اینستاگرام", type: "text" },
|
|||
|
|
{ key: "telegram", label: "تلگرام", type: "text" },
|
|||
|
|
{ key: "supportEmail", label: "ایمیل پشتیبانی", type: "text" },
|
|||
|
|
{ key: "supportPhone", label: "تلفن پشتیبانی", type: "text" },
|
|||
|
|
{ key: "appVersion", label: "نسخهٔ اپ", type: "text" },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export default function AdminPage() {
|
|||
|
|
const [token, setToken] = useState("");
|
|||
|
|
const [authed, setAuthed] = useState(false);
|
|||
|
|
const [links, setLinks] = useState<SiteLinks>(FALLBACK_LINKS);
|
|||
|
|
const [msg, setMsg] = useState<string | null>(null);
|
|||
|
|
const [busy, setBusy] = useState(false);
|
|||
|
|
|
|||
|
|
async function login() {
|
|||
|
|
setBusy(true);
|
|||
|
|
setMsg(null);
|
|||
|
|
const l = await fetchLinks();
|
|||
|
|
setLinks(l);
|
|||
|
|
setAuthed(true);
|
|||
|
|
setBusy(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function save() {
|
|||
|
|
setBusy(true);
|
|||
|
|
setMsg(null);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(`${API_URL}/api/admin/site/links`, {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json", "X-Admin-Token": token },
|
|||
|
|
body: JSON.stringify(links),
|
|||
|
|
});
|
|||
|
|
if (res.status === 401) {
|
|||
|
|
setMsg("توکن نامعتبر است.");
|
|||
|
|
} else if (!res.ok) {
|
|||
|
|
setMsg("خطا در ذخیره.");
|
|||
|
|
} else {
|
|||
|
|
setLinks(await res.json());
|
|||
|
|
setMsg("ذخیره شد ✓");
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
setMsg("سرور در دسترس نیست.");
|
|||
|
|
}
|
|||
|
|
setBusy(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function set<K extends keyof SiteLinks>(k: K, v: SiteLinks[K]) {
|
|||
|
|
setLinks((p) => ({ ...p, [k]: v }));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!authed) {
|
|||
|
|
return (
|
|||
|
|
<section className="mx-auto max-w-md px-4 py-20">
|
|||
|
|
<h1 className="text-2xl font-black gold-text">ورود مدیریت</h1>
|
|||
|
|
<p className="mt-2 text-sm text-cream/60">توکن مدیریت را وارد کن.</p>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
value={token}
|
|||
|
|
onChange={(e) => setToken(e.target.value)}
|
|||
|
|
placeholder="ADMIN_TOKEN"
|
|||
|
|
className="mt-5 w-full rounded-xl bg-navy-800 px-4 py-3 text-cream outline-none ring-1 ring-gold/20 focus:ring-gold/50"
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
onClick={login}
|
|||
|
|
disabled={!token || busy}
|
|||
|
|
className="mt-4 w-full rounded-xl btn-gold px-4 py-3 disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
ورود
|
|||
|
|
</button>
|
|||
|
|
<p className="mt-3 text-xs text-cream/45">
|
|||
|
|
توکن همان مقدار ADMIN_TOKEN در فایل محیطی سرور است. ذخیره هنگام «ثبت» اعتبارسنجی میشود.
|
|||
|
|
</p>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<section className="mx-auto max-w-2xl px-4 py-14">
|
|||
|
|
<h1 className="text-2xl font-black gold-text">مدیریت لینکها</h1>
|
|||
|
|
<p className="mt-2 text-sm text-cream/60">لینکهای کافهبازار، مایکت، شبکههای اجتماعی و پشتیبانی را اینجا تنظیم کن.</p>
|
|||
|
|
|
|||
|
|
<div className="mt-8 space-y-4">
|
|||
|
|
{FIELDS.map((f) =>
|
|||
|
|
f.type === "bool" ? (
|
|||
|
|
<label key={f.key} className="glass flex items-center justify-between rounded-xl px-4 py-3">
|
|||
|
|
<span>{f.label}</span>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={Boolean(links[f.key])}
|
|||
|
|
onChange={(e) => set(f.key, e.target.checked as never)}
|
|||
|
|
className="h-5 w-5 accent-[#d4af37]"
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
) : (
|
|||
|
|
<div key={f.key}>
|
|||
|
|
<label className="mb-1 block text-sm text-cream/70">{f.label}</label>
|
|||
|
|
<input
|
|||
|
|
dir="ltr"
|
|||
|
|
value={String(links[f.key] ?? "")}
|
|||
|
|
onChange={(e) => set(f.key, e.target.value as never)}
|
|||
|
|
className="w-full rounded-xl bg-navy-800 px-4 py-2.5 text-cream outline-none ring-1 ring-gold/15 focus:ring-gold/50"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="mt-8 flex items-center gap-3">
|
|||
|
|
<button onClick={save} disabled={busy} className="rounded-xl btn-gold px-6 py-3 disabled:opacity-50">
|
|||
|
|
ثبت تغییرات
|
|||
|
|
</button>
|
|||
|
|
{msg && <span className="text-sm text-cream/80">{msg}</span>}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|