mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
106 lines
2.8 KiB
TypeScript
106 lines
2.8 KiB
TypeScript
import React, {useEffect, useRef} from 'react';
|
||
import {IntlShape, useIntl} from 'react-intl';
|
||
import {Search, SearchResultProps} from 'semantic-ui-react';
|
||
import {JsonIndi} from 'topola';
|
||
import {formatDateOrRange} from '../util/date_util';
|
||
import {SearchResult} from './search_index';
|
||
import {
|
||
registerSearchInput,
|
||
unregisterSearchInput,
|
||
} from './use_search_shortcut';
|
||
|
||
const SHORTCUT_INPUT_PROP = {
|
||
'aria-keyshortcuts': '/',
|
||
};
|
||
|
||
function getNameLine(result: SearchResult) {
|
||
const name = [result.indi.firstName, result.indi.lastName].join(' ').trim();
|
||
if (result.id.length > 8) {
|
||
return name;
|
||
}
|
||
return (
|
||
<>
|
||
{name} <i>({result.id})</i>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function getDescriptionLine(indi: JsonIndi, currentIntl: IntlShape) {
|
||
const birthDate = formatDateOrRange(indi.birth, currentIntl);
|
||
const deathDate = formatDateOrRange(indi.death, currentIntl);
|
||
if (!deathDate) {
|
||
return birthDate;
|
||
}
|
||
return `${birthDate} – ${deathDate}`;
|
||
}
|
||
|
||
function displaySearchResult(
|
||
result: SearchResult,
|
||
currentIntl: IntlShape,
|
||
): SearchResultProps {
|
||
return {
|
||
id: result.id,
|
||
key: result.id,
|
||
title: getNameLine(result),
|
||
description: getDescriptionLine(result.indi, currentIntl),
|
||
} as SearchResultProps;
|
||
}
|
||
|
||
interface Props {
|
||
results: SearchResult[];
|
||
value: string;
|
||
onSearchChange: (value: string) => void;
|
||
onResultSelect: (id: string) => void;
|
||
hideShortcutHint?: boolean;
|
||
}
|
||
|
||
/** Displays and handles the search box in the top bar. */
|
||
export const SearchBar = React.memo(function SearchBar(props: Props) {
|
||
const intl = useIntl();
|
||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const el = wrapperRef.current;
|
||
if (!el) {
|
||
return;
|
||
}
|
||
const input = el.querySelector('input');
|
||
if (input) {
|
||
registerSearchInput(input);
|
||
return () => unregisterSearchInput(input);
|
||
}
|
||
}, []);
|
||
|
||
const placeholder = props.hideShortcutHint
|
||
? intl.formatMessage({
|
||
id: 'menu.search.placeholder',
|
||
defaultMessage: 'Search for people',
|
||
})
|
||
: intl.formatMessage({
|
||
id: 'menu.search.placeholder_with_shortcut',
|
||
defaultMessage: "Search for people (press '/')",
|
||
});
|
||
|
||
const transformedResults = props.results.map((r) =>
|
||
displaySearchResult(r, intl),
|
||
);
|
||
|
||
return (
|
||
<div className="item" ref={wrapperRef}>
|
||
<Search
|
||
onSearchChange={(_, data) => props.onSearchChange(data.value || '')}
|
||
onResultSelect={(_, data) => props.onResultSelect(data.result.id)}
|
||
results={transformedResults}
|
||
noResultsMessage={intl.formatMessage({
|
||
id: 'menu.search.no_results',
|
||
defaultMessage: 'No results found',
|
||
})}
|
||
placeholder={placeholder}
|
||
selectFirstResult={true}
|
||
value={props.value}
|
||
input={SHORTCUT_INPUT_PROP}
|
||
/>
|
||
</div>
|
||
);
|
||
});
|