Files
Investidor-V2/resources/js/components/portal-grouped-menu.tsx
T

442 lines
20 KiB
TypeScript

import PortalProductIdentity from '@/components/portal-product-identity';
import { searchTerritoryHref } from '@/lib/search-territory';
import { Link, router, usePage } from '@inertiajs/react';
import {
ChevronDown,
ArrowUpRight,
LayoutDashboard,
LogOut,
Menu,
UserRound,
X,
} from 'lucide-react';
import { Fragment, useEffect, useId, useRef, useState } from 'react';
import { useIsMobile } from '@/hooks/use-mobile';
import { useNavigationContext } from '@/components/navigation-context';
import { tempoSocialLinks, useTempoUi } from '@/components/tempo-brand';
import {
TempoLanguageSelector,
TempoWeather,
} from '@/components/tempo-header-tools';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
portalNavigation,
currentNavigationGroup,
currentNavigationItem,
} from '@/lib/portal-navigation';
import type { PortalNavItem } from '@/lib/portal-navigation';
import type { ProfileAccess } from '@/types/profile-access';
import type { OpportunityManagementAccess } from '@/types/opportunity-management';
import { logout } from '@/routes';
export function PortalGroupedMenu() {
const { props, url } = usePage<{
auth: {
user: { name?: string; email?: string } | null;
canManagePortal: boolean;
};
existingBusinessEnabled?: boolean;
businessReviewUrl?: string | null;
educationEnabled?: boolean;
procurementEnabled?: boolean;
aiEnabled?: boolean;
legacyContentEnabled?: boolean;
profileAccess?: ProfileAccess | null;
opportunityManagement?: OpportunityManagementAccess | null;
scopedAdministration?: {label:string;href:string}[];
}>();
const { t, language, requestCount } = useTempoUi();
const mobile = useIsMobile();
const navigation = useNavigationContext();
const groups = portalNavigation({
context: navigation.view
? { id: navigation.active, ...navigation.view.tools }
: undefined,
authenticated: !!props.auth?.user,
manager: !!props.auth?.canManagePortal,
businesses: props.existingBusinessEnabled,
businessReviewUrl: props.businessReviewUrl,
education: props.educationEnabled,
procurement: props.procurementEnabled,
ai: props.aiEnabled,
legacyContent: props.legacyContentEnabled,
opportunityManagement: props.opportunityManagement,
profileAccess: props.profileAccess,
});
if (props.scopedAdministration?.length) groups.push({id:'scoped-administration',label:'Administração',children:props.scopedAdministration.map((item,index)=>({id:`scoped-${index}`,label:item.label,href:item.href}))});
const current = currentNavigationGroup(groups, url);
const currentItem = currentNavigationItem(groups, url);
const [loggingOut, setLoggingOut] = useState(false);
const [expanded, setExpanded] = useState(false);
const navigationId = useId();
const toolsId = useId();
const interestId = useId();
const toggle = useRef<HTMLButtonElement>(null);
useEffect(() => router.on('navigate', () => setExpanded(false)), []);
const items = (children: PortalNavItem[]) =>
children.map((item) =>
item.children ? (
mobile ? (
<Fragment key={item.id}>
<DropdownMenuLabel className="mt-2 border-t px-3 pt-3 text-xs text-slate-500">
{t(item.label)}
</DropdownMenuLabel>
{items(item.children)}
</Fragment>
) : (
<DropdownMenuSub key={item.id}>
<DropdownMenuSubTrigger className="tempo-menu-item font-semibold">
{t(item.label)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
collisionPadding={12}
className="tempo-dropdown tempo-submenu max-h-[min(70vh,34rem)] w-72 overflow-y-auto p-2"
>
{items(item.children)}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
) : (
<DropdownMenuItem
key={item.id}
asChild
className="tempo-menu-item"
>
<Link
href={searchTerritoryHref(item.href!, url)}
aria-current={
currentItem === item.id ? 'page' : undefined
}
>
{t(item.label)}
</Link>
</DropdownMenuItem>
),
);
const accountGroups = groups.filter(
(group) =>
!['home', 'discovery', 'realization', 'support'].includes(group.id),
);
const panelLabel = `${t('O meu painel')} — ${requestCount} ${t(requestCount === 1 ? 'pedido recebido' : 'pedidos recebidos')}`;
const institutionalCurrent = url.split('?')[0] === '/contacto';
return (
<header
className="tempo-header"
lang={language}
data-expanded={expanded || undefined}
onKeyDown={(event) => {
if (
event.key === 'Escape' &&
expanded &&
!event.defaultPrevented &&
!(
event.target instanceof Element &&
event.target.closest('[role="menu"]')
)
) {
setExpanded(false);
toggle.current?.focus();
}
}}
>
<div className="tempo-header-inner">
<Link
href="/"
aria-label="Tempo — início"
className="tempo-logo-link"
>
<img
src="/images/logo/logo-vermelho.png"
alt="Tempo"
width="1787"
height="564"
className="tempo-header-logo"
/>
</Link>
<nav
id={navigationId}
aria-label="Áreas do portal"
className="tempo-primary-navigation"
>
<ul>
<li>
<Link
href="/"
className="tempo-nav-trigger"
aria-current={
url.split('?')[0] === '/'
? 'page'
: undefined
}
data-current-group={
url.split('?')[0] === '/' || undefined
}
>
{t('Início')}
</Link>
</li>
{groups
.filter((group) =>
[
'discovery',
'realization',
'support',
].includes(group.id),
)
.map((group) => (
<li key={group.id}>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
className="tempo-nav-trigger"
data-current-group={
current === group.id ||
undefined
}
>
{t(group.label)}
<ChevronDown
size={14}
aria-hidden="true"
/>
{current === group.id && (
<span className="sr-only">
Grupo actual
</span>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={12}
collisionPadding={12}
className="tempo-dropdown max-h-[min(75vh,36rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
{items(group.children ?? [])}
</DropdownMenuContent>
</DropdownMenu>
</li>
))}
<li>
<Link
href="/sobre"
className="tempo-nav-trigger"
aria-current={
url.split('?')[0] === '/sobre'
? 'page'
: undefined
}
data-current-group={
url.split('?')[0] === '/sobre' || undefined
}
>
{t('Sobre nós')}
</Link>
</li>
<li>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
className="tempo-nav-trigger"
data-current-group={
institutionalCurrent || undefined
}
>
Tempo
<ChevronDown size={14} aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={12}
collisionPadding={12}
className="tempo-dropdown max-h-[75vh] w-64 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
<DropdownMenuItem
asChild
className="tempo-menu-item"
>
<a
href="https://revista.tempo.co.mz/"
target="_blank"
rel="noopener noreferrer"
aria-label={`${t('Revista Tempo')} — ${t('Abre num novo separador')}`}
>
{t('Revista Tempo')}
<ArrowUpRight
size={14}
aria-hidden="true"
/>
</a>
</DropdownMenuItem>
{[['/contacto', 'Contacto']].map(
([href, label]) => (
<DropdownMenuItem
key={href}
asChild
className="tempo-menu-item"
>
<Link
href={href}
aria-current={
url.split('?')[0] ===
href
? 'page'
: undefined
}
>
{t(label)}
</Link>
</DropdownMenuItem>
),
)}
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-slate-500">
Revista Tempo
</DropdownMenuLabel>
{tempoSocialLinks.map(
({ name, href, icon: Icon }) => (
<DropdownMenuItem
key={name}
asChild
className="tempo-menu-item"
>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={`Revista Tempo — ${name}`}
>
<Icon
size={16}
aria-hidden="true"
/>
{name}
<ArrowUpRight
size={12}
aria-hidden="true"
/>
</a>
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
</li>
</ul>
</nav>
<div id={toolsId} className="tempo-header-extras">
<div className="tempo-header-utilities">
<div className="flex items-center gap-1">
<TempoWeather />
<TempoLanguageSelector />
</div>
</div>
</div>
<div className="tempo-header-actions">
<Link
href="/investidor/manifestacoes"
className="tempo-panel-summary"
aria-label={panelLabel}
title={panelLabel}
>
<LayoutDashboard size={19} aria-hidden="true" />
<span className="tempo-panel-count" aria-hidden="true">
{requestCount}
</span>
</Link>
<span
className="sr-only"
role="status"
aria-live="polite"
aria-atomic="true"
>
{panelLabel}
</span>
{props.auth?.user ? (
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="tempo-login-button">
<UserRound size={16} aria-hidden="true" />
<span className="max-w-24 truncate">
{props.auth.user.name || t('A minha área')}
</span>
<ChevronDown size={12} aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={12}
collisionPadding={12}
className="tempo-dropdown max-h-[75vh] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
<DropdownMenuLabel className="truncate text-xs text-slate-500">
{props.auth.user.email}
</DropdownMenuLabel>
{items(accountGroups)}
<DropdownMenuSeparator />
<DropdownMenuItem
asChild
className="tempo-menu-item text-red-700"
>
<Link
href={logout()}
as="button"
type="button"
disabled={loggingOut}
data-test="portal-logout-button"
onClick={() => router.flushAll()}
onStart={() => setLoggingOut(true)}
onFinish={() => setLoggingOut(false)}
>
<LogOut size={16} aria-hidden="true" />
{loggingOut
? 'A terminar sessão…'
: t('Terminar sessão')}
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Link href="/login" className="tempo-login-button">
<UserRound size={15} aria-hidden="true" />
{t('Entrar')}
</Link>
)}
</div>
<Link
id={interestId}
href="/manifestar-interesse"
className="tempo-interest-button"
>
{t('Manifestar interesse')}
<ArrowUpRight size={15} aria-hidden="true" />
</Link>
<button
ref={toggle}
type="button"
className="tempo-header-toggle"
aria-label={
expanded
? 'Fechar menu e ferramentas'
: 'Abrir menu e ferramentas'
}
aria-expanded={expanded}
aria-controls={`${navigationId} ${toolsId} ${interestId}`}
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<X size={22} aria-hidden="true" />
) : (
<Menu size={22} aria-hidden="true" />
)}
</button>
</div>
</header>
);
}