TorbatYar/frontend/components/SiteHeader.tsx
2026-07-28 20:39:10 +03:30

216 lines
8.3 KiB
TypeScript
Raw Permalink 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 { useAuth } from "@/hooks/useAuth";
import { useHeaderSession } from "@/hooks/useHeaderSession";
import { useTenantHost } from "@/hooks/useTenantHost";
import { useTheme } from "@/hooks/useTheme";
import { Button, Container } from "@/components/ui";
import { useColorMode } from "@/components/providers/ColorModeProvider";
import { loadCommercialNotifications } from "@/modules/commercial/adapters";
import type { CommercialNotification } from "@/modules/commercial/types";
export function SiteHeader() {
const { theme } = useTheme();
const host = useTenantHost();
const { login, loading, user } = useAuth();
const { isSsoAuth, isLoggedIn } = useHeaderSession();
const { mode, toggle } = useColorMode();
const isPlatformAdmin =
user?.roles?.includes("platform_admin") || user?.roles?.includes("tenant_admin");
const brandName = host.isTenantHost
? theme?.site_name ?? "Workspace"
: theme?.site_name ?? "Torbatyar";
const brandSub = host.isTenantHost ? "workspace روی تربت‌یار" : "Commercial SaaS Platform";
const [notifications, setNotifications] = useState<CommercialNotification[]>([]);
const [notifOpen, setNotifOpen] = useState(false);
const [notifMsg, setNotifMsg] = useState<string | null>(null);
const [search, setSearch] = useState("");
useEffect(() => {
if (!isLoggedIn || !isSsoAuth) return;
void loadCommercialNotifications().then((r) => {
if (r.availability === "ready") {
setNotifications(r.data);
setNotifMsg(null);
} else {
setNotifications([]);
setNotifMsg(r.message || null);
}
});
}, [isLoggedIn, isSsoAuth]);
const onSearch = (e: React.FormEvent) => {
e.preventDefault();
const q = search.trim();
if (!q) {
window.location.href = "/search";
return;
}
window.location.href = `/search?q=${encodeURIComponent(q)}`;
};
return (
<header className="sticky top-0 z-50 border-b border-gray-100/80 bg-white/80 backdrop-blur-md dark:border-gray-800 dark:bg-gray-950/80">
<Container className="flex h-16 items-center justify-between gap-3">
<Link href="/" className="flex shrink-0 items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary text-lg font-bold text-white shadow-md">
{brandName.charAt(0)}
</div>
<div className="hidden sm:block">
<p className="text-base font-bold text-secondary dark:text-gray-100">{brandName}</p>
<p className="text-xs text-gray-500">{brandSub}</p>
</div>
</Link>
{isLoggedIn && isSsoAuth ? (
<form onSubmit={onSearch} className="hidden min-w-0 flex-1 md:block md:max-w-xs lg:max-w-sm">
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="جستجوی اپ‌ها…"
className="w-full rounded-xl border border-gray-200 bg-white px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900"
aria-label="جستجو"
/>
</form>
) : null}
<nav className="flex flex-wrap items-center justify-end gap-1 sm:gap-2">
{!host.isTenantHost && !isLoggedIn ? (
<>
<Link href="/discover">
<Button variant="ghost" size="sm">
کشف
</Button>
</Link>
<Link href="/pricing">
<Button variant="ghost" size="sm">
قیمت
</Button>
</Link>
<Link href="/help">
<Button variant="ghost" size="sm" className="hidden md:inline-flex">
راهنما
</Button>
</Link>
</>
) : null}
{!host.isTenantHost && isLoggedIn ? (
<Link href="/apps">
<Button variant="ghost" size="sm">
Products
</Button>
</Link>
) : null}
{isLoggedIn && isSsoAuth ? (
<>
<Link href="/dashboard">
<Button variant="ghost" size="sm">
Workspace
</Button>
</Link>
<Link href="/apps">
<Button variant="ghost" size="sm" className="hidden sm:inline-flex">
Apps
</Button>
</Link>
<Link href="/billing">
<Button variant="ghost" size="sm">
Billing
</Button>
</Link>
<Link href="/domains">
<Button variant="ghost" size="sm" className="hidden md:inline-flex">
دامنه
</Button>
</Link>
<div className="relative hidden sm:block">
<Button
variant="ghost"
size="sm"
onClick={() => setNotifOpen((v) => !v)}
aria-expanded={notifOpen}
aria-haspopup="true"
>
اعلان
{notifications.length ? (
<span className="ms-1 text-[10px] text-primary">{notifications.length}</span>
) : null}
</Button>
{notifOpen ? (
<div className="absolute end-0 z-50 mt-2 w-72 rounded-xl border border-gray-100 bg-white p-3 shadow-lg dark:border-gray-800 dark:bg-gray-950">
{notifications.length ? (
<ul className="max-h-64 space-y-2 overflow-auto text-sm">
{notifications.slice(0, 5).map((n) => (
<li key={n.id} className="border-b border-gray-50 pb-2 last:border-0 dark:border-gray-800">
<p className="font-medium text-secondary dark:text-gray-100">{n.title}</p>
{n.body ? (
<p className="mt-0.5 text-xs text-gray-500">{n.body}</p>
) : null}
</li>
))}
</ul>
) : (
<p className="text-xs text-gray-500">
{notifMsg || "اعلانی از رجیستری نیست"}
</p>
)}
<Link
href="/notifications"
className="mt-2 block text-center text-xs font-semibold text-primary"
onClick={() => setNotifOpen(false)}
>
همه اعلانها
</Link>
</div>
) : null}
</div>
{!host.isTenantHost && isPlatformAdmin ? (
<Link href="/admin">
<Button variant="primary" size="sm">
Admin
</Button>
</Link>
) : null}
<Link
href={
isPlatformAdmin && !host.isTenantHost
? "/admin/settings"
: "/dashboard/settings"
}
>
<Button variant="outline" size="sm">
Account
</Button>
</Link>
</>
) : (
<>
<Link href="/register?redirect=/discover">
<Button variant="outline" size="sm" className="hidden sm:inline-flex">
ثبتنام
</Button>
</Link>
<Button
variant="primary"
size="sm"
onClick={() => login()}
loading={loading}
loadingText="..."
>
ورود
</Button>
</>
)}
<Button variant="ghost" size="sm" onClick={toggle} aria-label="تغییر تم">
{mode === "dark" ? "☀" : "☾"}
</Button>
</nav>
</Container>
</header>
);
}