diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index 6d87632..36bd219 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import { Link, NavLink, Outlet } from 'react-router-dom' +import { Link, NavLink, Outlet, useLocation } from 'react-router-dom' import { ChevronDown, Circle, type LucideIcon } from 'lucide-react' import * as Icons from 'lucide-react' import { useState } from 'react' @@ -142,27 +142,39 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) { ) } +// So sánh 2 query string dạng key-value set (thứ tự param không quan trọng). +// Fix bug: /contracts?type=1 và ?type=1&pendingMe=1 cùng highlight vì NavLink +// built-in `end` prop chỉ match pathname, không check query string. +function queryMatches(current: string, target: string): boolean { + const a = new URLSearchParams(current) + const b = new URLSearchParams(target) + const aKeys = [...a.keys()].sort() + const bKeys = [...b.keys()].sort() + if (aKeys.length !== bKeys.length) return false + return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k)) +} + function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) { const Icon = getIcon(node.icon) const path = resolvePath(node.key) + const location = useLocation() if (!path) return null const isDeep = depth >= 2 + const [targetPath, targetQuery = ''] = path.split('?') + const isActive = location.pathname === targetPath + && queryMatches(location.search.replace(/^\?/, ''), targetQuery) + return ( - cn( - 'flex items-center gap-2.5 rounded-md transition', - isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium', - isActive - ? 'bg-brand-50 text-brand-700' - : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', - ) - } + className={cn( + 'flex items-center gap-2.5 rounded-md transition', + isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium', + isActive + ? 'bg-brand-50 text-brand-700' + : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', + )} > {node.label} diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 35c4aff..0e1e188 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -185,25 +185,43 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) { ) } +// So sánh 2 query string dạng key-value set (thứ tự param không quan trọng). +// Dùng để distinguish /path?type=2 vs /path?type=2&pendingMe=1 — NavLink isActive +// built-in chỉ match pathname, không check query string. +function queryMatches(current: string, target: string): boolean { + const a = new URLSearchParams(current) + const b = new URLSearchParams(target) + const aKeys = [...a.keys()].sort() + const bKeys = [...b.keys()].sort() + if (aKeys.length !== bKeys.length) return false + return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k)) +} + function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) { const Icon = getIcon(node.icon) const path = resolvePath(node.key) + const location = useLocation() if (!path) return null const isDeep = depth >= 2 + // Custom active check: pathname match + query string match (set equality). + // Fix bug: /purchase-evaluations?type=2 và ?type=2&pendingMe=1 cùng highlight + // vì NavLink default chỉ check pathname. + const [targetPath, targetQuery = ''] = path.split('?') + const pathnameMatches = location.pathname === targetPath + const qMatches = queryMatches(location.search.replace(/^\?/, ''), targetQuery) + const isActive = pathnameMatches && qMatches + return ( - cn( - 'flex items-center gap-2.5 rounded-md transition', - isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium', - isActive - ? 'bg-brand-50 text-brand-700' - : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', - ) - } + className={cn( + 'flex items-center gap-2.5 rounded-md transition', + isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium', + isActive + ? 'bg-brand-50 text-brand-700' + : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', + )} > {node.label}