fix: unify horizontal portal menus and remove discovery duplicates
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-10 15:48:23 +02:00
parent d5cb41b111
commit d216830b06
10 changed files with 305 additions and 136 deletions
@@ -1,3 +1,13 @@
## 10/11/2026 — janela CBF15 instalada; menus horizontais uniformizados em QA
**Homologação: `d5cb41b111cac95c431a3072b8c4cc0141481ec2`**, promovida às13:43:40UTC e conferida às13:44:18UTC (15:44 locais). Delta12hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos,89e6d72c preservada. Recibos `outputs/cbf15-window-delivery-20261110/`. Janela de cinco exercícios CBF15 deixa de ser pendência de instalação; transporte de créditos anteriores e restantes resíduos fiscais continuam separados.
**Delta posterior em QA, ainda não instalado — pedido expresso da Tempo10/11, corrigido para menus horizontais com dropdown:** cabeçalho comum passa a ser montado uma vez pelo layout global em todas as páginas; área privada usa o mesmo cabeçalho sem navegação lateral concorrente. Autenticação, definições, gestão e páginas públicas conservam conteúdo e breadcrumbs; filtros de capacidades, direitos e destinos privados existentes preservados. Esconder cabeçalho na impressão mantém-se. Os cinco atalhos (Porquê investir,Partir de uma ideia,Consultar oportunidades,Simulador Fiscal,Sectores) ficam intactos na entrada principal. Descoberta conserva localizações/mapa,catálogos e módulos opcionais,sem repetir os atalhos; Partir de uma ideia já estava fora desse grupo. CSS responsivo e implementação comum dos dropdowns/teclado/Escape/toque reutilizados,sem novo menu alternativo.
Conferência estrutural dos layouts das146páginasTSX existentes: exactamente um cabeçalho comum por composição, incluindo páginas privadas,autenticação/definições, sem sidebar global; conteúdo/contexto/breadcrumbs e cinco atalhos preservados. Provas `tests/frontend/portal-horizontal-shell.test.mjs`, navegação e renderização dirigida (23casos aprovados). É SSR com dependências de UI substituídas,não prova visual de navegador. Tentativa única de inventárioCUA nesta continuação devolveu exactamente `Browsers: Error: The browser access host is unavailable`; nenhuma alteração/reinício de navegador. Conferência visual mantém-se pendente no registo existente. Tipos/lint/build QA aprovados, logs desta entrega; base activa/dados/permissões intocados.
O saldo integral dos restantes módulos e L1–L7/restanteL8/L9–L16 permanece no plano existente; nenhuma contagem de páginas/testes declara o portal concluído.
## 10/11/2026 — janela de cinco exercícios CBF15 em QA
Homologação mantém `89e6d72c`; o delta seguinte ainda não está instalado. Pendência encerrada em QA: contagem inclusiva dos cinco exercícios civis do CBF15(4/7) na liquidação2026, para investimento efectivo do próprio2026. Início de exploração para projecto novo; início dos procedimentos após aprovação para projecto em funcionamento. Não usa cinco anos móveis nem reinicia o prazo pela data de aquisição. Início2022, mesmo em31/12 → quinto exercício2026, dedução200 pela quota residual, saldo500MZN. Início2021 → sexto exercício2026, prazo expirado, dedução0 e saldo700; activo e documento preservados. Datas inválidas/futuras e activos antigos apresentados como investimento corrente permanecem indeterminados/recusados, não zero por omissão.
@@ -1,3 +1,13 @@
## 10/11/2026 — janela CBF15 instalada; menus horizontais uniformizados em QA
**Homologação: `d5cb41b111cac95c431a3072b8c4cc0141481ec2`**, promovida às13:43:40UTC e conferida às13:44:18UTC (15:44 locais). Delta12hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos,89e6d72c preservada. Recibos `outputs/cbf15-window-delivery-20261110/`. Janela de cinco exercícios CBF15 deixa de ser pendência de instalação; transporte de créditos anteriores e restantes resíduos fiscais continuam separados.
**Delta posterior em QA, ainda não instalado — pedido expresso da Tempo10/11, corrigido para menus horizontais com dropdown:** cabeçalho comum passa a ser montado uma vez pelo layout global em todas as páginas; área privada usa o mesmo cabeçalho sem navegação lateral concorrente. Autenticação, definições, gestão e páginas públicas conservam conteúdo e breadcrumbs; filtros de capacidades, direitos e destinos privados existentes preservados. Esconder cabeçalho na impressão mantém-se. Os cinco atalhos (Porquê investir,Partir de uma ideia,Consultar oportunidades,Simulador Fiscal,Sectores) ficam intactos na entrada principal. Descoberta conserva localizações/mapa,catálogos e módulos opcionais,sem repetir os atalhos; Partir de uma ideia já estava fora desse grupo. CSS responsivo e implementação comum dos dropdowns/teclado/Escape/toque reutilizados,sem novo menu alternativo.
Conferência estrutural dos layouts das146páginasTSX existentes: exactamente um cabeçalho comum por composição, incluindo páginas privadas,autenticação/definições, sem sidebar global; conteúdo/contexto/breadcrumbs e cinco atalhos preservados. Provas `tests/frontend/portal-horizontal-shell.test.mjs`, navegação e renderização dirigida (23casos aprovados). É SSR com dependências de UI substituídas,não prova visual de navegador. Tentativa única de inventárioCUA nesta continuação devolveu exactamente `Browsers: Error: The browser access host is unavailable`; nenhuma alteração/reinício de navegador. Conferência visual mantém-se pendente no registo existente. Tipos/lint/build QA aprovados, logs desta entrega; base activa/dados/permissões intocados.
O saldo integral dos restantes módulos e L1–L7/restanteL8/L9–L16 permanece no plano existente; nenhuma contagem de páginas/testes declara o portal concluído.
## 10/11/2026 — janela de cinco exercícios CBF15 em QA
Homologação mantém `89e6d72c`; o delta seguinte ainda não está instalado. Pendência encerrada em QA: contagem inclusiva dos cinco exercícios civis do CBF15(4/7) na liquidação2026, para investimento efectivo do próprio2026. Início de exploração para projecto novo; início dos procedimentos após aprovação para projecto em funcionamento. Não usa cinco anos móveis nem reinicia o prazo pela data de aquisição. Início2022, mesmo em31/12 → quinto exercício2026, dedução200 pela quota residual, saldo500MZN. Início2021 → sexto exercício2026, prazo expirado, dedução0 e saldo700; activo e documento preservados. Datas inválidas/futuras e activos antigos apresentados como investimento corrente permanecem indeterminados/recusados, não zero por omissão.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,13 @@
## 10/11/2026 — janela CBF15 instalada; menus horizontais uniformizados em QA
**Homologação: `d5cb41b111cac95c431a3072b8c4cc0141481ec2`**, promovida às13:43:40UTC e conferida às13:44:18UTC (15:44 locais). Delta12hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos,89e6d72c preservada. Recibos `outputs/cbf15-window-delivery-20261110/`. Janela de cinco exercícios CBF15 deixa de ser pendência de instalação; transporte de créditos anteriores e restantes resíduos fiscais continuam separados.
**Delta posterior em QA, ainda não instalado — pedido expresso da Tempo10/11, corrigido para menus horizontais com dropdown:** cabeçalho comum passa a ser montado uma vez pelo layout global em todas as páginas; área privada usa o mesmo cabeçalho sem navegação lateral concorrente. Autenticação, definições, gestão e páginas públicas conservam conteúdo e breadcrumbs; filtros de capacidades, direitos e destinos privados existentes preservados. Esconder cabeçalho na impressão mantém-se. Os cinco atalhos (Porquê investir,Partir de uma ideia,Consultar oportunidades,Simulador Fiscal,Sectores) ficam intactos na entrada principal. Descoberta conserva localizações/mapa,catálogos e módulos opcionais,sem repetir os atalhos; Partir de uma ideia já estava fora desse grupo. CSS responsivo e implementação comum dos dropdowns/teclado/Escape/toque reutilizados,sem novo menu alternativo.
Conferência estrutural dos layouts das146páginasTSX existentes: exactamente um cabeçalho comum por composição, incluindo páginas privadas,autenticação/definições, sem sidebar global; conteúdo/contexto/breadcrumbs e cinco atalhos preservados. Provas `tests/frontend/portal-horizontal-shell.test.mjs`, navegação e renderização dirigida (23casos aprovados). É SSR com dependências de UI substituídas,não prova visual de navegador. Tentativa única de inventárioCUA nesta continuação devolveu exactamente `Browsers: Error: The browser access host is unavailable`; nenhuma alteração/reinício de navegador. Conferência visual mantém-se pendente no registo existente. Tipos/lint/build QA aprovados, logs desta entrega; base activa/dados/permissões intocados.
O saldo integral dos restantes módulos e L1–L7/restanteL8/L9–L16 permanece no plano existente; nenhuma contagem de páginas/testes declara o portal concluído.
## 10/11/2026 — janela de cinco exercícios CBF15 em QA
Homologação mantém `89e6d72c`; o delta seguinte ainda não está instalado. Pendência encerrada em QA: contagem inclusiva dos cinco exercícios civis do CBF15(4/7) na liquidação2026, para investimento efectivo do próprio2026. Início de exploração para projecto novo; início dos procedimentos após aprovação para projecto em funcionamento. Não usa cinco anos móveis nem reinicia o prazo pela data de aquisição. Início2022, mesmo em31/12 → quinto exercício2026, dedução200 pela quota residual, saldo500MZN. Início2021 → sexto exercício2026, prazo expirado, dedução0 e saldo700; activo e documento preservados. Datas inválidas/futuras e activos antigos apresentados como investimento corrente permanecem indeterminados/recusados, não zero por omissão.
+1 -1
View File
@@ -1,4 +1,4 @@
import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout';
import AppLayoutTemplate from '@/layouts/app/app-header-layout';
import type { BreadcrumbItem } from '@/types';
export default function AppLayout({
+11 -8
View File
@@ -1,16 +1,19 @@
import { AppContent } from '@/components/app-content';
import { AppHeader } from '@/components/app-header';
import { AppShell } from '@/components/app-shell';
import { Breadcrumbs } from '@/components/breadcrumbs';
import type { AppLayoutProps } from '@/types';
/** Global navigation is owned by PortalNavigation for every page. */
export default function AppHeaderLayout({
children,
breadcrumbs,
breadcrumbs = [],
}: AppLayoutProps) {
return (
<AppShell variant="header">
<AppHeader breadcrumbs={breadcrumbs} />
<AppContent variant="header">{children}</AppContent>
</AppShell>
<div className="flex min-w-0 flex-col">
{breadcrumbs.length > 0 && (
<div className="border-b px-5 py-3">
<Breadcrumbs breadcrumbs={breadcrumbs} />
</div>
)}
<main className="flex min-w-0 flex-1 flex-col">{children}</main>
</div>
);
}
+84 -93
View File
@@ -65,27 +65,18 @@ export default function PortalNavigation({
}: {
children: ReactNode;
}) {
return <TempoUiProvider><PageTranslator /><PortalNavigationContent>{children}</PortalNavigationContent></TempoUiProvider>;
return (
<TempoUiProvider>
<PageTranslator />
<PortalNavigationContent>{children}</PortalNavigationContent>
</TempoUiProvider>
);
}
function PortalNavigationContent({ children }: { children: ReactNode }) {
const { t } = useTempoUi();
const { url, props, component } = usePage();
const printable = component === 'land-assistance/print';
const portalShell =
['welcome', 'PortalInvestidor', 'management-access'].includes(
component,
) ||
[
'qa/',
'portal/',
'investment/',
'portfolio/',
'desk/',
'investor-path/',
'auth/',
'land-assistance/',
].some((prefix) => component.startsWith(prefix));
const path = url.split('?')[0];
const parent = Object.keys(names)
.filter((p) => p !== '/' && path.startsWith(`${p}/`))
@@ -111,92 +102,92 @@ function PortalNavigationContent({ children }: { children: ReactNode }) {
? 'Definições da conta'
: 'Detalhes');
return (
<NavigationContextProvider>
<MenuPlacement.Provider value={true}>
<div className="min-h-screen [&_button]:scroll-mb-24 [&_input]:scroll-mb-24 [&_select]:scroll-mb-24 [&_textarea]:scroll-mb-24">
<nav
aria-label={t('Atalho para o conteúdo')}
className={printable ? 'print:hidden' : undefined}
<NavigationContextProvider>
<MenuPlacement.Provider value={true}>
<div className="min-h-screen [&_button]:scroll-mb-24 [&_input]:scroll-mb-24 [&_select]:scroll-mb-24 [&_textarea]:scroll-mb-24">
<nav
aria-label={t('Atalho para o conteúdo')}
className={printable ? 'print:hidden' : undefined}
>
<a
href="#portal-content"
className="sr-only focus:not-sr-only focus:block focus:bg-white focus:p-4 focus:text-emerald-900"
>
<a
href="#portal-content"
className="sr-only focus:not-sr-only focus:block focus:bg-white focus:p-4 focus:text-emerald-900"
>
{t('Saltar para o conteúdo')}
</a>
{t('Saltar para o conteúdo')}
</a>
</nav>
{props.homologationReserved === true && (
<aside
aria-label="Ambiente de homologação"
className="border-b border-amber-200 bg-amber-50 px-5 py-2 text-sm text-amber-950 print:hidden"
>
Homologação reservada à revisão da Tempo. Consulte o
conteúdo público; entre na sua conta quando iniciar
uma operação.
</aside>
)}
{
<div className={printable ? 'print:hidden' : undefined}>
<PortalMenu primary />
</div>
}
{path !== '/' && (
<nav
aria-label={t('Caminho de navegação')}
className={`bg-background text-foreground flex flex-wrap items-center gap-2 border-b px-5 py-3 text-sm ${printable ? 'print:hidden' : ''}`}
>
<>
<Link href="/" className="underline">
{t('Início')}
</Link>
<span aria-hidden="true">/</span>
{parent && (
<>
<Link
href={searchTerritoryHref(
parent,
url,
)}
className="underline"
>
{t(names[parent])}
</Link>
<span aria-hidden="true">/</span>
</>
)}
<span aria-current="page">{t(title)}</span>
</>
</nav>
{props.homologationReserved === true && (
<aside aria-label="Ambiente de homologação" className="border-b border-amber-200 bg-amber-50 px-5 py-2 text-sm text-amber-950 print:hidden">
Homologação reservada à revisão da Tempo. Consulte o conteúdo público; entre na sua conta quando iniciar uma operação.
</aside>
)}
{portalShell && (
)}
<div id="portal-content" tabIndex={-1}>
{
<div
className={
printable ? 'print:hidden' : undefined
}
>
<PortalMenu primary />
<NavigationContextSummary />
</div>
)}
{path !== '/' && (
<nav
aria-label={t('Caminho de navegação')}
className={`bg-background text-foreground flex flex-wrap items-center gap-2 border-b px-5 py-3 text-sm ${printable ? 'print:hidden' : ''}`}
>
<>
<Link href="/" className="underline">
{t('Início')}
</Link>
<span aria-hidden="true">/</span>
{parent && (
<>
<Link
href={searchTerritoryHref(
parent,
url,
)}
className="underline"
>
{t(names[parent])}
</Link>
<span aria-hidden="true">/</span>
</>
)}
<span aria-current="page">{t(title)}</span>
</>
</nav>
)}
<div id="portal-content" tabIndex={-1}>
{portalShell && (
<div
className={
printable ? 'print:hidden' : undefined
}
>
<NavigationContextSummary />
</div>
)}
<SearchFeedback />
{children}
</div>
<aside
aria-label={t('Contacto de apoio ao investidor')}
className={`mx-auto flex max-w-6xl justify-center px-5 pt-4 ${printable ? 'print:hidden' : ''}`}
>
<a
href="/apoio"
className="inline-flex min-h-11 max-w-full items-center justify-center rounded-lg bg-[#ed1c24] px-4 py-2 text-center text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[#c9141b] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#ed1c24]"
>
{t('Contactar Apoio ao Investidor')}
</a>
</aside>
<div className={printable ? 'print:hidden' : undefined}>
<TempoFooter />
</div>
}
<SearchFeedback />
{children}
</div>
</MenuPlacement.Provider>
</NavigationContextProvider>
<aside
aria-label={t('Contacto de apoio ao investidor')}
className={`mx-auto flex max-w-6xl justify-center px-5 pt-4 ${printable ? 'print:hidden' : ''}`}
>
<a
href="/apoio"
className="inline-flex min-h-11 max-w-full items-center justify-center rounded-lg bg-[#ed1c24] px-4 py-2 text-center text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[#c9141b] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#ed1c24]"
>
{t('Contactar Apoio ao Investidor')}
</a>
</aside>
<div className={printable ? 'print:hidden' : undefined}>
<TempoFooter />
</div>
</div>
</MenuPlacement.Provider>
</NavigationContextProvider>
);
}
-16
View File
@@ -482,22 +482,6 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
id: 'discovery',
label: 'Descoberta',
children: [
link(
'why',
'Porquê investir em Moçambique?',
'/porque-investir',
),
link(
'opportunities',
'Consultar oportunidades',
'/oportunidades',
),
link(
'simulator',
'Simulador Fiscal',
access.context?.simulator || '/simulador-fiscal',
),
link('sectors', 'Sectores de investimento', '/sectores'),
link('locations', 'Localizações', '/oportunidades?view=map'),
...(access.funding
? [
@@ -0,0 +1,158 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { compileFunction } from 'node:vm';
import { createRequire } from 'node:module';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import ts from 'typescript';
const require = createRequire(import.meta.url),
root = new URL('../../resources/js/', import.meta.url);
let page = {
component: 'portal/about',
url: '/sobre',
props: { auth: { user: null } },
};
const MenuPlacement = React.createContext(false);
const PortalMenu = ({ primary = false }) =>
React.useContext(MenuPlacement) && !primary
? null
: React.createElement(
'header',
{ 'data-portal-horizontal-menu': true },
'Dropdown common header',
);
const passthrough = ({ children }) => children;
const mocks = {
'@inertiajs/react': {
usePage: () => page,
Link: ({ href, children, ...props }) =>
React.createElement('a', { href, ...props }, children),
},
'@/components/portal-menu': { MenuPlacement, PortalMenu },
'@/components/search-feedback': { default: () => null },
'@/lib/search-territory': { searchTerritoryHref: (href) => href },
'@/components/tempo-brand': {
TempoUiProvider: passthrough,
useTempoUi: () => ({ t: (x) => x }),
},
'@/translation/page-translator': { PageTranslator: () => null },
'@/components/tempo-footer': { TempoFooter: () => null },
'@/components/navigation-context': {
NavigationContextProvider: passthrough,
NavigationContextSummary: () =>
React.createElement('div', { 'data-context-summary': true }),
},
'@/components/breadcrumbs': {
Breadcrumbs: ({ breadcrumbs }) =>
React.createElement(
'nav',
{ 'aria-label': 'Local breadcrumbs' },
breadcrumbs.map((b) => b.title).join('/'),
),
},
};
function load(name) {
const output = ts.transpileModule(
fs.readFileSync(new URL(name, root), 'utf8'),
{
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.ReactJSX,
},
},
).outputText;
const module = { exports: {} };
compileFunction(output, ['require', 'module', 'exports'])(
(name) => mocks[name] ?? require(name),
module,
module.exports,
);
return module.exports.default;
}
const PortalNavigation = load('layouts/portal-navigation.tsx'),
AppHeaderLayout = load('layouts/app/app-header-layout.tsx');
mocks['@/layouts/app/app-header-layout'] = { default: AppHeaderLayout };
const AppLayout = load('layouts/app-layout.tsx');
const app = fs.readFileSync(new URL('app.tsx', root), 'utf8'),
match = app.match(
/layout: (\(name\) => \{[\s\S]*?\n \}),\n strictMode/,
);
assert.ok(match);
const resolveLayout = compileFunction('return ' + match[1], [
'PortalNavigation',
'AuthLayout',
'AppLayout',
'SettingsLayout',
])(PortalNavigation, passthrough, AppLayout, passthrough);
const files = fs
.readdirSync(new URL('pages/', root), { recursive: true })
.filter((f) => f.endsWith('.tsx'));
void test('every registered page layout renders exactly one common horizontal header, including auth/settings/private screens', () => {
for (const file of files) {
const name = file.replaceAll(path.sep, '/').replace(/\.tsx$/, '');
page = {
component: name,
url: '/qa-layout-check',
props: { auth: { user: null } },
};
const layouts = [resolveLayout(name)].flat(),
child = React.createElement(
'section',
{ 'data-page-content': true },
React.createElement(PortalMenu),
name,
);
const tree = layouts.reduceRight(
(children, Layout) =>
React.createElement(
Layout,
{ breadcrumbs: [{ title: 'QA breadcrumb', href: '/qa' }] },
children,
),
child,
);
const html = renderToStaticMarkup(tree);
assert.equal(
(html.match(/data-portal-horizontal-menu/g) ?? []).length,
1,
name,
);
assert.match(html, /data-page-content/, name);
assert.match(html, /data-context-summary/, name);
assert.doesNotMatch(html, /sidebar-trigger|data-sidebar=/, name);
}
});
void test('private layout keeps page breadcrumbs without reintroducing a separate navigation or sidebar provider', () => {
const html = renderToStaticMarkup(
React.createElement(
AppLayout,
{ breadcrumbs: [{ title: 'Histórico', href: '/historico' }] },
'Existing private content',
),
);
assert.match(html, /Histórico/);
assert.match(html, /Existing private content/);
assert.doesNotMatch(html, /Dropdown common header/);
});
void test('main five shortcuts remain in the hero while discovery no longer repeats them', () => {
const hero = fs.readFileSync(
new URL('components/portal-hero.tsx', root),
'utf8',
);
for (const href of [
'/porque-investir',
'/investimentos/iniciar',
'/oportunidades',
'/simulador-fiscal',
'/sectores',
])
assert.ok(hero.includes('href="' + href + '"'), href);
});
console.log(
'Page layout coverage:',
files.length,
'existing TSX pages; structural SSR only, not browser/viewport proof.',
);
+11 -8
View File
@@ -97,12 +97,9 @@ void test('reconciled navigation selects discovery, realization and private supp
const groups = portalNavigation({ authenticated: false, manager: false });
assert.equal(
currentNavigationGroup(groups, '/oportunidades/fixture'),
'discovery',
);
assert.equal(
currentNavigationGroup(groups, '/simulador-fiscal'),
'discovery',
null,
);
assert.equal(currentNavigationGroup(groups, '/simulador-fiscal'), null);
assert.equal(
currentNavigationGroup(groups, '/balcao-virtual/dossiers/fixture'),
'support',
@@ -110,7 +107,7 @@ void test('reconciled navigation selects discovery, realization and private supp
assert.equal(currentNavigationGroup(groups, '/management/crm/leads'), null);
});
void test('discovery keeps four entry points and catalogue destinations without duplicates', () => {
void test('discovery retains distinct catalogues without repeating main entry points', () => {
const groups = portalNavigation({
authenticated: false,
manager: false,
@@ -125,10 +122,16 @@ void test('discovery keeps four entry points and catalogue destinations without
});
collect(groups);
assert.equal(new Set(links).size, links.length);
const discovery = groups.find((group) => group.id === 'discovery');
const retained = JSON.stringify(discovery);
for (const id of ['why', 'opportunities', 'simulator', 'sectors', 'start'])
assert.equal(
discovery.children.some((item) => item.id === id),
false,
);
assert.equal(retained.includes('/investimentos/iniciar'), false);
for (const href of [
'/investimentos/iniciar',
'/oportunidades',
'/sectores',
'/oportunidades?view=map',
'/carteira',
'/zonas',