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 NavbarMenu({ 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) : ""; // 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) { setActiveLink(normalizedPathname); } }, [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) => prev.includes(label) ? prev.filter((key) => key !== label) : [label] ); }; return ( {items.map((item) => ( 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; }} /> ))} ); } // Komponen Item Menu function MenuItem({ item, onClose, activeLink, setActiveLink, isOpen, toggleOpen, isActivePath, isMostSpecificActive, }: { item: NavbarItem; onClose?: () => void; activeLink: string | null; setActiveLink: (link: string | null) => void; isOpen: boolean; toggleOpen: () => void; isActivePath: (itemPath: string | undefined) => boolean; isMostSpecificActive: (item: NavbarItem) => boolean; }) { const isActive = isMostSpecificActive(item); 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) => { // Untuk sub-item, kita gunakan logika aktif berdasarkan isActivePath const isSubActive = isActivePath(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", }, });