406 lines
18 KiB
TypeScript
406 lines
18 KiB
TypeScript
import '@/components/portal-product-identity';
|
|
import { searchTerritoryHref } from '@/lib/search-territory';
|
|
import { Link, router, usePage } from '@inertiajs/react';
|
|
import {
|
|
ChevronDown,
|
|
ArrowUpRight,
|
|
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 { useTempoUi } from '@/components/tempo-brand';
|
|
import { TempoLanguageSelector } from '@/components/tempo-header-tools';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import {
|
|
portalNavigation,
|
|
canShowLegacyManagement,
|
|
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;
|
|
legacyManagementAccess?: boolean;
|
|
};
|
|
existingBusinessEnabled?: boolean;
|
|
businessReviewUrl?: string | null;
|
|
educationEnabled?: boolean;
|
|
procurementEnabled?: boolean;
|
|
fundingEnabled?: boolean;
|
|
aiEnabled?: boolean;
|
|
legacyContentEnabled?: boolean;
|
|
profileAccess?: ProfileAccess | null;
|
|
opportunityManagement?: OpportunityManagementAccess | null;
|
|
scopedAdministration?: { label: string; href: string }[];
|
|
contentManagementHref?: string | null;
|
|
contentManagementAccess?: {
|
|
opportunities?: { index: string; create?: string | null };
|
|
};
|
|
}>();
|
|
const { t, language } = 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: canShowLegacyManagement(props.auth),
|
|
businesses: props.existingBusinessEnabled,
|
|
businessReviewUrl: props.businessReviewUrl,
|
|
education: props.educationEnabled,
|
|
procurement: props.procurementEnabled,
|
|
funding: props.fundingEnabled,
|
|
ai: props.aiEnabled,
|
|
legacyContent: props.legacyContentEnabled,
|
|
opportunityManagement: props.opportunityManagement,
|
|
profileAccess: props.profileAccess,
|
|
contentManagementHref: props.contentManagementHref,
|
|
contentManagementAccess: props.contentManagementAccess,
|
|
scopedAdministration: props.scopedAdministration,
|
|
});
|
|
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',
|
|
'procurement-area',
|
|
'discovery',
|
|
'realization',
|
|
'support',
|
|
'work',
|
|
].includes(group.id),
|
|
);
|
|
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) =>
|
|
[
|
|
'procurement-area',
|
|
'discovery',
|
|
'realization',
|
|
'support',
|
|
'work',
|
|
].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
|
|
}
|
|
>
|
|
Mais
|
|
<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>
|
|
),
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
<div id={toolsId} className="tempo-header-extras">
|
|
<div className="tempo-header-utilities">
|
|
<div className="flex items-center gap-1">
|
|
<TempoLanguageSelector />
|
|
</div>
|
|
</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>
|
|
<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>
|
|
);
|
|
}
|