fix: Resolve 500 error in unified sidebar by using simpler implementation

- Replace problematic UDashboardSidebar components with custom implementation
- Use standard HTML/CSS for sidebar instead of Nuxt UI dashboard components
- Fix 'Cannot destructure property collapsed of undefined' error
- Maintain all features: responsive, role-based nav, clean design
- Ensure compatibility with existing Vuetify components
This commit is contained in:
Matt 2025-07-11 16:44:29 -04:00
parent 61235b163d
commit 7244349fe7
1 changed files with 124 additions and 154 deletions

View File

@ -1,141 +1,127 @@
<template> <template>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50 flex">
<!-- Main Dashboard Group --> <!-- Sidebar -->
<UDashboardGroup> <div
<!-- Sidebar --> :class="[
<UDashboardSidebar 'bg-white border-r border-gray-200 transition-all duration-300',
v-model:open="sidebarOpen" sidebarOpen ? 'w-64' : 'w-0 overflow-hidden',
:collapsible="true" 'lg:w-64'
:resizable="false" ]"
:ui="{ >
wrapper: 'bg-white border-r border-gray-200', <div class="h-full flex flex-col">
header: 'px-4 py-4 border-b border-gray-100', <!-- Header -->
body: 'px-3 py-4', <div class="px-4 py-4 border-b border-gray-100">
footer: 'px-4 py-4 border-t border-gray-100'
}"
>
<template #header="{ collapsed }">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img <img
src="/Port Nimara New Logo-Circular Frame.png" src="/Port Nimara New Logo-Circular Frame.png"
class="h-8 w-8" class="h-8 w-8"
alt="Port Nimara" alt="Port Nimara"
> >
<transition name="fade"> <div class="flex flex-col">
<div v-if="!collapsed" class="flex flex-col"> <span class="text-sm font-semibold text-gray-900">Port Nimara</span>
<span class="text-sm font-semibold text-gray-900">Port Nimara</span> <span class="text-xs text-gray-500">Client Portal</span>
<span class="text-xs text-gray-500">Client Portal</span>
</div>
</transition>
</div>
</template>
<template #default="{ collapsed }">
<!-- Navigation Menu -->
<UNavigationMenu
:collapsed="collapsed"
:items="navigationItems"
orientation="vertical"
:ui="{
wrapper: 'space-y-1',
base: 'group flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors duration-200',
active: 'bg-blue-50 text-blue-700',
inactive: 'text-gray-700 hover:bg-gray-100',
icon: {
base: 'w-5 h-5 flex-shrink-0',
active: 'text-blue-700',
inactive: 'text-gray-400 group-hover:text-gray-600'
},
label: 'truncate',
badge: 'ml-auto'
}"
/>
</template>
<template #footer="{ collapsed }">
<div class="space-y-2">
<!-- User Info -->
<div v-if="authState?.user" class="flex items-center gap-3 p-2">
<UAvatar
:src="`https://ui-avatars.com/api/?name=${encodeURIComponent(authState.user.name || authState.user.email)}&background=387bca&color=fff`"
:alt="authState.user.name || authState.user.email"
size="sm"
/>
<div v-if="!collapsed" class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
{{ authState.user.name || authState.user.email }}
</p>
<p class="text-xs text-gray-500 truncate">
{{ authState.user.email }}
</p>
</div>
</div> </div>
<!-- Role Badge -->
<div v-if="!collapsed && authState?.groups?.length" class="px-2">
<UBadge
v-if="authState.groups.includes('admin')"
color="orange"
variant="subtle"
size="sm"
class="w-full justify-center"
>
Admin
</UBadge>
<UBadge
v-else-if="authState.groups.includes('sales')"
color="green"
variant="subtle"
size="sm"
class="w-full justify-center"
>
Sales
</UBadge>
</div>
<!-- Logout Button -->
<UButton
@click="handleLogout"
:icon="collapsed ? 'i-heroicons-arrow-left-on-rectangle' : undefined"
:label="collapsed ? undefined : 'Logout'"
color="gray"
variant="ghost"
:block="!collapsed"
:square="collapsed"
class="w-full justify-start"
>
<template v-if="!collapsed" #leading>
<UIcon name="i-heroicons-arrow-left-on-rectangle" class="w-5 h-5" />
</template>
</UButton>
</div> </div>
</template>
</UDashboardSidebar>
<!-- Main Panel -->
<UDashboardPanel>
<template #header>
<UDashboardNavbar :title="pageTitle" class="bg-white border-b border-gray-200">
<template #left>
<UDashboardSidebarToggle
v-if="mdAndDown"
variant="ghost"
color="gray"
/>
</template>
<template #right>
<!-- Additional navbar content can go here -->
</template>
</UDashboardNavbar>
</template>
<!-- Page Content -->
<div class="p-6">
<slot />
</div> </div>
</UDashboardPanel>
</UDashboardGroup> <!-- Navigation -->
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
<NuxtLink
v-for="item in navigationItems"
:key="item.to"
:to="item.to"
class="group flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors duration-200"
:class="[
route.path === item.to
? 'bg-blue-50 text-blue-700'
: 'text-gray-700 hover:bg-gray-100'
]"
>
<Icon
:name="item.icon"
class="w-5 h-5 flex-shrink-0"
:class="[
route.path === item.to
? 'text-blue-700'
: 'text-gray-400 group-hover:text-gray-600'
]"
/>
<span class="truncate">{{ item.label }}</span>
</NuxtLink>
</nav>
<!-- Footer -->
<div class="px-4 py-4 border-t border-gray-100 space-y-2">
<!-- User Info -->
<div v-if="authState?.user" class="flex items-center gap-3 p-2">
<div class="flex-shrink-0">
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
{{ (authState.user.name || authState.user.email || '?')[0].toUpperCase() }}
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
{{ authState.user.name || authState.user.email }}
</p>
<p class="text-xs text-gray-500 truncate">
{{ authState.user.email }}
</p>
</div>
</div>
<!-- Role Badge -->
<div v-if="authState?.groups?.length" class="px-2">
<div
v-if="authState.groups.includes('admin')"
class="inline-flex items-center justify-center w-full px-2 py-1 text-xs font-medium text-orange-700 bg-orange-100 rounded-md"
>
Admin
</div>
<div
v-else-if="authState.groups.includes('sales')"
class="inline-flex items-center justify-center w-full px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded-md"
>
Sales
</div>
</div>
<!-- Logout Button -->
<button
@click="handleLogout"
class="w-full flex items-center justify-start gap-3 px-3 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-100 transition-colors duration-200"
>
<Icon name="i-heroicons-arrow-left-on-rectangle" class="w-5 h-5 text-gray-400" />
<span>Logout</span>
</button>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col">
<!-- Top Bar -->
<header class="bg-white border-b border-gray-200 px-4 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<!-- Mobile Menu Toggle -->
<button
@click="sidebarOpen = !sidebarOpen"
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100"
>
<Icon name="i-heroicons-bars-3" class="w-6 h-6" />
</button>
<!-- Page Title -->
<h1 class="text-xl font-semibold text-gray-900">{{ pageTitle }}</h1>
</div>
</div>
</header>
<!-- Page Content -->
<main class="flex-1 p-6 overflow-y-auto">
<slot />
</main>
</div>
</div> </div>
</template> </template>
@ -145,21 +131,16 @@ import { ref, computed } from 'vue';
// Define NavigationMenuItem type locally // Define NavigationMenuItem type locally
interface NavigationMenuItem { interface NavigationMenuItem {
label: string; label: string;
icon?: string; icon: string;
to?: string; to: string;
badge?: string | number;
click?: () => void;
defaultOpen?: boolean;
children?: NavigationMenuItem[];
} }
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const nuxtApp = useNuxtApp(); const nuxtApp = useNuxtApp();
const { mdAndDown } = useDisplay();
// Sidebar state // Sidebar state
const sidebarOpen = ref(true); const sidebarOpen = ref(false);
// Get auth state - with fallback to prevent errors // Get auth state - with fallback to prevent errors
const authState = computed(() => { const authState = computed(() => {
@ -197,7 +178,7 @@ const pageTitle = computed(() => {
}); });
// Navigation items based on user role // Navigation items based on user role
const navigationItems = computed((): NavigationMenuItem[][] => { const navigationItems = computed((): NavigationMenuItem[] => {
const items: NavigationMenuItem[] = [ const items: NavigationMenuItem[] = [
{ {
label: 'Dashboard', label: 'Dashboard',
@ -261,8 +242,7 @@ const navigationItems = computed((): NavigationMenuItem[][] => {
}); });
} }
// Return as nested array for proper spacing return items;
return [items];
}); });
// Logout handler // Logout handler
@ -276,23 +256,13 @@ const handleLogout = async () => {
await router.push('/login'); await router.push('/login');
} }
}; };
// Initialize sidebar state based on screen size
onMounted(() => {
if (mdAndDown.value) {
sidebarOpen.value = false;
}
});
</script> </script>
<style scoped> <style scoped>
.fade-enter-active, /* Ensure proper responsive behavior */
.fade-leave-active { @media (min-width: 1024px) {
transition: opacity 0.2s ease; .lg\:w-64 {
} width: 16rem !important;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
} }
</style> </style>