✨ feat (layout): implement app layout with sidebar and header
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
const navMenu: any[] = [
|
||||
{
|
||||
heading: 'General',
|
||||
items: [
|
||||
{
|
||||
title: 'Home',
|
||||
icon: 'i-lucide-home',
|
||||
link: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const navMenuBottom: any[] = [
|
||||
{
|
||||
title: 'Help & Support',
|
||||
icon: 'i-lucide-circle-help',
|
||||
link: 'https://github.com/simrs/simrs-fe',
|
||||
},
|
||||
]
|
||||
|
||||
function resolveNavItemComponent(item: any): any {
|
||||
if ('children' in item) return resolveComponent('LayoutSidebarNavGroup')
|
||||
|
||||
return resolveComponent('LayoutSidebarNavLink')
|
||||
}
|
||||
|
||||
const teams: {
|
||||
name: string
|
||||
logo: string
|
||||
plan: string
|
||||
}[] = [
|
||||
{
|
||||
name: 'SIMRS - RSSA',
|
||||
logo: 'i-lucide-gallery-vertical-end',
|
||||
plan: 'Saiful Anwar Hospital',
|
||||
},
|
||||
]
|
||||
|
||||
const user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
} = {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@email.com',
|
||||
avatar: '/avatars/avatartion.png',
|
||||
}
|
||||
|
||||
// const { sidebar } = useAppSettings()
|
||||
const sidebar = {
|
||||
collapsible: 'offcanvas', // 'offcanvas' | 'icon' | 'none'
|
||||
side: 'left', // 'left' | 'right'
|
||||
variant: 'sidebar', // 'sidebar' | 'floating' | 'inset'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sidebar :collapsible="sidebar.collapsible" :side="sidebar.side" :variant="sidebar.variant">
|
||||
<SidebarHeader>
|
||||
<LayoutSidebarNavHeader :teams="teams" />
|
||||
<Search />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup v-for="(nav, indexGroup) in navMenu" :key="indexGroup">
|
||||
<SidebarGroupLabel v-if="nav.heading">
|
||||
{{ nav.heading }}
|
||||
</SidebarGroupLabel>
|
||||
<component :is="resolveNavItemComponent(item)" v-for="(item, index) in nav.items" :key="index" :item="item" />
|
||||
</SidebarGroup>
|
||||
<SidebarGroup class="mt-auto">
|
||||
<component
|
||||
:is="resolveNavItemComponent(item)"
|
||||
v-for="(item, index) in navMenuBottom"
|
||||
:key="index"
|
||||
:item="item"
|
||||
size="sm"
|
||||
/>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<LayoutSidebarNavFooter :user="user" />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
function setLinks() {
|
||||
if (route.fullPath === '/') {
|
||||
return [{ title: 'Home', href: '/' }]
|
||||
}
|
||||
|
||||
const segments = route.fullPath.split('/').filter((item) => item !== '')
|
||||
|
||||
const breadcrumbs = segments.map((item, index) => {
|
||||
const str = item.replace(/-/g, ' ')
|
||||
const title = str
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ')
|
||||
|
||||
return {
|
||||
title,
|
||||
href: `/${segments.slice(0, index + 1).join('/')}`,
|
||||
}
|
||||
})
|
||||
|
||||
return [{ title: 'Home', href: '/' }, ...breadcrumbs]
|
||||
}
|
||||
|
||||
const links = ref<
|
||||
{
|
||||
title: string
|
||||
href: string
|
||||
}[]
|
||||
>(setLinks())
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(val) => {
|
||||
if (val) {
|
||||
links.value = setLinks()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="h-53px bg-background sticky top-0 z-10 flex items-center gap-4 border-b px-4 md:px-6">
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" class="h-4" />
|
||||
<!-- <BaseBreadcrumbCustom :links="links" /> -->
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { useSidebar } from '~/components/pub/ui/sidebar'
|
||||
|
||||
defineProps<{
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
function handleLogout() {
|
||||
navigateTo('/login')
|
||||
}
|
||||
|
||||
const showModalTheme = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar class="h-8 w-8 rounded-lg">
|
||||
<AvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<AvatarFallback class="rounded-lg">
|
||||
{{
|
||||
user.name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
}}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ user.name }}</span>
|
||||
<span class="truncate text-xs">{{ user.email }}</span>
|
||||
</div>
|
||||
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
:side="isMobile ? 'bottom' : 'right'"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuLabel class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar class="h-8 w-8 rounded-lg">
|
||||
<AvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<AvatarFallback class="rounded-lg">
|
||||
{{
|
||||
user.name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
}}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ user.name }}</span>
|
||||
<span class="truncate text-xs">{{ user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem @click="showModalTheme = true">
|
||||
<Icon name="i-lucide-paintbrush" />
|
||||
Theme
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="handleLogout">
|
||||
<Icon name="i-lucide-log-out" />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
<Dialog v-model:open="showModalTheme">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Customize</DialogTitle>
|
||||
<DialogDescription class="text-muted-foreground text-xs"> Customize & Preview in Real Time </DialogDescription>
|
||||
</DialogHeader>
|
||||
<ThemeCustomize />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import type { SidebarMenuButtonVariants } from '~/components/pub/ui/sidebar'
|
||||
import { useSidebar } from '~/components/pub/ui/sidebar'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
item: any
|
||||
size?: SidebarMenuButtonVariants['size']
|
||||
}>(),
|
||||
{
|
||||
size: 'default',
|
||||
}
|
||||
)
|
||||
|
||||
const { setOpenMobile } = useSidebar()
|
||||
|
||||
const openCollapsible = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<Collapsible :key="item.title" v-model:open="openCollapsible" as-child class="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton :tooltip="item.title" :size="size">
|
||||
<Icon :name="item.icon || ''" mode="svg" />
|
||||
<span>{{ item.title }}</span>
|
||||
<span
|
||||
v-if="item.new"
|
||||
class="bg-#adfa1d rounded-md px-1.5 py-0.5 text-xs leading-none text-black no-underline group-hover:no-underline"
|
||||
>
|
||||
New
|
||||
</span>
|
||||
<Icon
|
||||
name="i-lucide-chevron-right"
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem v-for="subItem in item.children" :key="subItem.title">
|
||||
<SidebarMenuSubButton as-child>
|
||||
<NuxtLink :to="subItem.link" @click="setOpenMobile(false)">
|
||||
<span>{{ subItem.title }}</span>
|
||||
<span
|
||||
v-if="subItem.new"
|
||||
class="bg-#adfa1d rounded-md px-1.5 py-0.5 text-xs leading-none text-black no-underline group-hover:no-underline"
|
||||
>
|
||||
New
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
teams: {
|
||||
name: string
|
||||
logo: string
|
||||
plan: string
|
||||
}[]
|
||||
}>()
|
||||
|
||||
const activeTeam = ref(props.teams[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div
|
||||
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
|
||||
>
|
||||
<Icon :name="activeTeam.logo" class="size-4" />
|
||||
</div>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">
|
||||
{{ activeTeam.name }}
|
||||
</span>
|
||||
<span class="truncate text-xs">{{ activeTeam.plan }}</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { SidebarMenuButtonVariants } from '~/components/comps-pub/ui/sidebar'
|
||||
import { useSidebar } from '~/components/pub/ui/sidebar'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
item: any
|
||||
size?: SidebarMenuButtonVariants['size']
|
||||
}>(),
|
||||
{
|
||||
size: 'default',
|
||||
}
|
||||
)
|
||||
|
||||
const { setOpenMobile } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton as-child :tooltip="item.title" :size="size">
|
||||
<NuxtLink :to="item.link" @click="setOpenMobile(false)">
|
||||
<Icon :name="item.icon || ''" mode="svg" />
|
||||
<span>{{ item.title }}</span>
|
||||
<span
|
||||
v-if="item.new"
|
||||
class="bg-#adfa1d rounded-md px-1.5 py-0.5 text-xs leading-none text-black no-underline group-hover:no-underline"
|
||||
>
|
||||
New
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user