You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Hydro/packages/ui-default/components/omnisearch/index.page.tsx

127 lines
4.2 KiB
TypeScript

import { debounce } from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { STATUS_CODES } from 'vj/constant/record';
import { AutoloadPage } from 'vj/misc/Page';
import {
api, gql, i18n, request,
} from 'vj/utils';
export default new AutoloadPage('omnibar', () => {
if (document.documentElement.dataset.layout !== 'basic') return;
const $search = $(`
<div class='omnibar' data-hotkey="esc:click" style="opacity:0;display:none;">
<div class='omnibar-main'>
<div>
<input placeholder="Search" />
<span class="icon icon-search"></span>
</div>
<div id='omnibar-content'></div>
</div>
</div>
`);
const $entry = $(`
<button class="omnibar-toggle" data-global-hotkey="ctrl+k:click">
<span class="icon icon-search"></span>
</button>
`);
$(document.body).append($search);
$(document.body).append($entry);
const $input = $('.omnibar input') as JQuery<HTMLInputElement>;
const prefix = window.location.pathname.startsWith('/d/') ? `/d/${UiContext.domainId}` : '';
let setSearching;
let pdocs = [];
let psdict = {};
let udocs = [];
function SearchResult() {
const [searching, set] = React.useState(false);
setSearching = set;
return (
<>
{searching && <div>Searching...</div>}
{pdocs.length > 0 && <div className="omnibar-content-title">{i18n('Problems')}</div>}
{pdocs.map((i) => ({ ...i, base: i.domainId !== UiContext.domainId ? `/d/${i.domainId}` : prefix })).map(({
domainId, docId, pid, title, nSubmit, nAccept, base,
}) => (
<a
key={domainId + docId}
className="omnibar-content-section omnibar-problem"
href={`${base}/p/${pid || docId}`}
>
<div>
<a
href={psdict[`${domainId}#${docId}`]?.rid && `${base}/record/${psdict[`${domainId}#${docId}`]?.rid}`}
className={`record-status--text ${STATUS_CODES[psdict[`${domainId}#${docId}`]?.status]}`}
>
<span className={`icon record-status--icon ${STATUS_CODES[psdict[`${domainId}#${docId}`]?.status]}`}></span>
</a>
<div>{title}</div>
</div>
<div>
<span className="icon icon-book" />{domainId !== UiContext.domainId ? `${domainId}#` : ''}{pid || docId}
<span className="icon icon-pie-chart" />{nAccept}/{nSubmit}
</div>
</a>
))}
{udocs.length > 0 && <div className="omnibar-content-title">{i18n('Users')}</div>}
{udocs.map(({
_id, uname, avatarUrl,
}) => (
<a key={_id} className="omnibar-content-section omnibar-user" href={`${prefix}/user/${_id}`}>
<div>
<img src={avatarUrl} alt={uname} />
<div className="omnibar-user-info">
<span>UID {_id}</span><br />
<div><span style={{ lineHeight: '20px' }}>{uname}</span></div>
</div>
</div>
</a>
))}
</>
);
}
ReactDOM.createRoot(document.getElementById('omnibar-content')!).render(<SearchResult />);
async function search(query: string) {
if (!query?.trim()) {
pdocs = [];
setSearching?.(false);
return;
}
setSearching?.(true);
[{ pdocs, psdict }, udocs] = await Promise.all([
request.get(`/d/${UiContext.domainId}/p`, { q: query, limit: 10 }),
api(gql`
users(search: ${query}) {
_id
uname
displayName
avatarUrl
}
`, ['data', 'users']),
]);
setSearching?.(false);
}
$input.on('input', debounce((ev) => search(ev.target.value), 1000));
let open = false;
const show = () => {
$search.show();
setTimeout(() => {
$search.css('opacity', '1');
$input.focus();
}, 20);
open = true;
};
const hide = () => {
$search.css('opacity', '0');
setTimeout(() => $search.hide(), 200);
open = false;
};
$entry.on('click', () => (open ? hide() : show()));
$input.on('click', (ev) => ev.stopPropagation());
$('.omnibar-main').on('click', '.omnibar-content-section', (ev) => ev.stopPropagation());
$search.on('click', hide);
});