diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx index e964490..ad768bd 100644 --- a/app/(application)/(user)/home.tsx +++ b/app/(application)/(user)/home.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ -import { StackCustom, ViewWrapper } from "@/components"; +import { BasicWrapper, StackCustom, ViewWrapper } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth"; import { useNotificationStore } from "@/hooks/use-notification-store"; @@ -61,14 +61,31 @@ export default function Application() { if (data && data?.active === false) { console.log("User is not active"); - return ; + return ( + + + + ); } if (data && data?.Profile === null) { console.log("Profile is null"); - return ; + return ( + + + + ); } + // if (data && data?.masterUserRoleId !== "1") { + // console.log("User is not admin"); + // return ( + // + // + // + // ); + // } + return ( <> - setOpenDrawerNavbar(false)} + /> */} + + setOpenDrawerNavbar(false)} /> @@ -198,7 +212,7 @@ export default function AdminLayout() { // size={ICON_SIZE_SMALL} // color={MainColor.white} // /> - + ), path: "/admin/notification", }, diff --git a/components/Drawer/NavbarMenu.back.tsx b/components/Drawer/NavbarMenu.back.tsx new file mode 100644 index 0000000..0ff8708 --- /dev/null +++ b/components/Drawer/NavbarMenu.back.tsx @@ -0,0 +1,276 @@ +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { Ionicons } from "@expo/vector-icons"; +import { router, usePathname } from "expo-router"; +import React, { useEffect, useRef, useState } from "react"; +import { + Animated, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; + +export interface NavbarItem { + label: string; + icon?: keyof typeof Ionicons.glyphMap; + color?: string; + link?: string; + links?: { + label: string; + link: string; + }[]; + initiallyOpened?: boolean; +} + +interface NavbarMenuProps { + items: NavbarItem[]; + onClose?: () => void; +} + +export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) { + const pathname = usePathname(); + const [activeLink, setActiveLink] = useState(null); + const [openKeys, setOpenKeys] = useState([]); // Untuk kontrol dropdown + + // Normalisasi path: hapus trailing slash + const normalizePath = (path: string) => path.replace(/\/+$/, ""); + const normalizedPathname = pathname ? normalizePath(pathname) : ""; + + // Set activeLink saat pathname berubah + useEffect(() => { + if (normalizedPathname) { + setActiveLink(normalizedPathname); + } + }, [normalizedPathname]); + + // Toggle dropdown + const toggleOpen = (label: string) => { + setOpenKeys((prev) => + prev.includes(label) ? prev.filter((key) => key !== label) : [label] + ); + }; + + return ( + + + {items.map((item) => ( + toggleOpen(item.label)} + /> + ))} + + + ); +} + +// Komponen Item Menu +function MenuItem({ + item, + onClose, + activeLink, + setActiveLink, + isOpen, + toggleOpen, +}: { + item: NavbarItem; + onClose?: () => void; + activeLink: string | null; + setActiveLink: (link: string | null) => void; + isOpen: boolean; + toggleOpen: () => void; +}) { + const isActive = activeLink === item.link; + const animatedHeight = useRef(new Animated.Value(0)).current; + + // Animasi saat isOpen berubah + React.useEffect(() => { + Animated.timing(animatedHeight, { + toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0, + duration: 200, + useNativeDriver: false, + }).start(); + }, [isOpen, item.links, animatedHeight]); + + // Jika ada submenu + if (item.links && item.links.length > 0) { + return ( + + {/* Parent Item */} + + + {item.label} + + + + {/* Submenu (Animated) */} + + {item.links.map((subItem, index) => { + const isSubActive = activeLink === subItem.link; + return ( + { + setActiveLink(subItem.link); + onClose?.(); + router.push(subItem.link as any); + }} + > + + + {subItem.label} + + + ); + })} + + + ); + } + + // Menu tanpa submenu + return ( + { + setActiveLink(item.link || null); + onClose?.(); + router.push(item.link as any); + }} + > + + + {item.label} + + + ); +} +// Styles +const styles = StyleSheet.create({ + container: { + marginBottom: 5, + }, + parentItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + paddingHorizontal: 10, + // backgroundColor: AccentColor.darkblue, + borderRadius: 8, + marginBottom: 5, + justifyContent: "space-between", + }, + parentText: { + flex: 1, + fontSize: 16, + fontWeight: "500", + marginLeft: 10, + color: MainColor.white, + }, + singleItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + paddingHorizontal: 10, + // backgroundColor: AccentColor.darkblue, + borderRadius: 8, + marginBottom: 5, + }, + singleItemActive: { + backgroundColor: AccentColor.blue, + }, + singleText: { + fontSize: 16, + fontWeight: "500", + marginLeft: 10, + color: MainColor.white, + }, + icon: { + width: 24, + textAlign: "center", + paddingRight: 10, + }, + submenu: { + overflow: "hidden", + marginLeft: 30, + marginTop: 5, + }, + subItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + paddingHorizontal: 10, + borderRadius: 6, + marginBottom: 4, + }, + subItemActive: { + backgroundColor: AccentColor.blue, + }, + subText: { + color: MainColor.white, + fontSize: 16, + fontWeight: "500", + }, +}); diff --git a/components/Drawer/NavbarMenu.tsx b/components/Drawer/NavbarMenu.tsx index 6416647..ffb379d 100644 --- a/components/Drawer/NavbarMenu.tsx +++ b/components/Drawer/NavbarMenu.tsx @@ -37,6 +37,90 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { const normalizePath = (path: string) => path.replace(/\/+$/, ""); const normalizedPathname = pathname ? normalizePath(pathname) : ""; + // Fungsi untuk mengecek apakah path cocok dengan item menu + // Ini akan mengecek kecocokan eksak atau pola path + const isActivePath = (itemPath: string | undefined): boolean => { + if (!itemPath || !normalizedPathname) return false; + + // Cocokan eksak + if (normalizePath(itemPath) === normalizedPathname) return true; + + // Cocokan pola path seperti /user-access/[id]/index dengan /user-access/index + // atau /donation/[id]/detail dengan /donation/index + const normalizedItemPath = normalizePath(itemPath); + + // Jika path item adalah bagian dari path saat ini (prefix match) + if (normalizedPathname.startsWith(normalizedItemPath + '/')) return true; + + // Jika path saat ini adalah bagian dari path item (misalnya /user-access/detail cocok dengan /user-access) + if (normalizedItemPath.startsWith(normalizedPathname + '/')) return true; + + // Jika path item adalah bagian dari path saat ini tanpa id (misalnya /user-access/[id]/index cocok dengan /user-access/index) + const itemParts = normalizedItemPath.split('/'); + const currentParts = normalizedPathname.split('/'); + + // Jika panjangnya sama dan hanya berbeda di bagian dinamis [id] + if (itemParts.length === currentParts.length) { + let match = true; + for (let i = 0; i < itemParts.length; i++) { + // Jika bagian path item adalah placeholder [id], abaikan + if (itemParts[i].startsWith('[') && itemParts[i].endsWith(']')) continue; + + // Jika bagian path saat ini adalah ID (angka), abaikan + if (/^\d+$/.test(currentParts[i])) continue; + + // Jika tidak cocok dan bukan placeholder atau ID, maka tidak cocok + if (itemParts[i] !== currentParts[i]) { + match = false; + break; + } + } + if (match) return true; + } + + // Tambahkan logika khusus untuk menangani file index.tsx sebagai halaman dashboard + // Jika path saat ini adalah versi index dari path item (misalnya /admin/event/index cocok dengan /admin/event) + if (normalizedPathname === normalizedItemPath + '/index') return true; + + return false; + }; + + // Fungsi untuk menentukan item mana yang paling spesifik aktif + // Ini akan memastikan hanya satu item yang aktif pada satu waktu + const findMostSpecificActiveItem = (): { parentLabel?: string; subItemLink?: string } | null => { + // Cek setiap item menu + for (const item of items) { + // Jika item memiliki sub-menu + if (item.links && item.links.length > 0) { + // Urutkan sub-menu berdasarkan panjang path (terpanjang dulu untuk prioritas lebih spesifik) + const sortedSubItems = [...item.links].sort((a, b) => { + if (a.link && b.link) { + return b.link.length - a.link.length; // Urutan menurun (terpanjang dulu) + } + return 0; + }); + + // Cek setiap sub-menu dalam urutan yang telah diurutkan + for (const subItem of sortedSubItems) { + if (isActivePath(subItem.link)) { + return { parentLabel: item.label, subItemLink: subItem.link }; + } + } + } + + // Jika tidak ada sub-menu yang cocok, cek item utama + if (isActivePath(item.link)) { + return { parentLabel: item.label }; + } + } + + return null; + }; + + + // Hitung item aktif terlebih dahulu + const mostSpecificActive = findMostSpecificActiveItem(); + // Set activeLink saat pathname berubah useEffect(() => { if (normalizedPathname) { @@ -44,6 +128,15 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { } }, [normalizedPathname]); + // Fungsi untuk menentukan apakah dropdown harus tetap terbuka + // Dropdown tetap terbuka jika salah satu dari sub-menu cocok dengan path saat ini + const shouldDropdownBeOpen = (item: NavbarItem): boolean => { + if (!normalizedPathname || !item.links || item.links.length === 0) return false; + + // Cek apakah salah satu sub-menu cocok dengan path saat ini + return item.links.some(subItem => isActivePath(subItem.link)); + }; + // Toggle dropdown const toggleOpen = (label: string) => { setOpenKeys((prev) => @@ -56,7 +149,7 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { style={{ // flex: 1, // backgroundColor: MainColor.black, - marginBottom: 20, + marginBottom: 20, }} > toggleOpen(item.label)} + isActivePath={isActivePath} + isMostSpecificActive={(menuItem) => { + if (!mostSpecificActive) return false; + + // Jika item memiliki sub-menu + if (menuItem.links && menuItem.links.length > 0) { + // Jika item ini adalah parent dari sub-menu yang aktif, menu utama tidak aktif + return false; + } + + // Jika tidak ada sub-menu, hanya periksa kecocokan langsung + return mostSpecificActive.parentLabel === menuItem.label && !mostSpecificActive.subItemLink; + }} /> ))} @@ -89,6 +195,8 @@ function MenuItem({ setActiveLink, isOpen, toggleOpen, + isActivePath, + isMostSpecificActive, }: { item: NavbarItem; onClose?: () => void; @@ -96,8 +204,10 @@ function MenuItem({ setActiveLink: (link: string | null) => void; isOpen: boolean; toggleOpen: () => void; + isActivePath: (itemPath: string | undefined) => boolean; + isMostSpecificActive: (item: NavbarItem) => boolean; }) { - const isActive = activeLink === item.link; + const isActive = isMostSpecificActive(item); const animatedHeight = useRef(new Animated.Value(0)).current; // Animasi saat isOpen berubah @@ -121,7 +231,9 @@ function MenuItem({ color={MainColor.white} style={styles.icon} /> - {item.label} + + {item.label} + {item.links.map((subItem, index) => { - const isSubActive = activeLink === subItem.link; + // Untuk sub-item, kita gunakan logika aktif berdasarkan isActivePath + const isSubActive = isActivePath(subItem.link); return ( void; +} + +export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) { + const pathname = usePathname(); + const [openKeys, setOpenKeys] = useState([]); + + // Normalisasi path: hapus trailing slash + const normalizePath = (path: string) => path.replace(/\/+$/, ""); + const normalizedPathname = pathname ? normalizePath(pathname) : ""; + + // Auto-open parent menu jika submenu aktif + useEffect(() => { + if (!normalizedPathname || !items || items.length === 0) { + return; + } + + try { + const newOpenKeys: string[] = []; + + // Helper function yang sama dengan di MenuItem + const checkPathMatch = (linkPath: string, detailPattern?: string) => { + const normalizedLink = linkPath.replace(/\/+$/, ""); + + // Exact match + if (normalizedPathname === normalizedLink) return true; + + // Detail pattern match + if (detailPattern) { + const patternRegex = new RegExp( + "^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$", + ); + if (patternRegex.test(normalizedPathname)) { + return true; + } + } + + // Detail page match (fallback) + if (normalizedPathname.startsWith(normalizedLink + "/")) { + const remainder = normalizedPathname.substring( + normalizedLink.length + 1, + ); + const segments = remainder.split("/").filter((s) => s.length > 0); + + if (segments.length === 0) return false; + + const commonWords = [ + // Event + "type-create", + + // Other + "detail", + "edit", + "create", + "new", + "add", + "delete", + "view", + "publish", + "review", + "reject", + "status", + "category", + "history", + "type-of-event", + "posting", + "report-posting", + "report-comment", + "group", + "dashboard", + "sticker", + "active", + "inactive", + "pending", + "transaction-detail", + "transaction", + "payment", + "disbursement", + "list-of-investor", + ]; + + const hasIdSegment = segments.some((segment) => { + if (commonWords.includes(segment.toLowerCase())) { + return false; + } + + const isPureNumber = /^\d+$/.test(segment); + const isUUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + segment, + ); + const hasNumber = /\d/.test(segment); + const isAlphanumericId = + /^[a-z0-9_-]+$/i.test(segment) && + segment.length <= 50 && + hasNumber; + + return isPureNumber || isUUID || isAlphanumericId; + }); + + return hasIdSegment; + } + + return false; + }; + + items.forEach((item) => { + if (item.links && item.links.length > 0) { + // Check jika ada submenu yang match dengan current path + const hasActiveSubmenu = item.links.some((subItem) => { + return checkPathMatch(subItem.link, subItem.detailPattern); + }); + + if (hasActiveSubmenu) { + newOpenKeys.push(item.label); + } + } + }); + + setOpenKeys(newOpenKeys); + } catch (error) { + console.error("Error in NavbarMenu useEffect:", error); + } + }, [normalizedPathname, items]); + + // Toggle dropdown + const toggleOpen = (label: string) => { + setOpenKeys((prev) => + prev.includes(label) + ? prev.filter((key) => key !== label) + : [...prev, label], + ); + }; + + return ( + + + {items && items.length > 0 + ? items.map((item) => ( + toggleOpen(item.label)} + /> + )) + : null} + + + ); +} + +// Komponen Item Menu +function MenuItem({ + item, + onClose, + currentPath, + isOpen, + toggleOpen, +}: { + item: NavbarItem_V2; + onClose?: () => void; + currentPath: string; + isOpen: boolean; + toggleOpen: () => void; +}) { + const animatedHeight = useRef(new Animated.Value(0)).current; + + // Helper function untuk check apakah path aktif + const isPathActive = ( + linkPath: string | undefined, + detailPattern?: string, + ) => { + if (!linkPath) return false; + const normalizedLink = linkPath.replace(/\/+$/, ""); + + // 1. Match exact - prioritas tertinggi + if (currentPath === normalizedLink) return true; + + // 2. Jika ada detailPattern, cek pattern dulu + if (detailPattern) { + // detailPattern contoh: "/admin/job/*/review" + // akan match dengan: + // - /admin/job/123/review ✅ + // - /admin/job/123/review/transaction-detail ✅ + // - /admin/job/123/review/anything/nested ✅ + const patternRegex = new RegExp( + "^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$", + ); + const isMatch = patternRegex.test(currentPath); + + // Debug log untuk pattern matching + if ( + currentPath.includes("list-of-investor") || + currentPath.includes("type-create") + ) { + console.log( + "🔍 Pattern Match Check:", + JSON.stringify( + { + currentPath, + detailPattern, + regex: patternRegex.toString(), + isMatch, + }, + null, + 2, + ), + ); + } + + if (isMatch) { + return true; + } + } + + // 3. Match untuk detail pages (fallback) + if (currentPath.startsWith(normalizedLink + "/")) { + const remainder = currentPath.substring(normalizedLink.length + 1); + const segments = remainder.split("/").filter((s) => s.length > 0); + + if (segments.length === 0) return false; + + const commonWords = [ + // Event + "type-create", + "detail", + "edit", + "create", + "new", + "add", + "delete", + "view", + "publish", + "review", + "reject", + "status", + "category", + "history", + "type-of-event", + "posting", + "report-posting", + "report-comment", + "group", + "dashboard", + "sticker", + "active", + "inactive", + "pending", + "transaction-detail", + "transaction", + "payment", + "disbursement", + ]; + + const hasIdSegment = segments.some((segment) => { + if (commonWords.includes(segment.toLowerCase())) { + return false; + } + + const isPureNumber = /^\d+$/.test(segment); + const isUUID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + segment, + ); + const hasNumber = /\d/.test(segment); + const isAlphanumericId = + /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber; + + return isPureNumber || isUUID || isAlphanumericId; + }); + + return hasIdSegment; + } + + return false; + }; + + // Check apakah menu item ini atau submenu-nya yang aktif + const isActive = isPathActive(item.link); + const hasActiveSubmenu = + item.links?.some((subItem) => + isPathActive(subItem.link, subItem.detailPattern), + ) || false; + + // Animasi saat isOpen berubah + useEffect(() => { + Animated.timing(animatedHeight, { + toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0, + duration: 200, + useNativeDriver: false, + }).start(); + }, [isOpen, item.links, animatedHeight]); + + // Jika ada submenu + if (item.links && item.links.length > 0) { + return ( + + {/* Parent Item */} + + + + {item.label} + + + + + {/* Submenu (Animated) */} + + {item.links.map((subItem, index) => { + const isSubActive = isPathActive( + subItem.link, + subItem.detailPattern, + ); + + // CRITICAL FIX: Jika submenu ini aktif, cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif + // Jika ada yang lebih panjang dan aktif, maka yang pendek TIDAK AKTIF + const hasMoreSpecificMatch = item.links!.some((otherSubItem) => { + if (otherSubItem.link === subItem.link) return false; // Skip self + + const otherIsActive = isPathActive( + otherSubItem.link, + otherSubItem.detailPattern, + ); + const isOtherLonger = + otherSubItem.link.length > subItem.link.length; + + // Debug log + if (isSubActive && otherIsActive) { + console.log( + "🔍 CONFLICT DETECTED:", + JSON.stringify( + { + current: subItem.label, + currentPath: subItem.link, + currentLength: subItem.link.length, + other: otherSubItem.label, + otherPath: otherSubItem.link, + otherLength: otherSubItem.link.length, + isOtherLonger, + currentURL: currentPath, + }, + null, + 2, + ), + ); + } + + // Jika submenu lain JUGA aktif DAN lebih panjang (lebih spesifik), + // maka submenu yang pendek ini TIDAK boleh aktif + return otherIsActive && isOtherLonger; + }); + + // Final decision: aktif HANYA jika match DAN tidak ada yang lebih spesifik + const finalIsActive = isSubActive && !hasMoreSpecificMatch; + + // Debug final decision + if (isSubActive) { + console.log( + "✅ Active check:", + JSON.stringify( + { + label: subItem.label, + link: subItem.link, + isSubActive, + hasMoreSpecificMatch, + finalIsActive, + }, + null, + 2, + ), + ); + } + + return ( + { + onClose?.(); + router.push(subItem.link as any); + }} + > + + + {subItem.label} + + + ); + })} + + + ); + } + + // Menu tanpa submenu + return ( + { + onClose?.(); + router.push(item.link as any); + }} + > + + + {item.label} + + + ); +} + +// Styles +const styles = StyleSheet.create({ + container: { + marginBottom: 5, + }, + parentItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + paddingHorizontal: 10, + borderRadius: 8, + marginBottom: 5, + justifyContent: "space-between", + }, + parentItemActive: { + backgroundColor: AccentColor.blue, + }, + parentText: { + flex: 1, + fontSize: 16, + fontWeight: "500", + marginLeft: 10, + color: MainColor.white, + }, + singleItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + paddingHorizontal: 10, + borderRadius: 8, + marginBottom: 5, + }, + singleItemActive: { + backgroundColor: AccentColor.blue, + }, + singleText: { + fontSize: 16, + fontWeight: "500", + marginLeft: 10, + color: MainColor.white, + }, + icon: { + width: 24, + textAlign: "center", + paddingRight: 10, + }, + submenu: { + overflow: "hidden", + marginLeft: 30, + marginTop: 5, + }, + subItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + paddingHorizontal: 10, + borderRadius: 6, + marginBottom: 4, + }, + subItemActive: { + backgroundColor: AccentColor.blue, + }, + subText: { + color: MainColor.white, + fontSize: 16, + fontWeight: "500", + }, +}); diff --git a/components/_ShareComponent/BasicWrapper.tsx b/components/_ShareComponent/BasicWrapper.tsx new file mode 100644 index 0000000..a26d48a --- /dev/null +++ b/components/_ShareComponent/BasicWrapper.tsx @@ -0,0 +1,16 @@ +import { MainColor } from "@/constants/color-palet"; +import { View } from "react-native"; + +export default function BasicWrapper({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + + {children} + + + ); +} diff --git a/components/index.ts b/components/index.ts index f53c40f..27e7c1f 100644 --- a/components/index.ts +++ b/components/index.ts @@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput"; import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage"; import GridComponentView from "./_ShareComponent/GridSectionView"; import NewWrapper from "./_ShareComponent/NewWrapper"; +import BasicWrapper from "./_ShareComponent/BasicWrapper"; // Progress import ProgressCustom from "./Progress/ProgressCustom"; // Loader @@ -121,6 +122,7 @@ export { GridComponentView, Spacing, NewWrapper, + BasicWrapper, // Stack StackCustom, TabBarBackground, diff --git a/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md index 18fa314..d5fb8fd 100644 --- a/docs/prompt-for-qwen-code.md +++ b/docs/prompt-for-qwen-code.md @@ -86,4 +86,36 @@ Setelah itu jalankan perintah ini: git add . Setelah itu jalankan perintah ini: git commit -m " " -Setelah itu jalankan perintah ini: git push origin "Branch" \ No newline at end of file +Setelah itu jalankan perintah ini: git push origin "Branch" + + + + + +Saya memiliki case pada file ini: @components/Drawer/NavbarMenu.tsx +Pada file ini saya ingin jika saat pindah halaman ( ke detail contoh : /user-access/[id]/index.tsx) maka navbar tetap menandai menu yang sedang aktif, tapi yang terjadi sekarang jika masuk ke detail maka warnanya hilang karena tidak mendeteksi halaman tersebut. +Apakah anda paham maksud saya ? + + +Ya, dalam fitur yang anda perbaharui masih terjadi bug. Saya akan berikan case nya secara perlahan +Saat klik sebuah menu maka sub menu akan terbuka +Saat klik sub menu maka sub menu maka akan menuju ke halaman sesuai path +Dalam bug diawal tadi untuk menu yang aktif jika masuk ke detail memang terselesaikan. Tapi muncul bug baru jika menu tersebut memiliki sub menu dan jika sub menu tersebut di klik (kecuali dashboard) yang aktif adalah bagian sub menu dashbaord dan sub menu yang kita klik, tapi jika sub menu yang di klik adalah dashboard maka semau sub menu aktif. Apakah anda mengerti maksud dari pernyataan saya ? Jika masih kurang paham saya bisa berikan masukan yang lain + +Masih terjadi bug, mengapa saat klik menu yang memiliki dashboard maka sub menu dashboard dan sub menu yang kita klik menjadi aktif ? + + + + +export interface NavbarItem_V2 { + label: string; + icon?: keyof typeof Ionicons.glyphMap; + color?: string; + link?: string; + links?: { + label: string; + link: string; + detailPattern?: string; + }[]; + initiallyOpened?: boolean; +} \ No newline at end of file diff --git a/screens/Admin/listPageAdmin_V2.tsx b/screens/Admin/listPageAdmin_V2.tsx new file mode 100644 index 0000000..dc5d759 --- /dev/null +++ b/screens/Admin/listPageAdmin_V2.tsx @@ -0,0 +1,461 @@ +import { NavbarItem_V2 } from "@/components/Drawer/NavbarMenu_V2"; + + +export { adminListMenu_V2, superAdminListMenu_V2 } + +const adminListMenu_V2: NavbarItem_V2[] = [ + { + label: "Main Dashboard", + icon: "home", + link: "/admin/dashboard", + }, + { + label: "Investasi", + icon: "wallet", + links: [ + { + label: "Dashboard", + link: "/admin/investment", + // Dashboard tidak perlu detailPattern, akan auto-match dengan /admin/investment/123/... + }, + { + label: "Publish", + link: "/admin/investment/publish/status", + detailPattern: "/admin/investment/*/publish", // Match: /admin/investment/123/publish + }, + { + label: "Review", + link: "/admin/investment/review/status", + detailPattern: "/admin/investment/*/review", // Match: /admin/investment/123/review + }, + { + label: "Reject", + link: "/admin/investment/reject/status", + detailPattern: "/admin/investment/*/reject", // Match: /admin/investment/123/reject + }, + ], + }, + { + label: "Donasi", + icon: "hand-right", + links: [ + { + label: "Dashboard", + link: "/admin/donation", + }, + { + label: "Publish", + link: "/admin/donation/publish/status", + detailPattern: "/admin/donation/*/publish", + }, + { + label: "Review", + link: "/admin/donation/review/status", + detailPattern: "/admin/donation/*/review", + }, + { + label: "Reject", + link: "/admin/donation/reject/status", + detailPattern: "/admin/donation/*/reject", + }, + { + label: "Kategori", + link: "/admin/donation/category", + }, + ], + }, + { + label: "Event", + icon: "calendar-clear", + links: [ + { + label: "Dashboard", + link: "/admin/event", + }, + { + label: "Publish", + link: "/admin/event/publish/status", + detailPattern: "/admin/event/*/publish", + }, + { + label: "Review", + link: "/admin/event/review/status", + detailPattern: "/admin/event/*/review", + }, + { + label: "Reject", + link: "/admin/event/reject/status", + detailPattern: "/admin/event/*/reject", + }, + { + label: "Tipe Acara", + link: "/admin/event/type-of-event", + }, + { + label: "Riwayat", + link: "/admin/event/history/status", + detailPattern: "/admin/event/*/history", + }, + ], + }, + { + label: "Voting", + icon: "accessibility-outline", + links: [ + { + label: "Dashboard", + link: "/admin/voting", + }, + { + label: "Publish", + link: "/admin/voting/publish/status", + detailPattern: "/admin/voting/*/publish", + }, + { + label: "Review", + link: "/admin/voting/review/status", + detailPattern: "/admin/voting/*/review", + }, + { + label: "Reject", + link: "/admin/voting/reject/status", + detailPattern: "/admin/voting/*/reject", + }, + { + label: "Riwayat", + link: "/admin/voting/history", + detailPattern: "/admin/voting/*/history", + }, + ], + }, + { + label: "Job", + icon: "desktop-outline", + links: [ + { + label: "Dashboard", + link: "/admin/job", + }, + { + label: "Publish", + link: "/admin/job/publish/status", + detailPattern: "/admin/job/*/publish", + }, + { + label: "Review", + link: "/admin/job/review/status", + detailPattern: "/admin/job/*/review", + }, + { + label: "Reject", + link: "/admin/job/reject/status", + detailPattern: "/admin/job/*/reject", + }, + ], + }, + { + label: "Forum", + icon: "chatbubble-ellipses-outline", + links: [ + { + label: "Dashboard", + link: "/admin/forum", + }, + { + label: "Posting", + link: "/admin/forum/posting", + }, + { + label: "Report Posting", + link: "/admin/forum/report-posting", + }, + { + label: "Report Komentar", + link: "/admin/forum/report-comment", + }, + ], + }, + { + label: "Collaboration", + icon: "people", + links: [ + { + label: "Dashboard", + link: "/admin/collaboration", + }, + { + label: "Publish", + link: "/admin/collaboration/publish", + }, + { + label: "Group", + link: "/admin/collaboration/group", + }, + { + label: "Reject", + link: "/admin/collaboration/reject", + }, + ], + }, + { + label: "Maps", + icon: "map", + link: "/admin/maps", + }, + { + label: "App Information", + icon: "information-circle", + link: "/admin/app-information", + }, + { + label: "User Access", + icon: "people", + link: "/admin/user-access", + }, +]; + +const superAdminListMenu_V2: NavbarItem_V2[] = [ + { + label: "Main Dashboard", + icon: "home", + link: "/admin/dashboard", + }, + { + label: "Investasi", + icon: "wallet", + links: [ + { + label: "Dashboard", + link: "/admin/investment", + }, + { + label: "Publish", + link: "/admin/investment/publish/status", + detailPattern: "/admin/investment/*/publish", + }, + { + label: "Review", + link: "/admin/investment/review/status", + detailPattern: "/admin/investment/*/review", + }, + { + label: "Reject", + link: "/admin/investment/reject/status", + detailPattern: "/admin/investment/*/reject", + }, + ], + }, + { + label: "Donasi", + icon: "hand-right", + links: [ + { + label: "Dashboard", + link: "/admin/donation", + }, + { + label: "Publish", + link: "/admin/donation/publish/status", + detailPattern: "/admin/donation/*/publish", + }, + { + label: "Review", + link: "/admin/donation/review/status", + detailPattern: "/admin/donation/*/review", + }, + { + label: "Reject", + link: "/admin/donation/reject/status", + detailPattern: "/admin/donation/*/reject", + }, + { + label: "Kategori", + link: "/admin/donation/category", + }, + ], + }, + { + label: "Event", + icon: "calendar-clear", + links: [ + { + label: "Dashboard", + link: "/admin/event", + }, + { + label: "Publish", + link: "/admin/event/publish/status", + detailPattern: "/admin/event/*/publish", + }, + { + label: "Review", + link: "/admin/event/review/status", + detailPattern: "/admin/event/*/review", + }, + { + label: "Reject", + link: "/admin/event/reject/status", + detailPattern: "/admin/event/*/reject", + }, + { + label: "Tipe Acara", + link: "/admin/event/type-of-event", + }, + { + label: "Riwayat", + link: "/admin/event/history/status", + detailPattern: "/admin/event/*/history", + }, + ], + }, + { + label: "Voting", + icon: "accessibility-outline", + links: [ + { + label: "Dashboard", + link: "/admin/voting", + }, + { + label: "Publish", + link: "/admin/voting/publish/status", + detailPattern: "/admin/voting/*/publish", + }, + { + label: "Review", + link: "/admin/voting/review/status", + detailPattern: "/admin/voting/*/review", + }, + { + label: "Reject", + link: "/admin/voting/reject/status", + detailPattern: "/admin/voting/*/reject", + }, + { + label: "Riwayat", + link: "/admin/voting/history", + detailPattern: "/admin/voting/*/history", + }, + ], + }, + { + label: "Job", + icon: "desktop-outline", + links: [ + { + label: "Dashboard", + link: "/admin/job", + }, + { + label: "Publish", + link: "/admin/job/publish/status", + detailPattern: "/admin/job/*/publish", + }, + { + label: "Review", + link: "/admin/job/review/status", + detailPattern: "/admin/job/*/review", + }, + { + label: "Reject", + link: "/admin/job/reject/status", + detailPattern: "/admin/job/*/reject", + }, + ], + }, + { + label: "Forum", + icon: "chatbubble-ellipses-outline", + links: [ + { + label: "Dashboard", + link: "/admin/forum", + }, + { + label: "Posting", + link: "/admin/forum/posting", + }, + { + label: "Report Posting", + link: "/admin/forum/report-posting", + }, + { + label: "Report Komentar", + link: "/admin/forum/report-comment", + }, + ], + }, + { + label: "Collaboration", + icon: "people", + links: [ + { + label: "Dashboard", + link: "/admin/collaboration", + }, + { + label: "Publish", + link: "/admin/collaboration/publish", + }, + { + label: "Group", + link: "/admin/collaboration/group", + }, + { + label: "Reject", + link: "/admin/collaboration/reject", + }, + ], + }, + { + label: "Maps", + icon: "map", + link: "/admin/maps", + }, + { + label: "App Information", + icon: "information-circle", + link: "/admin/app-information", + }, + { + label: "User Access", + icon: "people", + link: "/admin/user-access", + }, + { + label: "Super Admin", + icon: "globe", + link: "/admin/super-admin", + }, +]; + +/* +================================================================================= +PENJELASAN detailPattern: +================================================================================= + +detailPattern digunakan untuk match dengan URL detail page yang strukturnya: +/admin/{module}/[id]/[status] + +Contoh untuk Job Review: +- Link: /admin/job/review/status (halaman list review) +- detailPattern: /admin/job/* /review (detail dari review) +- Match dengan: /admin/job/123/review, /admin/job/456/review, dll + +Wildcard "*" akan match dengan ID apapun (angka, UUID, alphanumeric). + +Modul yang PERLU detailPattern: +✅ Investasi - Publish, Review, Reject (ada [id]/[status]) +✅ Donasi - Publish, Review, Reject (ada [id]/[status]) +✅ Event - Publish, Review, Reject, Riwayat (ada [id]/[status]) +✅ Voting - Publish, Review, Reject, Riwayat (ada [id]/[status]) +✅ Job - Publish, Review, Reject (ada [id]/[status]) + +Modul yang TIDAK PERLU detailPattern: +❌ Forum - posting, report-posting, report-comment (struktur berbeda) +❌ Collaboration - struktur berbeda +❌ Maps, App Information, User Access - single page +❌ Dashboard submenu - auto-match dengan parent path + +================================================================================= +*/ \ No newline at end of file