Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.

This commit is contained in:
Haqeem Solehan
2025-10-16 16:05:39 +08:00
commit b124ff8092
336 changed files with 94392 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
<script setup>
const route = useRoute();
// Get breadcrumb from page meta
const breadcrumb = computed(() => {
let breadcrumb = null;
const matched = route.matched;
if (matched[matched.length - 1].meta?.breadcrumb) {
breadcrumb = matched[matched.length - 1].meta.breadcrumb;
} else {
// if no breadcrumb in page meta, get breadcrumb from route matched
breadcrumb = matched.map((item) => {
return {
name: item.name,
path: item.path,
};
});
return breadcrumb;
}
// if type current overwrite path to its own path
if (breadcrumb) {
breadcrumb.forEach((item) => {
if (item.type == "current") {
item.path = route.path;
} else if (item.type == "parent") {
item.path = route.path.split("/").slice(0, -item.parentNo).join("/");
}
});
}
return breadcrumb;
});
// Get title from page meta
const title = computed(() => {
const matched = route.matched;
const title = matched[matched.length - 1].name;
return title;
});
async function navigateMenu(path) {
try {
await navigateTo(path);
} catch (e) {
return;
}
}
</script>
<template>
<div v-if="breadcrumb" class="mb-6">
<nav aria-label="Breadcrumb" class="mb-4">
<ol class="flex items-center text-sm">
<li class="flex items-center">
<NuxtLink to="/" class="text-gray-500 hover:text-gray-700">
<Icon name="mdi:home" size="16" />
</NuxtLink>
</li>
<li v-for="(item, index) in breadcrumb" :key="index" class="flex items-center">
<Icon
name="mdi:chevron-right"
size="16"
class="mx-2 text-gray-400"
aria-hidden="true"
/>
<a
@click="navigateMenu(item.path)"
class="cursor-pointer capitalize"
:class="{
'text-gray-500 hover:text-gray-700': index !== breadcrumb.length - 1,
'text-primary font-medium': index === breadcrumb.length - 1,
}"
:aria-current="index === breadcrumb.length - 1 ? 'page' : undefined"
>
{{ item.name }}
</a>
</li>
</ol>
</nav>
<!-- <div class="flex justify-between items-center">
<h1 class="text-2xl font-semibold text-gray-800">{{ title }}</h1>
<slot name="right"></slot>
</div> -->
</div>
</template>

View File

