TorbatYar/frontend/modules/commercial/features/landing-marketing.tsx
2026-07-28 20:39:10 +03:30

332 lines
15 KiB
TypeScript
Raw 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 { useEffect, useState } from "react";
import Link from "next/link";
import { Badge, Button, Container } from "@/components/ui";
import { useAuth } from "@/hooks/useAuth";
import { useTheme } from "@/hooks/useTheme";
import {
loadCommercialBundles,
loadCommercialPricing,
loadCommercialProducts,
} from "../adapters";
import { commercialGet, extractItems } from "../adapters/http";
import type { CommercialBundle, CommercialPricingItem, CommercialProduct } from "../types";
const JOURNEY = [
"کشف نیاز",
"پیشنهاد هوشمند",
"باندل و قیمت",
"Trial رایگان",
"Workspace",
"تولید",
];
const FAQ = [
{
q: "آیا می‌توانم بدون کارت بانکی شروع کنم؟",
a: "بله. پس از ساخت workspace می‌توانید دوره آزمایشی را از Billing فعال کنید — جزئیات trial از رجیستری قیمت‌گذاری می‌آید.",
},
{
q: "محصولات جدید چطور اضافه می‌شوند؟",
a: "با ثبت در رجیستری تجاری Admin. بدون تغییر frontend در کاتالوگ و لانچر ظاهر می‌شوند.",
},
{
q: "دامنه اختصاصی کی باز می‌شود؟",
a: "بر اساس سیاست دامنه پس از entitlement / اشتراک فعال — تا آن زمان فقط زیردامنه پلتفرم.",
},
{
q: "آیا باندل قیمت ثابت دارد؟",
a: "خیر. باندل فقط به pricing_refs ارجاع می‌دهد؛ مبالغ از کاتالوگ قیمت runtime خوانده می‌شود.",
},
];
export function CommercialMarketingLanding() {
const { theme } = useTheme();
const { isAuthenticated, login, loading: authLoading } = useAuth();
const siteName = theme?.site_name ?? "TorbatYar";
const [products, setProducts] = useState<CommercialProduct[]>([]);
const [bundles, setBundles] = useState<CommercialBundle[]>([]);
const [pricing, setPricing] = useState<CommercialPricingItem[]>([]);
const [businessTypes, setBusinessTypes] = useState<
Array<{ business_type_code: string; display_name: string }>
>([]);
useEffect(() => {
void Promise.all([
loadCommercialProducts(),
loadCommercialBundles(),
loadCommercialPricing(),
commercialGet<unknown>("/api/v1/commercial/business-types"),
]).then(([p, b, pr, bt]) => {
if (p.availability === "ready") setProducts(p.data.filter((x) => x.visibility !== "hidden"));
if (b.availability === "ready") setBundles(b.data);
if (pr.availability === "ready") setPricing(pr.data.slice(0, 4));
if (bt.ok) {
setBusinessTypes(
extractItems<{ business_type_code: string; display_name: string }>(bt.data, [
"items",
"business_types",
])
);
}
});
}, []);
return (
<div className="pb-20">
{/* Hero — one composition, brand-first, full-bleed atmosphere */}
<section className="relative min-h-[min(92vh,820px)] overflow-hidden">
<div
className="absolute inset-0"
style={{
background:
"radial-gradient(ellipse 80% 60% at 70% 20%, rgba(2,132,199,0.22), transparent 55%), radial-gradient(ellipse 50% 40% at 10% 80%, rgba(15,23,42,0.08), transparent 50%), linear-gradient(165deg, #0b1220 0%, #0f2744 42%, #0c4a6e 100%)",
}}
/>
<div
className="pointer-events-none absolute inset-0 opacity-[0.12]"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\")",
}}
/>
<Container className="relative flex min-h-[min(92vh,820px)] flex-col justify-center py-16">
<p className="text-sm font-semibold tracking-[0.2em] text-sky-200/90">{siteName}</p>
<h1 className="mt-4 max-w-3xl text-4xl font-extrabold leading-[1.15] text-white sm:text-5xl lg:text-6xl">
سیستمعامل کسبوکار شما روی ابر
</h1>
<p className="mt-5 max-w-xl text-base leading-8 text-sky-100/85 sm:text-lg">
از کشف نیاز تا داشبورد تولید محصولات، باندل و قیمت از رجیستری زنده؛ بدون کاتالوگ ثابت.
</p>
<div className="mt-9 flex flex-wrap gap-3">
<Link href="/discover">
<Button size="lg" className="bg-white text-secondary hover:bg-sky-50">
شروع Trial رایگان
</Button>
</Link>
<Link href="/pricing">
<Button
size="lg"
variant="outline"
className="border-white/40 bg-transparent text-white hover:bg-white/10"
>
مقایسه قیمت
</Button>
</Link>
{isAuthenticated ? (
<Link href="/dashboard">
<Button size="lg" variant="ghost" className="text-sky-100 hover:bg-white/10">
داشبورد
</Button>
</Link>
) : (
<Button
size="lg"
variant="ghost"
className="text-sky-100 hover:bg-white/10"
loading={authLoading}
onClick={() => login()}
>
ورود
</Button>
)}
</div>
</Container>
</section>
{/* Business selector teaser */}
<section className="border-b border-gray-100 bg-white py-16 dark:border-gray-800 dark:bg-gray-950">
<Container>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">کشف کسبوکار</h2>
<p className="mt-2 max-w-2xl text-sm text-gray-600 dark:text-gray-400">
نوع کسبوکار از رجیستری میآید؛ ارزیابی چندمرحلهای نیازها را به پیشنهاد محصول و باندل وصل
میکند.
</p>
<div className="mt-8 flex flex-wrap gap-2">
{businessTypes.length ? (
businessTypes.slice(0, 12).map((t) => (
<Link
key={t.business_type_code}
href={`/discover?type=${encodeURIComponent(t.business_type_code)}`}
className="rounded-full border border-gray-200 px-4 py-2 text-sm text-secondary transition hover:border-primary hover:bg-[var(--color-primary-soft)] dark:border-gray-700 dark:text-gray-200"
>
{t.display_name}
</Link>
))
) : (
<Link href="/discover">
<Button>شروع ارزیابی کسبوکار</Button>
</Link>
)}
</div>
</Container>
</section>
{/* Product ecosystem */}
<section className="py-16">
<Container>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">اکوسیستم محصولات</h2>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
کشف پویا از رجیستری افزودن محصول در Admin بدون تغییر این صفحه.
</p>
{products.length ? (
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{products.slice(0, 9).map((p) => (
<li
key={p.product_code}
className="border-b border-gray-100 pb-4 dark:border-gray-800"
>
<p className="font-semibold text-secondary dark:text-gray-100">{p.display_name}</p>
<p className="mt-1 text-xs text-gray-500">{p.description || p.category || "—"}</p>
</li>
))}
</ul>
) : (
<p className="mt-6 text-sm text-gray-500">
کاتالوگ محصول هنوز از API پر نشده داده جعلی نمایش داده نمیشود.
</p>
)}
</Container>
</section>
{/* Feature / industry / bundle / pricing teasers */}
<section className="bg-[var(--surface-muted)] py-16 dark:bg-gray-900/40">
<Container className="space-y-14">
<div>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">قابلیتهای پلتفرم</h2>
<p className="mt-2 max-w-2xl text-sm text-gray-600 dark:text-gray-400">
ارزیابی metadata-driven، پیشنهاد runtime، قفل قابلیت، دامنه بر اساس سیاست، Billing و
لانچر اپها همه از Core Commercial.
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">صنایع</h2>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
صنایع نامحدود از business-types registry نه لیست ثابت frontend.
</p>
{!businessTypes.length ? (
<p className="mt-4 text-sm text-gray-500">انواع کسبوکار از API در دسترس نیست.</p>
) : null}
</div>
<div>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">باندلها</h2>
{bundles.length ? (
<ul className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{bundles.slice(0, 6).map((b) => (
<li key={b.bundle_code}>
<p className="text-lg font-bold text-secondary dark:text-gray-100">
{b.display_name}
</p>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
{b.description || "باندل تجاری از رجیستری"}
</p>
{b.recommendation_badge ? (
<Badge variant="primary" className="mt-2">
{b.recommendation_badge}
</Badge>
) : null}
</li>
))}
</ul>
) : (
<p className="mt-4 text-sm text-gray-500">باندلی در رجیستری نیست.</p>
)}
<Link href="/discover" className="mt-6 inline-block text-sm font-semibold text-primary">
مقایسه و انتخاب باندل
</Link>
</div>
<div>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">نمای قیمت</h2>
{pricing.length ? (
<ul className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{pricing.map((item) => (
<li key={item.pricing_item_code}>
<p className="font-semibold text-secondary dark:text-gray-100">
{item.display_name}
</p>
<p className="mt-1 text-xs text-gray-500">
{item.pricing_model || "—"} ·{" "}
{item.amount_minor != null
? `${item.amount_minor} ${item.currency || ""}`
: "مبلغ از بک‌اند"}
</p>
</li>
))}
</ul>
) : (
<p className="mt-4 text-sm text-gray-500">آیتم قیمت از API نیست بدون مبلغ ساختگی.</p>
)}
<Link href="/pricing" className="mt-6 inline-block text-sm font-semibold text-primary">
مشاهده کامل قیمتگذاری
</Link>
</div>
</Container>
</section>
{/* Customer journey */}
<section className="py-16">
<Container>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">مسیر مشتری</h2>
<ol className="mt-8 grid gap-4 sm:grid-cols-3 lg:grid-cols-6">
{JOURNEY.map((step, i) => (
<li key={step} className="text-sm">
<span className="font-mono text-xs text-primary">{String(i + 1).padStart(2, "0")}</span>
<p className="mt-1 font-semibold text-secondary dark:text-gray-100">{step}</p>
</li>
))}
</ol>
</Container>
</section>
{/* Trust + FAQ + CTA */}
<section className="border-t border-gray-100 bg-white py-16 dark:border-gray-800 dark:bg-gray-950">
<Container className="grid gap-12 lg:grid-cols-2">
<div>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">اعتماد سازمانی</h2>
<ul className="mt-4 space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li>چندمستأجری و SSO یکپارچه</li>
<li>سیاست دامنه و entitlement قابل ممیزی</li>
<li>رجیستری نسخهدار محصولات و قیمت</li>
<li>بدون کاتالوگ جعلی در frontend</li>
</ul>
</div>
<div>
<h2 className="text-2xl font-bold text-secondary dark:text-gray-100">سوالات متداول</h2>
<dl className="mt-4 space-y-4">
{FAQ.map((item) => (
<div key={item.q}>
<dt className="text-sm font-semibold text-secondary dark:text-gray-100">{item.q}</dt>
<dd className="mt-1 text-sm text-gray-600 dark:text-gray-400">{item.a}</dd>
</div>
))}
</dl>
</div>
</Container>
<Container className="mt-14 text-center">
<h2 className="text-3xl font-extrabold text-secondary dark:text-gray-100">
آماده ساخت workspace هستید؟
</h2>
<p className="mx-auto mt-3 max-w-lg text-sm text-gray-600 dark:text-gray-400">
ارزیابی را شروع کنید، باندل را انتخاب کنید، trial بگیرید و وارد داشبورد تجاری شوید.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-3">
<Link href="/discover">
<Button size="lg">شروع رایگان</Button>
</Link>
<Link href="/register?redirect=/onboarding">
<Button size="lg" variant="outline">
ثبتنام
</Button>
</Link>
</div>
</Container>
</section>
</div>
);
}