ui: add omni-search

This commit is contained in:
undefined committed 2022-12-31 18:59:19 +08:00
1 parent c991062738
commit d4cc5e9c5b
3 files changed
+259

No files matched your search

@@ -0,0 +1,125 @@
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', () => {
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.href.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(({
domainId, docId, pid, title, nSubmit, nAccept,
}) => (
<a
key={domainId + docId}
className="omnibar-content-section omnibar-problem"
href={`${domainId !== UiContext.domainId ? `/d/${domainId}` : prefix}/p/${pid || docId}`}
>
<div>
<a
href={psdict[`${domainId}#${docId}`]?.rid && `/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=${encodeURIComponent(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);
});
@@ -0,0 +1,133 @@
.omnibar {
position: fixed;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(3px);
z-index: 999;
transition: 0.2s;
}
.omnibar-main {
height: min(600px, 100% - 20px);
width: min(750px, 60%);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
>div:first-child {
transition: 0.2s;
margin-bottom: 10px;
input {
background-color: #fff;
border-radius: 5px;
border: 2px solid #aaa;
padding-left: 42px;
height: 48px;
width: 100%;
font-size: 18px;
outline: 0;
box-sizing: border-box;
}
span {
height: 48px;
width: 48px;
position: absolute;
display: grid;
scale: 1.5;
place-items: center;
top: 0px;
left: 0px;
}
}
>div:last-child {
flex: 1;
scrollbar-width: none;
-ms-overflow-style: none;
overflow-x: hidden;
overflow-y: auto;
&::-webkit-scrollbar { width: 0 !important; }
}
}
.omnibar-toggle {
z-index: 99;
background: white;
display: grid;
place-items: center;
width: 40px;
height: 40px;
position: fixed;
right: 20px;
bottom: 30px;
transition: 0.2s;
border-radius: 5px;
&:hover {
background: #ccc;
}
span {
scale: 1.5;
}
}
#omnibar-content {
overscroll-behavior: none;
}
.omnibar-content-title {
text-align: left;
margin-bottom: 10px;
width: 100%;
font-size: 18px;
font-weight: bold;
}
.omnibar-content-section {
background: white;
border-radius: 10px;
display: flex;
flex-direction: column;
cursor: pointer;
padding: 10px;
line-height: 1;
margin-bottom: 10px;
border: 2px solid #888;
box-sizing: border-box;
width: 100%;
text-decoration: none !important;
&:hover {
background: #eee;
}
>div {
width: 100%;
display: flex;
flex-direction: row;
}
}
.omnibar-user img {
height: 36px;
width: 36px;
border-radius: 50%;
margin-right: 10px;
}
.omnibar-user-info {
>span {
font-size: 14px;
margin-bottom: 3px;
display: inline-block;
}
>div {
display: flex;
flex-direction: row;
}
}
.omnibar-problem a {
margin: 2px 0px 3px 0px;
}
.omnibar-problem .icon {
margin-right: 3px;
&:not(:first-child) {
margin-left: 10px;
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1000" width="1000"><path d="M437 542c-1.8-3.2-2.4-7-1.7-10.6l76.3-405c1.7-9-4.5-17.6-13.5-18.9-20.8-3.1-42.1-4.8-63.8-4.8C187.6 102.7-11 308.3.5 557.5c10.4 226 196.4 407.9 422.6 413.6 64.3 1.6 125.6-10.7 181-34.3 20.6-8.7 28.8-33.6 18.1-53.3L437 542zm533.8-161.7c-6.3-16.2-25-23.7-40.8-16.3L594.2 523.5c-15.5 7.4-21.6 26.2-13.5 41.2l177.2 326.8c8.3 15.4 28 20.3 42.7 10.8C920.6 825 1000 690.3 1000 537c0-55.3-10.4-108.1-29.2-156.7zM560.6 432 891 275.1c11.8-5.6 16.7-20.1 10.4-31.6C843.7 138 743.4 59 623.7 29.6c-12.7-3.1-25.3 5.3-27.7 18.1l-67.7 359.4c-3.5 18.4 15.4 32.9 32.3 24.9z"/></svg>

After

Width:  |  Height:  |  Size: 639 B