@@ -0,0 +1,98 @@
<script setup>
const route = useRoute();
// Get breadcrumb from page meta
const breadcrumb = computed(() => {
let breadcrumb = null;
const matched = route.matched;
if (matched[matched.length - 1].meta?.breadcrumb) {
breadcrumb = matched[matched.length - 1].meta.breadcrumb;
} else {
// if no breadcrumb in page meta, get breadcrumb from route matched
breadcrumb = matched.map((item) => {
return {
name: item.meta.title,
path: item.path,
};
});
return breadcrumb;
}
// if type current overwrite path to its own path
if (breadcrumb) {
breadcrumb.forEach((item) => {
if (item.type == "current") {
item.path = route.path;
} else if (item.type == "parent") {
item.path = route.path.split("/").slice(0, -item.parentNo).join("/");
}
});
}
return breadcrumb;
});
// Get title from page meta
const title = computed(() => {
const matched = route.matched;
const title = matched[matched.length - 1].meta.title;
return title;
});
async function navigateMenu(path) {
try {
await navigateTo(path);
} catch (e) {
return;
}
}
</script>
<template>
<div v-if="breadcrumb">
<div class="flex flex-col md:flex-row items-stretch justify-between">
<div
class="flex items-center"
v-if="breadcrumb && breadcrumb.length != 0"
>
<span
v-for="(item, index) in breadcrumb"
:key="index"
class="flex items-center text-primary"
>
<Icon
v-if="index != 0"
name="ic:round-chevron-right"
size="14"
class="mr-1 text-gray-500"
></Icon>
<a
@click="navigateMenu(item.path)"
class="cursor-pointer hover:underline pr-1 font-normal text-sm text-gray-500"
:class="{
'!text-primary': breadcrumb.length - 1 == index,
}"
>{{ item.name }}</a
>
</span>
</div>
<slot name="right"></slot>
</div>
<div class="relative flex flex-col min-w-0 lg:mb-0 break-words w-full">
<div class="rounded-t mb-5">
<div class="flex flex-wrap items-center">
<div>
<div>
<span class="text-xl font-semibold text-gray-600">
{{ title }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup></script>
<template>
<div>
<div
class="w-full h-14 z-20 bg-white dark:bg-slate-800 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md shadow-slate-200 dark:shadow-slate-900"
>
<div class="flex justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-800 dark:text-white">
<Icon
name="material-symbols:chevron-left-rounded"
class="inline-block w-6 h-6 mr-2"
/>
</h1>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,327 @@
<script setup>
const isVertical = ref(true);
const isDesktop = ref(true);
const emit = defineEmits(["toggleMenu"]);
// Use site settings composable
const { siteSettings, setTheme, getCurrentTheme } = useSiteSettings();
// const { locale } = useI18n();
// const colorMode = useColorMode();
const langList = languageList();
const locale = ref("en");
const themes = themeList();
const themes2 = themeList2();
function setThemeLocal(theme) {
setTheme(theme); // Use the site settings setTheme function
}
function rgbToHex(rgbString) {
// Split the input string into an array of components
const rgbArray = rgbString.split(",");
// Convert each component to its numeric value
const r = parseInt(rgbArray[0].trim(), 10);
const g = parseInt(rgbArray[1].trim(), 10);
const b = parseInt(rgbArray[2].trim(), 10);
// Convert the numeric RGB values to hexadecimal
const rHex = r.toString(16).padStart(2, "0");
const gHex = g.toString(16).padStart(2, "0");
const bHex = b.toString(16).padStart(2, "0");
// Concatenate the components and return the final hexadecimal color code
return `#${rHex}${gHex}${bHex}`;
}
// Toggle Open/Close menu
const toggleMenu = (event) => {
emit("toggleMenu", event);
};
// Focus on search input
function toggleSearch() {
document.getElementById("header-search").value = "";
document.getElementById("header-search").focus();
}
// Change language
const changeLanguage = (lang) => {
locale.value = lang;
};
const languageNow = computed(() => {
return langList.find((lang) => lang.value == locale.value);
});
// Get current theme icon
const currentThemeIcon = computed(() => {
const theme = getCurrentTheme();
return theme === "dark" ? "ic:outline-dark-mode" : "ic:outline-light-mode";
});
onMounted(() => {
// If mobile toggleMenu
if (window.innerWidth < 768) {
emit("toggleMenu", true);
}
// Load site settings on mount and ensure they're properly populated
const { loadSiteSettings } = useSiteSettings();
loadSiteSettings().then(() => {
nextTick(() => {
// console.log('[Header.vue] Site settings loaded. Name:', siteSettings.value?.siteName, 'ShowInHeader:', siteSettings.value?.showSiteNameInHeader, 'Logo:', siteSettings.value?.siteLogo);
});
});
});
// Add computed to ensure logo reactivity
const currentLogo = computed(() => {
const logoUrl = siteSettings.value?.siteLogo;
if (logoUrl && logoUrl.trim() !== "") {
return logoUrl; // Use logo from settings if available and not empty
}
return "/img/logo/corradAF-logo.svg"; // Ultimate fallback
});
</script>
<template>
<div class="w-header">
<div class="flex items-stretch justify-between">
<div v-if="isVertical" class="flex">
<span class="flex items-center justify-center">
<button class="icon-btn h-10 w-10 rounded-full" @click="toggleMenu">
<Icon name="ic:round-menu" class="" />
</button>
</span>
<!-- Site logo and name for vertical layout - only show if explicitly enabled -->
<div
v-if="siteSettings?.value?.showSiteNameInHeader"
class="flex items-center ml-4"
>
<img
:src="currentLogo"
:alt="siteSettings?.value?.siteName || 'Site Logo'"
class="h-8 block"
@error="$event.target.src = '/img/logo/corradAF-logo.svg'"
/>
<span
v-if="siteSettings?.value?.siteName"
class="text-lg font-semibold"
:class="{ 'ml-3': siteSettings?.value?.siteLogo }"
:style="{
fontSize: (siteSettings?.value?.siteNameFontSize || 18) + 'px',
}"
>
{{ siteSettings?.value?.siteName }}
</span>
</div>
</div>
<div class="flex" v-else>
<nuxt-link to="/">
<div class="flex flex-auto gap-3 justify-center items-center">
<img
:src="currentLogo"
:alt="siteSettings?.value?.siteName || 'Site Logo'"
class="h-8 block"
@error="$event.target.src = '/img/logo/corradAF-logo.svg'"
/>
<span
v-if="
siteSettings?.value?.siteName && siteSettings?.value?.showSiteNameInHeader
"
class="text-lg font-semibold"
:style="{
fontSize: (siteSettings?.value?.siteNameFontSize || 18) + 'px',
}"
>
{{ siteSettings?.value?.siteName }}
</span>
</div>
</nuxt-link>
</div>
<div class="flex gap-2 item-center justify-items-end">
<VoiceReader class="ml-4" />
<!-- New dropdown for themeList.js -->
<VDropdown placement="bottom-end" distance="13" name="theme">
<button class="icon-btn h-10 w-10 rounded-full">
<Icon size="22px" name="ph:paint-brush-broad" />
</button>
<template #popper>
<ul class="header-dropdown w-full md:w-52">
<li v-for="(val, index) in themes" :key="index">
<a
@click="setThemeLocal(val.theme)"
class="flex justify-between items-center cursor-pointer py-2 px-4 hover:bg-[rgb(var(--bg-1))]"
>
<span class="capitalize"> {{ val.theme }} </span>
<div class="flex items-center gap-x-1">
<div
v-for="(color, colorIndex) in val.colors"
:key="colorIndex"
class="h-[25px] w-[10px] rounded-lg"
:style="{
backgroundColor: rgbToHex(color.value),
}"
></div>
</div>
</a>
</li>
</ul>
</template>
</VDropdown>
<!-- New dropdown for themeList2.js -->
<VDropdown placement="bottom-end" distance="13" name="theme2">
<button class="icon-btn h-10 w-10 rounded-full">
<Icon size="22px" name="ph:wheelchair" />
</button>
<template #popper>
<ul class="header-dropdown w-full md:w-52">
<li v-for="(val, index) in themes2" :key="index">
<a
@click="setThemeLocal(val.theme)"
class="flex justify-between items-center cursor-pointer py-2 px-4 hover:bg-[rgb(var(--bg-1))]"
>
<span class="capitalize"> {{ val.theme }} </span>
<div class="flex items-center gap-x-1">
<div
v-for="(color, colorIndex) in val.colors"
:key="colorIndex"
class="h-[25px] w-[10px] rounded-lg"
:style="{
backgroundColor: rgbToHex(color.value),
}"
></div>
</div>
</a>
</li>
</ul>
</template>
</VDropdown>
<VDropdown placement="bottom-end" distance="13" name="notification">
<button class="relative icon-btn h-10 w-10 rounded-full">
<span class="w-3 h-3 absolute top-1 right-2 rounded-full bg-primary"></span>
<Icon name="ic:round-notifications-none" class="" />
</button>
<template #popper>
<ul class="header-dropdown w-full md:w-80 text-[#4B5563]">
<li class="d-head flex items-center justify-between py-2 px-4">
<span class="font-semibold">Notification</span>
<div
class="flex items-center text-primary cursor-pointer hover:underline"
>
<a class="ml-2">View All</a>
</div>
</li>
<NuxtScrollbar>
<li>
<div class="bg-[rgb(var(--bg-1))] py-2 px-4">Today</div>
<a class="py-2 px-4 block">
<div class="flex items-center">
<Icon name="ic:outline-circle" class="text-primary flex-none" />
<span class="mx-2"
>Terdapat Satu Pembayaran yang berlaku menggunakan bil Kuih Raya
Cik Kiah</span
>
<div class="w-12 h-12 rounded-full ml-auto flex-none">
<img class="rounded-full" src="@/assets/img/user/default.svg" />
</div>
</div>
</a>
<a class="py-2 px-4 block">
<div class="flex items-center">
<Icon name="ic:outline-circle" class="text-primary flex-none" />
<span class="mx-2"
>Terdapat Satu Pembayaran yang berlaku menggunakan bil
Mercun</span
>
<div class="w-12 h-12 rounded-full ml-auto flex-none">
<img
class="rounded-full"
src="@/assets/img/user/default.svg"
alt=""
/>
</div>
</div>
</a>
</li>
</NuxtScrollbar>
</ul>
</template>
</VDropdown>
<VDropdown
placement="bottom-end"
distance="13"
name="profile"
class="flex justify-center item-center"
>
<button class="icon-btn profile px-2">
<img
class="w-8 h-8 object-cover rounded-full"
src="@/assets/img/user/default.svg"
/>
<div v-if="isDesktop" class="grid grid-cols-1 text-left ml-3 flex-none">
<p class="font-semibold text-sm truncate w-24 mb-0">Johan</p>
</div>
<Icon name="ic:outline-keyboard-arrow-down" class="ml-3" />
</button>
<template #popper>
<ul class="header-dropdown w-full md:w-52">
<li>
<a
href="/logout"
class="flex items-center cursor-pointer py-2 px-4 hover:bg-[rgb(var(--bg-1))]"
>
<Icon name="ic:outline-logout" class="mr-2" />
Logout
</a>
</li>
</ul>
</template>
</VDropdown>
</div>
</div>
</div>
<!-- Search Nav for Layout Vertical -->
<div tabindex="0" class="w-header-search">
<Icon name="ic:outline-search" class="mr-3" />
<FormKit
id="header-search"
:classes="{
outer: 'mb-0 flex-1',
}"
type="search"
placeholder="Search..."
/>
</div>
</template>
<style scoped>
:deep(.popper) {
background: #e92791;
padding: 20px;
border-radius: 20px;
color: #fff;
font-weight: bold;
text-transform: uppercase;
}
:deep(.popper #arrow::before) {
background: #e92791;
}
:deep(.popper:hover),
:deep(.popper:hover > #arrow::before) {
background: #e92791;
}
</style>

