557 lines
13 KiB
Vue
557 lines
13 KiB
Vue
<template>
|
|
<v-navigation-drawer
|
|
:model-value="drawer"
|
|
:rail="rail"
|
|
permanent
|
|
app
|
|
class="bg-white d-flex flex-column"
|
|
@update:model-value="emit('update:drawer', $event)"
|
|
@mouseenter="handleMouseEnter"
|
|
@mouseleave="handleMouseLeave"
|
|
>
|
|
<v-list-item class="py-4 px-3 sidebar-logo-container" :class="{ 'text-center': rail }">
|
|
<NuxtLink to="/dashboard" class="sidebar-logo-link">
|
|
<div class="d-flex align-center sidebar-logo-wrapper" :class="{ 'justify-center': rail }">
|
|
<div class="sidebar-logo-image-wrapper">
|
|
<img
|
|
src="/love logo biru small.png"
|
|
alt="Antrean Logo"
|
|
class="sidebar-logo"
|
|
:class="{ 'sidebar-logo-rail': rail }"
|
|
/>
|
|
</div>
|
|
<v-list-item-title
|
|
v-if="!rail"
|
|
class="ml-2 text-h6 font-weight-bold sidebar-title"
|
|
>
|
|
<span class="sidebar-title-accent">Antrean</span> RSSA
|
|
</v-list-item-title>
|
|
</div>
|
|
</NuxtLink>
|
|
</v-list-item>
|
|
<v-divider></v-divider>
|
|
|
|
<v-list
|
|
density="compact"
|
|
nav
|
|
class="mt-2 flex-grow-1"
|
|
v-model:opened="openedGroups"
|
|
>
|
|
<template v-for="item in items" :key="item.name">
|
|
<!-- Popup Menu ketika locked dan rail -->
|
|
<v-menu
|
|
v-if="item.children && rail && isLocked"
|
|
location="end"
|
|
offset="8"
|
|
>
|
|
<template #activator="{ props: menuProps }">
|
|
<v-tooltip
|
|
:disabled="!rail"
|
|
open-on-hover
|
|
location="end"
|
|
:text="item.name"
|
|
>
|
|
<template #activator="{ props: tooltipProps }">
|
|
<v-list-item
|
|
v-bind="{ ...menuProps, ...tooltipProps }"
|
|
:prepend-icon="item.icon"
|
|
:title="item.name"
|
|
class="sidebar-nav-item"
|
|
active-class="sidebar-nav-item-active"
|
|
></v-list-item>
|
|
</template>
|
|
</v-tooltip>
|
|
</template>
|
|
<v-list density="compact" class="sidebar-popup-menu">
|
|
<v-list-item
|
|
v-for="child in item.children"
|
|
:key="child.name"
|
|
:to="child.path"
|
|
:title="child.name"
|
|
:prepend-icon="child.icon"
|
|
link
|
|
class="sidebar-nav-item"
|
|
active-class="sidebar-nav-item-active"
|
|
@click="closePopup"
|
|
></v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
|
|
<!-- Dropdown normal ketika tidak locked atau expanded -->
|
|
<v-list-group
|
|
v-else-if="item.children"
|
|
:value="item.name"
|
|
:disabled="rail"
|
|
>
|
|
<template v-slot:activator="{ props: groupProps }">
|
|
<v-list-item
|
|
v-bind="groupProps"
|
|
:prepend-icon="item.icon"
|
|
:title="item.name"
|
|
class="sidebar-nav-item"
|
|
active-class="sidebar-nav-item-active"
|
|
></v-list-item>
|
|
</template>
|
|
|
|
<v-list-item
|
|
v-for="child in item.children"
|
|
:key="child.name"
|
|
:to="child.path"
|
|
:title="child.name"
|
|
:prepend-icon="child.icon"
|
|
link
|
|
class="pl-8 sidebar-nav-item"
|
|
active-class="sidebar-nav-item-active"
|
|
></v-list-item>
|
|
</v-list-group>
|
|
|
|
<v-tooltip
|
|
v-else
|
|
:disabled="!rail"
|
|
open-on-hover
|
|
location="end"
|
|
:text="item.name"
|
|
>
|
|
<template #activator="{ props: tooltipProps }">
|
|
<v-list-item
|
|
v-bind="tooltipProps"
|
|
:prepend-icon="item.icon"
|
|
:title="item.name"
|
|
:to="item.path"
|
|
link
|
|
class="sidebar-nav-item"
|
|
active-class="sidebar-nav-item-active"
|
|
></v-list-item>
|
|
</template>
|
|
</v-tooltip>
|
|
</template>
|
|
</v-list>
|
|
|
|
<v-divider></v-divider>
|
|
|
|
<v-list v-if="user" nav density="compact" class="pa-2 flex-shrink-0">
|
|
<ProfilePopup :user="user" @logout="handleLogout" :rail="rail" />
|
|
</v-list>
|
|
<v-list v-else nav density="compact" class="flex-shrink-0">
|
|
<v-list-item
|
|
@click="redirectToLogin"
|
|
prepend-icon="mdi-login"
|
|
title="Login"
|
|
link
|
|
class="sidebar-login-item"
|
|
></v-list-item>
|
|
</v-list>
|
|
|
|
<!-- Lock Sidebar Toggle - Positioned at bottom left -->
|
|
<div class="sidebar-lock-container">
|
|
<v-tooltip
|
|
:disabled="!rail"
|
|
open-on-hover
|
|
location="end"
|
|
:text="isLocked ? 'Buka Sidebar' : 'Kunci Sidebar'"
|
|
>
|
|
<template #activator="{ props: tooltipProps }">
|
|
<v-btn
|
|
v-bind="tooltipProps"
|
|
:class="['sidebar-lock-btn', { 'sidebar-lock-btn-locked': isLocked }]"
|
|
variant="text"
|
|
size="small"
|
|
@click="handleLockToggle(!isLocked)"
|
|
>
|
|
<!-- Icon Collapsed Sidebar (sidebar collapsed/locked) -->
|
|
<svg
|
|
v-if="isLocked"
|
|
class="sidebar-toggle-icon"
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 20 20"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<!-- Rounded rectangle container -->
|
|
<rect
|
|
x="2"
|
|
y="2"
|
|
width="16"
|
|
height="16"
|
|
rx="3"
|
|
fill="var(--color-primary-100)"
|
|
/>
|
|
<!-- Sidebar area (kiri, narrow ~30%, biru gelap) -->
|
|
<rect
|
|
x="2"
|
|
y="2"
|
|
width="6"
|
|
height="16"
|
|
rx="3"
|
|
fill="var(--color-primary-600)"
|
|
/>
|
|
</svg>
|
|
<!-- Icon Expanded Sidebar (sidebar expanded/unlocked) -->
|
|
<svg
|
|
v-else
|
|
class="sidebar-toggle-icon"
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 20 20"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<!-- Rounded rectangle container -->
|
|
<rect
|
|
x="2"
|
|
y="2"
|
|
width="16"
|
|
height="16"
|
|
rx="3"
|
|
fill="var(--color-primary-100)"
|
|
/>
|
|
<!-- Sidebar area (kiri, wider ~55%, biru gelap) -->
|
|
<rect
|
|
x="2"
|
|
y="2"
|
|
width="11"
|
|
height="16"
|
|
rx="3"
|
|
fill="var(--color-primary-600)"
|
|
/>
|
|
<!-- Left-pointing arrow in the white area -->
|
|
<path
|
|
d="M 15.5 10 L 13.5 8 M 15.5 10 L 13.5 12"
|
|
stroke="var(--color-primary-600)"
|
|
stroke-width="1.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</svg>
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
</div>
|
|
</v-navigation-drawer>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { defineProps, defineEmits, onMounted, ref, watch } from "vue";
|
|
import { navigateTo } from "#app";
|
|
import { useLocalStorage } from "@vueuse/core";
|
|
import ProfilePopup from "./ProfilePopup.vue";
|
|
import { useAuth } from "~/composables/useAuth";
|
|
|
|
const { user, logout, checkAuth } = useAuth();
|
|
|
|
interface NavItem {
|
|
id: number;
|
|
name: string;
|
|
path: string;
|
|
icon: string;
|
|
children?: NavItem[];
|
|
}
|
|
|
|
const props = defineProps({
|
|
items: {
|
|
type: Array as () => NavItem[],
|
|
required: true,
|
|
},
|
|
rail: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
drawer: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(["update:drawer", "toggle-rail"]);
|
|
|
|
const openedGroups = ref<string[]>([]);
|
|
|
|
watch(
|
|
() => props.rail,
|
|
(newRailState) => {
|
|
if (newRailState === true) {
|
|
openedGroups.value = [];
|
|
}
|
|
}
|
|
);
|
|
|
|
const isHovering = ref(false);
|
|
const isLocked = useLocalStorage("sidebar-locked", false);
|
|
|
|
const handleMouseEnter = () => {
|
|
if (props.rail && !isLocked.value) {
|
|
isHovering.value = true;
|
|
emit("toggle-rail");
|
|
}
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
if (isHovering.value && !isLocked.value) {
|
|
isHovering.value = false;
|
|
emit("toggle-rail");
|
|
}
|
|
};
|
|
|
|
const handleLockToggle = (value?: boolean | null) => {
|
|
const lockValue = value ?? !isLocked.value;
|
|
isLocked.value = lockValue;
|
|
// Jika di-lock dan sedang expanded, collapse sidebar
|
|
if (lockValue && !props.rail) {
|
|
emit("toggle-rail");
|
|
}
|
|
};
|
|
|
|
const closePopup = () => {
|
|
// Popup akan otomatis tertutup saat item diklik
|
|
};
|
|
|
|
// --- AUTH LOGIC (Kept the same) ---
|
|
|
|
const handleLogout = async () => {
|
|
console.log("🚪 SideBar logout initiated...");
|
|
try {
|
|
await logout();
|
|
} catch (error) {
|
|
console.error("❌ SideBar logout error:", error);
|
|
}
|
|
};
|
|
|
|
const redirectToLogin = () => {
|
|
navigateTo("/LoginPage");
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await checkAuth();
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@use '~/assets/scss/_colors.scss' as *;
|
|
|
|
:deep(.v-navigation-drawer) {
|
|
position: relative;
|
|
}
|
|
|
|
.sidebar-logo-container {
|
|
overflow: visible !important;
|
|
height: 80px !important;
|
|
|
|
:deep(.v-list-item__content) {
|
|
overflow: visible !important;
|
|
}
|
|
|
|
&.text-center {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding-left: 8px !important;
|
|
padding-right: 8px !important;
|
|
}
|
|
}
|
|
|
|
.sidebar-logo-link {
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: block;
|
|
width: 100%;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s ease;
|
|
overflow: visible;
|
|
|
|
&:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
&:active {
|
|
opacity: 0.6;
|
|
}
|
|
}
|
|
|
|
.sidebar-logo-wrapper {
|
|
width: 100%;
|
|
overflow: visible;
|
|
|
|
&.justify-center {
|
|
justify-content: center;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
}
|
|
}
|
|
|
|
.sidebar-logo-image-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 40px;
|
|
padding: 2px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.sidebar-logo {
|
|
width: 32px;
|
|
height: 32px;
|
|
min-width: 32px;
|
|
min-height: 32px;
|
|
object-fit: contain;
|
|
display: block;
|
|
cursor: pointer;
|
|
transition: transform 0.2s ease;
|
|
flex-shrink: 0;
|
|
|
|
&:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
}
|
|
|
|
.sidebar-logo-rail {
|
|
margin: 0 auto;
|
|
width: auto;
|
|
max-width: none;
|
|
}
|
|
|
|
.sidebar-title {
|
|
color: var(--color-neutral-800);
|
|
}
|
|
|
|
.sidebar-title-accent {
|
|
color: var(--color-primary-600);
|
|
}
|
|
|
|
.sidebar-nav-item {
|
|
color: var(--color-neutral-700);
|
|
|
|
:deep(.v-list-item__prepend) {
|
|
.v-icon {
|
|
color: var(--color-neutral-600);
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
background-color: var(--color-neutral-300);
|
|
color: var(--color-primary-600);
|
|
|
|
:deep(.v-list-item__prepend) {
|
|
.v-icon {
|
|
color: var(--color-primary-600);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.sidebar-nav-item-active {
|
|
background-color: var(--color-primary-50) !important;
|
|
color: var(--color-primary-600) !important;
|
|
font-weight: bold;
|
|
|
|
:deep(.v-list-item__prepend) {
|
|
.v-icon {
|
|
color: var(--color-primary-600) !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
.sidebar-login-item {
|
|
color: var(--color-neutral-800);
|
|
|
|
:deep(.v-list-item__prepend) {
|
|
.v-icon {
|
|
color: var(--color-neutral-700);
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
background-color: var(--color-neutral-300);
|
|
color: var(--color-primary-600);
|
|
|
|
:deep(.v-list-item__prepend) {
|
|
.v-icon {
|
|
color: var(--color-primary-600);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.sidebar-lock-container {
|
|
position: absolute;
|
|
bottom: 16px;
|
|
left: 8px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.sidebar-lock-btn {
|
|
color: var(--color-neutral-600) !important;
|
|
min-width: 40px !important;
|
|
width: 40px !important;
|
|
height: 40px !important;
|
|
|
|
&:hover {
|
|
background-color: var(--color-neutral-300) !important;
|
|
color: var(--color-primary-600) !important;
|
|
}
|
|
|
|
&.sidebar-lock-btn-locked {
|
|
color: var(--color-primary-600) !important;
|
|
|
|
&:hover {
|
|
background-color: var(--color-primary-50) !important;
|
|
}
|
|
}
|
|
|
|
:deep(.v-icon) {
|
|
font-size: 20px;
|
|
}
|
|
}
|
|
|
|
.sidebar-toggle-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
display: block;
|
|
}
|
|
|
|
.sidebar-popup-menu {
|
|
min-width: 200px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
background-color: var(--color-neutral-100);
|
|
|
|
:deep(.v-list-item) {
|
|
min-height: 40px;
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
|
|
&.sidebar-nav-item {
|
|
color: var(--color-neutral-700);
|
|
|
|
.v-list-item__prepend {
|
|
.v-icon {
|
|
color: var(--color-neutral-600);
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
background-color: var(--color-neutral-300);
|
|
color: var(--color-primary-600);
|
|
|
|
.v-list-item__prepend {
|
|
.v-icon {
|
|
color: var(--color-primary-600);
|
|
}
|
|
}
|
|
}
|
|
|
|
&.sidebar-nav-item-active {
|
|
background-color: var(--color-primary-50) !important;
|
|
color: var(--color-primary-600) !important;
|
|
font-weight: bold;
|
|
|
|
.v-list-item__prepend {
|
|
.v-icon {
|
|
color: var(--color-primary-600) !important;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|