928 lines
30 KiB
React
928 lines
30 KiB
React
import { useMemo, useState } from "react";
|
||
import {
|
||
Search,
|
||
Leaf,
|
||
Zap,
|
||
Hotel,
|
||
Truck,
|
||
Waves,
|
||
Banknote,
|
||
ArrowRight,
|
||
ArrowLeft,
|
||
MapPin,
|
||
CheckCircle2,
|
||
X,
|
||
} from "lucide-react";
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Dados de exemplo (substituir depois por chamadas à API Laravel) */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
const SECTORS = [
|
||
{
|
||
slug: "agronegocio",
|
||
name: "Agronegócio",
|
||
icon: Leaf,
|
||
description:
|
||
"Produção, agroprocessamento, irrigação e cadeias de valor agrícolas.",
|
||
},
|
||
{
|
||
slug: "energia",
|
||
name: "Energia e Mineração",
|
||
icon: Zap,
|
||
description:
|
||
"Energias renováveis, transição energética e mineração responsável.",
|
||
},
|
||
{
|
||
slug: "turismo",
|
||
name: "Turismo",
|
||
icon: Hotel,
|
||
description: "Hotelaria, turismo costeiro, cultural e ecológico.",
|
||
},
|
||
{
|
||
slug: "infraestruturas",
|
||
name: "Infraestruturas",
|
||
icon: Truck,
|
||
description: "Logística, transportes, parques industriais e energia.",
|
||
},
|
||
{
|
||
slug: "economia-azul",
|
||
name: "Economia Azul",
|
||
icon: Waves,
|
||
description: "Pesca, aquacultura, portos e logística marítima.",
|
||
},
|
||
{
|
||
slug: "mercado-financeiro",
|
||
name: "Mercado Financeiro",
|
||
icon: Banknote,
|
||
description: "Fintech, capital de risco e instrumentos financeiros.",
|
||
},
|
||
];
|
||
|
||
const PROVINCES = [
|
||
"Maputo Cidade",
|
||
"Maputo Província",
|
||
"Gaza",
|
||
"Inhambane",
|
||
"Sofala",
|
||
"Manica",
|
||
"Tete",
|
||
"Zambézia",
|
||
"Nampula",
|
||
"Cabo Delgado",
|
||
"Niassa",
|
||
];
|
||
|
||
const MATURITY_LEVELS = [
|
||
"Ideia identificada",
|
||
"Pré-estudo disponível",
|
||
"Estudo de viabilidade em curso",
|
||
"Projecto estruturado",
|
||
"Pronto para investimento",
|
||
"Em negociação ou implementação",
|
||
];
|
||
|
||
const OPPORTUNITIES = [
|
||
{
|
||
reference: "INV-MZ-AGR-001",
|
||
title: "Unidade de processamento agrícola em Nampula",
|
||
sector: "agronegocio",
|
||
province: "Nampula",
|
||
maturity: 3,
|
||
amount: 2500000,
|
||
financing: "Capital e parceria técnica",
|
||
impact: "120 empregos directos e substituição de importações.",
|
||
summary:
|
||
"Instalação de uma unidade de processamento para agregar valor à produção agrícola local, reduzir perdas pós-colheita e melhorar o acesso a mercados.",
|
||
featured: true,
|
||
},
|
||
{
|
||
reference: "INV-MZ-ENE-002",
|
||
title: "Mini-rede solar para polo produtivo",
|
||
sector: "energia",
|
||
province: "Zambézia",
|
||
maturity: 2,
|
||
amount: 1200000,
|
||
financing: "PPP ou financiamento misto",
|
||
impact: "Acesso a energia para 40 unidades produtivas.",
|
||
summary:
|
||
"Solução de energia renovável para reforçar o fornecimento eléctrico a uma zona produtiva com potencial agrícola e comercial.",
|
||
featured: true,
|
||
},
|
||
{
|
||
reference: "INV-MZ-TUR-003",
|
||
title: "Eco-lodge costeiro com integração comunitária",
|
||
sector: "turismo",
|
||
province: "Inhambane",
|
||
maturity: 1,
|
||
amount: 1800000,
|
||
financing: "Capital privado ou joint venture",
|
||
impact: "Emprego local e conservação da faixa costeira.",
|
||
summary:
|
||
"Desenvolvimento de uma unidade turística com participação comunitária, focada em sustentabilidade e experiências autênticas.",
|
||
featured: true,
|
||
},
|
||
{
|
||
reference: "INV-MZ-INF-004",
|
||
title: "Centro logístico provincial",
|
||
sector: "infraestruturas",
|
||
province: "Sofala",
|
||
maturity: 3,
|
||
amount: 8500000,
|
||
financing: "Concessão ou PPP",
|
||
impact: "Redução de custos logísticos regionais.",
|
||
summary:
|
||
"Infraestrutura de armazenamento e distribuição para apoiar a comercialização de produtos regionais e reduzir perdas na cadeia.",
|
||
featured: false,
|
||
},
|
||
{
|
||
reference: "INV-MZ-BLUE-005",
|
||
title: "Processamento de pescado e cadeia fria",
|
||
sector: "economia-azul",
|
||
province: "Zambézia",
|
||
maturity: 1,
|
||
amount: 900000,
|
||
financing: "Capital, crédito ou parceria operacional",
|
||
impact: "Redução de perdas pós-captura na pesca artesanal.",
|
||
summary:
|
||
"Reforço da capacidade de conservação, processamento e distribuição de pescado para pequenos operadores locais.",
|
||
featured: false,
|
||
},
|
||
{
|
||
reference: "INV-MZ-FIN-006",
|
||
title: "Plataforma de crédito para PME agrícolas",
|
||
sector: "mercado-financeiro",
|
||
province: "Maputo Cidade",
|
||
maturity: 2,
|
||
amount: 3000000,
|
||
financing: "Fundo de investimento ou parceiro financeiro",
|
||
impact: "Acesso a financiamento para 300+ pequenas empresas.",
|
||
summary:
|
||
"Estrutura de financiamento dedicada a pequenas e médias empresas do sector agrícola com dificuldade de acesso a crédito bancário.",
|
||
featured: false,
|
||
},
|
||
];
|
||
|
||
const money = (amount) =>
|
||
new Intl.NumberFormat("pt-MZ", {
|
||
style: "currency",
|
||
currency: "USD",
|
||
maximumFractionDigits: 0,
|
||
}).format(amount);
|
||
|
||
const sectorOf = (slug) => SECTORS.find((s) => s.slug === slug);
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Componentes */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
function TopBar({ onNavigate, requestCount }) {
|
||
return (
|
||
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/95 backdrop-blur">
|
||
<div className="mx-auto flex max-w-6xl items-center justify-between px-5 py-4">
|
||
<button
|
||
onClick={() => onNavigate("home")}
|
||
className="flex items-baseline gap-1 font-black tracking-tight text-slate-900"
|
||
>
|
||
<span className="text-lg">INVESTE</span>
|
||
<span className="text-lg text-emerald-700">Moçambique</span>
|
||
</button>
|
||
|
||
<nav className="hidden gap-8 text-sm font-semibold text-slate-600 md:flex">
|
||
<button
|
||
onClick={() => onNavigate("home")}
|
||
className="hover:text-slate-900"
|
||
>
|
||
Início
|
||
</button>
|
||
<button
|
||
onClick={() => onNavigate("opportunities")}
|
||
className="hover:text-slate-900"
|
||
>
|
||
Oportunidades
|
||
</button>
|
||
<button
|
||
onClick={() => onNavigate("interest")}
|
||
className="hover:text-slate-900"
|
||
>
|
||
Manifestar interesse
|
||
</button>
|
||
</nav>
|
||
|
||
<div className="flex items-center gap-3 text-xs text-slate-500">
|
||
<span className="hidden sm:inline">
|
||
{requestCount} pedido{requestCount === 1 ? "" : "s"} recebido
|
||
{requestCount === 1 ? "" : "s"}
|
||
</span>
|
||
<button
|
||
onClick={() => onNavigate("interest")}
|
||
className="rounded-lg bg-slate-900 px-4 py-2 text-sm font-bold text-white hover:bg-slate-700"
|
||
>
|
||
Manifestar interesse
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
);
|
||
}
|
||
|
||
function Hero({ query, setQuery, onSearch, onNavigate }) {
|
||
return (
|
||
<section className="border-b border-slate-200 bg-[#0B2038] text-white">
|
||
<div className="mx-auto grid max-w-6xl gap-10 px-5 py-16 md:grid-cols-[1.2fr,1fr] md:py-24">
|
||
<div>
|
||
<p className="text-xs font-bold uppercase tracking-[0.2em] text-emerald-300">
|
||
Portal do Investidor
|
||
</p>
|
||
<h1 className="mt-4 max-w-xl text-4xl font-black leading-[1.05] tracking-tight md:text-5xl">
|
||
Oportunidades reais de investimento em Moçambique,
|
||
<span className="text-amber-400"> organizadas por sector.</span>
|
||
</h1>
|
||
<p className="mt-5 max-w-lg text-slate-300">
|
||
Consulte projectos por província e estágio de maturidade,
|
||
compreenda os requisitos legais e fiscais, e manifeste
|
||
interesse directamente junto da entidade promotora.
|
||
</p>
|
||
|
||
<div className="mt-8 flex flex-wrap gap-3">
|
||
<button
|
||
onClick={() => onNavigate("opportunities")}
|
||
className="inline-flex items-center gap-2 rounded-lg bg-amber-400 px-5 py-3 font-bold text-[#0B2038] hover:bg-amber-300"
|
||
>
|
||
Explorar oportunidades <ArrowRight size={16} />
|
||
</button>
|
||
<button
|
||
onClick={() => onNavigate("interest")}
|
||
className="rounded-lg border border-white/30 px-5 py-3 font-semibold text-white hover:bg-white/10"
|
||
>
|
||
Manifestar interesse
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<form
|
||
onSubmit={onSearch}
|
||
className="h-fit rounded-2xl border border-white/10 bg-white/10 p-5"
|
||
>
|
||
<label className="mb-2 block text-sm font-semibold text-slate-200">
|
||
Pesquisa rápida
|
||
</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
placeholder="Ex.: agronegócio, Nampula, energia..."
|
||
className="w-full rounded-lg px-3 py-2.5 text-sm text-slate-900 outline-none ring-2 ring-transparent focus:ring-amber-400"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
className="rounded-lg bg-amber-400 px-4 text-[#0B2038]"
|
||
>
|
||
<Search size={18} />
|
||
</button>
|
||
</div>
|
||
<div className="mt-4 flex flex-wrap gap-3 border-t border-white/10 pt-4 text-center text-xs text-slate-300">
|
||
<div>
|
||
<p className="text-xl font-black text-white">
|
||
{OPPORTUNITIES.length}
|
||
</p>
|
||
oportunidades
|
||
</div>
|
||
<div className="ml-auto">
|
||
<p className="text-xl font-black text-white">
|
||
{SECTORS.length}
|
||
</p>
|
||
sectores
|
||
</div>
|
||
<div className="ml-auto">
|
||
<p className="text-xl font-black text-white">
|
||
{PROVINCES.length}
|
||
</p>
|
||
províncias
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function SectorGrid({ onSelectSector }) {
|
||
return (
|
||
<section className="mx-auto max-w-6xl px-5 py-16">
|
||
<h2 className="text-2xl font-black text-slate-900">
|
||
Sectores estratégicos
|
||
</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-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||
{SECTORS.map((sector) => {
|
||
const Icon = sector.icon;
|
||
const count = OPPORTUNITIES.filter(
|
||
(o) => o.sector === sector.slug
|
||
).length;
|
||
|
||
return (
|
||
<button
|
||
key={sector.slug}
|
||
onClick={() => onSelectSector(sector.slug)}
|
||
className="group rounded-2xl border border-slate-200 bg-white p-6 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-emerald-300 hover:shadow-md"
|
||
>
|
||
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-50 text-emerald-700">
|
||
<Icon size={22} />
|
||
</div>
|
||
<h3 className="mt-4 font-bold text-slate-900">
|
||
{sector.name}
|
||
</h3>
|
||
<p className="mt-2 text-sm leading-relaxed text-slate-600">
|
||
{sector.description}
|
||
</p>
|
||
<p className="mt-4 text-sm font-semibold text-emerald-700 group-hover:underline">
|
||
{count} oportunidade{count === 1 ? "" : "s"} →
|
||
</p>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function OpportunityCard({ opportunity, onView, onInterest }) {
|
||
const sector = sectorOf(opportunity.sector);
|
||
|
||
return (
|
||
<article className="flex h-full flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
||
<div className="flex flex-wrap gap-2">
|
||
{opportunity.featured && (
|
||
<span className="rounded-full bg-amber-100 px-3 py-1 text-xs font-bold text-amber-800">
|
||
Destaque
|
||
</span>
|
||
)}
|
||
<span className="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">
|
||
{sector.name}
|
||
</span>
|
||
</div>
|
||
|
||
<p className="mt-3 text-xs font-semibold uppercase tracking-wide text-slate-400">
|
||
{opportunity.reference}
|
||
</p>
|
||
<h3 className="mt-1 text-lg font-bold text-slate-900">
|
||
{opportunity.title}
|
||
</h3>
|
||
<p className="mt-2 flex items-center gap-1 text-sm text-slate-500">
|
||
<MapPin size={14} /> {opportunity.province}
|
||
</p>
|
||
|
||
<dl className="mt-4 space-y-2 border-t border-slate-100 pt-4 text-sm">
|
||
<div className="flex justify-between">
|
||
<dt className="text-slate-500">Investimento</dt>
|
||
<dd className="font-semibold text-slate-800">
|
||
{money(opportunity.amount)}
|
||
</dd>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<dt className="text-slate-500">Maturidade</dt>
|
||
<dd className="font-semibold text-slate-800">
|
||
{MATURITY_LEVELS[opportunity.maturity]}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<div className="mt-6 flex gap-2">
|
||
<button
|
||
onClick={() => onView(opportunity)}
|
||
className="flex-1 rounded-lg bg-slate-900 py-2.5 text-sm font-bold text-white hover:bg-slate-700"
|
||
>
|
||
Ver ficha
|
||
</button>
|
||
<button
|
||
onClick={() => onInterest(opportunity)}
|
||
className="flex-1 rounded-lg border border-slate-300 py-2.5 text-sm font-bold text-slate-700 hover:bg-slate-50"
|
||
>
|
||
Manifestar interesse
|
||
</button>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
function OpportunitiesView({ filters, setFilters, onView, onInterest }) {
|
||
const results = useMemo(() => {
|
||
return OPPORTUNITIES.filter((o) => {
|
||
const matchesQuery = filters.query
|
||
? (o.title + o.province + sectorOf(o.sector).name)
|
||
.toLowerCase()
|
||
.includes(filters.query.toLowerCase())
|
||
: true;
|
||
const matchesSector = filters.sector ? o.sector === filters.sector : true;
|
||
const matchesProvince = filters.province
|
||
? o.province === filters.province
|
||
: true;
|
||
const matchesMaturity =
|
||
filters.maturity !== "" ? o.maturity === Number(filters.maturity) : true;
|
||
|
||
return matchesQuery && matchesSector && matchesProvince && matchesMaturity;
|
||
});
|
||
}, [filters]);
|
||
|
||
return (
|
||
<section className="mx-auto max-w-6xl px-5 py-12">
|
||
<h1 className="text-3xl font-black text-slate-900">
|
||
Oportunidades de investimento
|
||
</h1>
|
||
<p className="mt-2 text-slate-600">
|
||
{results.length} oportunidade{results.length === 1 ? "" : "s"}{" "}
|
||
encontrada{results.length === 1 ? "" : "s"}.
|
||
</p>
|
||
|
||
<div className="mt-6 grid gap-3 rounded-2xl border border-slate-200 bg-white p-4 sm:grid-cols-2 lg:grid-cols-4">
|
||
<input
|
||
value={filters.query}
|
||
onChange={(e) => setFilters({ ...filters, query: e.target.value })}
|
||
placeholder="Pesquisar..."
|
||
className="rounded-lg border border-slate-200 px-3 py-2 text-sm"
|
||
/>
|
||
<select
|
||
value={filters.sector}
|
||
onChange={(e) => setFilters({ ...filters, sector: e.target.value })}
|
||
className="rounded-lg border border-slate-200 px-3 py-2 text-sm"
|
||
>
|
||
<option value="">Todos os sectores</option>
|
||
{SECTORS.map((s) => (
|
||
<option key={s.slug} value={s.slug}>
|
||
{s.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<select
|
||
value={filters.province}
|
||
onChange={(e) =>
|
||
setFilters({ ...filters, province: e.target.value })
|
||
}
|
||
className="rounded-lg border border-slate-200 px-3 py-2 text-sm"
|
||
>
|
||
<option value="">Todas as províncias</option>
|
||
{PROVINCES.map((p) => (
|
||
<option key={p} value={p}>
|
||
{p}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<select
|
||
value={filters.maturity}
|
||
onChange={(e) =>
|
||
setFilters({ ...filters, maturity: e.target.value })
|
||
}
|
||
className="rounded-lg border border-slate-200 px-3 py-2 text-sm"
|
||
>
|
||
<option value="">Toda a maturidade</option>
|
||
{MATURITY_LEVELS.map((label, index) => (
|
||
<option key={label} value={index}>
|
||
{label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{results.length === 0 ? (
|
||
<div className="mt-10 rounded-2xl border border-dashed border-slate-300 bg-slate-50 p-10 text-center text-slate-600">
|
||
Nenhuma oportunidade corresponde aos filtros seleccionados.
|
||
</div>
|
||
) : (
|
||
<div className="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||
{results.map((o) => (
|
||
<OpportunityCard
|
||
key={o.reference}
|
||
opportunity={o}
|
||
onView={onView}
|
||
onInterest={onInterest}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function OpportunityDetail({ opportunity, onBack, onInterest }) {
|
||
const sector = sectorOf(opportunity.sector);
|
||
|
||
return (
|
||
<section className="mx-auto max-w-3xl px-5 py-12">
|
||
<button
|
||
onClick={onBack}
|
||
className="mb-6 inline-flex items-center gap-2 text-sm font-semibold text-slate-600 hover:text-slate-900"
|
||
>
|
||
<ArrowLeft size={16} /> Voltar às oportunidades
|
||
</button>
|
||
|
||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
||
{opportunity.reference}
|
||
</p>
|
||
<h1 className="mt-1 text-3xl font-black text-slate-900">
|
||
{opportunity.title}
|
||
</h1>
|
||
<p className="mt-2 flex items-center gap-2 text-sm text-slate-500">
|
||
<MapPin size={14} /> {opportunity.province} · {sector.name}
|
||
</p>
|
||
|
||
<p className="mt-6 leading-relaxed text-slate-700">
|
||
{opportunity.summary}
|
||
</p>
|
||
|
||
<dl className="mt-8 grid gap-4 rounded-2xl border border-slate-200 bg-white p-6 sm:grid-cols-2">
|
||
<div>
|
||
<dt className="text-xs uppercase text-slate-400">Investimento estimado</dt>
|
||
<dd className="mt-1 font-bold text-slate-900">
|
||
{money(opportunity.amount)}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt className="text-xs uppercase text-slate-400">Maturidade</dt>
|
||
<dd className="mt-1 font-bold text-slate-900">
|
||
{MATURITY_LEVELS[opportunity.maturity]}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt className="text-xs uppercase text-slate-400">Financiamento pretendido</dt>
|
||
<dd className="mt-1 font-bold text-slate-900">
|
||
{opportunity.financing}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt className="text-xs uppercase text-slate-400">Impacto esperado</dt>
|
||
<dd className="mt-1 font-bold text-slate-900">{opportunity.impact}</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<div className="mt-6 rounded-xl bg-amber-50 p-4 text-sm text-amber-900">
|
||
A informação legal, fiscal e financeira apresentada é orientadora e
|
||
deve ser confirmada junto das entidades competentes antes da tomada
|
||
de decisão.
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => onInterest(opportunity)}
|
||
className="mt-8 rounded-lg bg-slate-900 px-6 py-3 font-bold text-white hover:bg-slate-700"
|
||
>
|
||
Manifestar interesse nesta oportunidade
|
||
</button>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function InterestForm({ preselected, onSubmitted }) {
|
||
const [form, setForm] = useState({
|
||
name: "",
|
||
email: "",
|
||
phone: "",
|
||
investorType: "",
|
||
country: "Moçambique",
|
||
sectorInterest: preselected ? sectorOf(preselected.sector).name : "",
|
||
opportunityReference: preselected ? preselected.reference : "",
|
||
investmentRange: "",
|
||
message: "",
|
||
privacyConsent: false,
|
||
});
|
||
const [errors, setErrors] = useState({});
|
||
const [result, setResult] = useState(null);
|
||
|
||
function update(field, value) {
|
||
setForm((prev) => ({ ...prev, [field]: value }));
|
||
}
|
||
|
||
function validate() {
|
||
const next = {};
|
||
if (form.name.trim().length < 3) next.name = "Indique o nome completo.";
|
||
if (!/^\S+@\S+\.\S+$/.test(form.email)) next.email = "Indique um email válido.";
|
||
if (!form.investorType) next.investorType = "Seleccione o tipo de investidor.";
|
||
if (!form.sectorInterest.trim())
|
||
next.sectorInterest = "Indique o sector de interesse.";
|
||
if (form.message.trim().length < 20)
|
||
next.message = "A mensagem deve ter pelo menos 20 caracteres.";
|
||
if (!form.privacyConsent)
|
||
next.privacyConsent = "É necessário aceitar a política de privacidade.";
|
||
setErrors(next);
|
||
return Object.keys(next).length === 0;
|
||
}
|
||
|
||
function handleSubmit(event) {
|
||
event.preventDefault();
|
||
if (!validate()) return;
|
||
|
||
const reference = `IR-${new Date().getFullYear()}-${String(
|
||
Math.floor(Math.random() * 900000) + 100000
|
||
)}`;
|
||
|
||
setResult({ reference });
|
||
onSubmitted?.();
|
||
}
|
||
|
||
if (result) {
|
||
return (
|
||
<section className="mx-auto max-w-xl px-5 py-16 text-center">
|
||
<CheckCircle2 className="mx-auto text-emerald-600" size={48} />
|
||
<h1 className="mt-4 text-2xl font-black text-slate-900">
|
||
Manifestação de interesse registada
|
||
</h1>
|
||
<p className="mt-2 text-slate-600">
|
||
A sua referência é <strong>{result.reference}</strong>. A equipa
|
||
do portal irá analisar o pedido e poderá solicitar informação
|
||
adicional.
|
||
</p>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<section className="mx-auto max-w-xl px-5 py-12">
|
||
<h1 className="text-3xl font-black text-slate-900">
|
||
Manifestar interesse
|
||
</h1>
|
||
<p className="mt-2 text-slate-600">
|
||
Preencha os dados abaixo para que a equipa do portal possa analisar
|
||
o seu interesse e entrar em contacto.
|
||
</p>
|
||
|
||
{preselected && (
|
||
<div className="mt-5 rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900">
|
||
Oportunidade seleccionada: <strong>{preselected.title}</strong> (
|
||
{preselected.reference})
|
||
</div>
|
||
)}
|
||
|
||
<form onSubmit={handleSubmit} className="mt-6 space-y-5">
|
||
<Field label="Nome completo" error={errors.name} required>
|
||
<input
|
||
className="input"
|
||
value={form.name}
|
||
onChange={(e) => update("name", e.target.value)}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Email" error={errors.email} required>
|
||
<input
|
||
type="email"
|
||
className="input"
|
||
value={form.email}
|
||
onChange={(e) => update("email", e.target.value)}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Telefone" error={errors.phone}>
|
||
<input
|
||
className="input"
|
||
value={form.phone}
|
||
onChange={(e) => update("phone", e.target.value)}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Tipo de investidor" error={errors.investorType} required>
|
||
<select
|
||
className="input"
|
||
value={form.investorType}
|
||
onChange={(e) => update("investorType", e.target.value)}
|
||
>
|
||
<option value="">Seleccione</option>
|
||
<option value="individual">Investidor individual</option>
|
||
<option value="empresa">Empresa</option>
|
||
<option value="fundo">Fundo de investimento</option>
|
||
<option value="entidade_publica">Entidade pública</option>
|
||
<option value="outro">Outro</option>
|
||
</select>
|
||
</Field>
|
||
|
||
<Field label="País" error={errors.country}>
|
||
<input
|
||
className="input"
|
||
value={form.country}
|
||
onChange={(e) => update("country", e.target.value)}
|
||
/>
|
||
</Field>
|
||
|
||
<Field
|
||
label="Sector de interesse"
|
||
error={errors.sectorInterest}
|
||
required
|
||
>
|
||
<select
|
||
className="input"
|
||
value={form.sectorInterest}
|
||
onChange={(e) => update("sectorInterest", e.target.value)}
|
||
>
|
||
<option value="">Seleccione</option>
|
||
{SECTORS.map((s) => (
|
||
<option key={s.slug} value={s.name}>
|
||
{s.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
|
||
<Field label="Faixa de investimento" error={errors.investmentRange}>
|
||
<select
|
||
className="input"
|
||
value={form.investmentRange}
|
||
onChange={(e) => update("investmentRange", e.target.value)}
|
||
>
|
||
<option value="">Seleccione</option>
|
||
<option value="ate-100000">Até USD 100.000</option>
|
||
<option value="100000-500000">USD 100.000 – 500.000</option>
|
||
<option value="500000-1000000">USD 500.000 – 1.000.000</option>
|
||
<option value="acima-1000000">Acima de USD 1.000.000</option>
|
||
</select>
|
||
</Field>
|
||
|
||
<Field label="Mensagem" error={errors.message} required>
|
||
<textarea
|
||
rows={4}
|
||
className="input"
|
||
value={form.message}
|
||
onChange={(e) => update("message", e.target.value)}
|
||
placeholder="Descreva o seu interesse ou a informação pretendida."
|
||
/>
|
||
</Field>
|
||
|
||
<label className="flex items-start gap-3 text-sm text-slate-600">
|
||
<input
|
||
type="checkbox"
|
||
checked={form.privacyConsent}
|
||
onChange={(e) => update("privacyConsent", e.target.checked)}
|
||
className="mt-1"
|
||
/>
|
||
Aceito que os meus dados sejam usados para efeitos de contacto e
|
||
análise da manifestação de interesse.
|
||
</label>
|
||
{errors.privacyConsent && (
|
||
<p className="text-sm text-red-600">{errors.privacyConsent}</p>
|
||
)}
|
||
|
||
<button
|
||
type="submit"
|
||
className="w-full rounded-lg bg-slate-900 py-3 font-bold text-white hover:bg-slate-700"
|
||
>
|
||
Enviar manifestação de interesse
|
||
</button>
|
||
</form>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function Field({ label, required, error, children }) {
|
||
return (
|
||
<div>
|
||
<label className="mb-1 block text-sm font-semibold text-slate-700">
|
||
{label} {required && <span className="text-red-600">*</span>}
|
||
</label>
|
||
{children}
|
||
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Footer() {
|
||
return (
|
||
<footer className="border-t border-slate-200 bg-slate-950 py-10 text-slate-400">
|
||
<div className="mx-auto max-w-6xl px-5 text-sm">
|
||
<p className="font-bold text-white">INVESTE Moçambique</p>
|
||
<p className="mt-2 max-w-md">
|
||
Protótipo do Portal do Investidor — dados de demonstração,
|
||
preparado para ligação futura à API real (Laravel + React).
|
||
</p>
|
||
</div>
|
||
</footer>
|
||
);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* App */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
export default function PortalInvestidorApp() {
|
||
const [view, setView] = useState("home");
|
||
const [selected, setSelected] = useState(null);
|
||
const [requestCount, setRequestCount] = useState(0);
|
||
const [heroQuery, setHeroQuery] = useState("");
|
||
const [filters, setFilters] = useState({
|
||
query: "",
|
||
sector: "",
|
||
province: "",
|
||
maturity: "",
|
||
});
|
||
|
||
function goToOpportunities() {
|
||
setView("opportunities");
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
}
|
||
|
||
function handleHeroSearch(event) {
|
||
event.preventDefault();
|
||
setFilters((f) => ({ ...f, query: heroQuery }));
|
||
goToOpportunities();
|
||
}
|
||
|
||
function handleSelectSector(slug) {
|
||
setFilters((f) => ({ ...f, sector: slug, query: "" }));
|
||
goToOpportunities();
|
||
}
|
||
|
||
function handleView(opportunity) {
|
||
setSelected(opportunity);
|
||
setView("detail");
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
}
|
||
|
||
function handleInterest(opportunity) {
|
||
setSelected(opportunity || null);
|
||
setView("interest");
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
|
||
<TopBar onNavigate={setView} requestCount={requestCount} />
|
||
|
||
{view === "home" && (
|
||
<>
|
||
<Hero
|
||
query={heroQuery}
|
||
setQuery={setHeroQuery}
|
||
onSearch={handleHeroSearch}
|
||
onNavigate={setView}
|
||
/>
|
||
<SectorGrid onSelectSector={handleSelectSector} />
|
||
|
||
<section className="border-t border-slate-200 bg-white py-16">
|
||
<div className="mx-auto max-w-6xl px-5">
|
||
<div className="mb-8 flex items-end justify-between">
|
||
<h2 className="text-2xl font-black text-slate-900">
|
||
Oportunidades em destaque
|
||
</h2>
|
||
<button
|
||
onClick={goToOpportunities}
|
||
className="text-sm font-bold text-emerald-700 hover:underline"
|
||
>
|
||
Ver todas →
|
||
</button>
|
||
</div>
|
||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||
{OPPORTUNITIES.filter((o) => o.featured).map((o) => (
|
||
<OpportunityCard
|
||
key={o.reference}
|
||
opportunity={o}
|
||
onView={handleView}
|
||
onInterest={handleInterest}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</>
|
||
)}
|
||
|
||
{view === "opportunities" && (
|
||
<OpportunitiesView
|
||
filters={filters}
|
||
setFilters={setFilters}
|
||
onView={handleView}
|
||
onInterest={handleInterest}
|
||
/>
|
||
)}
|
||
|
||
{view === "detail" && selected && (
|
||
<OpportunityDetail
|
||
opportunity={selected}
|
||
onBack={() => setView("opportunities")}
|
||
onInterest={handleInterest}
|
||
/>
|
||
)}
|
||
|
||
{view === "interest" && (
|
||
<InterestForm
|
||
preselected={selected}
|
||
onSubmitted={() => setRequestCount((c) => c + 1)}
|
||
/>
|
||
)}
|
||
|
||
<Footer />
|
||
|
||
<style>{`
|
||
.input {
|
||
width: 100%;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 0.5rem;
|
||
padding: 0.6rem 0.75rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
.input:focus {
|
||
outline: none;
|
||
box-shadow: 0 0 0 2px #10b981;
|
||
}
|
||
`}</style>
|
||
</div>
|
||
);
|
||
}
|