View File

@@ -0,0 +1,241 @@
<script setup>
import { useThemeStore } from "~/stores/theme";
const colorMode = useColorMode();
const themeStore = useThemeStore();
const hideConfigMenu = ref(true);
const rgbColor = ref({});
const hexToRgb = (hex) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `${r}, ${g}, ${b}`;
};
const setThemeColor = (type, color) => {
themeStore.setThemeColor(type, color);
// Remove Color form type string
const colorType = type.replace("Color", "").toLowerCase();
document.documentElement.style.setProperty(
`--color-${colorType}`,
hexToRgb(color)
);
};
function setColorMode() {
if (colorMode.preference == "light") {
colorMode.preference = "dark";
document.documentElement.classList.remove("light");
document.documentElement.classList.add("dark");
} else {
colorMode.preference = "light";
document.documentElement.classList.remove("dark");
document.documentElement.classList.add("light");
}
}
const setThemeGlobal = () => {
rgbColor.value.primary = themeStore.primaryColor;
rgbColor.value.secondary = themeStore.secondaryColor;
rgbColor.value.info = themeStore.infoColor;
rgbColor.value.success = themeStore.successColor;
rgbColor.value.warning = themeStore.warningColor;
rgbColor.value.danger = themeStore.dangerColor;
document.documentElement.style.setProperty(
"--color-primary",
hexToRgb(themeStore.primaryColor)
);
document.documentElement.style.setProperty(
"--color-secondary",
hexToRgb(themeStore.secondaryColor)
);
document.documentElement.style.setProperty(
"--color-info",
hexToRgb(themeStore.infoColor)
);
document.documentElement.style.setProperty(
"--color-success",
hexToRgb(themeStore.successColor)
);
document.documentElement.style.setProperty(
"--color-warning",
hexToRgb(themeStore.warningColor)
);
document.documentElement.style.setProperty(
"--color-danger",
hexToRgb(themeStore.dangerColor)
);
};
onMounted(() => {
setThemeGlobal();
});
const resetTheme = () => {
themeStore.resetThemeColor();
setThemeGlobal();
};
</script>
<template>
<div>
<div
:class="{
'right-[-300px]': hideConfigMenu,
}"
class="h-full w-[300px] bg-white dark:bg-slate-800 fixed top-0 right-0 z-20 shadow-md shadow-slate-200 dark:shadow-slate-900 duration-300"
>
<div
@click="hideConfigMenu = !hideConfigMenu"
class="py-3 px-4 bg-primary text-white rounded-l-lg absolute bottom-1 left-[-53px] cursor-pointer hover:bg-primary/90 hidden md:block"
>
<Icon name="fluent:paint-brush-16-regular" size="22px" />
</div>
<div class="p-4 flex font-normal border-b">
<p class="flex-1">
<span class="font-bold text-base text-gray-600 dark:text-white"
>Theme Customizer</span
>
<br />
<span class="text-sm text-gray-500 dark:text-gray-300">
Customize Theme Real Time
</span>
</p>
<div class="flex justify-center items-center">
<Icon
@click="hideConfigMenu = !hideConfigMenu"
class="cursor-pointer"
name="material-symbols:close-rounded"
/>
</div>
</div>
<div class="p-4">
<p class="text-sm mb-3 font-bold">Color Mode</p>
<FormKit
v-model="colorMode.value"
type="radio"
label="Theme"
:options="[
{
label: 'Light',
value: 'light',
},
{
label: 'Dark',
value: 'dark',
},
]"
:classes="{
fieldset: 'border-0 !p-0',
legend: '!font-medium !text-sm mb-0',
options: '!flex !flex-row gap-4 mt-2',
input: '!rounded-full',
}"
@change="setColorMode"
/>
<div class="flex justify-between">
<p class="text-sm mb-3 font-bold">Theming</p>
<a
@click="resetTheme"
class="underline text-blue-600 cursor-pointer hover:text-blue-400"
>Reset</a
>
</div>
<div class="grid grid-cols-2">
<FormKit
type="color"
v-model="rgbColor.primary"
label="Primary"
:classes="{
label: '!font-medium !text-sm mb-0',
outer: '!mb-0',
}"
@input="setThemeColor('primaryColor', rgbColor.primary)"
/>
<FormKit
type="color"
v-model="rgbColor.secondary"
label="Secondary"
:classes="{
label: '!font-medium !text-sm mb-0',
outer: '!mb-0',
}"
@input="setThemeColor('secondaryColor', rgbColor.secondary)"
/>
<FormKit
type="color"
v-model="rgbColor.info"
label="Info"
:classes="{
label: '!font-medium !text-sm mb-0',
outer: '!mb-0',
}"
@input="setThemeColor('infoColor', rgbColor.info)"
/>
<FormKit
type="color"
v-model="rgbColor.success"
label="Success"
:classes="{
label: '!font-medium !text-sm mb-0',
outer: '!mb-0',
}"
@input="setThemeColor('successColor', rgbColor.success)"
/>
<FormKit
type="color"
v-model="rgbColor.warning"
label="Warning"
:classes="{
label: '!font-medium !text-sm mb-0',
outer: '!mb-0',
}"
@input="setThemeColor('warningColor', rgbColor.warning)"
/>
<FormKit
type="color"
v-model="rgbColor.danger"
label="Danger"
:classes="{
label: '!font-medium !text-sm mb-0',
outer: '!mb-0',
}"
@input="setThemeColor('dangerColor', rgbColor.danger)"
/>
</div>
<hr class="my-4" />
<p class="text-sm mb-3 font-bold">Preview</p>
<rs-button variant="primary" class="w-full mb-4 cursor-default">
Primary Color
</rs-button>
<rs-button variant="secondary" class="w-full mb-4 cursor-default">
Secondary Color
</rs-button>
<rs-button variant="info" class="w-full mb-4 cursor-default">
Info Color
</rs-button>
<rs-button variant="success" class="w-full mb-4 cursor-default">
Success Color
</rs-button>
<rs-button variant="warning" class="w-full mb-4 cursor-default">
Warning Color
</rs-button>
<rs-button variant="danger" class="w-full mb-4 cursor-default">
Danger Color
</rs-button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<script setup>
</script>
<template>
<div></div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { useThemeStore } from "~/stores/theme";
// Import Layout Components
import RSVertical from "~/components/layouts/vertical/index.vue";
import RSHorizontal from "~/components/layouts/horizontal/index.vue";
const themeStore = useThemeStore();
const layoutType = themeStore.layoutType;
</script>
<template>
<div class="layout-wrapper">
<div v-if="layoutType === 'vertical'" class="v-layout h-100">
<RSVertical>
<slot />
</RSVertical>
</div>
<div v-if="layoutType === 'horizontal'" class="h-layout h-100">
<RSHorizontal>
<slot />
</RSHorizontal>
</div>
</div>
</template>

