Files
website/website/resources/ts/Layouts/AccountLayout.vue
Claude Dev d01ea28a8b Phase 1: Rebuild layout infrastructure with new shared components
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>
2026-03-14 16:55:54 -04:00

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">
&copy; {{ 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>