From 180b0dc0a3610574a6d1edb3f673e1c9e29164d1 Mon Sep 17 00:00:00 2001 From: weeihan Date: Sun, 12 Jul 2026 16:40:57 +0800 Subject: [PATCH] feat: persistent sidebar nav with role-scoped links, logout, and mobile bottom tab bar --- app/(protected)/layout.tsx | 23 +++- components/layout/sidebar.tsx | 216 ++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 components/layout/sidebar.tsx diff --git a/app/(protected)/layout.tsx b/app/(protected)/layout.tsx index e5f4289..fd98d25 100644 --- a/app/(protected)/layout.tsx +++ b/app/(protected)/layout.tsx @@ -1,9 +1,8 @@ -// app/(protected)/layout.tsx export const dynamic = 'force-dynamic' import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' -import { NotificationBell } from '@/components/notifications/bell' +import { Sidebar } from '@/components/layout/sidebar' export default async function ProtectedLayout({ children, @@ -17,10 +16,22 @@ export default async function ProtectedLayout({ if (!user) redirect('/login') + const { data: profile } = await supabase + .from('users') + .select('role, name, email') + .eq('id', user.id) + .single() + return ( - <> - - {children} - +
+ +
+ {children} +
+
) } diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx new file mode 100644 index 0000000..4ccfef3 --- /dev/null +++ b/components/layout/sidebar.tsx @@ -0,0 +1,216 @@ +'use client' + +import Link from 'next/link' +import { usePathname, useRouter } from 'next/navigation' +import { createBrowserClient } from '@supabase/ssr' +import { NotificationBell } from '@/components/notifications/bell' + +interface NavItem { + label: string + href: string + icon: React.ReactNode +} + +interface SidebarProps { + role: string + userName: string + userEmail: string +} + +const IconGrid = () => ( + + + +) +const IconList = () => ( + + + +) +const IconCheck = () => ( + + + +) +const IconChart = () => ( + + + +) +const IconSettings = () => ( + + + + +) +const IconUsers = () => ( + + + +) +const IconPlus = () => ( + + + +) +const IconLogout = () => ( + + + +) + +const NAV_ITEMS: Record = { + reporter: [ + { label: 'My Reports', href: '/reporter', icon: }, + { label: 'New Incident', href: '/report', icon: }, + ], + supervisor: [ + { label: 'Overview', href: '/supervisor', icon: }, + { label: 'Incidents', href: '/supervisor/incidents', icon: }, + ], + hse: [ + { label: 'Dashboard', href: '/hse/dashboard', icon: }, + { label: 'Incidents', href: '/hse/incidents', icon: }, + { label: 'CAPA', href: '/hse/capa', icon: }, + { label: 'Settings', href: '/hse/settings', icon: }, + ], + management: [ + { label: 'Dashboard', href: '/management', icon: }, + ], + capa_owner: [ + { label: 'My CAPAs', href: '/capa-owner', icon: }, + ], + admin: [ + { label: 'Admin', href: '/admin', icon: }, + { label: 'Dashboard', href: '/hse/dashboard', icon: }, + { label: 'Incidents', href: '/hse/incidents', icon: }, + { label: 'CAPA', href: '/hse/capa', icon: }, + { label: 'Management', href: '/management', icon: }, + ], +} + +function initials(name: string): string { + return name + .split(' ') + .map(w => w[0] ?? '') + .join('') + .toUpperCase() + .slice(0, 2) +} + +function useNavItems(role: string): NavItem[] { + return NAV_ITEMS[role] ?? NAV_ITEMS.reporter +} + +function NavLink({ item, pathname }: { item: NavItem; pathname: string }) { + // Exact match for root-level single-page roles, prefix match otherwise + const exactRoots = ['/reporter', '/supervisor', '/management', '/capa-owner', '/admin', '/report'] + const isActive = exactRoots.includes(item.href) + ? pathname === item.href + : pathname.startsWith(item.href) + + return ( + + {item.icon} + {item.label} + + ) +} + +export function Sidebar({ role, userName, userEmail }: SidebarProps) { + const pathname = usePathname() + const router = useRouter() + const items = useNavItems(role) + const mobileItems = items.slice(0, 4) + + const logout = async () => { + const supabase = createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + ) + await supabase.auth.signOut() + router.push('/login') + } + + const displayName = userName || userEmail || 'User' + const roleLabel = role.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) + + return ( + <> + {/* Desktop sidebar */} + + + {/* Mobile bottom tab bar */} + + + ) +}