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 { 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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user