177 lines
7.0 KiB
TypeScript
177 lines
7.0 KiB
TypeScript
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { ReactNode } from "react";
|
|
|
|
import { getLocale, getTranslator, type NavKey } from "@/lib/i18n";
|
|
import type { NavItem } from "@/lib/mock-data";
|
|
|
|
type ShellContext = {
|
|
userName: string;
|
|
roleLabel: string;
|
|
tenantName: string;
|
|
};
|
|
|
|
const navIconByKey: Record<NavKey, string> = {
|
|
dashboard: "dashboard",
|
|
shared_inbox: "inbox",
|
|
inbox: "inbox",
|
|
contacts: "contacts",
|
|
broadcast: "campaign",
|
|
templates: "article",
|
|
team: "group",
|
|
reports: "bar_chart",
|
|
settings: "settings",
|
|
billing: "payments",
|
|
audit_log: "history",
|
|
tenants: "domain",
|
|
channels: "settings_input_antenna",
|
|
security_events: "notifications_active",
|
|
webhook_logs: "webhook",
|
|
alerts: "warning",
|
|
quick_tools: "auto_fix_high",
|
|
performance: "trending_up",
|
|
new_chat: "chat",
|
|
search: "search",
|
|
logout: "logout",
|
|
global_search: "search",
|
|
campaign: "campaign"
|
|
};
|
|
|
|
function initials(name: string) {
|
|
return name
|
|
.split(" ")
|
|
.filter(Boolean)
|
|
.map((part) => part[0]?.toUpperCase())
|
|
.slice(0, 2)
|
|
.join("");
|
|
}
|
|
|
|
function LocaleSwitcher({ locale }: { locale: "id" | "en" }) {
|
|
const nextLocale = locale === "id" ? "en" : "id";
|
|
return (
|
|
<Link
|
|
href={`/locale?to=${nextLocale}`}
|
|
className="rounded-full border border-line bg-surface-container px-3 py-1 text-xs font-semibold text-on-surface"
|
|
>
|
|
{nextLocale.toUpperCase()}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export async function AppShell({
|
|
title,
|
|
subtitle,
|
|
nav,
|
|
context,
|
|
children
|
|
}: {
|
|
title: string;
|
|
subtitle: string;
|
|
nav: NavItem[];
|
|
context: ShellContext;
|
|
children: ReactNode;
|
|
}) {
|
|
const locale = await getLocale();
|
|
const t = getTranslator(locale);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background text-on-surface">
|
|
<div className="mx-auto flex min-h-screen max-w-[1700px]">
|
|
<aside className="hidden w-[268px] shrink-0 flex-col space-y-4 border-r border-line bg-surface-container-low px-4 py-6 lg:flex">
|
|
<div className="mb-6 rounded-[1.25rem] bg-surface-container-lowest px-3 py-3 shadow-sm">
|
|
<div className="flex items-center gap-3 px-2">
|
|
<Image
|
|
src="/logo_zappcare.png"
|
|
alt="ZappCare"
|
|
width={36}
|
|
height={36}
|
|
className="h-9 w-auto rounded-full"
|
|
/>
|
|
<div>
|
|
<p className="text-xs font-extrabold uppercase tracking-[0.24em] text-outline">{t("common", "zappcare")}</p>
|
|
<p className="text-lg font-black leading-tight font-headline text-on-surface">{t("common", "business_suite")}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="px-2">
|
|
<button className="flex w-full items-center justify-center gap-2 rounded-full bg-gradient-to-br from-primary to-primary-container px-4 py-3 text-sm font-bold text-white">
|
|
<span className="material-symbols-outlined text-sm">add_comment</span>
|
|
<span>{t("nav", "new_chat")}</span>
|
|
</button>
|
|
</div>
|
|
<nav className="space-y-1">
|
|
{nav.map((item) => {
|
|
const label = t("nav", item.labelKey);
|
|
const isDashboard = item.labelKey === "dashboard";
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`flex items-center gap-3 rounded-xl px-4 py-2.5 text-sm font-semibold font-headline transition-all ${
|
|
isDashboard
|
|
? "bg-primary-container/40 text-on-primary-container"
|
|
: "text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface"
|
|
}`}
|
|
>
|
|
<span
|
|
className="material-symbols-outlined text-sm"
|
|
style={{ fontVariationSettings: "'FILL' 1", fontSize: "20px" }}
|
|
>
|
|
{navIconByKey[item.labelKey]}
|
|
</span>
|
|
<span>{label}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
<div className="mt-auto border-t border-line px-2 pt-5">
|
|
<Link
|
|
href="/auth/logout"
|
|
className="flex items-center gap-3 rounded-xl px-4 py-2.5 text-sm font-semibold text-on-surface-variant transition hover:text-on-surface"
|
|
>
|
|
<span className="material-symbols-outlined text-sm">logout</span>
|
|
<span>{t("nav", "logout")}</span>
|
|
</Link>
|
|
</div>
|
|
</aside>
|
|
<div className="flex min-h-screen flex-1 flex-col">
|
|
<header className="border-b border-line bg-surface-container-lowest/85 backdrop-blur">
|
|
<div className="flex flex-wrap items-center justify-between gap-4 px-5 py-4 md:px-7">
|
|
<div>
|
|
<p className="text-xs font-bold uppercase tracking-[0.2em] text-outline">{subtitle}</p>
|
|
<h1 className="text-2xl font-black font-headline text-on-surface">{title}</h1>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="relative hidden w-72 rounded-full border border-line bg-surface-container-low px-4 py-2 md:block">
|
|
<span className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-sm">search</span>
|
|
<input
|
|
placeholder={t("common", "search_placeholder")}
|
|
className="w-full border-none bg-transparent pl-8 pr-2 text-sm outline-none placeholder:text-outline-variant"
|
|
/>
|
|
</div>
|
|
<LocaleSwitcher locale={locale} />
|
|
<button className="flex h-9 w-9 items-center justify-center rounded-full text-outline transition hover:bg-surface-container-low">
|
|
<span className="material-symbols-outlined text-sm">notifications</span>
|
|
</button>
|
|
<button className="flex h-9 w-9 items-center justify-center rounded-full text-outline transition hover:bg-surface-container-low">
|
|
<span className="material-symbols-outlined text-sm">help_outline</span>
|
|
</button>
|
|
<button className="flex h-9 w-9 items-center justify-center rounded-full text-outline transition hover:bg-surface-container-low">
|
|
<span className="material-symbols-outlined text-sm">app_shortcut</span>
|
|
</button>
|
|
<button className="ml-2 flex h-9 items-center justify-center rounded-full border border-line bg-surface-container px-3 text-xs font-bold text-on-surface-variant">
|
|
{initials(context.userName)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="px-7 pb-4 text-sm text-on-surface-variant">
|
|
{context.userName} • {context.roleLabel} • {context.tenantName}
|
|
</div>
|
|
</header>
|
|
<main className="min-h-0 flex-1 px-6 py-6 md:px-8">{children}</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|