View File

@@ -0,0 +1,166 @@
<script setup>
import { useLayoutStore } from "~/stores/layout";
import { useWindowSize } from "vue-window-size";
import RSChildItem from "~/components/layouts/sidemenu/ItemChild.vue";
import { useUserStore } from "~/stores/user";
const layoutStore = useLayoutStore();
const mobileWidth = layoutStore.mobileWidth;
const { width } = useWindowSize();
const user = useUserStore();
const route = useRoute();
const props = defineProps({
items: {
type: Array,
default: () => [],
required: true,
},
});
const username = user.username;
const roles = user.roles;
const menuItem = props.items ? props.items : [];
// validate userExist on meta.auth.user
function userExist(item) {
if (item.meta?.auth?.user) {
if (item.meta?.auth?.user.some((e) => e === username)) {
return true;
} else {
return false;
}
}
return true;
}
// validate roleExist on meta.auth.role
function roleExist(item) {
if (item.meta?.auth?.role) {
if (item.meta?.auth?.role.some((e) => roles?.includes(e))) {
return true;
} else {
return false;
}
}
return true;
}
// Toggle show and hide menu content
function openMenu(event) {
const target = event.currentTarget;
try {
target.querySelector("a").classList.toggle("nav-open");
target.querySelector("ul").classList.toggle("hide");
} catch (e) {
// console.log(e);
return;
}
}
// Active menu
function activeMenu(routePath) {
return route.path == routePath
? `bg-[rgb(var(--color-primary))] font-normal text-white active-menu`
: `font-light text-white/90 md:transition-all md:duration-200 hover:md:ml-2`;
}
function toggleMenu() {
document.querySelector(".v-layout").classList.toggle("menu-hide");
document.getElementsByClassName("menu-overlay")[0].classList.toggle("hide");
}
function navigationPage(path, external) {
if (width.value <= mobileWidth) toggleMenu();
navigateTo(path, {
external: external,
});
}
</script>
<template>
<div v-for="(item, index) in menuItem" :key="index">
<div
v-if="
!item.meta || !item.meta?.auth || (userExist(item) && roleExist(item))
"
class="navigation-wrapper"
>
<div
v-if="item.header"
class="text-left font-normal text-xs mx-6 mt-5 mb-2"
>
<span class="uppercase text-gray-400">
{{ item.header ? item.header : "" }}
</span>
<p class="text-gray-400">
{{ item.description ? item.description : "" }}
</p>
</div>
<ul class="navigation-menu">
<li
class="navigation-item"
v-for="(item2, index2) in item.child"
:key="index2"
@click.stop="
item2.child !== undefined ||
(item2.child && item2.child.length !== 0)
? openMenu($event)
: ''
"
>
<div
v-if="
!item2.meta ||
!item2.meta?.auth ||
(userExist(item2) && roleExist(item2))
"
class="navigation-item-wrapper"
>
<NuxtLink
v-if="
item2.child === undefined ||
(item2.child && item2.child.length === 0)
"
class="flex items-center px-6 py-3 cursor-pointer"
@click="navigationPage(item2.path, item2.external)"
:class="activeMenu(item2.path)"
>
<Icon v-if="item2.icon" :name="item2.icon" size="18"></Icon>
<Icon v-else name="mdi:circle-slice-8" size="18"></Icon>
<span class="mx-3 font-normal">{{ item2.title }}</span>
<Icon
v-if="item2.child && item2.child.length > 0"
class="ml-auto side-menu-arrow"
name="material-symbols:chevron-right-rounded"
size="18"
></Icon>
</NuxtLink>
<a
v-else
class="flex items-center px-6 py-3 rounded-lg cursor-pointer"
:class="activeMenu(item2.path)"
>
<Icon v-if="item2.icon" :name="item2.icon" size="18"></Icon>
<Icon v-else name="mdi:circle-slice-8" size="18"></Icon>
<span class="mx-3 font-normal">{{ item2.title }}</span>
<Icon
v-if="item2.child && item2.child.length > 0"
class="ml-auto side-menu-arrow"
name="material-symbols:chevron-right-rounded"
size="18"
></Icon>
</a>
<RSChildItem
v-if="item2.child"
:items="item2.child"
@openMenu="openMenu"
@activeMenu="activeMenu"
></RSChildItem>
</div>
</li>
</ul>
</div>
</div>
</template>

