256 lines
8.7 KiB
TypeScript
256 lines
8.7 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import { Badge, Button, Container } from "@/components/ui";
|
||
import { useAuth } from "@/hooks/useAuth";
|
||
import { useMe } from "@/hooks/useMe";
|
||
import { useTenantHost } from "@/hooks/useTenantHost";
|
||
import { api, type PublicTenantSite } from "@/lib/api";
|
||
import { applyTheme } from "@/lib/theme";
|
||
import { loadCommercialProducts } from "@/modules/commercial/adapters";
|
||
import { CommercialEmptyState, ProductCard } from "@/modules/commercial/components";
|
||
import type { CommercialProduct } from "@/modules/commercial/types";
|
||
|
||
/**
|
||
* صفحه سابدامین tenant:
|
||
* - مهمان / غیرعضو → لندینگ تمپلیت برند کسبوکار
|
||
* - عضو/مالک لاگینشده → دسترسی به سرویسها و داشبورد
|
||
*/
|
||
export function TenantSitePage() {
|
||
const host = useTenantHost();
|
||
const { isAuthenticated, login, loading: authLoading, signOut, user } = useAuth();
|
||
const { me, loading: meLoading } = useMe();
|
||
const [site, setSite] = useState<PublicTenantSite | null>(null);
|
||
const [error, setError] = useState("");
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
(async () => {
|
||
setLoading(true);
|
||
setError("");
|
||
try {
|
||
const data = await api.public.tenantSite({
|
||
slug: host.slug,
|
||
host: host.hostname,
|
||
});
|
||
if (cancelled) return;
|
||
setSite(data);
|
||
applyTheme({
|
||
site_name: data.name,
|
||
primary_color: data.primary_color || "#0284c7",
|
||
secondary_color: data.secondary_color || "#0f172a",
|
||
logo_url: data.logo_url || "",
|
||
support_email: "",
|
||
});
|
||
if (typeof document !== "undefined") {
|
||
document.title = data.name;
|
||
}
|
||
} catch (e) {
|
||
if (!cancelled) {
|
||
setError(e instanceof Error ? e.message : "بارگذاری سایت ناموفق بود");
|
||
setSite(null);
|
||
}
|
||
} finally {
|
||
if (!cancelled) setLoading(false);
|
||
}
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [host.slug, host.hostname]);
|
||
|
||
const membership = useMemo(() => {
|
||
if (!me || !site) return null;
|
||
return me.memberships.find((m) => m.tenant_slug === site.slug) ?? null;
|
||
}, [me, site]);
|
||
|
||
const isMember = Boolean(isAuthenticated && membership);
|
||
const isOwner = Boolean(membership?.is_owner);
|
||
|
||
if (loading || (isAuthenticated && meLoading)) {
|
||
return (
|
||
<div className="flex min-h-[60vh] items-center justify-center">
|
||
<div className="h-10 w-10 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error || !site) {
|
||
return (
|
||
<Container className="py-20 text-center">
|
||
<h1 className="text-2xl font-bold text-secondary">سایت یافت نشد</h1>
|
||
<p className="mt-3 text-gray-600">{error || "این دامنه به workspace فعالی وصل نیست."}</p>
|
||
<a href={`https://${host.baseDomain}`} className="mt-8 inline-block text-primary underline">
|
||
بازگشت به تربتیار
|
||
</a>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
if (isMember) {
|
||
return (
|
||
<TenantMemberHome
|
||
site={site}
|
||
isOwner={isOwner}
|
||
role={membership?.role}
|
||
userLabel={user?.username || user?.email || me?.mobile || "کاربر"}
|
||
onSignOut={signOut}
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<TenantPublicLanding
|
||
site={site}
|
||
onLogin={() => login()}
|
||
authLoading={authLoading}
|
||
platformUrl={`https://${host.baseDomain}`}
|
||
/>
|
||
);
|
||
}
|
||
|
||
function TenantPublicLanding({
|
||
site,
|
||
onLogin,
|
||
authLoading,
|
||
platformUrl,
|
||
}: {
|
||
site: PublicTenantSite;
|
||
onLogin: () => void;
|
||
authLoading: boolean;
|
||
platformUrl: string;
|
||
}) {
|
||
const initial = site.name.trim().charAt(0) || "ت";
|
||
return (
|
||
<div className="relative min-h-[calc(100vh-4rem)] overflow-hidden">
|
||
<div
|
||
className="pointer-events-none absolute inset-0 opacity-90"
|
||
style={{
|
||
background: `radial-gradient(ellipse 80% 60% at 50% -10%, var(--color-primary-muted), transparent),
|
||
linear-gradient(180deg, var(--color-primary-soft) 0%, #fff 55%)`,
|
||
}}
|
||
/>
|
||
<Container className="relative flex min-h-[calc(100vh-4rem)] flex-col items-center justify-center py-16 text-center">
|
||
<div className="mb-6 flex h-20 w-20 items-center justify-center overflow-hidden rounded-3xl bg-primary text-3xl font-bold text-white shadow-lg shadow-primary/25">
|
||
{site.logo_url ? (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img src={site.logo_url} alt="" className="h-full w-full object-cover" />
|
||
) : (
|
||
initial
|
||
)}
|
||
</div>
|
||
{site.business_type && (
|
||
<Badge variant="primary" className="mb-4">
|
||
{site.business_type}
|
||
</Badge>
|
||
)}
|
||
<h1 className="max-w-2xl text-4xl font-extrabold leading-tight text-secondary sm:text-5xl">
|
||
{site.name}
|
||
</h1>
|
||
<p className="mt-4 max-w-xl text-base leading-8 text-gray-600 sm:text-lg">
|
||
{site.tagline}. بهزودی صفحه عمومی و سرویسهای این کسبوکار اینجا تکمیل میشود.
|
||
</p>
|
||
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
|
||
<Button size="lg" onClick={onLogin} loading={authLoading} loadingText="...">
|
||
ورود مالک / همکاران
|
||
</Button>
|
||
<a href={platformUrl}>
|
||
<Button size="lg" variant="outline">
|
||
درباره تربتیار
|
||
</Button>
|
||
</a>
|
||
</div>
|
||
{site.primary_domain && (
|
||
<p dir="ltr" className="mt-10 font-mono text-xs text-gray-400">
|
||
{site.primary_domain}
|
||
</p>
|
||
)}
|
||
</Container>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TenantMemberHome({
|
||
site,
|
||
isOwner,
|
||
role,
|
||
userLabel,
|
||
onSignOut,
|
||
}: {
|
||
site: PublicTenantSite;
|
||
isOwner: boolean;
|
||
role?: string;
|
||
userLabel: string;
|
||
onSignOut: () => void;
|
||
}) {
|
||
return (
|
||
<div className="pb-16">
|
||
<section className="border-b border-gray-100 bg-gradient-to-b from-[var(--color-primary-soft)] to-white">
|
||
<Container className="py-10 sm:py-12">
|
||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<Badge variant="primary" className="mb-3">
|
||
{isOwner ? "مالک workspace" : role || "عضو"}
|
||
</Badge>
|
||
<h1 className="text-3xl font-extrabold text-secondary">{site.name}</h1>
|
||
<p className="mt-2 text-sm text-gray-600">
|
||
خوش آمدید {userLabel} — سرویسهای تربتیار برای این کسبوکار
|
||
</p>
|
||
{site.primary_domain && (
|
||
<p dir="ltr" className="mt-2 font-mono text-xs text-gray-400">
|
||
{site.primary_domain}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/dashboard">
|
||
<Button variant="primary">داشبورد workspace</Button>
|
||
</Link>
|
||
<Link href="/dashboard/settings">
|
||
<Button variant="outline">تنظیمات</Button>
|
||
</Link>
|
||
<Button variant="ghost" onClick={onSignOut}>
|
||
خروج
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Container>
|
||
</section>
|
||
|
||
<TenantDiscoveredApps />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TenantDiscoveredApps() {
|
||
const [products, setProducts] = useState<CommercialProduct[]>([]);
|
||
const [message, setMessage] = useState<string | undefined>();
|
||
|
||
useEffect(() => {
|
||
void loadCommercialProducts().then((r) => {
|
||
setProducts(r.data.filter((p) => p.visibility !== "hidden"));
|
||
setMessage(r.message);
|
||
});
|
||
}, []);
|
||
|
||
return (
|
||
<Container className="py-10">
|
||
<h2 className="mb-2 text-xl font-bold text-secondary">محصولات کشفشده</h2>
|
||
<p className="mb-8 text-sm text-gray-600">
|
||
از رجیستری سرویس/محصول — بدون آرایه ثابت frontend.
|
||
</p>
|
||
{products.length ? (
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{products.map((p) => (
|
||
<ProductCard key={p.product_code} product={p} />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<CommercialEmptyState title="محصولی کشف نشد" description={message} />
|
||
)}
|
||
</Container>
|
||
);
|
||
}
|