Files
Investidor-V2/resources/js/components/portal-grouped-menu.tsx
T
2026-10-20 11:39:25 +02:00

355 lines
15 KiB
TypeScript

import { Link, router, usePage } from '@inertiajs/react';
import {
ChevronDown,
ArrowUpRight,
LogOut,
Menu,
UserRound,
X,
} from 'lucide-react';
import { useEffect, useId, useRef, useState } from 'react';
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;
aiEnabled?: boolean;
legacyContentEnabled?: boolean;
profileAccess?: ProfileAccess | null;
opportunityManagement?: OpportunityManagementAccess | null;
}>();
const { t, language, requestCount } = useTempoUi();
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,
ai: props.aiEnabled,
legacyContent: props.legacyContentEnabled,
opportunityManagement: props.opportunityManagement,
profileAccess: props.profileAccess,
});
const current = currentNavigationGroup(groups, url);
const currentItem = currentNavigationItem(groups, url);
const [loggingOut, setLoggingOut] = useState(false);
const [expanded, setExpanded] = useState(false);
const institutionalId = useId();
const toolsId = useId();
const toggle = useRef<HTMLButtonElement>(null);
useEffect(() => router.on('navigate', () => setExpanded(false)), []);
const items = (children: PortalNavItem[]) =>
children.map((item) =>
item.children ? (
<DropdownMenuSub key={item.id}>
<DropdownMenuSubTrigger className="tempo-menu-item font-semibold">
{t(item.label)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
collisionPadding={12}
className="tempo-dropdown max-h-[min(70vh,34rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
{items(item.children)}
</DropdownMenuSubContent>
</DropdownMenuSub>
) : (
<DropdownMenuItem
key={item.id}
asChild
className="tempo-menu-item"
>
<Link
href={item.href!}
aria-current={
currentItem === item.id ? 'page' : undefined
}
>
{t(item.label)}
</Link>
</DropdownMenuItem>
),
);
const accountGroups = groups.filter(
(group) =>
!['home', 'discovery', 'realization', 'support'].includes(group.id),
);
return (
<header
className="tempo-header"
lang={language}
data-expanded={expanded || undefined}
onKeyDown={(event) => {
if (
event.key === 'Escape' &&
expanded &&
!event.defaultPrevented
) {
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={institutionalId}
aria-label="Navegação institucional"
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>
<li>
<a
href="https://revista.tempo.co.mz/"
target="_blank"
rel="noopener noreferrer"
className="tempo-nav-trigger"
aria-label={`${t('Revista Tempo')} — ${t('Abre num novo separador')}`}
>
{t('Revista Tempo')}
<ArrowUpRight size={14} aria-hidden="true" />
</a>
</li>
{[
['/sobre', 'Sobre nós'],
['/contacto', 'Contacto'],
].map(([href, label]) => (
<li key={href}>
<Link
href={href}
className="tempo-nav-trigger"
aria-current={
url.split('?')[0] === href
? 'page'
: undefined
}
data-current-group={
url.split('?')[0] === href || undefined
}
>
{t(label)}
</Link>
</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>
<TempoSocialLinks />
</div>
</div>
<div className="tempo-header-actions">
{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>
<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={`${institutionalId} ${toolsId}`}
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<X size={22} aria-hidden="true" />
) : (
<Menu size={22} aria-hidden="true" />
)}
</button>
</div>
<div className="tempo-section-bar">
<div className="tempo-section-inner">
<Link
href="/investidor/manifestacoes"
className="tempo-panel-summary"
>
<span>{t('O meu painel')}</span>
<span
className="tempo-panel-count"
aria-live="polite"
aria-atomic="true"
>
{requestCount}{' '}
{t(
requestCount === 1
? 'pedido recebido'
: 'pedidos recebidos',
)}
</span>
</Link>
<nav
aria-label="Áreas do portal"
className="tempo-section-navigation"
>
<ul>
{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={8}
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>
))}
</ul>
</nav>
<Link
href="/manifestar-interesse"
className="tempo-interest-button"
>
{t('Manifestar interesse')}
</Link>
</div>
</div>
</header>
);
}