Marketing site (bargevasat.ir) + admin-editable store links + subdomain split
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

- 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>
This commit is contained in:
soroush.asadi
2026-06-08 07:19:43 +03:30
parent 8d0d4dc991
commit 5d38312ef0
39 changed files with 8207 additions and 2 deletions
+9
View File
@@ -0,0 +1,9 @@
node_modules
.next
out
.git
*.md
Dockerfile
.dockerignore
npm-debug.log*
tsconfig.tsbuildinfo
+7
View File
@@ -0,0 +1,7 @@
/node_modules
/.next/
/out/
/.turbo
*.tsbuildinfo
next-env.d.ts
.DS_Store
+23
View File
@@ -0,0 +1,23 @@
# Barg-e Vasat marketing site (Next.js static export → nginx).
FROM mirror.soroushasadi.com/node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
ARG NPM_REGISTRY=http://171.22.25.73:8081/repository/npm-group/
RUN npm ci --legacy-peer-deps --strict-ssl=false --no-audit --no-fund \
--registry "${NPM_REGISTRY}"
COPY . .
# Public URLs baked at build time (browser-facing).
ARG NEXT_PUBLIC_API_URL=https://api.bargevasat.ir
ARG NEXT_PUBLIC_APP_URL=https://app.bargevasat.ir
ARG NEXT_PUBLIC_SITE_URL=https://bargevasat.ir
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
RUN npm run build
FROM mirror.soroushasadi.com/nginx:alpine
COPY --from=build /app/out /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=10s --timeout=5s --retries=6 --start-period=10s \
CMD wget -q -O- http://127.0.0.1/ || exit 1
+133
View File
@@ -0,0 +1,133 @@
"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>
);
}
+76
View File
@@ -0,0 +1,76 @@
import type { Metadata } from "next";
import { PageShell } from "@/components/PageShell";
import { DownloadButtons } from "@/components/DownloadButtons";
export const metadata: Metadata = {
title: "دانلود و نصب",
description:
"برگ وسط را روی اندروید (کافه‌بازار، مایکت)، آیفون (نصب وب/PWA) یا مستقیماً در مرورگر اجرا کن. راهنمای گام‌به‌گام نصب.",
alternates: { canonical: "/download" },
};
function Steps({ items }: { items: string[] }) {
return (
<ol className="space-y-3">
{items.map((s, i) => (
<li key={i} className="flex gap-3">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full btn-gold text-sm font-black">
{i + 1}
</span>
<span className="pt-0.5 text-cream/80">{s}</span>
</li>
))}
</ol>
);
}
export default function DownloadPage() {
return (
<PageShell title="دانلود و نصب" subtitle="هر طور که دوست داری بازی کن — روی گوشی نصب کن یا مستقیم در مرورگر اجرا کن.">
<div className="mb-8">
<DownloadButtons variant="full" />
</div>
{/* Web */}
<div className="glass rounded-2xl p-6">
<h2 className="text-xl font-bold text-cream">🌐 بازی در مرورگر (بدون نصب)</h2>
<p className="mt-2 text-cream/70">
سریعترین راه: کافی است آدرس بازی را در مرورگر باز کنی و وارد شوی. هیچ نصبی لازم نیست و روی هر دستگاهی کار میکند.
</p>
</div>
{/* Android */}
<div id="android" className="glass rounded-2xl p-6">
<h2 className="text-xl font-bold text-cream">🤖 اندروید</h2>
<p className="mt-2 mb-4 text-cream/70">از کافهبازار یا مایکت نصب کن، یا اپ وب را به صفحهٔ اصلی اضافه کن:</p>
<Steps
items={[
"آدرس بازی را در مرورگر کروم باز کن.",
"روی منوی سه‌نقطهٔ بالا-راست بزن.",
"گزینهٔ «افزودن به صفحهٔ اصلی / Install app» را انتخاب کن.",
"آیکن برگ وسط مثل یک اپ روی گوشی‌ات می‌نشیند.",
]}
/>
</div>
{/* iOS */}
<div id="ios" className="glass rounded-2xl p-6">
<h2 className="text-xl font-bold text-cream">🍏 آیفون و آیپد (iOS)</h2>
<p className="mt-2 mb-4 text-cream/70">
روی iOS بازی را بهصورت وباپ (PWA) نصب کن درست مثل یک اپ واقعی، با آیکن روی صفحهٔ اصلی:
</p>
<Steps
items={[
"آدرس بازی را در مرورگر Safari باز کن.",
"روی دکمهٔ «اشتراک‌گذاری» (مربع با فلش رو به بالا) بزن.",
"کمی پایین برو و «Add to Home Screen / افزودن به صفحهٔ اصلی» را انتخاب کن.",
"روی «Add» بزن — آیکن برگ وسط روی صفحهٔ اصلی اضافه می‌شود و تمام‌صفحه اجرا می‌شود.",
]}
/>
<p className="mt-4 text-sm text-cream/55">
نکته: روی آیفون حتماً از مرورگر Safari استفاده کن؛ افزودن به صفحهٔ اصلی فقط در Safari کار میکند.
</p>
</div>
</PageShell>
);
}
+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from "next";
import { PageShell } from "@/components/PageShell";
export const metadata: Metadata = {
title: "سوال‌های متداول",
description: "پاسخ پرسش‌های رایج دربارهٔ بازی حکم آنلاین برگ وسط — رایگان بودن، نصب، بازی با دوستان و سکه‌ها.",
alternates: { canonical: "/faq" },
};
const FAQ = [
{ q: "بازی رایگان است؟", a: "بله، برگ وسط کاملاً رایگان است. می‌توانی همهٔ بخش‌ها را بدون پرداخت بازی کنی. خرید سکه فقط اختیاری است." },
{ q: "چطور با دوستانم بازی کنم؟", a: "یک اتاق خصوصی بساز، کد اتاق را برای دوستانت بفرست و هم‌تیمی و حریف‌هایت را انتخاب کن." },
{ q: "اینترنت لازم دارم؟", a: "برای بازی آنلاین بله، اما بخش «بازی با کامپیوتر» کاملاً آفلاین کار می‌کند." },
{ q: "روی آیفون نصب می‌شود؟", a: "بله، روی iOS از طریق Safari بازی را به صفحهٔ اصلی اضافه کن (PWA). راهنمای کامل در صفحهٔ دانلود هست." },
{ q: "سکه‌ها به چه درد می‌خورند؟", a: "با سکه در لیگ‌های بالاتر بازی می‌کنی و آیتم‌های ظاهری مثل آواتار، طرح کارت و عنوان می‌خری." },
{ q: "اگر وسط بازی قطع شوم چه می‌شود؟", a: "بازی‌ات زنده می‌ماند و می‌توانی برگردی و ادامه دهی." },
{ q: "کُت (کوت) یعنی چه؟", a: "اگر تیم حاکم همهٔ ۷ دست را ببرد، حریف «کُت» می‌شود و امتیاز و جایزهٔ بیشتری می‌گیری." },
];
export default function FaqPage() {
const jsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: FAQ.map((f) => ({
"@type": "Question",
name: f.q,
acceptedAnswer: { "@type": "Answer", text: f.a },
})),
};
return (
<PageShell title="سوال‌های متداول">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<div className="space-y-3">
{FAQ.map((f) => (
<details key={f.q} className="glass group rounded-2xl p-5">
<summary className="cursor-pointer list-none text-lg font-bold text-cream marker:hidden">
{f.q}
</summary>
<p className="mt-3 text-cream/70">{f.a}</p>
</details>
))}
</div>
</PageShell>
);
}
+80
View File
@@ -0,0 +1,80 @@
@import "tailwindcss";
@import "@fontsource-variable/vazirmatn";
:root {
--navy-950: #070b18;
--navy-900: #0b1226;
--navy-800: #111a33;
--gold: #d4af37;
--gold-soft: #e7c873;
--teal: #2dd4bf;
--cream: #f5efe0;
}
@theme inline {
--color-navy-950: var(--navy-950);
--color-navy-900: var(--navy-900);
--color-navy-800: var(--navy-800);
--color-gold: var(--gold);
--color-gold-soft: var(--gold-soft);
--color-teal: var(--teal);
--color-cream: var(--cream);
--font-sans: "Vazirmatn Variable", ui-sans-serif, system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background: radial-gradient(120% 120% at 50% 0%, #0e1730 0%, var(--navy-950) 60%);
color: var(--cream);
font-family: "Vazirmatn Variable", ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
[dir="rtl"] {
font-family: "Vazirmatn Variable", ui-sans-serif, system-ui, sans-serif;
}
/* Utility helpers */
.gold-text {
background: linear-gradient(180deg, var(--gold-soft), var(--gold));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.glass {
background: rgba(17, 26, 51, 0.55);
border: 1px solid rgba(212, 175, 55, 0.18);
backdrop-filter: blur(10px);
}
.btn-gold {
background: linear-gradient(180deg, var(--gold-soft), var(--gold));
color: #1a1206;
font-weight: 800;
}
.btn-gold:hover {
filter: brightness(1.06);
}
.felt {
background:
radial-gradient(120% 120% at 50% 0%, rgba(45, 212, 191, 0.08), transparent 60%),
radial-gradient(80% 80% at 80% 90%, rgba(212, 175, 55, 0.06), transparent 60%);
}
.card-pattern {
background-image:
linear-gradient(45deg, rgba(212, 175, 55, 0.04) 25%, transparent 25%),
linear-gradient(-45deg, rgba(212, 175, 55, 0.04) 25%, transparent 25%);
background-size: 22px 22px;
}
+81
View File
@@ -0,0 +1,81 @@
import type { Metadata, Viewport } from "next";
import "./globals.css";
import { BRAND, SITE_URL } from "@/lib/site";
import { Nav } from "@/components/Nav";
import { Footer } from "@/components/Footer";
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: {
default: `${BRAND.nameFa} | بازی حکم آنلاین رایگان`,
template: `%s | ${BRAND.nameFa}`,
},
description: BRAND.descFa,
keywords: [
"حکم",
"بازی حکم",
"حکم آنلاین",
"بازی ورق ایرانی",
"برگ وسط",
"بازی کارتی آنلاین",
"حکم با دوستان",
"Hokm",
"Barg-e Vasat",
],
applicationName: BRAND.nameFa,
authors: [{ name: BRAND.nameFa }],
alternates: { canonical: "/" },
openGraph: {
type: "website",
locale: "fa_IR",
url: SITE_URL,
siteName: BRAND.nameFa,
title: `${BRAND.nameFa} | بازی حکم آنلاین رایگان`,
description: BRAND.descFa,
images: [{ url: "/og.png", width: 1200, height: 630, alt: BRAND.nameFa }],
},
twitter: {
card: "summary_large_image",
title: `${BRAND.nameFa} | بازی حکم آنلاین`,
description: BRAND.descFa,
images: ["/og.png"],
},
icons: { icon: "/icon.svg", apple: "/icon.svg" },
manifest: "/manifest.webmanifest",
};
export const viewport: Viewport = {
themeColor: "#070b18",
width: "device-width",
initialScale: 1,
};
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
name: "برگ وسط",
alternateName: "Barg-e Vasat",
description: BRAND.descFa,
url: SITE_URL,
applicationCategory: "GameApplication",
genre: "بازی کارتی",
operatingSystem: "Android, iOS, Web",
inLanguage: "fa-IR",
offers: { "@type": "Offer", price: "0", priceCurrency: "IRR" },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fa" dir="rtl">
<body>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<Nav />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
+20
View File
@@ -0,0 +1,20 @@
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "برگ وسط — بازی حکم آنلاین",
short_name: "برگ وسط",
description: "بازی حکم ایرانی آنلاین با دوستان و هوش مصنوعی.",
start_url: "/",
display: "standalone",
background_color: "#070b18",
theme_color: "#070b18",
dir: "rtl",
lang: "fa",
icons: [
{ src: "/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" },
],
};
}
+112
View File
@@ -0,0 +1,112 @@
import {
Users, Bot, Trophy, Gift, MessageCircle, Globe, ShieldCheck, Zap, Crown, Star,
} from "lucide-react";
import { DownloadButtons } from "@/components/DownloadButtons";
import { BRAND } from "@/lib/site";
const FEATURES = [
{ icon: Users, title: "حکم ۴ نفره آنلاین", desc: "با بازیکن‌های واقعی از سراسر ایران، دونفره و تیمی بازی کن." },
{ icon: Bot, title: "بازی با هوش مصنوعی", desc: "آفلاین و بدون اینترنت، با ربات‌های هوشمند تمرین کن." },
{ icon: Trophy, title: "لیگ و رتبه‌بندی", desc: "از لیگ مبتدی تا استاد بالا برو و در جدول قهرمانان بدرخش." },
{ icon: Gift, title: "جایزه‌های روزانه", desc: "هر روز سکه بگیر، دستاورد باز کن و جوایز ویژه ببر." },
{ icon: MessageCircle, title: "چت و شکلک", desc: "سر میز با هم‌تیمی و حریف کل‌کل کن؛ استیکرهای فارسی." },
{ icon: Globe, title: "همه‌جا در دسترس", desc: "اندروید، آیفون و مرورگر — پیشرفتت همه‌جا همگام می‌شود." },
];
const STEPS = [
{ n: "۱", title: "وارد شو", desc: "با شماره موبایل ثبت‌نام کن — سریع و رایگان." },
{ n: "۲", title: "میز انتخاب کن", desc: "بازی سریع آنلاین، اتاق خصوصی با دوستان، یا بازی با کامپیوتر." },
{ n: "۳", title: "حکم بزن و ببر", desc: "حاکم شو، خال حکم را انتخاب کن و حریف را کُت کن!" },
];
const STATS = [
{ icon: Zap, label: "بازی سریع", value: "زیر ۱۵ ثانیه شروع" },
{ icon: ShieldCheck, label: "بدون تقلب", value: "سرور منصف و امن" },
{ icon: Crown, label: "کاملاً رایگان", value: "بدون اجبار خرید" },
];
export default function Home() {
return (
<>
{/* Hero */}
<section className="felt card-pattern">
<div className="mx-auto max-w-6xl px-4 py-16 text-center sm:py-24">
<span className="inline-flex items-center gap-1.5 rounded-full glass px-3 py-1 text-xs text-gold-soft">
<Star size={13} /> بازی حکمِ ایرانی، حرفهایتر از همیشه
</span>
<h1 className="mx-auto mt-6 max-w-3xl text-4xl font-black leading-tight sm:text-6xl">
<span className="gold-text">{BRAND.nameFa}</span>
<br />
بازی حکم آنلاین با دوستان
</h1>
<p className="mx-auto mt-5 max-w-2xl text-base leading-8 text-cream/70 sm:text-lg">
{BRAND.descFa}
</p>
<div className="mt-9 flex justify-center">
<DownloadButtons variant="hero" />
</div>
<div className="mx-auto mt-12 grid max-w-3xl gap-3 sm:grid-cols-3">
{STATS.map((s) => (
<div key={s.label} className="glass rounded-2xl px-4 py-4">
<s.icon className="mx-auto text-teal" size={22} />
<div className="mt-2 text-sm font-bold text-cream">{s.label}</div>
<div className="text-xs text-cream/55">{s.value}</div>
</div>
))}
</div>
</div>
</section>
{/* Features */}
<section id="features" className="mx-auto max-w-6xl px-4 py-16">
<h2 className="text-center text-3xl font-black sm:text-4xl">
چرا <span className="gold-text">برگ وسط</span>؟
</h2>
<p className="mx-auto mt-3 max-w-xl text-center text-cream/60">
همهٔ چیزی که یک بازی حکم بینقص لازم دارد، در یک اپ.
</p>
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map((f) => (
<div key={f.title} className="glass rounded-2xl p-6 transition hover:border-gold/40">
<f.icon className="text-gold" size={28} />
<h3 className="mt-4 text-lg font-bold text-cream">{f.title}</h3>
<p className="mt-2 text-sm leading-7 text-cream/65">{f.desc}</p>
</div>
))}
</div>
</section>
{/* How to play */}
<section className="mx-auto max-w-6xl px-4 py-16">
<div className="glass rounded-3xl p-8 sm:p-12">
<h2 className="text-center text-3xl font-black sm:text-4xl">در ۳ قدم شروع کن</h2>
<div className="mt-10 grid gap-6 sm:grid-cols-3">
{STEPS.map((s) => (
<div key={s.n} className="text-center">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full btn-gold text-2xl font-black">
{s.n}
</div>
<h3 className="mt-4 text-lg font-bold text-cream">{s.title}</h3>
<p className="mt-2 text-sm leading-7 text-cream/65">{s.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* Final CTA */}
<section className="mx-auto max-w-4xl px-4 py-16 text-center">
<h2 className="text-3xl font-black sm:text-4xl">
همین حالا <span className="gold-text">حکم</span> را شروع کن
</h2>
<p className="mx-auto mt-3 max-w-lg text-cream/65">
رایگان روی مرورگر بازی کن یا اپ را روی گوشیات نصب کن.
</p>
<div className="mt-8 flex justify-center">
<DownloadButtons variant="full" />
</div>
</section>
</>
);
}
+51
View File
@@ -0,0 +1,51 @@
import type { Metadata } from "next";
import { PageShell } from "@/components/PageShell";
import { BRAND } from "@/lib/site";
export const metadata: Metadata = {
title: "حریم خصوصی",
description: "سیاست حریم خصوصی برگ وسط: چه داده‌هایی جمع‌آوری می‌شود و چگونه از آن محافظت می‌کنیم.",
alternates: { canonical: "/privacy" },
};
export default function PrivacyPage() {
return (
<PageShell title="سیاست حریم خصوصی" subtitle="آخرین به‌روزرسانی: ۱۴۰۴">
<p>
برگ وسط ({BRAND.nameEn}) به حریم خصوصی شما احترام میگذارد. این سند توضیح میدهد که چه اطلاعاتی جمعآوری
میشود و چگونه استفاده میشود.
</p>
<h2 className="text-xl font-bold text-cream">۱. اطلاعاتی که جمعآوری میکنیم</h2>
<ul className="list-disc space-y-2 pr-5">
<li>شمارهٔ موبایل برای ورود و احراز هویت.</li>
<li>اطلاعات نمایه که خودتان وارد میکنید (نام نمایشی، آواتار، تنظیمات).</li>
<li>دادههای بازی مانند امتیاز، رتبه، سکه و دستاوردها.</li>
<li>اطلاعات فنی پایه برای پایداری سرویس (مانند نوع دستگاه و خطاها).</li>
</ul>
<h2 className="text-xl font-bold text-cream">۲. استفاده از اطلاعات</h2>
<p>
از اطلاعات فقط برای ارائهٔ سرویس بازی، ذخیرهٔ پیشرفت شما، جلوگیری از تقلب و بهبود تجربهٔ کاربری استفاده
میکنیم. اطلاعات شما را به اشخاص ثالث نمیفروشیم.
</p>
<h2 className="text-xl font-bold text-cream">۳. پرداختها</h2>
<p>
خریدهای درونبرنامهای از طریق درگاههای معتبر (زرینپال) و فروشگاهها (کافهبازار، مایکت) انجام میشود و
اطلاعات کارت بانکی شما نزد ما ذخیره نمیشود.
</p>
<h2 className="text-xl font-bold text-cream">۴. امنیت</h2>
<p>برای محافظت از دادهها از رمزنگاری و سرورهای امن استفاده میکنیم.</p>
<h2 className="text-xl font-bold text-cream">۵. حذف حساب</h2>
<p>
برای حذف حساب و دادههای مرتبط، از طریق ایمیل {BRAND.email} با ما تماس بگیرید.
</p>
<h2 className="text-xl font-bold text-cream">۶. تماس</h2>
<p>برای هر پرسشی دربارهٔ حریم خصوصی به {BRAND.email} ایمیل بزنید.</p>
</PageShell>
);
}
+12
View File
@@ -0,0 +1,12 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/site";
export const dynamic = "force-static";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/", disallow: "/admin" },
sitemap: `${SITE_URL}/sitemap.xml`,
host: SITE_URL,
};
}
+13
View File
@@ -0,0 +1,13 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/lib/site";
export const dynamic = "force-static";
export default function sitemap(): MetadataRoute.Sitemap {
const routes = ["", "/download", "/faq", "/support", "/privacy", "/terms"];
return routes.map((r) => ({
url: `${SITE_URL}${r}`,
changeFrequency: r === "" ? "weekly" : "monthly",
priority: r === "" ? 1 : 0.7,
}));
}
+24
View File
@@ -0,0 +1,24 @@
import type { Metadata } from "next";
import { PageShell } from "@/components/PageShell";
import { SupportContact } from "@/components/SupportContact";
export const metadata: Metadata = {
title: "پشتیبانی",
description: "از تیم پشتیبانی برگ وسط کمک بگیرید — ایمیل، تلگرام و اینستاگرام.",
alternates: { canonical: "/support" },
};
export default function SupportPage() {
return (
<PageShell title="پشتیبانی" subtitle="سوالی داری یا مشکلی پیش آمده؟ ما اینجاییم.">
<SupportContact />
<p className="text-sm text-cream/55">
پیش از تماس، نگاهی به{" "}
<a href="/faq" className="text-gold-soft underline">
سوالهای متداول
</a>{" "}
بینداز شاید جوابت همانجا باشد.
</p>
</PageShell>
);
}
+41
View File
@@ -0,0 +1,41 @@
import type { Metadata } from "next";
import { PageShell } from "@/components/PageShell";
import { BRAND } from "@/lib/site";
export const metadata: Metadata = {
title: "قوانین و مقررات",
description: "قوانین و شرایط استفاده از بازی حکم آنلاین برگ وسط.",
alternates: { canonical: "/terms" },
};
export default function TermsPage() {
return (
<PageShell title="قوانین و مقررات" subtitle="آخرین به‌روزرسانی: ۱۴۰۴">
<p>با استفاده از برگ وسط، شرایط زیر را میپذیرید.</p>
<h2 className="text-xl font-bold text-cream">۱. استفادهٔ مجاز</h2>
<p>
استفاده از تقلب، رباتهای غیرمجاز، سوءاستفاده از باگها یا هرگونه رفتار مخل بازی ممنوع است و میتواند به
مسدودسازی حساب منجر شود.
</p>
<h2 className="text-xl font-bold text-cream">۲. حساب کاربری</h2>
<p>مسئولیت حفظ امنیت حساب و فعالیتهای انجامشده با آن بر عهدهٔ شماست.</p>
<h2 className="text-xl font-bold text-cream">۳. سکه و خریدها</h2>
<p>
سکهها و آیتمهای مجازی ارزش واقعی پولی ندارند و قابل بازگشت به وجه نقد نیستند. خریدهای درونبرنامهای پس از
انجام، طبق قوانین فروشگاه مربوطه قابل بازگشتاند.
</p>
<h2 className="text-xl font-bold text-cream">۴. رفتار سر میز</h2>
<p>توهین، آزار و محتوای نامناسب در چت ممنوع است.</p>
<h2 className="text-xl font-bold text-cream">۵. تغییرات</h2>
<p>ممکن است این قوانین بهمرور بهروزرسانی شوند. ادامهٔ استفاده بهمنزلهٔ پذیرش نسخهٔ جدید است.</p>
<h2 className="text-xl font-bold text-cream">۶. تماس</h2>
<p>برای سوالها به {BRAND.email} ایمیل بزنید.</p>
</PageShell>
);
}
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { useEffect, useState } from "react";
import { Play, Smartphone, Download } from "lucide-react";
import { fetchLinks, FALLBACK_LINKS, type SiteLinks } from "@/lib/links";
import { APP_URL } from "@/lib/site";
function BazaarIcon() {
return <span className="text-lg">🛒</span>;
}
function MyketIcon() {
return <span className="text-lg">🟢</span>;
}
export function DownloadButtons({ variant = "hero" }: { variant?: "hero" | "full" }) {
const [links, setLinks] = useState<SiteLinks>(FALLBACK_LINKS);
useEffect(() => {
let on = true;
fetchLinks().then((l) => on && setLinks(l));
return () => {
on = false;
};
}, []);
const webUrl = links.webPlayUrl || APP_URL;
return (
<div className={variant === "hero" ? "flex flex-wrap gap-3" : "grid gap-3 sm:grid-cols-2"}>
{/* Always available: play in browser */}
<a href={webUrl} className="flex items-center justify-center gap-2 rounded-2xl btn-gold px-6 py-3.5 text-base">
<Play size={18} /> بازی در مرورگر (رایگان)
</a>
{links.bazaarEnabled && links.bazaarUrl && (
<a
href={links.bazaarUrl}
target="_blank"
rel="noopener"
className="flex items-center justify-center gap-2 rounded-2xl glass px-6 py-3.5 text-base text-cream hover:border-gold/40"
>
<BazaarIcon /> کافهبازار
</a>
)}
{links.myketEnabled && links.myketUrl && (
<a
href={links.myketUrl}
target="_blank"
rel="noopener"
className="flex items-center justify-center gap-2 rounded-2xl glass px-6 py-3.5 text-base text-cream hover:border-gold/40"
>
<MyketIcon /> مایکت
</a>
)}
{links.iosPwaEnabled && (
<a
href="/download#ios"
className="flex items-center justify-center gap-2 rounded-2xl glass px-6 py-3.5 text-base text-cream hover:border-gold/40"
>
<span className="text-lg">🍏</span> نصب روی آیفون (iOS)
</a>
)}
{variant === "full" && links.iosPwaEnabled && (
<a
href="/download#android"
className="flex items-center justify-center gap-2 rounded-2xl glass px-6 py-3.5 text-base text-cream hover:border-gold/40"
>
<Smartphone size={18} /> نصب روی اندروید (PWA)
</a>
)}
{links.directApkEnabled && links.directApkUrl && (
<a
href={links.directApkUrl}
className="flex items-center justify-center gap-2 rounded-2xl glass px-6 py-3.5 text-base text-cream hover:border-gold/40"
>
<Download size={18} /> دانلود مستقیم APK
</a>
)}
</div>
);
}
+48
View File
@@ -0,0 +1,48 @@
import Link from "next/link";
import { Logo } from "./Logo";
import { BRAND } from "@/lib/site";
export function Footer() {
return (
<footer className="mt-24 border-t border-gold/10 bg-navy-950/60">
<div className="mx-auto grid max-w-6xl gap-8 px-4 py-12 sm:grid-cols-2 md:grid-cols-4">
<div className="sm:col-span-2 md:col-span-1">
<div className="flex items-center gap-2">
<Logo size={30} />
<span className="font-extrabold gold-text">{BRAND.nameFa}</span>
</div>
<p className="mt-3 max-w-xs text-sm leading-7 text-cream/60">{BRAND.descFa}</p>
</div>
<div>
<h4 className="mb-3 font-bold text-cream">بازی</h4>
<ul className="space-y-2 text-sm text-cream/65">
<li><Link href="/#features" className="hover:text-cream">ویژگیها</Link></li>
<li><Link href="/download" className="hover:text-cream">دانلود و نصب</Link></li>
<li><Link href="/faq" className="hover:text-cream">سوالهای متداول</Link></li>
</ul>
</div>
<div>
<h4 className="mb-3 font-bold text-cream">قوانین</h4>
<ul className="space-y-2 text-sm text-cream/65">
<li><Link href="/privacy" className="hover:text-cream">حریم خصوصی</Link></li>
<li><Link href="/terms" className="hover:text-cream">قوانین و مقررات</Link></li>
<li><Link href="/support" className="hover:text-cream">پشتیبانی</Link></li>
</ul>
</div>
<div>
<h4 className="mb-3 font-bold text-cream">ارتباط</h4>
<ul className="space-y-2 text-sm text-cream/65">
<li><a href={`mailto:${BRAND.email}`} className="hover:text-cream">{BRAND.email}</a></li>
</ul>
</div>
</div>
<div className="border-t border-gold/10 py-5 text-center text-xs text-cream/45">
© {new Date().getFullYear()} {BRAND.nameFa} همهٔ حقوق محفوظ است.
</div>
</footer>
);
}
+24
View File
@@ -0,0 +1,24 @@
export function Logo({ size = 36 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none" aria-hidden>
<rect x="6" y="6" width="88" height="88" rx="22" fill="url(#lg)" stroke="#d4af37" strokeWidth="3" />
<text
x="50"
y="62"
textAnchor="middle"
fontSize="46"
fontWeight="900"
fill="#d4af37"
fontFamily="Vazirmatn Variable, sans-serif"
>
و
</text>
<defs>
<linearGradient id="lg" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse">
<stop stopColor="#111a33" />
<stop offset="1" stopColor="#070b18" />
</linearGradient>
</defs>
</svg>
);
}
+72
View File
@@ -0,0 +1,72 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { Menu, X, Play } from "lucide-react";
import { Logo } from "./Logo";
import { APP_URL, BRAND } from "@/lib/site";
const NAV = [
{ href: "/#features", label: "ویژگی‌ها" },
{ href: "/download", label: "دانلود و نصب" },
{ href: "/faq", label: "سوال‌ها" },
{ href: "/support", label: "پشتیبانی" },
];
export function Nav() {
const [open, setOpen] = useState(false);
return (
<header className="sticky top-0 z-50 glass">
<nav className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3">
<Link href="/" className="flex items-center gap-2">
<Logo size={34} />
<span className="text-lg font-extrabold gold-text">{BRAND.nameFa}</span>
</Link>
<div className="hidden items-center gap-6 md:flex">
{NAV.map((n) => (
<Link key={n.href} href={n.href} className="text-sm text-cream/80 hover:text-cream">
{n.label}
</Link>
))}
</div>
<div className="flex items-center gap-2">
<a
href={APP_URL}
className="hidden items-center gap-1.5 rounded-xl btn-gold px-4 py-2 text-sm sm:flex"
>
<Play size={16} /> بازی در مرورگر
</a>
<button
className="rounded-lg p-2 text-cream md:hidden"
onClick={() => setOpen((v) => !v)}
aria-label="منو"
>
{open ? <X size={22} /> : <Menu size={22} />}
</button>
</div>
</nav>
{open && (
<div className="border-t border-gold/10 px-4 pb-4 md:hidden">
<div className="flex flex-col gap-1 pt-2">
{NAV.map((n) => (
<Link
key={n.href}
href={n.href}
onClick={() => setOpen(false)}
className="rounded-lg px-3 py-2 text-cream/85 hover:bg-navy-800"
>
{n.label}
</Link>
))}
<a href={APP_URL} className="mt-2 flex items-center justify-center gap-1.5 rounded-xl btn-gold px-4 py-2.5">
<Play size={16} /> بازی در مرورگر
</a>
</div>
</div>
)}
</header>
);
}
+21
View File
@@ -0,0 +1,21 @@
export function PageShell({
title,
subtitle,
children,
}: {
title: string;
subtitle?: string;
children: React.ReactNode;
}) {
return (
<section className="mx-auto max-w-3xl px-4 py-14">
<h1 className="text-3xl font-black sm:text-4xl gold-text">{title}</h1>
{subtitle && <p className="mt-3 text-cream/65">{subtitle}</p>}
<div className="mt-8 space-y-5 leading-8 text-cream/80">{children}</div>
</section>
);
}
export function Prose({ children }: { children: React.ReactNode }) {
return <div className="glass rounded-2xl p-6 leading-8 text-cream/80">{children}</div>;
}
+41
View File
@@ -0,0 +1,41 @@
"use client";
import { useEffect, useState } from "react";
import { Mail, Phone, Send } from "lucide-react";
import { fetchLinks, FALLBACK_LINKS, type SiteLinks } from "@/lib/links";
export function SupportContact() {
const [links, setLinks] = useState<SiteLinks>(FALLBACK_LINKS);
useEffect(() => {
let on = true;
fetchLinks().then((l) => on && setLinks(l));
return () => {
on = false;
};
}, []);
const email = links.supportEmail || FALLBACK_LINKS.supportEmail;
return (
<div className="grid gap-3 sm:grid-cols-2">
<a href={`mailto:${email}`} className="glass flex items-center gap-3 rounded-2xl p-5 hover:border-gold/40">
<Mail className="text-gold" /> <span>{email}</span>
</a>
{links.supportPhone && (
<a href={`tel:${links.supportPhone}`} className="glass flex items-center gap-3 rounded-2xl p-5 hover:border-gold/40">
<Phone className="text-gold" /> <span dir="ltr">{links.supportPhone}</span>
</a>
)}
{links.telegram && (
<a href={links.telegram} target="_blank" rel="noopener" className="glass flex items-center gap-3 rounded-2xl p-5 hover:border-gold/40">
<Send className="text-teal" /> <span>تلگرام پشتیبانی</span>
</a>
)}
{links.instagram && (
<a href={links.instagram} target="_blank" rel="noopener" className="glass flex items-center gap-3 rounded-2xl p-5 hover:border-gold/40">
<span className="text-lg">📷</span> <span>اینستاگرام</span>
</a>
)}
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
// Lint is skipped during `next build` (see next.config.ts). This minimal flat
// config keeps `eslint` runnable without extra deps.
export default [];
+46
View File
@@ -0,0 +1,46 @@
import { API_URL, APP_URL } from "./site";
export interface SiteLinks {
bazaarUrl: string;
bazaarEnabled: boolean;
myketUrl: string;
myketEnabled: boolean;
directApkUrl: string;
directApkEnabled: boolean;
webPlayUrl: string;
iosPwaEnabled: boolean;
instagram: string;
telegram: string;
supportEmail: string;
supportPhone: string;
appVersion: string;
}
// Safe defaults used until the API responds (or if it's unreachable).
export const FALLBACK_LINKS: SiteLinks = {
bazaarUrl: "",
bazaarEnabled: false,
myketUrl: "",
myketEnabled: false,
directApkUrl: "",
directApkEnabled: false,
webPlayUrl: APP_URL,
iosPwaEnabled: true,
instagram: "",
telegram: "",
supportEmail: "support@bargevasat.ir",
supportPhone: "",
appVersion: "",
};
/** Fetch admin-editable links at runtime (client-side). Falls back gracefully. */
export async function fetchLinks(): Promise<SiteLinks> {
try {
const res = await fetch(`${API_URL}/api/site/links`, { cache: "no-store" });
if (!res.ok) return FALLBACK_LINKS;
const data = (await res.json()) as Partial<SiteLinks>;
return { ...FALLBACK_LINKS, ...data };
} catch {
return FALLBACK_LINKS;
}
}
+13
View File
@@ -0,0 +1,13 @@
// Build-time public config (baked into the static bundle).
export const API_URL = (process.env.NEXT_PUBLIC_API_URL || "https://api.bargevasat.ir").replace(/\/$/, "");
export const APP_URL = (process.env.NEXT_PUBLIC_APP_URL || "https://app.bargevasat.ir").replace(/\/$/, "");
export const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL || "https://bargevasat.ir").replace(/\/$/, "");
export const BRAND = {
nameFa: "برگ وسط",
nameEn: "Barg-e Vasat",
taglineFa: "بازی حکمِ آنلاین، رایگان و حرفه‌ای",
descFa:
"برگ وسط، بازی حکم ایرانی به‌صورت آنلاین: با دوستان یا هوش مصنوعی بازی کن، در لیگ‌ها بالا برو، سکه و دستاورد جمع کن. روی اندروید، iOS و مرورگر.",
email: "support@bargevasat.ir",
};
+12
View File
@@ -0,0 +1,12 @@
import type { NextConfig } from "next";
// Static export — the marketing site is fully static (SEO-friendly pre-rendered
// HTML) and served by nginx. Store links are fetched client-side at runtime from
// the API (so the admin can change them without a rebuild).
const nextConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
trailingSlash: true,
};
export default nextConfig;
+22
View File
@@ -0,0 +1,22 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Static export with trailingSlash: serve dir/index.html, or the .html twin.
location / {
try_files $uri $uri/ $uri.html /index.html;
}
# Never cache HTML — new deploys (new chunk hashes) are picked up immediately.
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Long-cache immutable, content-hashed build assets.
location /_next/static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
+6693
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "barg-e-vasat-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3030",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@fontsource-variable/vazirmatn": "^5.2.8",
"clsx": "^2.1.1",
"lucide-react": "^1.17.0",
"next": "16.2.7",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.7",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+5
View File
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
<rect x="6" y="6" width="88" height="88" rx="22" fill="#0b1226" stroke="#d4af37" stroke-width="4"/>
<text x="50" y="64" text-anchor="middle" font-size="48" font-weight="900" fill="#d4af37" font-family="Tahoma, sans-serif">و</text>
</svg>

After

Width:  |  Height:  |  Size: 331 B

+41
View File
@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}