From 710ac253e222c044e77ce9866cf2eda9f7b26c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sat, 21 Mar 2026 20:59:09 +0100 Subject: [PATCH] feat: add AppShell with persistent icon sidebar and mobile bottom tab bar --- src/app/dashboard/layout.tsx | 15 +++--- src/components/ui/AppShell.tsx | 19 +++++++ src/components/ui/Sidebar.tsx | 94 ++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/components/ui/AppShell.tsx create mode 100644 src/components/ui/Sidebar.tsx diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index f75f6a0..f6f9af2 100755 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -3,19 +3,20 @@ import { SteamCMDProvider } from '@/lib/context/SteamCMDContext'; import { ServerCreationPopupProvider } from '@/lib/context/ServerCreationPopupContext'; import { ServerCreationPopupContainer } from '@/components/server/ServerCreationPopupContainer'; import { requireAuth } from '@/lib/auth/server'; +import { AppShell } from '@/components/ui/AppShell'; +import { hasPermission } from '@/lib/schemas'; -export default async function DashboardLayout({ - children -}: Readonly<{ - children: React.ReactNode; -}>) { +export default async function DashboardLayout({ children }: Readonly<{ children: React.ReactNode }>) { const session = await requireAuth(); + const showUsers = hasPermission(session.user!, 'membership.view'); return ( - {children} - + + {children} + + diff --git a/src/components/ui/AppShell.tsx b/src/components/ui/AppShell.tsx new file mode 100644 index 0000000..7c21026 --- /dev/null +++ b/src/components/ui/AppShell.tsx @@ -0,0 +1,19 @@ +// NOTE: intentionally no 'use client' — must remain a Server Component +import { Sidebar } from './Sidebar'; + +interface AppShellProps { + children: React.ReactNode; + showUsers: boolean; +} + +export function AppShell({ children, showUsers }: AppShellProps) { + return ( +
+ + {/* Offset for sidebar (desktop: left margin, mobile: bottom padding) */} +
+ {children} +
+
+ ); +} diff --git a/src/components/ui/Sidebar.tsx b/src/components/ui/Sidebar.tsx new file mode 100644 index 0000000..195b03e --- /dev/null +++ b/src/components/ui/Sidebar.tsx @@ -0,0 +1,94 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { cn } from '@/lib/utils'; + +interface SidebarProps { + showUsers: boolean; // true if user has membership.view permission +} + +function ServerIcon({ active }: { active: boolean }) { + return ( + + + + + + + + + ); +} + +function UsersIcon({ active }: { active: boolean }) { + return ( + + + + + + ); +} + +export function Sidebar({ showUsers }: SidebarProps) { + const pathname = usePathname(); + const isServers = !pathname.startsWith('/dashboard/membership'); + const isUsers = pathname.startsWith('/dashboard/membership'); + + const navItems = [ + { href: '/dashboard', label: 'Servers', active: isServers, icon: }, + ...(showUsers ? [{ href: '/dashboard/membership', label: 'Users', active: isUsers, icon: }] : []), + ]; + + return ( + <> + {/* Desktop icon sidebar */} + + + {/* Mobile bottom tab bar */} + + + ); +}