organizacao da tela inicial
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { useState } from 'react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import {
|
||||
HOME_SECTION,
|
||||
HOME_SECTION_HEADING,
|
||||
PORTAL_MORE_LINK,
|
||||
} from '@/lib/portal-presentation';
|
||||
|
||||
const contexts = [
|
||||
{
|
||||
key: 'clusters',
|
||||
href: '/clusters',
|
||||
title: 'Clusters',
|
||||
description: 'Cadeias de valor e lacunas produtivas.',
|
||||
image: 'clusters-agroindustria',
|
||||
alt: 'Triagem de produtos agrícolas numa unidade de transformação com área de cultivo ao fundo.',
|
||||
},
|
||||
{
|
||||
key: 'corredores',
|
||||
href: '/corredores',
|
||||
title: 'Corredores',
|
||||
description: 'Territórios, logística e conexões.',
|
||||
image: 'corredores-logisticos-v2',
|
||||
alt: 'Terminal de mercadorias junto à água, com contentores, uma grua e vias de acesso.',
|
||||
},
|
||||
{
|
||||
key: 'projectos',
|
||||
href: '/projectos-ancora',
|
||||
title: 'Projectos âncora',
|
||||
description: 'Projectos estruturantes e relações.',
|
||||
image: 'projectos-ancora',
|
||||
alt: 'Equipa de engenharia acompanha o planeamento de uma instalação de energia solar.',
|
||||
},
|
||||
{
|
||||
key: 'educacao',
|
||||
href: '/educacao',
|
||||
title: 'Educação',
|
||||
description: 'Investimentos em competências e formação.',
|
||||
image: 'educacao-formacao',
|
||||
alt: 'Jovens desenvolvem competências de electrónica e informática numa aula de formação técnica.',
|
||||
},
|
||||
{
|
||||
key: 'documentos',
|
||||
href: '/documentos',
|
||||
title: 'Documentos e Formulários',
|
||||
description:
|
||||
'Legislação, procedimentos, formulários e materiais de apoio.',
|
||||
image: 'documentos-formularios',
|
||||
alt: 'Profissional analisa documentação empresarial organizada numa secretária de escritório.',
|
||||
},
|
||||
];
|
||||
|
||||
function ContextImage({
|
||||
image,
|
||||
alt,
|
||||
preview,
|
||||
}: {
|
||||
image: string;
|
||||
alt: string;
|
||||
preview: boolean;
|
||||
}) {
|
||||
const [fallback, setFallback] = useState(0);
|
||||
const base = `/images/investment-context/${image}`;
|
||||
|
||||
return (
|
||||
<div className="aspect-video w-full shrink-0 overflow-hidden bg-slate-100">
|
||||
{fallback < 2 ? (
|
||||
<img
|
||||
src={`${base}${fallback ? '-fallback' : ''}.webp`}
|
||||
srcSet={
|
||||
fallback
|
||||
? undefined
|
||||
: `${base}-480.webp 480w, ${base}.webp 960w`
|
||||
}
|
||||
sizes={
|
||||
preview
|
||||
? '(min-width: 1152px) 266px, (min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw'
|
||||
: '(min-width: 1152px) 357px, (min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw'
|
||||
}
|
||||
alt={alt}
|
||||
width={960}
|
||||
height={540}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover object-center"
|
||||
onError={() =>
|
||||
setFallback((current) => Math.min(current + 1, 2))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
role="img"
|
||||
aria-label={alt}
|
||||
className="flex h-full items-center justify-center px-5 text-center text-sm text-slate-600"
|
||||
>
|
||||
Imagem indisponível
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InvestmentContextGrid({
|
||||
preview = false,
|
||||
}: {
|
||||
preview?: boolean;
|
||||
}) {
|
||||
const visibleContexts = preview ? contexts.slice(0, 4) : contexts;
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Contextos de investimento"
|
||||
className={
|
||||
preview
|
||||
? 'grid gap-4 sm:grid-cols-2 lg:grid-cols-4'
|
||||
: 'grid gap-5 sm:grid-cols-2 lg:grid-cols-3'
|
||||
}
|
||||
>
|
||||
{visibleContexts.map((context) => (
|
||||
<a
|
||||
key={context.key}
|
||||
href={context.href}
|
||||
aria-labelledby={`context-${context.key}-title`}
|
||||
aria-describedby={`context-${context.key}-description`}
|
||||
className="flex h-full min-w-0 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-colors hover:border-blue-700 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-700"
|
||||
>
|
||||
<ContextImage
|
||||
image={context.image}
|
||||
alt={context.alt}
|
||||
preview={preview}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-1.5 p-4">
|
||||
<h3
|
||||
id={`context-${context.key}-title`}
|
||||
className="text-lg leading-6 font-bold text-slate-900"
|
||||
>
|
||||
{context.title}
|
||||
</h3>
|
||||
<p
|
||||
id={`context-${context.key}-description`}
|
||||
className="text-sm leading-5 text-slate-600"
|
||||
>
|
||||
{context.description}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InvestmentContextCards() {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="investment-context-heading"
|
||||
className={`${HOME_SECTION} space-y-4`}
|
||||
>
|
||||
<h2
|
||||
id="investment-context-heading"
|
||||
className={HOME_SECTION_HEADING}
|
||||
>
|
||||
Encontre o contexto do seu investimento
|
||||
</h2>
|
||||
<InvestmentContextGrid preview />
|
||||
<div className="flex justify-center">
|
||||
<a
|
||||
href="/contextos-de-investimento"
|
||||
className={PORTAL_MORE_LINK}
|
||||
>
|
||||
Ver mais <ArrowRight size={14} aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useState } from 'react';
|
||||
import { HOME_SECTION, HOME_SECTION_HEADING } from '@/lib/portal-presentation';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
key: 'explorar',
|
||||
title: '1. Explorar',
|
||||
description: 'Identifique o sector, território e oportunidade.',
|
||||
href: '/oportunidades',
|
||||
alt: 'Dois profissionais avaliam uma oportunidade de investimento no terreno com apoio de um tablet.',
|
||||
},
|
||||
{
|
||||
key: 'preparar',
|
||||
title: '2. Preparar',
|
||||
description: 'Conheça o percurso e organize os documentos.',
|
||||
href: '/como-investir',
|
||||
alt: 'Empreendedora e consultor analisam documentos e organizam pastas numa reunião de preparação.',
|
||||
},
|
||||
{
|
||||
key: 'acompanhar',
|
||||
title: '3. Acompanhar',
|
||||
description: 'Envie o pedido e acompanhe o protocolo.',
|
||||
href: '/balcao-virtual',
|
||||
alt: 'Empreendedor consulta um pedido no computador portátil numa secretária de escritório.',
|
||||
},
|
||||
];
|
||||
|
||||
function StepImage({ name, alt }: { name: string; alt: string }) {
|
||||
const [fallback, setFallback] = useState(0);
|
||||
const base = `/images/investment-steps/${name}`;
|
||||
|
||||
return (
|
||||
<div className="aspect-video w-full shrink-0 overflow-hidden bg-slate-100">
|
||||
{fallback < 2 ? (
|
||||
<img
|
||||
src={`${base}${fallback ? '-fallback' : ''}.webp`}
|
||||
srcSet={
|
||||
fallback
|
||||
? undefined
|
||||
: `${base}-480.webp 480w, ${base}.webp 960w`
|
||||
}
|
||||
sizes="(min-width: 1152px) 357px, (min-width: 768px) 33vw, 100vw"
|
||||
alt={alt}
|
||||
width={960}
|
||||
height={540}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover object-center"
|
||||
onError={() =>
|
||||
setFallback((current) => Math.min(current + 1, 2))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
role="img"
|
||||
aria-label={alt}
|
||||
className="flex h-full items-center justify-center px-4 text-center text-sm text-slate-600"
|
||||
>
|
||||
Imagem indisponível
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InvestmentSteps() {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="investment-steps-heading"
|
||||
className={`${HOME_SECTION} space-y-4`}
|
||||
>
|
||||
<h2 id="investment-steps-heading" className={HOME_SECTION_HEADING}>
|
||||
Como Investir
|
||||
</h2>
|
||||
<div className="grid gap-5 md:grid-cols-3">
|
||||
{steps.map((step) => (
|
||||
<a
|
||||
key={step.key}
|
||||
href={step.href}
|
||||
aria-labelledby={`step-${step.key}-title`}
|
||||
aria-describedby={`step-${step.key}-description`}
|
||||
className="flex h-full min-w-0 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-colors hover:border-[#ed1c24] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#ed1c24]"
|
||||
>
|
||||
<StepImage name={step.key} alt={step.alt} />
|
||||
<div className="space-y-1.5 p-4">
|
||||
<h3
|
||||
id={`step-${step.key}-title`}
|
||||
className="text-lg leading-6 font-bold text-slate-900"
|
||||
>
|
||||
{step.title}
|
||||
</h3>
|
||||
<p
|
||||
id={`step-${step.key}-description`}
|
||||
className="text-sm leading-5 text-slate-600"
|
||||
>
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
HOME_SECTION,
|
||||
HOME_SECTION_HEADING,
|
||||
PORTAL_MORE_LINK,
|
||||
} from '@/lib/portal-presentation';
|
||||
|
||||
export type PublicSector = {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
opportunitiesCount: number;
|
||||
url?: string;
|
||||
specializations?: { id: number; name: string; url: string }[];
|
||||
};
|
||||
|
||||
const images: Record<string, string> = {
|
||||
agronegocio: 'agronegocio',
|
||||
turismo: 'turismo',
|
||||
infraestruturas: 'infraestruturas',
|
||||
'economia-azul': 'economia-azul',
|
||||
'mercado-financeiro': 'servicos-financeiros',
|
||||
'servicos-financeiros': 'servicos-financeiros',
|
||||
fintech: 'tic-economia-digital',
|
||||
mineracao: 'mineracao',
|
||||
energia: 'energia',
|
||||
'energia-especifica': 'energia',
|
||||
imobiliario: 'outros-sectores',
|
||||
'bens-de-consumo-e-franquias': 'industria',
|
||||
'bens-de-consumo-franquias': 'industria',
|
||||
'tecnologias-de-informacao': 'tic-economia-digital',
|
||||
'tic-economia-digital': 'tic-economia-digital',
|
||||
industria: 'industria',
|
||||
'logistica-transportes': 'logistica-transportes',
|
||||
};
|
||||
|
||||
const imageDescriptions: Record<string, string> = {
|
||||
agronegocio: 'Campos agrícolas e instalações de transformação de produtos.',
|
||||
turismo: 'Alojamento turístico junto à costa e à vegetação tropical.',
|
||||
infraestruturas: 'Ponte rodoviária sobre um rio, ligando áreas urbanas.',
|
||||
'economia-azul': 'Actividades de pesca e aquacultura numa zona costeira.',
|
||||
'servicos-financeiros':
|
||||
'Edifícios empresariais e serviços num espaço urbano.',
|
||||
mineracao:
|
||||
'Exploração mineira a céu aberto com instalações de processamento.',
|
||||
energia:
|
||||
'Painéis solares e infra-estruturas de transporte de electricidade.',
|
||||
industria:
|
||||
'Unidade industrial com área de armazenagem e movimentação de mercadorias.',
|
||||
'tic-economia-digital':
|
||||
'Infra-estruturas de telecomunicações num ambiente urbano.',
|
||||
'logistica-transportes':
|
||||
'Terminal portuário com contentores e transporte ferroviário.',
|
||||
'outros-sectores': 'Espaço empresarial e edifícios urbanos ao fundo.',
|
||||
};
|
||||
|
||||
function SectorImage({ slug, preview }: { slug: string; preview: boolean }) {
|
||||
const [failed, setFailed] = useState(0);
|
||||
const image = failed
|
||||
? 'outros-sectores'
|
||||
: (images[slug] ?? 'outros-sectores');
|
||||
const base = `/images/sectors/${image}`;
|
||||
if (failed > 1) return <span className="absolute inset-0 bg-slate-800" />;
|
||||
return (
|
||||
<img
|
||||
src={`${base}.webp`}
|
||||
srcSet={`${base}-480.webp 480w, ${base}.webp 960w`}
|
||||
sizes={
|
||||
preview
|
||||
? '(min-width: 1152px) 266px, (min-width: 1024px) 25vw, 50vw'
|
||||
: '(min-width: 1152px) 357px, (min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw'
|
||||
}
|
||||
width={960}
|
||||
height={540}
|
||||
alt={imageDescriptions[image]}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-105"
|
||||
onError={() => setFailed((current) => Math.min(current + 1, 2))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function PublicSectorCards({
|
||||
sectors,
|
||||
preview = false,
|
||||
}: {
|
||||
sectors: PublicSector[];
|
||||
preview?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
preview
|
||||
? 'grid grid-cols-2 gap-4 lg:grid-cols-4'
|
||||
: 'grid gap-5 sm:grid-cols-2 lg:grid-cols-3'
|
||||
}
|
||||
>
|
||||
{sectors.map((sector) => {
|
||||
const href =
|
||||
sector.url ??
|
||||
`/sectores/${sector.slug.split('/').map(encodeURIComponent).join('/')}`;
|
||||
const titleId = `public-sector-${sector.slug.replaceAll('/', '-')}`;
|
||||
return (
|
||||
<article
|
||||
key={sector.slug}
|
||||
className="group flex min-w-0 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm transition-colors hover:border-[#ed1c24]"
|
||||
>
|
||||
<a
|
||||
href={href}
|
||||
aria-labelledby={titleId}
|
||||
className="relative block h-40 shrink-0 overflow-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#ed1c24] sm:h-44"
|
||||
>
|
||||
<SectorImage slug={sector.slug} preview={preview} />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 bg-linear-to-t from-black/85 via-black/20 to-transparent"
|
||||
/>
|
||||
<h3
|
||||
id={titleId}
|
||||
className="absolute right-3 bottom-3 left-3 text-sm leading-5 font-bold wrap-break-word text-white drop-shadow-lg min-[375px]:text-base sm:text-lg sm:leading-6"
|
||||
>
|
||||
{sector.name}
|
||||
</h3>
|
||||
</a>
|
||||
<div className="flex flex-1 flex-col gap-2 p-3 sm:p-4">
|
||||
{sector.description && (
|
||||
<p className="line-clamp-3 text-sm leading-5 wrap-break-word text-slate-600">
|
||||
{sector.description}
|
||||
</p>
|
||||
)}
|
||||
{sector.specializations?.map((child) => (
|
||||
<a
|
||||
key={child.id}
|
||||
href={child.url}
|
||||
className="text-xs font-semibold text-slate-700 underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-[#ed1c24]"
|
||||
>
|
||||
Especialização:{' '}
|
||||
{child.name.replace(
|
||||
'Mercado Financeiro — ',
|
||||
'',
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
<div className="mt-auto pt-1">
|
||||
<p className="text-xs leading-5 text-slate-500">
|
||||
{sector.opportunitiesCount} oportunidade
|
||||
{sector.opportunitiesCount === 1 ? '' : 's'}
|
||||
</p>
|
||||
<a
|
||||
href={href}
|
||||
aria-label={`Ler mais sobre ${sector.name}`}
|
||||
className="inline-flex min-h-11 items-center gap-1.5 text-xs font-bold text-[#ed1c24] hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ed1c24]"
|
||||
>
|
||||
Ler mais{' '}
|
||||
<ArrowRight size={14} aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomepageSectors({ sectors }: { sectors: PublicSector[] }) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="home-sectors-heading"
|
||||
className={HOME_SECTION}
|
||||
>
|
||||
<h2 id="home-sectors-heading" className={HOME_SECTION_HEADING}>
|
||||
Sectores de investimento
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl text-slate-600">
|
||||
Explore as áreas prioritárias do programa de investimento
|
||||
nacional.
|
||||
</p>
|
||||
<div className="mt-5">
|
||||
<PublicSectorCards sectors={sectors.slice(0, 4)} preview />
|
||||
{!sectors.length && (
|
||||
<p className="text-slate-600">
|
||||
Não existem sectores publicados.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5 flex justify-center">
|
||||
<a href="/sectores" className={PORTAL_MORE_LINK}>
|
||||
Ver mais sectores{' '}
|
||||
<ArrowRight size={14} aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user