|
|
|
@@ -2,149 +2,190 @@ import {
|
|
|
|
|
AppShell,
|
|
|
|
|
Burger,
|
|
|
|
|
Group,
|
|
|
|
|
Skeleton,
|
|
|
|
|
Tooltip,
|
|
|
|
|
UnstyledButton,
|
|
|
|
|
ActionIcon,
|
|
|
|
|
Title,
|
|
|
|
|
useMantineTheme,
|
|
|
|
|
} from '@mantine/core';
|
|
|
|
|
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
|
|
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
|
|
|
|
|
|
|
|
import { default as LogoIcon } from '../../assets/icons/favicon';
|
|
|
|
|
import { default as LogoIcon } from '../../assets/icons/favicon';
|
|
|
|
|
import { mainLinksdata } from '../../data/navigationsLinks_1';
|
|
|
|
|
import { submenuLinks } from '../../data/submenuLinks_1';
|
|
|
|
|
|
|
|
|
|
import { useMantineTheme } from '@mantine/core';
|
|
|
|
|
import classes from './Appshell.module.css';
|
|
|
|
|
|
|
|
|
|
import { mainLinksdata } from '../../data/navigationsLinks_1';
|
|
|
|
|
import { submenuLinks } from '../../data/submenuLinks_1';
|
|
|
|
|
|
|
|
|
|
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
|
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
|
import { Link, useLocation } from 'react-router-dom';
|
|
|
|
|
import classes from './Appshell.module.css';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type AppShellWithMenuProps = {
|
|
|
|
|
children?: React.ReactNode;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type AppShellWithMenuProps = {
|
|
|
|
|
children?: React.ReactNode; // <- ahora es opcional
|
|
|
|
|
};
|
|
|
|
|
// Persistencia en localStorage
|
|
|
|
|
const STORAGE_KEY = 'lastSubmenuRoutes';
|
|
|
|
|
|
|
|
|
|
export function AppShellWithMenu({ children }: AppShellWithMenuProps) {
|
|
|
|
|
|
|
|
|
|
const theme = useMantineTheme();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
|
|
|
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure(false);
|
|
|
|
|
const [desktopOpened, { toggle: toggleDesktop, open: openDesktop }] = useDisclosure(true);
|
|
|
|
|
const isCollapsed = useMemo(() => (isMobile ? !mobileOpened : !desktopOpened), [isMobile, mobileOpened, desktopOpened]);
|
|
|
|
|
|
|
|
|
|
const [manualActiveTab, setManualActiveTab] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const matchedMain = Object.entries(submenuLinks).find(([mainKey, items]) =>
|
|
|
|
|
items.some((item) => location.pathname.startsWith(item.to))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const routeBasedActive = matchedMain?.[0] ?? 'Home';
|
|
|
|
|
const active = manualActiveTab ?? routeBasedActive;
|
|
|
|
|
const activeLink = submenuLinks[active as keyof typeof submenuLinks]?.find((item) => location.pathname === item.to)?.label ?? '';
|
|
|
|
|
|
|
|
|
|
const mainLinks = mainLinksdata.map((link) => (
|
|
|
|
|
<Tooltip
|
|
|
|
|
label={link.label}
|
|
|
|
|
position="right"
|
|
|
|
|
withArrow
|
|
|
|
|
transitionProps={{ duration: 0 }}
|
|
|
|
|
key={link.label}
|
|
|
|
|
>
|
|
|
|
|
<UnstyledButton
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setManualActiveTab(link.label);
|
|
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
className={classes.mainLink}
|
|
|
|
|
data-active={link.label === active || undefined}
|
|
|
|
|
>
|
|
|
|
|
<link.icon />
|
|
|
|
|
</UnstyledButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
const links = (submenuLinks[active as keyof typeof submenuLinks] || []).map((item) => (
|
|
|
|
|
<Link
|
|
|
|
|
className={classes.link}
|
|
|
|
|
data-active={activeLink === item.label || undefined}
|
|
|
|
|
to={item.to}
|
|
|
|
|
key={item.label}
|
|
|
|
|
style={{ display: isCollapsed ? 'none' : 'block' }}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (isMobile) closeMobile();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
</Link>
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setManualActiveTab(null);
|
|
|
|
|
}, [location.pathname]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isMobile) openDesktop();
|
|
|
|
|
}, [isMobile, openDesktop]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<AppShell
|
|
|
|
|
header={{ height: 60 }}
|
|
|
|
|
navbar={{
|
|
|
|
|
width: isCollapsed ? 60 : 300,
|
|
|
|
|
breakpoint: 'sm',
|
|
|
|
|
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
|
|
|
|
}}
|
|
|
|
|
padding="md"
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
|
|
|
|
<AppShell.Header>
|
|
|
|
|
<Group h="100%" px="sm">
|
|
|
|
|
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
|
|
|
|
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
|
|
|
|
<LogoIcon
|
|
|
|
|
style={{ width: 30, height: 30 }}
|
|
|
|
|
circleFill={theme.colors.brand[9]}
|
|
|
|
|
pathFill={theme.colors.secondary[2]}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
</Group>
|
|
|
|
|
</AppShell.Header>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Navbar */}
|
|
|
|
|
|
|
|
|
|
<AppShell.Navbar>
|
|
|
|
|
<div className={classes.wrapper}>
|
|
|
|
|
<div className={classes.aside}>
|
|
|
|
|
<div className={classes.topSection}>
|
|
|
|
|
|
|
|
|
|
{mainLinks}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
<div className={classes.main}>
|
|
|
|
|
{!isCollapsed && <Title order={4} className={classes.title}>{active}</Title>}
|
|
|
|
|
{links}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</AppShell.Navbar>
|
|
|
|
|
|
|
|
|
|
{/* Main Content */}
|
|
|
|
|
|
|
|
|
|
<AppShell.Main>
|
|
|
|
|
{children}
|
|
|
|
|
</AppShell.Main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</AppShell>
|
|
|
|
|
);
|
|
|
|
|
function getLastSubmenuRoute(section: string): string | null {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
|
|
|
const parsed = raw ? JSON.parse(raw) : {};
|
|
|
|
|
return parsed[section] ?? null;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setLastSubmenuRoute(section: string, route: string) {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
|
|
|
const parsed = raw ? JSON.parse(raw) : {};
|
|
|
|
|
parsed[section] = route;
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(parsed));
|
|
|
|
|
} catch {
|
|
|
|
|
// fallback silencioso
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function AppShellWithMenu({ children }: AppShellWithMenuProps) {
|
|
|
|
|
const theme = useMantineTheme();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
|
|
|
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure(false);
|
|
|
|
|
const [desktopOpened, { toggle: toggleDesktop, open: openDesktop }] = useDisclosure(true);
|
|
|
|
|
|
|
|
|
|
const isCollapsed = useMemo(
|
|
|
|
|
() => (isMobile ? !mobileOpened : !desktopOpened),
|
|
|
|
|
[isMobile, mobileOpened, desktopOpened]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Estado para el main link activo
|
|
|
|
|
const [activeMain, setActiveMain] = useState<string>('Home');
|
|
|
|
|
|
|
|
|
|
// Ref para saber si el usuario ha hecho clic manualmente en el main link
|
|
|
|
|
const userClickedMainRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const currentPath = location.pathname.toLowerCase().replace(/\/$/, '');
|
|
|
|
|
|
|
|
|
|
let matchedMain: string | null = null;
|
|
|
|
|
let maxMatchLength = 0;
|
|
|
|
|
|
|
|
|
|
Object.entries(submenuLinks).forEach(([main, items]) => {
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
const itemPath = item.to.toLowerCase().replace(/\/$/, '');
|
|
|
|
|
if (
|
|
|
|
|
currentPath === itemPath ||
|
|
|
|
|
currentPath.startsWith(itemPath + '/')
|
|
|
|
|
) {
|
|
|
|
|
if (itemPath.length > maxMatchLength) {
|
|
|
|
|
matchedMain = main;
|
|
|
|
|
maxMatchLength = itemPath.length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (matchedMain) {
|
|
|
|
|
setActiveMain(matchedMain);
|
|
|
|
|
}
|
|
|
|
|
}, [location.pathname]);
|
|
|
|
|
|
|
|
|
|
const activeLink =
|
|
|
|
|
submenuLinks[activeMain as keyof typeof submenuLinks]?.find(
|
|
|
|
|
(item) => item.to === location.pathname
|
|
|
|
|
)?.label ?? '';
|
|
|
|
|
|
|
|
|
|
const mainLinks = mainLinksdata.map((link) => (
|
|
|
|
|
<Tooltip
|
|
|
|
|
label={link.label}
|
|
|
|
|
position="right"
|
|
|
|
|
withArrow
|
|
|
|
|
transitionProps={{ duration: 0 }}
|
|
|
|
|
key={link.label}
|
|
|
|
|
>
|
|
|
|
|
<UnstyledButton
|
|
|
|
|
onClick={() => {
|
|
|
|
|
userClickedMainRef.current = true;
|
|
|
|
|
setActiveMain(link.label);
|
|
|
|
|
|
|
|
|
|
const remembered = getLastSubmenuRoute(link.label);
|
|
|
|
|
const fallback = submenuLinks[link.label as keyof typeof submenuLinks]?.[0]?.to;
|
|
|
|
|
|
|
|
|
|
if (isCollapsed && (remembered || fallback)) {
|
|
|
|
|
navigate(remembered ?? fallback);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className={classes.mainLink}
|
|
|
|
|
data-active={link.label === activeMain || undefined}
|
|
|
|
|
>
|
|
|
|
|
<link.icon />
|
|
|
|
|
</UnstyledButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
const links = (submenuLinks[activeMain as keyof typeof submenuLinks] || []).map((item) => (
|
|
|
|
|
<Link
|
|
|
|
|
className={classes.link}
|
|
|
|
|
data-active={activeLink === item.label || undefined}
|
|
|
|
|
to={item.to}
|
|
|
|
|
key={item.label}
|
|
|
|
|
style={{ display: isCollapsed ? 'none' : 'block' }}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setLastSubmenuRoute(activeMain, item.to);
|
|
|
|
|
if (isMobile) closeMobile();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
</Link>
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isMobile) openDesktop();
|
|
|
|
|
}, [isMobile, openDesktop]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<AppShell
|
|
|
|
|
header={{ height: 60 }}
|
|
|
|
|
navbar={{
|
|
|
|
|
width: isCollapsed ? 60 : 300,
|
|
|
|
|
breakpoint: 'sm',
|
|
|
|
|
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
|
|
|
|
}}
|
|
|
|
|
padding="md"
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<AppShell.Header>
|
|
|
|
|
<Group h="100%" px="sm">
|
|
|
|
|
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
|
|
|
|
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
|
|
|
|
<LogoIcon
|
|
|
|
|
style={{ width: 30, height: 30 }}
|
|
|
|
|
circleFill={theme.colors.brand[9]}
|
|
|
|
|
pathFill={theme.colors.secondary[2]}
|
|
|
|
|
/>
|
|
|
|
|
</Group>
|
|
|
|
|
</AppShell.Header>
|
|
|
|
|
|
|
|
|
|
{/* Navbar */}
|
|
|
|
|
<AppShell.Navbar>
|
|
|
|
|
<div className={classes.wrapper}>
|
|
|
|
|
<div className={classes.aside}>
|
|
|
|
|
<div className={classes.topSection}>{mainLinks}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={classes.main}>
|
|
|
|
|
{!isCollapsed && (
|
|
|
|
|
<Title order={4} className={classes.title}>
|
|
|
|
|
{activeMain}
|
|
|
|
|
</Title>
|
|
|
|
|
)}
|
|
|
|
|
{links}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</AppShell.Navbar>
|
|
|
|
|
|
|
|
|
|
{/* Main Content */}
|
|
|
|
|
<AppShell.Main>{children}</AppShell.Main>
|
|
|
|
|
</AppShell>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|