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>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user