chore: initial project import
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
This commit is contained in:
176
components/app-shell.tsx
Normal file
176
components/app-shell.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user