Files
Investidor-V2/resources/js/components/search-feedback.tsx
T

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 />}
</>
);
}