Delete old Vuexy VerticalNav layout system (5 components + 4 SCSS files) and replace with new modular components: AppSidebar, AppTopNavbar, CommandPalette, NotificationPanel, ToastStack, SkeletonLoader, EmptyState, Breadcrumbs. Rebuild all 4 layouts (Admin, Account, Marketing, Auth) using new components. Add Pinia toast store for flash message integration. Update navigation with cleaner groupings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
173 lines
5.2 KiB
Vue
173 lines
5.2 KiB
Vue
<script lang="ts" setup>
|
|
import { Link, usePage } from '@inertiajs/vue3'
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
import { accountNavItems } from '@/navigation/account'
|
|
import AppSidebar from '@/Components/AppSidebar.vue'
|
|
import AppTopNavbar from '@/Components/AppTopNavbar.vue'
|
|
import CommandPalette from '@/Components/CommandPalette.vue'
|
|
import NotificationPanel from '@/Components/NotificationPanel.vue'
|
|
import ToastStack from '@/Components/ToastStack.vue'
|
|
import { useToastStore } from '@/stores/toast'
|
|
|
|
interface AuthUser {
|
|
name: string
|
|
email: string
|
|
}
|
|
|
|
interface PageProps {
|
|
auth: { user: AuthUser | null }
|
|
domains: { marketing: string; account: string; admin: string }
|
|
impersonating: boolean
|
|
flash?: { success?: string; error?: string; info?: string }
|
|
}
|
|
|
|
const page = usePage()
|
|
const props = computed(() => page.props as unknown as PageProps)
|
|
const user = computed(() => props.value.auth?.user)
|
|
const isImpersonating = computed(() => props.value.impersonating)
|
|
const adminUrl = computed(() => `https://${props.value.domains?.admin}`)
|
|
|
|
const sidebarCollapsed = ref<boolean>(false)
|
|
const mobileOpen = ref<boolean>(false)
|
|
const notificationPanelOpen = ref<boolean>(false)
|
|
const notificationPanel = ref<InstanceType<typeof NotificationPanel> | null>(null)
|
|
const commandPalette = ref<InstanceType<typeof CommandPalette> | null>(null)
|
|
|
|
onMounted(() => {
|
|
const saved = localStorage.getItem('ezscale-account-nav-collapsed')
|
|
if (saved !== null) sidebarCollapsed.value = saved === 'true'
|
|
})
|
|
|
|
watch(sidebarCollapsed, (val) => {
|
|
localStorage.setItem('ezscale-account-nav-collapsed', String(val))
|
|
})
|
|
|
|
const toastStore = useToastStore()
|
|
watch(() => page.props, () => {
|
|
const flash = (page.props as unknown as PageProps).flash
|
|
if (flash?.success) toastStore.success(flash.success)
|
|
if (flash?.error) toastStore.error(flash.error)
|
|
if (flash?.info) toastStore.info(flash.info)
|
|
}, { immediate: true })
|
|
|
|
const paletteItems = computed(() => {
|
|
const items: Array<{ title: string; icon: string; href: string; section: string }> = []
|
|
for (const item of accountNavItems) {
|
|
if ('heading' in item) continue
|
|
if ('to' in item) {
|
|
items.push({ title: item.title, icon: item.icon || 'tabler-circle', href: item.to, section: 'Navigation' })
|
|
}
|
|
}
|
|
return items
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<VApp>
|
|
<div class="app-layout">
|
|
<AppSidebar
|
|
:nav-items="accountNavItems"
|
|
v-model:collapsed="sidebarCollapsed"
|
|
v-model:mobile-open="mobileOpen"
|
|
/>
|
|
|
|
<div
|
|
class="app-content-wrapper"
|
|
:style="{ marginLeft: sidebarCollapsed ? '72px' : '260px' }"
|
|
>
|
|
<AppTopNavbar
|
|
@toggle-sidebar="mobileOpen = !mobileOpen"
|
|
@open-command-palette="commandPalette?.open()"
|
|
>
|
|
<VBadge
|
|
:content="notificationPanel?.unreadCount"
|
|
:model-value="(notificationPanel?.unreadCount ?? 0) > 0"
|
|
color="error"
|
|
overlap
|
|
>
|
|
<VBtn
|
|
icon="tabler-bell"
|
|
variant="text"
|
|
size="small"
|
|
@click="notificationPanelOpen = true"
|
|
/>
|
|
</VBadge>
|
|
|
|
<span v-if="user" class="text-body-2 ms-3">
|
|
{{ user.name }}
|
|
</span>
|
|
|
|
<Link
|
|
v-if="user"
|
|
href="/logout"
|
|
method="post"
|
|
as="button"
|
|
class="text-decoration-none ms-2"
|
|
>
|
|
<VBtn variant="text" size="small" color="error">
|
|
<VIcon icon="tabler-logout" start />
|
|
Log out
|
|
</VBtn>
|
|
</Link>
|
|
</AppTopNavbar>
|
|
|
|
<main class="app-page-content">
|
|
<!-- Impersonation Alert -->
|
|
<VAlert
|
|
v-if="isImpersonating"
|
|
type="warning"
|
|
variant="tonal"
|
|
prominent
|
|
class="mb-6"
|
|
>
|
|
<VAlertTitle class="d-flex align-center justify-space-between flex-wrap gap-4">
|
|
<div class="d-flex align-center gap-2">
|
|
<VIcon icon="tabler-user-shield" />
|
|
<span class="font-weight-bold">Impersonation Active</span>
|
|
</div>
|
|
<Link
|
|
:href="adminUrl + '/impersonate/stop'"
|
|
method="post"
|
|
as="button"
|
|
class="text-decoration-none"
|
|
>
|
|
<VBtn color="warning" variant="flat" size="small">
|
|
<VIcon icon="tabler-logout" start />
|
|
Stop Impersonating
|
|
</VBtn>
|
|
</Link>
|
|
</VAlertTitle>
|
|
<div class="mt-2">
|
|
You are viewing the account as <strong>{{ user?.name }}</strong>. All actions will be attributed to this user.
|
|
</div>
|
|
</VAlert>
|
|
|
|
<slot />
|
|
</main>
|
|
|
|
<footer class="app-footer">
|
|
© {{ new Date().getFullYear() }} EZSCALE. All rights reserved.
|
|
</footer>
|
|
</div>
|
|
|
|
<div
|
|
class="app-overlay"
|
|
:class="{ 'app-overlay--visible': mobileOpen }"
|
|
@click="mobileOpen = false"
|
|
/>
|
|
</div>
|
|
|
|
<NotificationPanel
|
|
ref="notificationPanel"
|
|
v-model="notificationPanelOpen"
|
|
/>
|
|
|
|
<CommandPalette
|
|
ref="commandPalette"
|
|
:items="paletteItems"
|
|
/>
|
|
|
|
<ToastStack />
|
|
</VApp>
|
|
</template>
|