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_V3 { label: string; icon?: keyof typeof Ionicons.glyphMap; color?: string; link?: string; links?: { label: string; link: string; detailPattern?: string; // NEW: Pattern untuk match detail pages }[]; initiallyOpened?: boolean; } interface NavbarMenuProps { items: NavbarItem_V3[]; onClose?: () => void; } export default function NavbarMenu_V3({ 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 = [ // Actions 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view', // Status types 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending', // General pages 'category', 'history', 'dashboard', 'index', // Event specific 'type-of-event', 'type-create', 'type-update', // Forum specific 'posting', 'report-posting', 'report-comment', // Collaboration 'group', // App Information 'business-field', 'information-bank', 'sticker', 'bidang-update', 'sub-bidang-update', // Transaction/Finance related 'transaction-detail', 'transaction', 'payment', 'disbursement-of-funds', 'detail-disbursement-of-funds', 'list-disbursement-of-funds', // List pages (CRITICAL!) 'list-of-investor', 'list-of-donatur', 'list-of-participants', 'list-comment', 'list-report-comment', 'list-report-posting', // Input/Form pages 'reject-input', // Category pages 'category-create', 'category-update' ]; 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; }; // Calculate all potential matches for conflict resolution const allMatches = items.flatMap(item => { if (!item.links || item.links.length === 0) return []; return item.links .filter(subItem => checkPathMatch(subItem.link, subItem.detailPattern)) .map(subItem => ({ parentLabel: item.label, subItem, pathLength: subItem.link.length })); }); // Find the most specific match for each parent const uniqueParents = new Map(); allMatches.forEach(match => { const existing = uniqueParents.get(match.parentLabel); if (!existing || match.pathLength > existing.longestPathLength) { uniqueParents.set(match.parentLabel, { parentLabel: match.parentLabel, longestPathLength: match.pathLength }); } }); // Add only the parents with the most specific matches newOpenKeys.push(...Array.from(uniqueParents.values()).map(item => item.parentLabel)); // Additionally, if no specific submenu match was found but the current path // starts with one of the parent menu links, add that parent if (newOpenKeys.length === 0) { // Find the parent whose link is the longest prefix of the current path let longestMatchParent = null; let longestMatchLength = 0; items.forEach(item => { if (item.links && item.links.length > 0) { item.links.forEach(link => { const linkPath = link.link.replace(/\/+$/, ""); if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) { longestMatchLength = linkPath.length; longestMatchParent = item.label; } }); } }); if (longestMatchParent) { newOpenKeys.push(longestMatchParent); } } // NEW: Check if user is on a detail page (contains ID segments or specific keywords) const isOnDetailPage = (() => { // Check if current path has ID-like segments or detail keywords const segments = normalizedPathname.split('/').filter(s => s.length > 0); if (segments.length === 0) return false; const commonWords = [ // Actions 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view', // Status types 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending', // General pages 'category', 'history', 'dashboard', 'index', // Event specific 'type-of-event', 'type-create', 'type-update', // Forum specific 'posting', 'report-posting', 'report-comment', // Collaboration 'group', // App Information 'business-field', 'information-bank', 'sticker', 'bidang-update', 'sub-bidang-update', // Transaction/Finance related 'transaction-detail', 'transaction', 'payment', 'disbursement-of-funds', 'detail-disbursement-of-funds', 'list-disbursement-of-funds', // List pages (CRITICAL!) 'list-of-investor', 'list-of-donatur', 'list-of-participants', 'list-comment', 'list-report-comment', 'list-report-posting', // Input/Form pages 'reject-input', // Category pages 'category-create', 'category-update' ]; 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; })(); // NEW: Check if user is on a detail page (contains ID segments or specific keywords) const isOnDetailPageGlobal = (() => { // Check if current path has ID-like segments or detail keywords const segments = normalizedPathname.split('/').filter(s => s.length > 0); if (segments.length === 0) return false; const commonWords = [ // Actions 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view', // Status types 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending', // General pages 'category', 'history', 'dashboard', 'index', // Event specific 'type-of-event', 'type-create', 'type-update', // Forum specific 'posting', 'report-posting', 'report-comment', // Collaboration 'group', // App Information 'business-field', 'information-bank', 'sticker', 'bidang-update', 'sub-bidang-update', // Transaction/Finance related 'transaction-detail', 'transaction', 'payment', 'disbursement-of-funds', 'detail-disbursement-of-funds', 'list-disbursement-of-funds', // List pages (CRITICAL!) 'list-of-investor', 'list-of-donatur', 'list-of-participants', 'list-comment', 'list-report-comment', 'list-report-posting', // Input/Form pages 'reject-input', // Category pages 'category-create', 'category-update' ]; // Check if any segment is a common word const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase())); // Check if any segment looks like an ID (number, UUID, alphanumeric with numbers) const hasIdSegment = segments.some(segment => { 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; }); // A detail page is one that has either common words or ID segments return hasCommonWord || hasIdSegment; })(); // NEW: Only open parent menu if the current path is a detail page of the most relevant parent if (isOnDetailPageGlobal && newOpenKeys.length === 0) { // Find the parent whose link is the longest prefix of the current path let longestMatchParent = null; let longestMatchLength = 0; items.forEach(item => { if (item.links && item.links.length > 0) { item.links.forEach(link => { const linkPath = link.link.replace(/\/+$/, ""); if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) { longestMatchLength = linkPath.length; longestMatchParent = item.label; } }); } }); if (longestMatchParent) { newOpenKeys.push(longestMatchParent); } } 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, items, onClose, currentPath, isOpen, toggleOpen, }: { item: NavbarItem_V3; items: NavbarItem_V3[]; 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('transaction-detail') || currentPath.includes('disbursement')) { // console.log('🔍 Pattern Match Check:', { // currentPath, // detailPattern, // regex: patternRegex.toString(), // isMatch // }); // } 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 = [ // Actions 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view', // Status types 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending', // General pages 'category', 'history', 'index', // Event specific 'type-of-event', 'type-create', 'type-update', // Forum specific 'posting', 'report-posting', 'report-comment', // Collaboration 'group', // App Information 'business-field', 'information-bank', 'sticker', 'bidang-update', 'sub-bidang-update', // Transaction/Finance related 'transaction-detail', 'transaction', 'payment', 'disbursement-of-funds', 'detail-disbursement-of-funds', 'list-disbursement-of-funds', // List pages (CRITICAL!) 'list-of-investor', 'list-of-donatur', 'list-of-participants', 'list-comment', 'list-report-comment', 'list-report-posting', // Input/Form pages 'reject-input', // Category pages 'category-create', 'category-update' ]; const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()) ); // Hanya anggap sebagai detail page jika mengandung commonWords return hasCommonWord; } return false; }; // Check apakah menu item ini atau submenu-nya yang aktif const isActive = isPathActive(item.link); // NEW LOGIC: Check if user is on a detail page (contains ID segments or specific keywords) const isOnDetailPage = (() => { // Check if current path has ID-like segments or detail keywords const segments = currentPath.split('/').filter(s => s.length > 0); if (segments.length === 0) return false; const commonWords = [ // Actions 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view', // Status types 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending', // General pages 'category', 'history', 'dashboard', 'index', // Event specific 'type-of-event', 'type-create', 'type-update', // Forum specific 'posting', 'report-posting', 'report-comment', // Collaboration 'group', // App Information 'business-field', 'information-bank', 'sticker', 'bidang-update', 'sub-bidang-update', // Transaction/Finance related 'transaction-detail', 'transaction', 'payment', 'disbursement-of-funds', 'detail-disbursement-of-funds', 'list-disbursement-of-funds', // List pages (CRITICAL!) 'list-of-investor', 'list-of-donatur', 'list-of-participants', 'list-comment', 'list-report-comment', 'list-report-posting', // Input/Form pages 'reject-input', // Category pages 'category-create', 'category-update' ]; // Check if any segment is a common word const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase())); // Check if any segment looks like an ID (number, UUID, alphanumeric with numbers) const hasIdSegment = segments.some(segment => { 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; }); // A detail page is one that has either common words or ID segments return hasCommonWord || hasIdSegment; })(); // Calculate all submenu active states for conflict resolution const submenuActiveStates = item.links?.map(subItem => ({ subItem, isActive: isPathActive(subItem.link, subItem.detailPattern), pathLength: subItem.link.length })) || []; // Determine if any submenu is active considering conflicts const hasActiveSubmenu = submenuActiveStates.some(({ isActive: isSubActive, pathLength, subItem }) => { if (!isSubActive) return false; // Check if there's a more specific match elsewhere const hasMoreSpecificMatch = submenuActiveStates.some(other => { if (other.subItem.link === subItem.link) return false; // Skip self return other.isActive && other.pathLength > pathLength; }); return isSubActive && !hasMoreSpecificMatch; }) || false; // For parent menu detection, if current path contains common words, // check if this parent menu's link is a prefix of the current path const isParentOfDetailPage = !isActive && !hasActiveSubmenu && item.links && item.links.length > 0 && item.links.some(link => currentPath.startsWith(link.link.replace(/\/+$/, "") + "/")); // Determine if this is the most relevant parent menu for the current path const isMostRelevantParent = isParentOfDetailPage && (() => { let longestMatchLength = 0; let mostRelevantParent = null; // Find the parent with the longest matching prefix items.forEach(parentItem => { if (parentItem.links && parentItem.links.length > 0) { parentItem.links.forEach(link => { const linkPath = link.link.replace(/\/+$/, ""); if (currentPath.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) { longestMatchLength = linkPath.length; mostRelevantParent = parentItem.label; } }); } }); return mostRelevantParent === item.label; })(); // NEW LOGIC: If we're on a detail page, NO submenu should be active regardless of pattern matching const hasActiveSubmenuOnDetailPage = isOnDetailPage ? false : hasActiveSubmenu; // NEW LOGIC: If user is on a detail page that belongs to this parent menu, // activate only the parent menu (open dropdown) without activating any submenu const isDetailPageOfThisMenu = !isActive && !hasActiveSubmenuOnDetailPage && item.links && item.links.length > 0 && item.links.some(link => { const linkPath = link.link.replace(/\/+$/, ""); return currentPath.startsWith(linkPath + "/"); }) && !isMostRelevantParent; // Only apply this logic if this isn't the most relevant parent // NEW LOGIC: Check if this is a page that doesn't belong to any specific menu in the navbar const isUnlistedPage = !isActive && !hasActiveSubmenu && !isMostRelevantParent && !isDetailPageOfThisMenu && isOnDetailPage; // NEW LOGIC: If we're on a detail page and this menu is not the relevant parent or detail page owner, // then it should not be highlighted even if it would normally be the most relevant const isOnDetailPageAndNotRelevant = isOnDetailPage && !isMostRelevantParent && !isDetailPageOfThisMenu && !isActive; // NEW LOGIC: If this is an unlisted page, no menu should be highlighted const isUnlistedPageAndNotRelevant = isUnlistedPage; // FINAL LOGIC: Only activate this menu if: // 1. It's the exact match for current path, OR // 2. It's the most relevant parent, OR // 3. It's a detail page of this menu // But NOT if we're on a detail page and this isn't the relevant parent // And NOT if this is an unlisted page const isActuallyRelevant = (isActive || isMostRelevantParent || isDetailPageOfThisMenu) && !isOnDetailPageAndNotRelevant && !isUnlistedPageAndNotRelevant; // Animasi saat isOpen berubah useEffect(() => { Animated.timing(animatedHeight, { toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 44 : 0) : 0, duration: 200, useNativeDriver: false, }).start(); }, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu]); // Jika ada submenu if (item.links && item.links.length > 0) { return ( {/* Parent Item */} {item.label} {/* Submenu (Animated) */} {submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => { // CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif const hasMoreSpecificMatch = submenuActiveStates.some(other => { if (other.subItem.link === subItem.link) return false; // Skip self const isOtherLonger = other.pathLength > pathLength; // Debug log untuk Dashboard // if (subItem.label === "Dashboard" && isSubActive) { // console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, { // dashboardLink: subItem.link, // dashboardLength: pathLength, // otherLabel: other.subItem.label, // otherLink: other.subItem.link, // otherPattern: other.subItem.detailPattern, // otherLength: other.pathLength, // otherIsActive: other.isActive, // isOtherLonger, // willDisableDashboard: other.isActive && isOtherLonger, // currentURL: currentPath // }); // } // Conflict log // if (isSubActive && other.isActive) { // console.log('🔍 CONFLICT DETECTED:', { // current: subItem.label, // currentPath: subItem.link, // currentLength: pathLength, // other: other.subItem.label, // otherPath: other.subItem.link, // otherLength: other.pathLength, // isOtherLonger, // shouldDisableCurrent: isOtherLonger, // currentURL: currentPath // }); // } return other.isActive && isOtherLonger; }); // Final decision const finalIsActive = isSubActive && !hasMoreSpecificMatch; // NEW: If this is a detail page (regardless of which menu), don't highlight any submenu items // Also don't highlight if this is an unlisted page const shouldHighlight = (isOnDetailPage || isUnlistedPage) ? false : finalIsActive; // Debug final // if (isSubActive) { // console.log('✅ Active check:', { // label: subItem.label, // link: subItem.link, // isSubActive, // hasMoreSpecificMatch, // finalIsActive, // shouldHighlight, // isOnDetailPage, // isUnlistedPage // }); // } 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", }, });