Files
soroush.asadi 5d38312ef0
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 4m40s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m7s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 41s
Marketing site (bargevasat.ir) + admin-editable store links + subdomain split
- New standalone Next.js marketing site under site/ (static export, SEO):
  landing, download/install guide (Bazaar/Myket/iOS-PWA/web), FAQ (JSON-LD),
  privacy, terms, support, /admin link editor. fa RTL, sitemap/robots/manifest.
- Backend: SiteLinksService (JSON-file persisted) + GET /api/site/links (public)
  + POST /api/admin/site/links (X-Admin-Token). ADMIN_TOKEN + Site__DataDir via env.
- compose: hokm-site service (:1520) + hokm_data volume for links JSON.
- CI deploy job builds + deploys the site container.
- deploy/SUBDOMAIN_SPLIT.md: nginx blocks, cert reissue, DNS, ENV split.
- Exclude site/ from root tsc + web docker context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:19:43 +03:30

134 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}