diff --git a/src/app/(admin)/layout.tsx b/src/app/(admin)/layout.tsx index 199a969..9da8a8c 100644 --- a/src/app/(admin)/layout.tsx +++ b/src/app/(admin)/layout.tsx @@ -2,7 +2,7 @@ import { redirect } from "next/navigation"; import Link from "next/link"; import { CalendarCheck } from "lucide-react"; import { getCurrentSession } from "@/lib/auth/session"; -import { NavLinks } from "./nav-links"; +import { NavShell } from "./nav-shell"; import { ThemeToggle } from "./theme-toggle"; import { LogoutButton } from "./logout-button"; @@ -16,25 +16,28 @@ export default async function AdminLayout({ children }: { children: React.ReactN return (
-
-
- +
+
+
- top-reservation + top-reservation -
- +
+
-
+
{session.user.name}
-
{children}
+ {/* pb: clears the fixed mobile bottom-tab bar (~52px + safe-area) so it never covers the last bit of scrolled content */} +
+ {children} +
); } diff --git a/src/app/(admin)/mobile-nav.tsx b/src/app/(admin)/mobile-nav.tsx new file mode 100644 index 0000000..783eaf0 --- /dev/null +++ b/src/app/(admin)/mobile-nav.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { MoreHorizontal, X } from "lucide-react"; +import { links, PRIMARY_COUNT } from "./nav-data"; + +export function MobileNav({ isAdmin }: { isAdmin: boolean }) { + const pathname = usePathname(); + const [moreOpen, setMoreOpen] = useState(false); + + const visibleLinks = links.filter((link) => !link.adminOnly || isAdmin); + const primary = visibleLinks.slice(0, PRIMARY_COUNT); + const overflow = visibleLinks.slice(PRIMARY_COUNT); + const overflowActive = overflow.some((l) => pathname.startsWith(l.href)); + + function isActive(link: (typeof links)[number]) { + return link.exact ? pathname === link.href : pathname.startsWith(link.href); + } + + return ( + <> + {moreOpen && ( +
setMoreOpen(false)}> +
e.stopPropagation()} + > +
+ Ещё + +
+ {overflow.map((link) => { + const active = isActive(link); + return ( + 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" + }`} + > + + {link.label} + + ); + })} +
+
+ )} + + + + ); +} diff --git a/src/app/(admin)/nav-data.ts b/src/app/(admin)/nav-data.ts new file mode 100644 index 0000000..595f679 --- /dev/null +++ b/src/app/(admin)/nav-data.ts @@ -0,0 +1,25 @@ +import { LayoutDashboard, CalendarPlus, ListChecks, BarChart3, Building2, Users } from "lucide-react"; + +export interface NavLink { + href: string; + label: string; + /** Shorter label for the mobile bottom-bar slot, where space is tight — falls back to `label` if unset. */ + shortLabel?: string; + icon: typeof LayoutDashboard; + adminOnly: boolean; + exact: boolean; +} + +// The first 4 are the mobile bottom bar's fixed slots (thumb-reachable, +// matches exactly what a non-admin sees — no overflow for them at all); +// admin-only extras live behind "Ещё" on mobile, same as elmi-portal. +export const links: NavLink[] = [ + { href: "/", label: "Дашборд", icon: LayoutDashboard, adminOnly: false, exact: true }, + { href: "/bookings/new", label: "Новое бронирование", shortLabel: "Создать", icon: CalendarPlus, adminOnly: false, exact: true }, + { href: "/bookings", label: "Бронирования", shortLabel: "Брони", icon: ListChecks, adminOnly: false, exact: false }, + { href: "/statistics", label: "Статистика", icon: BarChart3, adminOnly: false, exact: false }, + { href: "/settings/objects", label: "Объекты", icon: Building2, adminOnly: true, exact: false }, + { href: "/settings/users", label: "Сотрудники", icon: Users, adminOnly: true, exact: false }, +]; + +export const PRIMARY_COUNT = 4; diff --git a/src/app/(admin)/nav-links.tsx b/src/app/(admin)/nav-links.tsx index 9896c88..e4e9bcf 100644 --- a/src/app/(admin)/nav-links.tsx +++ b/src/app/(admin)/nav-links.tsx @@ -2,16 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { LayoutDashboard, CalendarPlus, ListChecks, BarChart3, Building2, Users } from "lucide-react"; - -const links = [ - { href: "/", label: "Дашборд", icon: LayoutDashboard, adminOnly: false, exact: true }, - { href: "/bookings/new", label: "Новое бронирование", icon: CalendarPlus, adminOnly: false, exact: true }, - { href: "/bookings", label: "Бронирования", icon: ListChecks, adminOnly: false, exact: false }, - { href: "/statistics", label: "Статистика", icon: BarChart3, adminOnly: false, exact: false }, - { href: "/settings/objects", label: "Объекты", icon: Building2, adminOnly: true, exact: false }, - { href: "/settings/users", label: "Сотрудники", icon: Users, adminOnly: true, exact: false }, -]; +import { links } from "./nav-data"; export function NavLinks({ isAdmin }: { isAdmin: boolean }) { const pathname = usePathname(); diff --git a/src/app/(admin)/nav-shell.tsx b/src/app/(admin)/nav-shell.tsx new file mode 100644 index 0000000..44c40cf --- /dev/null +++ b/src/app/(admin)/nav-shell.tsx @@ -0,0 +1,14 @@ +import { NavLinks } from "./nav-links"; +import { MobileNav } from "./mobile-nav"; + +/** Desktop top nav (>=lg) + the mobile bottom tab bar ( +
+ +
+ + + ); +}