190 lines
7.1 KiB
TypeScript
190 lines
7.1 KiB
TypeScript
import { router, usePage } from '@inertiajs/react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import CatalogueFilterContext from './catalogue-filter-context';
|
|
// Shared feedback for existing Inertia search consumers; form state is never reset.
|
|
const collections = [
|
|
'results',
|
|
'records',
|
|
'resources',
|
|
'partnerships',
|
|
'opportunities',
|
|
'interests',
|
|
'investors',
|
|
'leads',
|
|
'articles',
|
|
'services',
|
|
'items',
|
|
'entries',
|
|
'documents',
|
|
'requests',
|
|
'batches',
|
|
'users',
|
|
'assignments',
|
|
'zones',
|
|
'matches',
|
|
'businesses',
|
|
];
|
|
function count(props: Record<string, unknown>): number | null {
|
|
if (props.demands && props.capacities)
|
|
return (
|
|
Number((props.demands as { total: number }).total) +
|
|
Number((props.capacities as { total: number }).total)
|
|
);
|
|
if (props.plan && Array.isArray((props.plan as { rows: unknown[] }).rows))
|
|
return (props.plan as { rows: unknown[] }).rows.length;
|
|
if (typeof props.total === 'number') return props.total;
|
|
if (typeof props.match === 'boolean' && Array.isArray(props.articles))
|
|
return props.match ? props.articles.length : 0;
|
|
for (const key of collections) {
|
|
const value = props[key] as
|
|
| { total?: number; meta?: { total?: number }; data?: unknown[] }
|
|
| unknown[]
|
|
| undefined;
|
|
if (!value) continue;
|
|
if (Array.isArray(value)) return value.length;
|
|
if (typeof value.total === 'number') return value.total;
|
|
if (typeof value.meta?.total === 'number') return value.meta.total;
|
|
if (Array.isArray(value.data)) return value.data.length;
|
|
}
|
|
return null;
|
|
}
|
|
export default function SearchFeedback() {
|
|
const page = usePage();
|
|
const handledLocally = page.component === 'investment/catalogue';
|
|
const [state, setState] = useState<'idle' | 'loading' | 'error'>('idle');
|
|
const latest = useRef<{
|
|
url: string;
|
|
data: Record<string, unknown>;
|
|
} | null>(null);
|
|
const active = useRef(false);
|
|
const searchable =
|
|
page.props.filters !== undefined ||
|
|
[
|
|
'portal/legacy',
|
|
'desk/catalogue',
|
|
'investment/zones',
|
|
'partnerships/catalogue',
|
|
'education/index',
|
|
'management/territory-geometry-import',
|
|
].includes(page.component);
|
|
// A page may contain several independent catalogues. Never infer a public
|
|
// result universe by whichever collection happens to occur first.
|
|
const descriptors: Record<string, [string, string]> = {
|
|
'portfolio/index': ['records', 'Fichas da carteira'],
|
|
'investment/zones': ['records', 'Zonas e parques'],
|
|
'investment/business-marketplace': ['results', 'Anúncios de negócios'],
|
|
'portal/legacy': ['articles', 'Artigos'],
|
|
};
|
|
const descriptor =
|
|
page.component === 'portal/services'
|
|
? page.props.section === 'documents' ||
|
|
page.props.section === 'legislation'
|
|
? ['resources', 'Documentos']
|
|
: page.props.section === 'sectors'
|
|
? ['sectors', 'Sectores']
|
|
: null
|
|
: descriptors[page.component];
|
|
const total = descriptor
|
|
? page.component === 'portal/legacy' && page.props.match === false
|
|
? 0
|
|
: count({ results: page.props[descriptor[0]] })
|
|
: searchable
|
|
? count(page.props)
|
|
: null;
|
|
const label = descriptor?.[1];
|
|
useEffect(() => {
|
|
if (handledLocally) return;
|
|
const off = [
|
|
router.on('before', (event) => {
|
|
const visit = event.detail.visit;
|
|
if (visit.method !== 'get') {
|
|
active.current = false;
|
|
return;
|
|
}
|
|
const data = visit.data as Record<string, unknown>;
|
|
const url = visit.url;
|
|
const isSearch =
|
|
total !== null ||
|
|
['q', 'query', 'search', 'filter', 'status', 'sector'].some(
|
|
(key) => key in data || url.searchParams.has(key),
|
|
);
|
|
active.current = isSearch;
|
|
if (isSearch) {
|
|
latest.current = { url: url.toString(), data };
|
|
setState('loading');
|
|
}
|
|
}),
|
|
router.on('success', () => {
|
|
if (active.current) {
|
|
active.current = false;
|
|
setState('idle');
|
|
}
|
|
}),
|
|
router.on('error', () => {
|
|
if (active.current) setState('error');
|
|
}),
|
|
router.on('httpException', (event) => {
|
|
if (active.current) {
|
|
event.preventDefault();
|
|
setState('error');
|
|
}
|
|
}),
|
|
router.on('networkError', (event) => {
|
|
if (active.current) {
|
|
event.preventDefault();
|
|
setState('error');
|
|
}
|
|
}),
|
|
];
|
|
return () => off.forEach((remove) => remove());
|
|
}, [total, handledLocally]);
|
|
if (handledLocally) return null;
|
|
if (total === null && state === 'idle') return null;
|
|
return (
|
|
<>
|
|
<aside
|
|
data-search-feedback
|
|
role={state === 'error' ? 'alert' : 'status'}
|
|
aria-live="polite"
|
|
aria-atomic="true"
|
|
className="border-b bg-white p-3 text-center text-slate-950"
|
|
>
|
|
{label && <strong>{label}: </strong>}
|
|
{state === 'loading' ? (
|
|
'A pesquisar…'
|
|
) : state === 'error' ? (
|
|
<>
|
|
<span>
|
|
Não foi possível concluir a pesquisa. Os resultados
|
|
anteriores mantêm-se abaixo.{' '}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() => {
|
|
const request = latest.current;
|
|
if (request)
|
|
router.get(
|
|
request.url,
|
|
request.data as never,
|
|
{
|
|
preserveState: true,
|
|
preserveScroll: true,
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
Tentar novamente
|
|
</button>
|
|
</>
|
|
) : total === 0 ? (
|
|
'Não foram encontrados resultados para esta pesquisa'
|
|
) : (
|
|
`${total} ${total === 1 ? 'resultado encontrado' : 'resultados encontrados'}`
|
|
)}
|
|
</aside>
|
|
{descriptor && <CatalogueFilterContext />}
|
|
</>
|
|
);
|
|
}
|