Dev cleaning (#106)
This commit is contained in:
No files matched your search
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
const navMenu = ref([])
|
||||
|
||||
const teams: {
|
||||
name: string
|
||||
logo: string
|
||||
plan: string
|
||||
}[] = [
|
||||
{
|
||||
name: 'SIMRS - RSSA',
|
||||
logo: '/rssa-logo.png',
|
||||
plan: 'Saiful Anwar Hospital',
|
||||
},
|
||||
]
|
||||
const sidebar = {
|
||||
collapsible: 'offcanvas', // 'offcanvas' | 'icon' | 'none'
|
||||
side: 'left', // 'left' | 'right'
|
||||
variant: 'sidebar', // 'sidebar' | 'floating' | 'inset'
|
||||
}
|
||||
const navMenuBottom: any[] = [
|
||||
{
|
||||
title: 'Help & Support',
|
||||
icon: 'i-lucide-circle-help',
|
||||
link: 'https://github.com/simrs/simrs-fe',
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await setMenu()
|
||||
})
|
||||
|
||||
function resolveNavItemComponent(item: any): any {
|
||||
if ('children' in item) return resolveComponent('LayoutSidebarNavGroup')
|
||||
|
||||
return resolveComponent('LayoutSidebarNavLink')
|
||||
}
|
||||
|
||||
async function setMenu() {
|
||||
const position_code = 'sys'
|
||||
const res = await fetch(`/side-menu-items/${position_code}.json`)
|
||||
const rawMenu = await res.text()
|
||||
navMenu.value = JSON.parse(rawMenu)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sidebar :collapsible="sidebar.collapsible" :side="sidebar.side" :variant="sidebar.variant">
|
||||
<SidebarHeader>
|
||||
<LayoutSidebarNavHeader :teams="teams" />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<template v-if="navMenu.length > 0">
|
||||
<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"
|
||||
class="my-1 mb-1"
|
||||
/>
|
||||
</SidebarGroup>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="p-5">
|
||||
<div v-for="n in 10" :key="n" class="my-4 h-8 animate-pulse rounded bg-gray-200 py-2"></div>
|
||||
</div>
|
||||
</template>
|
||||
<SidebarGroup class="mt-auto">
|
||||
<component
|
||||
:is="resolveNavItemComponent(item)"
|
||||
v-for="(item, index) in navMenuBottom"
|
||||
:key="index"
|
||||
:item="item"
|
||||
size="sm"
|
||||
/>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<LayoutSidebarNavFooter />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
reverse?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex h-dvh items-center justify-center px-4 lg:max-w-none lg:px-0"
|
||||
:class="{ 'flex-row-reverse': reverse }"
|
||||
>
|
||||
<div class="bg-muted relative hidden h-full flex-1 flex-col p-10 text-white lg:flex dark:border-r">
|
||||
<div class="absolute inset-0 bg-zinc-900" />
|
||||
<div class="relative z-20 flex items-center text-lg font-medium">RSSA</div>
|
||||
<div class="relative z-20 mt-auto">
|
||||
<blockquote class="space-y-2">
|
||||
<p class="text-lg">
|
||||
“This library has saved me countless hours of work and helped me deliver stunning designs to my
|
||||
clients faster than ever before.”
|
||||
</p>
|
||||
<footer class="text-sm">Sofia Davis</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto flex-1 lg:p-8">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import ThemeToggle from "./ThemeToggle.vue"
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
function setLinks() {
|
||||
if (route.path === '/') {
|
||||
return [{ title: 'Home', href: '/' }]
|
||||
}
|
||||
|
||||
const segments = route.path.split('/').filter((item) => item !== '')
|
||||
|
||||
const breadcrumbs = segments.map((item, index) => {
|
||||
// Bersihkan query parameters dari segment jika ada
|
||||
const cleanItem = item.split('?')[0] || item
|
||||
const str = cleanItem.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).map(seg => seg.split('?')[0] || seg).join('/')}`,
|
||||
}
|
||||
})
|
||||
|
||||
return [{ title: 'Home', href: '/' }, ...breadcrumbs]
|
||||
}
|
||||
|
||||
const links = ref<
|
||||
{
|
||||
title: string
|
||||
href: string
|
||||
}[]
|
||||
>(setLinks())
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(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" />
|
||||
<PubMyUiBreadcrumb :links="links" />
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<slot />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { useSidebar } from '~/components/pub/ui/sidebar'
|
||||
|
||||
// defineProps<{
|
||||
// user: {
|
||||
// name: string
|
||||
// email: string
|
||||
// avatar: string
|
||||
// }
|
||||
// }>()
|
||||
|
||||
const { isMobile } = useSidebar()
|
||||
const { logout } = useUserStore()
|
||||
const userStore = useUserStore().user
|
||||
|
||||
function handleLogout() {
|
||||
navigateTo('/auth/login')
|
||||
logout()
|
||||
}
|
||||
|
||||
const showModalTheme = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<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="" :alt="userStore?.user_name || 'system'" />
|
||||
<AvatarFallback class="rounded-lg">
|
||||
{{
|
||||
userStore?.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">{{ userStore?.user_name || '' }}</span>
|
||||
<span class="truncate text-xs">{{ userStore?.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 bg-white"
|
||||
: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="" :alt="userStore?.user_name || 'system'" />
|
||||
<AvatarFallback class="rounded-lg">
|
||||
{{
|
||||
userStore?.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">{{ userStore?.user_name || '' }}</span>
|
||||
<span class="truncate text-xs">{{ userStore?.user_email || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem class="hover:bg-gray-100" @click="showModalTheme = true">
|
||||
<Icon name="i-lucide-user" />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem class="hover:bg-gray-100" @click="handleLogout">
|
||||
<Icon name="i-lucide-log-out" />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<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" class="md:!text-xs xl:!text-sm 2xl:!text-base">
|
||||
<Icon :name="item.icon || ''" mode="svg" />
|
||||
<span class="mx-2">{{ item.title }}</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"
|
||||
class="mx-4 rounded-lg py-5 text-sm transition-all duration-200 md:!text-xs xl:!text-sm 2xl:!text-base"
|
||||
active-class="bg-primary text-white"
|
||||
@click="setOpenMobile(false)"
|
||||
>
|
||||
<span>{{ subItem.title }}</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"
|
||||
>
|
||||
<img :src="activeTeam.logo" class="h-full w-full" />
|
||||
</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,42 @@
|
||||
<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()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton as-child :tooltip="item.title" :size="size" class="">
|
||||
<NuxtLink
|
||||
:to="item.link"
|
||||
class="group flex items-center gap-3 rounded-lg px-2 py-4 text-sm transition-all duration-200 md:!text-xs xl:!text-sm 2xl:!text-base"
|
||||
active-class="bg-primary text-white"
|
||||
@click="setOpenMobile(false)"
|
||||
>
|
||||
<Icon :name="item.icon || ''" mode="svg" />
|
||||
<span class="mx-1">{{ 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>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '~/composables/useTheme'
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
@click="toggleTheme"
|
||||
class="ml-2 rounded border px-2 py-1 text-sm"
|
||||
:title="theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||
>
|
||||
<span v-if="theme === 'dark'"><Icon name="i-lucide-moon" class="h-4 w-4 mt-1" /></span>
|
||||
<span v-else><Icon name="i-lucide-sun" class="h-4 w-4 mt-1" /></span>
|
||||
</button>
|
||||
</template>
|
||||
Reference in New Issue
Block a user