pesquisa avancada no portal

This commit is contained in:
2026-11-04 12:38:21 +02:00
parent bf2c054943
commit c5756c0282
15 changed files with 639 additions and 7 deletions
+3 -3
View File
@@ -231,14 +231,14 @@ export default function PortalHero({
<form
onSubmit={onSearch}
role="search"
aria-label="Pesquisar oportunidades"
aria-label="Pesquisar no InvesteMoz"
className="mt-7 w-full max-w-4xl"
>
<label
htmlFor="portal-opportunity-search"
className="mb-2 block text-left text-sm font-medium text-white/90"
>
Pesquisa rápida de oportunidades
Pesquisa rápida no InvesteMoz
</label>
<div className="flex min-w-0 flex-col gap-2 rounded-2xl border border-white/30 bg-white p-1.5 shadow-2xl shadow-black/20 focus-within:ring-2 focus-within:ring-white focus-within:ring-offset-4 focus-within:ring-offset-[#101820] min-[400px]:flex-row">
<input
@@ -247,7 +247,7 @@ export default function PortalHero({
name="q"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Ex.: agronegócio, Nampula, energia..."
placeholder="Ex.: agronegócio, Nampula, legislação..."
className="h-12 w-full min-w-0 flex-1 rounded-xl border-0 bg-white px-4 text-base text-slate-900 outline-none placeholder:text-slate-500"
/>
<button
+1 -1
View File
@@ -973,7 +973,7 @@ export default function PortalInvestidorApp({
function handleHeroSearch(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
router.get('/oportunidades', { q: heroQuery.trim() });
router.get('/pesquisa', { q: heroQuery.trim() });
}
function handleView(opportunity: Opportunity) {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Head, Link, router } from '@inertiajs/react';
import { useEffect, useState } from 'react';
import { PortalMenu } from '@/layouts/portal-navigation';
import { PortalMenu } from '@/components/portal-menu';
import { fiscalForms, fiscalFormFields, fiscalPreparationIssues, newFiscalPreparation, readFiscalPreparation, type FiscalFormKind, type FiscalPreparation } from '@/lib/fiscal-form-preparation';
const input = 'mt-1 block w-full rounded border border-slate-400 bg-white p-3 text-slate-900';
+54
View File
@@ -0,0 +1,54 @@
import { Head, Link } from '@inertiajs/react';
import { useEffect, useState } from 'react';
import { Search as SearchIcon } from 'lucide-react';
type Result = { title: string; description: string | null; url: string; category: string };
type Results = { data: Result[]; total: number; current_page: number; last_page: number; per_page: number; unavailable: string[] };
function pageNumbers(current: number, last: number): (number | string)[] {
const pages = [...new Set([1, last, ...Array.from({ length: 5 }, (_, i) => current - 2 + i)])]
.filter((page) => page >= 1 && page <= last).sort((a, b) => a - b);
return pages.flatMap((page, i) => i > 0 && page - pages[i - 1] > 1 ? [`gap-${page}`, page] : [page]);
}
export default function Search({ query, results }: { query: string; results: Results }) {
const [input, setInput] = useState(query);
useEffect(() => setInput(query), [query]);
const { data, total, current_page: current, last_page: last, per_page: perPage, unavailable } = results;
const pageUrl = (page: number) => `/pesquisa?${new URLSearchParams({ q: query, page: String(page) })}`;
const start = total ? (current - 1) * perPage + 1 : 0;
const end = Math.min(start + data.length - 1, total);
return <main className="min-h-[65vh] bg-white text-[#101820]">
<Head title="Pesquisa" />
<div className="border-b border-slate-200 bg-[#101820] text-white">
<div className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
<h1 className="text-3xl font-bold tracking-tight">Pesquisa no InvesteMoz</h1>
<form action="/pesquisa" role="search" className="mt-5 flex max-w-3xl flex-col gap-2 sm:flex-row">
<label htmlFor="search-query" className="sr-only">Pesquisar no InvesteMoz</label>
<input id="search-query" type="search" name="q" value={input} onChange={(event) => setInput(event.target.value)} maxLength={120} className="min-h-12 min-w-0 flex-1 rounded-lg border border-slate-300 bg-white px-4 text-base text-[#101820] outline-none focus:ring-2 focus:ring-[#ed1c24]" placeholder="Oportunidades, documentos, sectores..." />
<button className="inline-flex min-h-12 items-center justify-center gap-2 rounded-lg bg-[#ed1c24] px-6 font-semibold text-white hover:bg-[#c9141b] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"><SearchIcon size={18} aria-hidden="true" />Pesquisar</button>
</form>
</div>
</div>
<div className="mx-auto max-w-5xl px-4 py-7 sm:px-6">
<p className="max-w-3xl text-sm leading-relaxed text-slate-600">Pesquise por palavras em qualquer ordem. Pode escrever com ou sem acentos. Os documentos são pesquisados pelos seus dados descritivos.</p>
{unavailable.length > 0 && <p role="alert" className="mt-4 rounded-lg border border-amber-400 bg-amber-50 p-3 text-sm">Não foi possível consultar: {unavailable.join(', ')}. Os restantes resultados continuam disponíveis; tente novamente mais tarde.</p>}
{query && <p className="mt-6 text-sm text-slate-600" aria-live="polite">{total > 0 ? `Resultados ${start}–${end} de ${total}` : 'Nenhum resultado nas fontes consultadas'} para “{query}”.</p>}
{query && total === 0 && <p className="mt-4 text-slate-600">Experimente outras palavras ou uma expressão mais curta.</p>}
<ol start={start || 1} className="mt-2 max-w-3xl divide-y divide-slate-200">
{data.map((item) => <li key={`${item.category}:${item.url}`} className="py-5">
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-slate-600">{item.category}</p>
<Link href={item.url} className="text-lg font-semibold text-[#b5121b] underline-offset-4 hover:underline focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#ed1c24]">{item.title}</Link>
{item.description && <p className="mt-1 line-clamp-3 text-sm leading-relaxed text-slate-600">{item.description}</p>}
</li>)}
</ol>
{last > 1 && <nav className="mt-8 flex flex-wrap items-center gap-2" aria-label="Páginas de resultados">
{current > 1 && <Link href={pageUrl(current - 1)} rel="prev" className="rounded-md px-3 py-2 text-sm font-semibold text-[#b5121b] hover:bg-red-50">Anterior</Link>}
{pageNumbers(current, last).map((page) => typeof page === 'string' ? <span key={page} className="px-1 text-slate-500" aria-hidden="true">…</span> :
<Link key={page} href={pageUrl(page)} aria-label={`Página ${page}`} aria-current={page === current ? 'page' : undefined} className={page === current ? 'rounded-md bg-[#101820] px-3 py-2 text-sm font-bold text-white' : 'rounded-md px-3 py-2 text-sm font-semibold text-[#b5121b] hover:bg-red-50'}>{page}</Link>)}
{current < last && <Link href={pageUrl(current + 1)} rel="next" className="rounded-md px-3 py-2 text-sm font-semibold text-[#b5121b] hover:bg-red-50">Seguinte</Link>}
</nav>}
</div>
</main>;
}