fix: Switch to pure Vuetify components for unified sidebar
- Remove Nuxt UI dependency that was causing conflicts - Use v-navigation-drawer with rail prop for collapsible functionality - Implement proper Vuetify list components for navigation - Add responsive behavior with drawer toggle on mobile - Fix layout structure to work with existing Vuetify setup
This commit is contained in:
parent
7244349fe7
commit
9b045c7b97
|
|
@ -1,146 +1,132 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 flex">
|
<v-app>
|
||||||
<!-- Sidebar -->
|
<v-navigation-drawer
|
||||||
<div
|
v-model="drawer"
|
||||||
:class="[
|
:rail="rail"
|
||||||
'bg-white border-r border-gray-200 transition-all duration-300',
|
permanent
|
||||||
sidebarOpen ? 'w-64' : 'w-0 overflow-hidden',
|
color="white"
|
||||||
'lg:w-64'
|
class="elevation-2"
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<div class="h-full flex flex-col">
|
<!-- Logo and Title -->
|
||||||
<!-- Header -->
|
<v-list>
|
||||||
<div class="px-4 py-4 border-b border-gray-100">
|
<v-list-item
|
||||||
<div class="flex items-center gap-3">
|
class="px-2"
|
||||||
<img
|
prepend-avatar="/Port Nimara New Logo-Circular Frame.png"
|
||||||
src="/Port Nimara New Logo-Circular Frame.png"
|
:title="rail ? '' : 'Port Nimara'"
|
||||||
class="h-8 w-8"
|
:subtitle="rail ? '' : 'Client Portal'"
|
||||||
alt="Port Nimara"
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click.stop="rail = !rail"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<v-icon>{{ rail ? 'mdi-menu' : 'mdi-chevron-left' }}</v-icon>
|
||||||
<span class="text-sm font-semibold text-gray-900">Port Nimara</span>
|
</v-btn>
|
||||||
<span class="text-xs text-gray-500">Client Portal</span>
|
</template>
|
||||||
</div>
|
</v-list-item>
|
||||||
</div>
|
</v-list>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
<v-divider></v-divider>
|
||||||
<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 -->
|
<!-- Navigation Items -->
|
||||||
<div class="px-4 py-4 border-t border-gray-100 space-y-2">
|
<v-list density="compact" nav>
|
||||||
<!-- User Info -->
|
<v-list-item
|
||||||
<div v-if="authState?.user" class="flex items-center gap-3 p-2">
|
v-for="item in navigationItems"
|
||||||
<div class="flex-shrink-0">
|
:key="item.to"
|
||||||
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
|
:prepend-icon="item.icon"
|
||||||
{{ (authState.user.name || authState.user.email || '?')[0].toUpperCase() }}
|
:title="item.label"
|
||||||
</div>
|
:value="item.to"
|
||||||
</div>
|
:to="item.to"
|
||||||
<div class="flex-1 min-w-0">
|
color="primary"
|
||||||
<p class="text-sm font-medium text-gray-900 truncate">
|
rounded="xl"
|
||||||
{{ authState.user.name || authState.user.email }}
|
class="mx-1"
|
||||||
</p>
|
></v-list-item>
|
||||||
<p class="text-xs text-gray-500 truncate">
|
</v-list>
|
||||||
{{ authState.user.email }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Role Badge -->
|
<template v-slot:append>
|
||||||
<div v-if="authState?.groups?.length" class="px-2">
|
<v-divider></v-divider>
|
||||||
<div
|
|
||||||
v-if="authState.groups.includes('admin')"
|
<!-- User Info Section -->
|
||||||
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"
|
<div class="pa-2">
|
||||||
|
<v-list v-if="authState?.user">
|
||||||
|
<v-list-item
|
||||||
|
:prepend-avatar="`https://ui-avatars.com/api/?name=${encodeURIComponent(authState.user.name || authState.user.email)}&background=387bca&color=fff`"
|
||||||
|
:title="authState.user.name || authState.user.email"
|
||||||
|
:subtitle="authState.user.email"
|
||||||
|
class="px-2"
|
||||||
>
|
>
|
||||||
Admin
|
<template v-slot:append v-if="!rail && authState?.groups?.length">
|
||||||
</div>
|
<div>
|
||||||
<div
|
<v-chip
|
||||||
v-else-if="authState.groups.includes('sales')"
|
v-if="authState.groups.includes('admin')"
|
||||||
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"
|
size="small"
|
||||||
>
|
color="orange"
|
||||||
Sales
|
variant="tonal"
|
||||||
</div>
|
>
|
||||||
</div>
|
Admin
|
||||||
|
</v-chip>
|
||||||
<!-- Logout Button -->
|
<v-chip
|
||||||
<button
|
v-else-if="authState.groups.includes('sales')"
|
||||||
@click="handleLogout"
|
size="small"
|
||||||
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"
|
color="green"
|
||||||
>
|
variant="tonal"
|
||||||
<Icon name="i-heroicons-arrow-left-on-rectangle" class="w-5 h-5 text-gray-400" />
|
>
|
||||||
<span>Logout</span>
|
Sales
|
||||||
</button>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</v-list-item>
|
||||||
|
|
||||||
<!-- 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 -->
|
<v-list-item
|
||||||
<h1 class="text-xl font-semibold text-gray-900">{{ pageTitle }}</h1>
|
@click="handleLogout"
|
||||||
</div>
|
prepend-icon="mdi-logout"
|
||||||
|
title="Logout"
|
||||||
|
class="px-2 mt-1"
|
||||||
|
base-color="error"
|
||||||
|
rounded="xl"
|
||||||
|
></v-list-item>
|
||||||
|
</v-list>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<!-- Page Content -->
|
<v-app-bar
|
||||||
<main class="flex-1 p-6 overflow-y-auto">
|
flat
|
||||||
<slot />
|
color="white"
|
||||||
</main>
|
class="border-b"
|
||||||
</div>
|
>
|
||||||
</div>
|
<v-app-bar-nav-icon
|
||||||
|
@click="drawer = !drawer"
|
||||||
|
class="d-lg-none"
|
||||||
|
></v-app-bar-nav-icon>
|
||||||
|
|
||||||
|
<v-toolbar-title class="text-h6">
|
||||||
|
{{ pageTitle }}
|
||||||
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-main class="bg-grey-lighten-4">
|
||||||
|
<slot />
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useDisplay } from 'vuetify';
|
||||||
// Define NavigationMenuItem type locally
|
|
||||||
interface NavigationMenuItem {
|
|
||||||
label: string;
|
|
||||||
icon: string;
|
|
||||||
to: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(false);
|
const drawer = ref(true);
|
||||||
|
const rail = ref(false);
|
||||||
|
|
||||||
// Get auth state - with fallback to prevent errors
|
// Get auth state - with fallback to prevent errors
|
||||||
const authState = computed(() => {
|
const authState = computed(() => {
|
||||||
|
|
@ -157,6 +143,7 @@ const pageTitle = computed(() => {
|
||||||
const routeName = route.name as string;
|
const routeName = route.name as string;
|
||||||
const pageTitles: Record<string, string> = {
|
const pageTitles: Record<string, string> = {
|
||||||
'dashboard': 'Dashboard',
|
'dashboard': 'Dashboard',
|
||||||
|
'dashboard-index': 'Dashboard',
|
||||||
'dashboard-expenses': 'Expense Tracking',
|
'dashboard-expenses': 'Expense Tracking',
|
||||||
'dashboard-interest-list': 'Interest List',
|
'dashboard-interest-list': 'Interest List',
|
||||||
'dashboard-berth-list': 'Berth List',
|
'dashboard-berth-list': 'Berth List',
|
||||||
|
|
@ -167,52 +154,52 @@ const pageTitle = computed(() => {
|
||||||
'dashboard-interest-analytics': 'Analytics',
|
'dashboard-interest-analytics': 'Analytics',
|
||||||
'dashboard-file-browser': 'File Browser',
|
'dashboard-file-browser': 'File Browser',
|
||||||
'dashboard-admin': 'Admin Console',
|
'dashboard-admin': 'Admin Console',
|
||||||
|
'dashboard-admin-index': 'Admin Console',
|
||||||
'dashboard-admin-audit-logs': 'Audit Logs',
|
'dashboard-admin-audit-logs': 'Audit Logs',
|
||||||
'dashboard-admin-system-logs': 'System Logs',
|
'dashboard-admin-system-logs': 'System Logs',
|
||||||
'dashboard-admin-duplicates': 'Duplicate Management',
|
'dashboard-admin-duplicates': 'Duplicate Management',
|
||||||
|
'dashboard-sidebar-demo': 'Sidebar Demo',
|
||||||
};
|
};
|
||||||
|
|
||||||
return pageTitles[routeName] || routeName?.split('-').map(word =>
|
return pageTitles[routeName] || 'Dashboard';
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
|
||||||
).join(' ') || 'Dashboard';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigation items based on user role
|
// Navigation items based on user role
|
||||||
const navigationItems = computed((): NavigationMenuItem[] => {
|
const navigationItems = computed(() => {
|
||||||
const items: NavigationMenuItem[] = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
icon: 'i-heroicons-home',
|
icon: 'mdi-home',
|
||||||
to: '/dashboard',
|
to: '/dashboard',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Analytics',
|
label: 'Analytics',
|
||||||
icon: 'i-heroicons-chart-bar',
|
icon: 'mdi-chart-bar',
|
||||||
to: '/dashboard/interest-analytics',
|
to: '/dashboard/interest-analytics',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Berth List',
|
label: 'Berth List',
|
||||||
icon: 'i-heroicons-table-cells',
|
icon: 'mdi-table',
|
||||||
to: '/dashboard/interest-berth-list',
|
to: '/dashboard/interest-berth-list',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Berth Status',
|
label: 'Berth Status',
|
||||||
icon: 'i-heroicons-map',
|
icon: 'mdi-map',
|
||||||
to: '/dashboard/interest-berth-status',
|
to: '/dashboard/interest-berth-status',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Interest List',
|
label: 'Interest List',
|
||||||
icon: 'i-heroicons-users',
|
icon: 'mdi-account-multiple',
|
||||||
to: '/dashboard/interest-list',
|
to: '/dashboard/interest-list',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Interest Status',
|
label: 'Interest Status',
|
||||||
icon: 'i-heroicons-clipboard-document-check',
|
icon: 'mdi-clipboard-check',
|
||||||
to: '/dashboard/interest-status',
|
to: '/dashboard/interest-status',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'File Browser',
|
label: 'File Browser',
|
||||||
icon: 'i-heroicons-folder-open',
|
icon: 'mdi-folder-open',
|
||||||
to: '/dashboard/file-browser',
|
to: '/dashboard/file-browser',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -222,12 +209,12 @@ const navigationItems = computed((): NavigationMenuItem[] => {
|
||||||
items.push(
|
items.push(
|
||||||
{
|
{
|
||||||
label: 'Expenses',
|
label: 'Expenses',
|
||||||
icon: 'i-heroicons-receipt-percent',
|
icon: 'mdi-receipt',
|
||||||
to: '/dashboard/expenses',
|
to: '/dashboard/expenses',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Interest Emails',
|
label: 'Interest Emails',
|
||||||
icon: 'i-heroicons-envelope',
|
icon: 'mdi-email',
|
||||||
to: '/dashboard/interest-emails',
|
to: '/dashboard/interest-emails',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -237,7 +224,7 @@ const navigationItems = computed((): NavigationMenuItem[] => {
|
||||||
if (authState.value?.groups?.includes('admin')) {
|
if (authState.value?.groups?.includes('admin')) {
|
||||||
items.push({
|
items.push({
|
||||||
label: 'Admin Console',
|
label: 'Admin Console',
|
||||||
icon: 'i-heroicons-shield-check',
|
icon: 'mdi-shield-crown',
|
||||||
to: '/dashboard/admin',
|
to: '/dashboard/admin',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -256,13 +243,25 @@ const handleLogout = async () => {
|
||||||
await router.push('/login');
|
await router.push('/login');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize drawer state on mobile
|
||||||
|
onMounted(() => {
|
||||||
|
if (mdAndDown.value) {
|
||||||
|
drawer.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Ensure proper responsive behavior */
|
.border-b {
|
||||||
@media (min-width: 1024px) {
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12) !important;
|
||||||
.lg\:w-64 {
|
}
|
||||||
width: 16rem !important;
|
|
||||||
}
|
.v-navigation-drawer--rail .v-list-item__append {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer--rail.v-navigation-drawer--is-hovering .v-list-item__append {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ export default defineNuxtConfig({
|
||||||
ssr: false,
|
ssr: false,
|
||||||
compatibilityDate: "2024-11-01",
|
compatibilityDate: "2024-11-01",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt", "@nuxt/ui"],
|
modules: ["nuxt-directus", "vuetify-nuxt-module", "@vite-pwa/nuxt"],
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
titleTemplate: "%s • Port Nimara Portal",
|
titleTemplate: "%s • Port Nimara Portal",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue