Link active apps on homepage and refresh landing page design.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
28f84b1274
commit
579e0cdaf5
@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { AppStoreGrid } from "@/components/AppStoreGrid";
|
||||
import { TenantSitePage } from "@/components/TenantSitePage";
|
||||
import { Badge, Button, Container } from "@/components/ui";
|
||||
import { PLATFORM_APPS } from "@/lib/apps-catalog";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useTenantHost } from "@/hooks/useTenantHost";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
@ -16,51 +17,140 @@ export default function HomePage() {
|
||||
return <PlatformHomePage />;
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
{ icon: "🔐", label: "ورود واحد SSO" },
|
||||
{ icon: "🏢", label: "چند کسبوکار" },
|
||||
{ icon: "☁️", label: "ابری و مقیاسپذیر" },
|
||||
{ icon: "🎨", label: "White-label" },
|
||||
];
|
||||
|
||||
function PlatformHomePage() {
|
||||
const { theme } = useTheme();
|
||||
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 (
|
||||
<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">
|
||||
<Container className="relative py-12 sm:py-16">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<Badge variant="primary" className="mb-4">
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-primary-soft)] via-white to-[var(--color-secondary-muted)]" />
|
||||
<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
|
||||
</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>
|
||||
<p className="mt-4 text-base leading-8 text-gray-600 sm:text-lg">
|
||||
یک پلتفرم، دهها سرویس — حسابداری، CRM، فروشگاه، پیامک و بیشتر.
|
||||
همه با یک ورود واحد (SSO).
|
||||
|
||||
<p className="mx-auto mt-5 max-w-xl text-base leading-8 text-gray-600 sm:text-lg">
|
||||
یک پلتفرم، دهها سرویس — حسابداری، CRM، سلامت، زیبایی و بیشتر.
|
||||
همه با یک ورود واحد.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
{isAuthenticated ? (
|
||||
<Link href="/dashboard">
|
||||
<Button size="lg">رفتن به داشبورد</Button>
|
||||
<Button size="lg" className="min-w-[160px] shadow-lg shadow-primary/20">
|
||||
رفتن به داشبورد
|
||||
</Button>
|
||||
</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>
|
||||
)}
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<Container className="py-10 sm:py-14">
|
||||
{/* Features strip */}
|
||||
<section className="border-y border-gray-100 bg-white/60 backdrop-blur-sm">
|
||||
<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">
|
||||
<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>
|
||||
© {new Date().getFullYear()} {theme?.site_name ?? "Torbatyar"} — White-label SaaS
|
||||
Platform
|
||||
© {new Date().getFullYear()} {siteName} — White-label SaaS Platform
|
||||
</p>
|
||||
<p>{theme?.support_email ?? "support@example.com"}</p>
|
||||
</Container>
|
||||
|
||||
@ -4,17 +4,34 @@ import { PLATFORM_APPS } from "@/lib/apps-catalog";
|
||||
import { AppTile, SectionTitle } from "@/components/ui";
|
||||
|
||||
export function AppStoreGrid() {
|
||||
const availableApps = PLATFORM_APPS.filter((app) => app.status === "available");
|
||||
const comingSoonApps = PLATFORM_APPS.filter((app) => app.status === "coming_soon");
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<section>
|
||||
<SectionTitle
|
||||
title="اپلیکیشنها"
|
||||
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">
|
||||
{availableApps.map((app) => (
|
||||
<AppTile key={app.id} app={app} />
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
import Link from "next/link";
|
||||
import type { PlatformApp } from "@/lib/apps-catalog";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
export interface AppTileProps {
|
||||
app: PlatformApp;
|
||||
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 isCompact = variant === "compact";
|
||||
|
||||
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
|
||||
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>
|
||||
{app.icon}
|
||||
@ -27,11 +27,54 @@ export function AppTile({ app, onClick }: AppTileProps) {
|
||||
<Badge variant="muted">بهزودی</Badge>
|
||||
</span>
|
||||
) : 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 className="min-w-0">
|
||||
<p className="truncate text-sm font-bold text-secondary sm:text-[15px]">{app.name}</p>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={`truncate font-bold text-secondary ${isCompact ? "text-sm" : "text-sm sm:text-[15px]"}`}>
|
||||
{app.name}
|
||||
</p>
|
||||
{!isCompact ? (
|
||||
<p className="mt-0.5 line-clamp-2 text-xs text-gray-500">{app.description}</p>
|
||||
) : null}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user