View File

@@ -0,0 +1,145 @@
<script setup>
import { useLayoutStore } from "~/stores/layout";
import { useWindowSize } from "vue-window-size";
import RSChildItem from "~/components/layouts/sidemenu/ItemChild.vue";
import { useUserStore } from "~/stores/user";
const layoutStore = useLayoutStore();
const mobileWidth = layoutStore.mobileWidth;
const { width } = useWindowSize();
const user = useUserStore();
const route = useRoute();
const props = defineProps({
items: {
type: Array,
required: true,
},
indent: {
type: Number,
default: 0.5,
},
});
const emit = defineEmits(["openMenu"]);
const indent = ref(props.indent);
const menuItem = props.items ? props.items : [];
const username = user.username;
const roles = user.roles;
// validate userExist on meta.auth.user
function userExist(item) {
if (item.meta?.auth?.user) {
if (item.meta?.auth?.user.includes(username)) {
return true;
} else {
return false;
}
}
return true;
}
// validate roleExist on meta.auth.role
function roleExist(item) {
if (item.meta?.auth?.role) {
if (item.meta?.auth?.role.some((r) => roles.includes(r))) {
return true;
} else {
return false;
}
}
return true;
}
// Toggle Open/Close menu
function openMenu(event) {
emit("openMenu", event);
}
// Active menu
function activeMenu(routePath) {
return route.path == routePath
? `bg-[rgb(var(--color-primary))] font-normal text-white active-menu`
: `font-light text-white/90 md:transition-all md:duration-200 hover:md:ml-2`;
}
function toggleMenu() {
document.querySelector(".v-layout").classList.toggle("menu-hide");
document.getElementsByClassName("menu-overlay")[0].classList.toggle("hide");
}
function navigationPage(path, external) {
if (width.value <= mobileWidth) toggleMenu();
navigateTo(path, {
external: external,
});
}
const indentStyle = computed(() => {
return { "background-color": `rgba(var(--sidebar-menu), ${indent.value})` };
});
</script>
<template>
<ul
class="menu-content hide transition-all duration-300"
:style="indentStyle"
>
<li
v-for="(item, index) in menuItem"
:key="index"
@click.stop="
item.child !== undefined || (item.child && item.child.length !== 0)
? openMenu($event)
: ''
"
>
<div
v-if="
!item.meta || !item.meta?.auth || (userExist(item) && roleExist(item))
"
class="navigation-item-wrapper"
>
<NuxtLink
v-if="
item.child === undefined || (item.child && item.child.length === 0)
"
class="flex items-center px-6 py-3 cursor-pointer"
@click="navigationPage(item.path, item.external)"
:class="activeMenu(item.path)"
>
<Icon v-if="item.icon" :name="item.icon" size="18"></Icon>
<span class="mx-4 font-normal">{{ item.title }}</span>
<Icon
v-if="item.child && item.child.length > 0"
class="ml-auto side-menu-arrow"
name="material-symbols:chevron-right-rounded"
size="18"
></Icon>
</NuxtLink>
<a
v-else
class="flex items-center px-6 py-3 rounded-lg cursor-pointer"
:class="activeMenu(item.path)"
>
<span class="mx-3 font-normal">{{ item.title }}</span>
<Icon
v-if="item.child && item.child.length > 0"
class="ml-auto side-menu-arrow"
name="material-symbols:chevron-right-rounded"
size="18"
></Icon>
</a>
<RSChildItem
v-if="item.child"
:items="item.child"
:indent="indent + 0.1"
@openMenu="openMenu"
@activeMenu="activeMenu"
></RSChildItem>
</div>
</li>
</ul>
</template>

