Link active apps on homepage and refresh landing page design.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Mortezakoohjani 2026-07-26 22:48:46 +03:30
parent 28f84b1274
commit 579e0cdaf5
3 changed files with 190 additions and 40 deletions

View File

@ -4,6 +4,7 @@ import Link from "next/link";
import { AppStoreGrid } from "@/components/AppStoreGrid"; import { AppStoreGrid } from "@/components/AppStoreGrid";
import { TenantSitePage } from "@/components/TenantSitePage"; import { TenantSitePage } from "@/components/TenantSitePage";
import { Badge, Button, Container } from "@/components/ui"; import { Badge, Button, Container } from "@/components/ui";
import { PLATFORM_APPS } from "@/lib/apps-catalog";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { useTenantHost } from "@/hooks/useTenantHost"; import { useTenantHost } from "@/hooks/useTenantHost";
import { useTheme } from "@/hooks/useTheme"; import { useTheme } from "@/hooks/useTheme";
@ -16,51 +17,140 @@ export default function HomePage() {
return <PlatformHomePage />; return <PlatformHomePage />;
} }
const FEATURES = [
{ icon: "🔐", label: "ورود واحد SSO" },
{ icon: "🏢", label: "چند کسب‌وکار" },
{ icon: "☁️", label: "ابری و مقیاس‌پذیر" },
{ icon: "🎨", label: "White-label" },
];
function PlatformHomePage() { function PlatformHomePage() {
const { theme } = useTheme(); const { theme } = useTheme();
const { isAuthenticated, login, loading } = useAuth(); const { isAuthenticated, login, loading } = useAuth();
const siteName = theme?.site_name ?? "Torbatyar";
const availableCount = PLATFORM_APPS.filter((app) => app.status === "available").length;
const quickApps = PLATFORM_APPS.filter((app) => app.status === "available" && app.href).slice(0, 4);
return ( return (
<div className="pb-16"> <div className="pb-16">
<section className="relative overflow-hidden border-b border-gray-100 bg-gradient-to-b from-[var(--color-primary-soft)] to-white"> {/* Hero */}
<Container className="relative py-12 sm:py-16"> <section className="relative overflow-hidden">
<div className="mx-auto max-w-2xl text-center"> <div className="absolute inset-0 bg-gradient-to-br from-[var(--color-primary-soft)] via-white to-[var(--color-secondary-muted)]" />
<Badge variant="primary" className="mb-4"> <div
className="absolute inset-0 opacity-[0.35]"
style={{
backgroundImage:
"radial-gradient(circle at 1px 1px, rgba(2,132,199,0.15) 1px, transparent 0)",
backgroundSize: "28px 28px",
}}
/>
<div className="pointer-events-none absolute -left-32 top-0 h-96 w-96 rounded-full bg-[var(--color-primary-muted)] blur-3xl" />
<div className="pointer-events-none absolute -right-20 bottom-0 h-72 w-72 rounded-full bg-[var(--color-secondary-muted)] blur-3xl" />
<Container className="relative py-14 sm:py-20">
<div className="mx-auto max-w-3xl text-center">
<Badge variant="primary" className="mb-5 px-4 py-1.5 text-sm">
نسل جدید SuperApp نسل جدید SuperApp
</Badge> </Badge>
<h1 className="text-3xl font-extrabold leading-tight text-secondary sm:text-4xl lg:text-5xl">
{theme?.site_name ?? "Torbatyar"} <h1 className="text-4xl font-extrabold leading-tight tracking-tight text-secondary sm:text-5xl lg:text-6xl">
{siteName}
</h1> </h1>
<p className="mt-4 text-base leading-8 text-gray-600 sm:text-lg">
یک پلتفرم، دهها سرویس حسابداری، CRM، فروشگاه، پیامک و بیشتر. <p className="mx-auto mt-5 max-w-xl text-base leading-8 text-gray-600 sm:text-lg">
همه با یک ورود واحد (SSO). یک پلتفرم، دهها سرویس حسابداری، CRM، سلامت، زیبایی و بیشتر.
همه با یک ورود واحد.
</p> </p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3"> <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
{isAuthenticated ? ( {isAuthenticated ? (
<Link href="/dashboard"> <Link href="/dashboard">
<Button size="lg">رفتن به داشبورد</Button> <Button size="lg" className="min-w-[160px] shadow-lg shadow-primary/20">
رفتن به داشبورد
</Button>
</Link> </Link>
) : ( ) : (
<Button size="lg" onClick={() => login()} loading={loading} loadingText="..."> <Button
size="lg"
className="min-w-[160px] shadow-lg shadow-primary/20"
onClick={() => login()}
loading={loading}
loadingText="..."
>
ورود / ثبتنام ورود / ثبتنام
</Button> </Button>
)} )}
<a href="#apps">
<Button variant="outline" size="lg">
مشاهده اپلیکیشنها
</Button>
</a>
</div>
{/* Stats */}
<div className="mt-10 flex flex-wrap items-center justify-center gap-6 sm:gap-10">
<div className="text-center">
<p className="text-3xl font-extrabold text-primary">{availableCount}+</p>
<p className="mt-0.5 text-sm text-gray-500">اپلیکیشن فعال</p>
</div>
<div className="hidden h-10 w-px bg-gray-200 sm:block" />
<div className="text-center">
<p className="text-3xl font-extrabold text-primary">۱</p>
<p className="mt-0.5 text-sm text-gray-500">ورود واحد</p>
</div>
<div className="hidden h-10 w-px bg-gray-200 sm:block" />
<div className="text-center">
<p className="text-3xl font-extrabold text-primary"></p>
<p className="mt-0.5 text-sm text-gray-500">کسبوکار</p>
</div>
</div>
</div>
{/* Quick access chips */}
<div className="mx-auto mt-12 max-w-2xl">
<p className="mb-3 text-center text-xs font-medium text-gray-400">دسترسی سریع</p>
<div className="flex flex-wrap items-center justify-center gap-2">
{quickApps.map((app) => (
<Link
key={app.id}
href={app.href!}
className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white/80 px-4 py-2 text-sm font-medium text-secondary shadow-sm backdrop-blur-sm transition-all hover:border-primary hover:text-primary hover:shadow-md"
>
<span>{app.icon}</span>
<span>{app.name}</span>
</Link>
))}
</div> </div>
</div> </div>
</Container> </Container>
<div className="pointer-events-none absolute -left-20 top-10 h-64 w-64 rounded-full bg-[var(--color-primary-muted)] blur-3xl" />
<div className="pointer-events-none absolute -right-10 bottom-0 h-48 w-48 rounded-full bg-[var(--color-secondary-muted)] blur-3xl" />
</section> </section>
<Container className="py-10 sm:py-14"> {/* Features strip */}
<AppStoreGrid /> <section className="border-y border-gray-100 bg-white/60 backdrop-blur-sm">
</Container> <Container className="py-5">
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
{FEATURES.map((f) => (
<div key={f.label} className="flex items-center gap-2 text-sm text-gray-600">
<span className="text-base">{f.icon}</span>
<span className="font-medium">{f.label}</span>
</div>
))}
</div>
</Container>
</section>
{/* App grid */}
<section id="apps" className="scroll-mt-20">
<Container className="py-12 sm:py-16">
<AppStoreGrid />
</Container>
</section>
{/* Footer */}
<footer className="border-t border-gray-100 bg-gray-50/80"> <footer className="border-t border-gray-100 bg-gray-50/80">
<Container className="flex flex-col items-center justify-between gap-3 py-6 text-center text-sm text-gray-500 sm:flex-row sm:text-right"> <Container className="flex flex-col items-center justify-between gap-3 py-6 text-center text-sm text-gray-500 sm:flex-row sm:text-right">
<p> <p>
© {new Date().getFullYear()} {theme?.site_name ?? "Torbatyar"} White-label SaaS © {new Date().getFullYear()} {siteName} White-label SaaS Platform
Platform
</p> </p>
<p>{theme?.support_email ?? "support@example.com"}</p> <p>{theme?.support_email ?? "support@example.com"}</p>
</Container> </Container>

View File

@ -4,17 +4,34 @@ import { PLATFORM_APPS } from "@/lib/apps-catalog";
import { AppTile, SectionTitle } from "@/components/ui"; import { AppTile, SectionTitle } from "@/components/ui";
export function AppStoreGrid() { export function AppStoreGrid() {
const availableApps = PLATFORM_APPS.filter((app) => app.status === "available");
const comingSoonApps = PLATFORM_APPS.filter((app) => app.status === "coming_soon");
return ( return (
<section> <div className="space-y-12">
<SectionTitle <section>
title="اپلیکیشن‌ها" <SectionTitle
subtitle="همه سرویس‌های پلتفرم در یک جا — مثل App Store" title="اپلیکیشن‌های فعال"
/> subtitle={`${availableApps.length} سرویس آماده استفاده — روی هر کدام کلیک کنید`}
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4 sm:gap-4 md:grid-cols-5 lg:grid-cols-6"> />
{PLATFORM_APPS.map((app) => ( <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 sm:gap-4 md:grid-cols-4 lg:grid-cols-5">
<AppTile key={app.id} app={app} /> {availableApps.map((app) => (
))} <AppTile key={app.id} app={app} />
</div> ))}
</section> </div>
</section>
<section>
<SectionTitle
title="به‌زودی"
subtitle="سرویس‌هایی که در حال توسعه هستند"
/>
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4 sm:gap-3 md:grid-cols-5 lg:grid-cols-6">
{comingSoonApps.map((app) => (
<AppTile key={app.id} app={app} variant="compact" />
))}
</div>
</section>
</div>
); );
} }

View File

@ -1,23 +1,23 @@
import Link from "next/link";
import type { PlatformApp } from "@/lib/apps-catalog"; import type { PlatformApp } from "@/lib/apps-catalog";
import { Badge } from "./Badge"; import { Badge } from "./Badge";
export interface AppTileProps { export interface AppTileProps {
app: PlatformApp; app: PlatformApp;
onClick?: () => void; onClick?: () => void;
variant?: "default" | "compact";
} }
export function AppTile({ app, onClick }: AppTileProps) { function AppTileContent({ app, variant = "default" }: { app: PlatformApp; variant?: "default" | "compact" }) {
const isAvailable = app.status === "available"; const isAvailable = app.status === "available";
const isCompact = variant === "compact";
return ( return (
<button <>
type="button"
onClick={onClick}
disabled={!isAvailable && !app.href}
className="group flex w-full flex-col items-center gap-3 rounded-2xl p-3 text-center transition-all hover:bg-[var(--color-primary-soft)] disabled:cursor-default sm:p-4"
>
<div <div
className={`relative flex h-[72px] w-[72px] items-center justify-center rounded-[22px] bg-gradient-to-br ${app.gradient} text-3xl shadow-lg shadow-black/10 transition-transform group-hover:scale-105 sm:h-20 sm:w-20 sm:text-4xl`} className={`relative flex items-center justify-center rounded-[22px] bg-gradient-to-br ${app.gradient} shadow-lg shadow-black/10 transition-transform duration-300 group-hover:scale-105 group-hover:shadow-xl ${
isCompact ? "h-14 w-14 text-2xl" : "h-[72px] w-[72px] text-3xl sm:h-20 sm:w-20 sm:text-4xl"
}`}
> >
<span role="img" aria-hidden> <span role="img" aria-hidden>
{app.icon} {app.icon}
@ -27,11 +27,54 @@ export function AppTile({ app, onClick }: AppTileProps) {
<Badge variant="muted">بهزودی</Badge> <Badge variant="muted">بهزودی</Badge>
</span> </span>
) : null} ) : null}
{isAvailable ? (
<span className="absolute -bottom-1 -left-1 opacity-0 transition-opacity group-hover:opacity-100">
<Badge variant="primary">فعال</Badge>
</span>
) : null}
</div> </div>
<div className="min-w-0"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-bold text-secondary sm:text-[15px]">{app.name}</p> <p className={`truncate font-bold text-secondary ${isCompact ? "text-sm" : "text-sm sm:text-[15px]"}`}>
<p className="mt-0.5 line-clamp-2 text-xs text-gray-500">{app.description}</p> {app.name}
</p>
{!isCompact ? (
<p className="mt-0.5 line-clamp-2 text-xs text-gray-500">{app.description}</p>
) : null}
</div> </div>
{isAvailable && app.href ? (
<span className="mt-auto text-xs font-medium text-primary opacity-0 transition-opacity group-hover:opacity-100">
ورود
</span>
) : null}
</>
);
}
const tileClassName =
"group flex w-full flex-col items-center gap-3 rounded-2xl border border-transparent bg-white p-3 text-center shadow-sm transition-all duration-300 hover:border-[var(--color-primary-muted)] hover:bg-[var(--color-primary-soft)] hover:shadow-md sm:p-4";
const disabledClassName =
"group flex w-full flex-col items-center gap-3 rounded-2xl border border-gray-100 bg-gray-50/60 p-3 text-center opacity-70 sm:p-4";
export function AppTile({ app, onClick, variant = "default" }: AppTileProps) {
const isAvailable = app.status === "available";
if (isAvailable && app.href && !onClick) {
return (
<Link href={app.href} className={tileClassName}>
<AppTileContent app={app} variant={variant} />
</Link>
);
}
return (
<button
type="button"
onClick={onClick}
disabled={!isAvailable && !onClick}
className={isAvailable || onClick ? tileClassName : disabledClassName}
>
<AppTileContent app={app} variant={variant} />
</button> </button>
); );
} }