Files
ontime/apps/client/src/views/editor/finder/Finder.tsx
T
2026-07-14 12:04:44 +02:00

124 lines
3.9 KiB
TypeScript

import { useDebouncedCallback } from '@mantine/hooks';
import { SupportedEntry } from 'ontime-types';
import { KeyboardEvent, useState } from 'react';
import Input from '../../../common/components/input/input/Input';
import Kbd from '../../../common/components/kbd/Kbd';
import Modal from '../../../common/components/modal/Modal';
import useFinder from './useFinder';
import style from './Finder.module.scss';
interface FinderProps {
isOpen: boolean;
onClose: () => void;
}
export default function Finder({ isOpen, onClose }: FinderProps) {
const { find, select, results, error } = useFinder();
const [selected, setSelected] = useState(0);
const debouncedFind = useDebouncedCallback(find, 100);
const navigate = (event: KeyboardEvent<HTMLDivElement>) => {
// all operations need results
if (results.length === 0) {
return;
}
if (event.key === 'ArrowDown') {
setSelected((prev) => (prev + 1) % results.length);
}
if (event.key === 'ArrowUp') {
setSelected((prev) => (prev - 1 + results.length) % results.length);
}
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
submit();
}
};
const submit = () => {
const selectedEvent = results[selected];
select(selectedEvent);
onClose();
};
const handleMouseMoveEvent = (event: React.MouseEvent<HTMLUListElement>) => {
const target = event.target as HTMLElement;
const li = target.closest('li');
if (li) {
const index = Number(li.dataset.index);
if (!isNaN(index)) {
setSelected(index);
}
}
};
return (
<Modal
title=''
isOpen={isOpen}
onClose={onClose}
showBackdrop
bodyElements={
<div onKeyDown={navigate}>
<Input height='large' fluid onChange={debouncedFind} placeholder='Search...' />
<ul className={style.scrollContainer} onMouseMove={handleMouseMoveEvent}>
{error && <li className={style.error}>{error}</li>}
{results.length === 0 && <li className={style.empty}>No results</li>}
{results.length > 0 &&
results.map((entry, index) => {
const isSelected = selected === index;
const displayIndex = entry.type === SupportedEntry.Event ? entry.eventIndex : '-';
const displayCue = 'cue' in entry ? entry.cue : '';
return (
<li
key={entry.id}
className={style.entry}
data-selected={isSelected}
data-index={index}
onClick={submit}
>
<div className={style.data}>
<div className={style.index} style={{ '--color': entry.colour }}>
{displayIndex}
</div>
<div className={style.cue}>{displayCue}</div>
<div className={style.title}>{entry.title}</div>
</div>
{isSelected && <span>Go </span>}
</li>
);
})}
</ul>
</div>
}
footerElements={
<div className={style.footer}>
<div className={style.hints}>
<span className={style.hintItem}>
<Kbd></Kbd>
<Kbd></Kbd>
Navigate
</span>
<span className={style.hintItem}>
<Kbd>Enter</Kbd>
Go
</span>
<span className={style.hintItem}>
<Kbd>Esc</Kbd>
Close
</span>
</div>
<div className={style.filterHint}>
Filter by <span className={style.em}>cue</span>, <span className={style.em}>index</span>, or
<span className={style.em}>title</span>
</div>
</div>
}
/>
);
}