View File

@@ -0,0 +1,72 @@
<script setup>
import Menu from "~/navigation/index.js";
import RSItem from "~/components/layouts/sidemenu/Item.vue";
// Use site settings composable
const { siteSettings } = useSiteSettings();
// Add computed to ensure logo reactivity
const logoToShow = computed(() => {
// Always try to use the siteLogo from settings first
if (siteSettings.value?.siteLogo && siteSettings.value.siteLogo.trim() !== "") {
return siteSettings.value.siteLogo;
}
// Fallback to default logo if siteLogo is not set
return "/img/logo/corradAF-logo.svg";
});
const siteNameToShow = computed(() => {
return siteSettings.value.siteName || "Jabatan Imigresen Malaysia";
});
// const menuItem = Menu;
const props = defineProps({
menuItem: {
type: Array,
default: () => Menu,
required: false,
},
sidebarToggle: {
type: Boolean,
default: false,
},
});
onMounted(() => {
try {
const el = document.querySelector(".active-menu").closest(".menu-content");
const elParent = el.parentElement.parentElement;
if (elParent) {
elParent.classList.remove("hide");
elParent.querySelector("a").classList.add("nav-open");
}
if (el) el.classList.remove("hide");
} catch (e) {
// console.log(e);
return;
}
});
</script>
<template>
<div class="vertical-menu">
<div class="py-2 px-4 bg-[rgb(var(--header))]">
<nuxt-link to="/">
<div class="flex flex-auto gap-3 justify-center items-center h-[48px]">
<div
class="app-logo text-center h-20 flex justify-center items-center gap-3 px-4"
>
<nuxt-link to="/" class="flex items-center justify-center">
<img src="@/assets/img/logo/lzs-logo.png" class="h-12" alt="logo" />
</nuxt-link>
</div>
</div>
</nuxt-link>
</div>
<NuxtScrollbar class="flex flex-col justify-between my-6" style="max-height: 82dvh">
<RSItem :items="menuItem"></RSItem>
</NuxtScrollbar>
</div>
</template>

