Initial commit: thbd-portal, duplicated from elmi-portal
Rebranded for Thunderbird Portal (THBD): numbering prefix EL->TH, all "ELMI"/"ЭЛМИ" UI strings genericized, ELMI's real contract templates and company legal/bank details removed (not applicable to this company), fresh VAPID keypair and encryption key, fresh single-migration schema history. Same architecture as elmi-portal: Web Push notifications, unread-chat app badge, cookie-session auth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
commit
579528edf7
197 files changed
+25022
No files matched your search
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Inbox, Truck, MessageCircle, ListTodo, MoreHorizontal, X } from "lucide-react";
|
||||
import { links, type Role } from "./nav-data";
|
||||
|
||||
// The 4 most-used sections get their own bottom-bar slot (bar's too narrow
|
||||
// for all 8, and cramming more in defeats the point of a thumb-reachable
|
||||
// tab bar); everything else lives behind "Ещё".
|
||||
const PRIMARY_HREFS = ["/chat", "/orders", "/tasks", "/inquiries"];
|
||||
const PRIMARY_ICONS: Record<string, typeof Inbox> = {
|
||||
"/chat": MessageCircle,
|
||||
"/orders": Truck,
|
||||
"/tasks": ListTodo,
|
||||
"/inquiries": Inbox,
|
||||
};
|
||||
|
||||
export function MobileNav({ role, unread }: { role: Role; unread: number }) {
|
||||
const pathname = usePathname();
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
|
||||
const visibleLinks = links.filter((link) => link.roles.includes(role));
|
||||
const primary = PRIMARY_HREFS.map((href) => visibleLinks.find((l) => l.href === href)).filter((l) => l !== undefined);
|
||||
const overflow = visibleLinks.filter((l) => !PRIMARY_HREFS.includes(l.href));
|
||||
const overflowActive = overflow.some((l) => pathname.startsWith(l.href));
|
||||
|
||||
return (
|
||||
<>
|
||||
{moreOpen && (
|
||||
<div className="fixed inset-0 z-40 bg-black/40 lg:hidden" onClick={() => setMoreOpen(false)}>
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 rounded-t-xl border-t border-border bg-surface p-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between px-2 py-1.5">
|
||||
<span className="text-sm font-semibold">Ещё</span>
|
||||
<button onClick={() => setMoreOpen(false)} className="rounded-md p-1 text-text-faint hover:bg-surface-hover">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{overflow.map(({ href, label, icon: Icon }) => {
|
||||
const active = pathname.startsWith(href);
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
onClick={() => setMoreOpen(false)}
|
||||
className={`flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium ${
|
||||
active ? "bg-accent-soft text-accent-soft-text" : "text-text-muted hover:bg-surface-hover hover:text-text"
|
||||
}`}
|
||||
>
|
||||
<Icon size={17} />
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav
|
||||
className="app-mobile-nav fixed inset-x-0 bottom-0 z-30 flex border-t border-border bg-surface pb-[env(safe-area-inset-bottom)] lg:hidden"
|
||||
aria-label="Основная навигация"
|
||||
>
|
||||
{primary.map(({ href, label, icon: FallbackIcon }) => {
|
||||
const active = pathname.startsWith(href);
|
||||
const Icon = PRIMARY_ICONS[href] ?? FallbackIcon;
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={`relative flex flex-1 flex-col items-center gap-0.5 py-2 text-[11px] font-medium ${
|
||||
active ? "text-accent" : "text-text-faint"
|
||||
}`}
|
||||
>
|
||||
<Icon size={20} />
|
||||
{label}
|
||||
{href === "/chat" && unread > 0 && (
|
||||
<span className="absolute right-[22%] top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-danger px-1 text-[10px] font-semibold text-white">
|
||||
{unread}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
onClick={() => setMoreOpen(true)}
|
||||
className={`flex flex-1 flex-col items-center gap-0.5 py-2 text-[11px] font-medium ${
|
||||
overflowActive ? "text-accent" : "text-text-faint"
|
||||
}`}
|
||||
>
|
||||
<MoreHorizontal size={20} />
|
||||
Ещё
|
||||
</button>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user