View File

@@ -0,0 +1,41 @@
<script setup>
import { useLayoutStore } from "~/stores/layout";
import RSHeader from "~/components/layouts/Header.vue";
import RSSideMenu from "~~/components/layouts/sidemenu/index.vue";
// import RSConfigMenu from "~~/components/layouts/configmenu/index.vue";
// import RSFooter from "~/components/layouts/Footer.vue";
import { useWindowSize } from "vue-window-size";
const { width } = useWindowSize();
const layoutStore = useLayoutStore();
const mobileWidth = layoutStore.mobileWidth;
// watch for window size changes
watch(
() => [width.value],
([width]) => {
if (width <= mobileWidth) {
document.querySelector(".v-layout").classList.add("menu-hide");
document.getElementsByClassName("menu-overlay")[0].classList.add("hide");
} else document.querySelector(".v-layout").classList.remove("menu-hide");
}
);
function toggleMenu(event) {
document.querySelector(".v-layout").classList.toggle("menu-hide");
document.getElementsByClassName("menu-overlay")[0].classList.toggle("hide");
}
</script>
<template>
<RSHeader @toggleMenu="toggleMenu" />
<RSSideMenu />
<div class="content-page duration-300">
<slot />
</div>
<!-- <RSConfigMenu /> -->
<div @click="toggleMenu" class="menu-overlay"></div>
<!-- <RSFooter /> -->
</template>