nitro-v36-with-renderer-v36
/
src /
components /
housekeeping /
views /
shell /
HousekeepingGlobalSearch.tsx
HousekeepingGlobalSearch.tsx 8.1 Ko
Télécharger le fichier
import { Autocomplete } from '@base-ui/react/autocomplete';
import { FC, useEffect, useRef, useState } from 'react';
import { FaCircle, FaHome, FaSearch, FaUser } from 'react-icons/fa';
import { HousekeepingTabId, LocalizeText, parseHousekeepingSearch } from '../../../../api';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
import { useHousekeepingStore } from '../../../../hooks';
interface SearchRow {
key: string;
kind: 'user' | 'room';
id: number;
label: string;
hint: string;
online?: boolean;
}
/**
* One box for users and rooms: a name searches both, "#12" opens user 12,
* "s:411" opens room 411, and an empty box lists the recent lookups.
*/
export const HousekeepingGlobalSearch: FC = () => {
const { userSuggestions, roomSuggestions, requestUserSuggestions, requestRoomSuggestions, recentLookups, lookupUserById, lookupRoomById, setActiveTab } =
useHousekeepingStore();
const [query, setQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);
const boxRef = useRef<HTMLDivElement>(null);
const listHostRef = useRef<HTMLDivElement>(null);
const parsed = parseHousekeepingSearch(query);
const searchText = parsed.kind === 'text' ? parsed.text : '';
useEffect(() => {
requestUserSuggestions(searchText);
requestRoomSuggestions(searchText);
}, [searchText, requestUserSuggestions, requestRoomSuggestions]);
const rows: SearchRow[] = (() => {
switch (parsed.kind) {
case 'user-id':
return [
{
key: `u${parsed.id}`,
kind: 'user',
id: parsed.id,
label: LocalizeText('housekeeping.search.open_user', ['id'], [String(parsed.id)]),
hint: ''
}
];
case 'room-id':
return [
{
key: `r${parsed.id}`,
kind: 'room',
id: parsed.id,
label: LocalizeText('housekeeping.search.open_room', ['id'], [String(parsed.id)]),
hint: ''
}
];
case 'text':
return [
...userSuggestions.map((user) => ({
key: `u${user.id}`,
kind: 'user' as const,
id: user.id,
label: user.username,
hint: `#${user.id}`,
online: user.online
})),
...roomSuggestions.map((room) => ({ key: `r${room.id}`, kind: 'room' as const, id: room.id, label: room.name, hint: room.ownerName }))
];
case 'empty':
return recentLookups
.slice(0, 8)
.map((entry) => ({ key: `${entry.kind}${entry.id}`, kind: entry.kind, id: entry.id, label: entry.label, hint: `#${entry.id}` }));
default:
return [];
}
})();
const open = (row: SearchRow) => {
if (row.kind === 'user') {
setActiveTab(HousekeepingTabId.USERS);
lookupUserById(row.id);
} else {
setActiveTab(HousekeepingTabId.ROOMS);
lookupRoomById(row.id);
}
setQuery('');
setIsOpen(false);
};
const rowByKey = new Map(rows.map((row) => [row.key, row]));
return (
<NativeTextTree>
<div className="relative">
<div ref={listHostRef} />
<Autocomplete.Root
autoHighlight="always"
filter={null}
items={rows.map((row) => row.key)}
mode="list"
open={isOpen && (rows.length > 0 || parsed.kind === 'text')}
value={query}
onOpenChange={setIsOpen}
onValueChange={(next, details) => {
// The rows are actions, not values: a picked row opens its user or room and the box is cleared.
if (details.reason === 'item-press') {
const row = rowByKey.get(next);
if (row) open(row);
return;
}
setQuery(next);
setIsOpen(true);
}}
>
<div
ref={boxRef}
className="flex items-center gap-1 rounded-md border border-zinc-300 bg-white px-1.5 py-1 focus-within:border-sky-400 focus-within:ring-1 focus-within:ring-sky-300"
>
<FaSearch className="shrink-0 text-zinc-400" size={10} />
<Autocomplete.Input
aria-label={LocalizeText('housekeeping.search.placeholder')}
className="w-full min-w-0 bg-transparent text-xs outline-none placeholder:italic placeholder:text-zinc-400"
placeholder={LocalizeText('housekeeping.search.placeholder')}
onFocus={() => setIsOpen(true)}
/>
</div>
<Autocomplete.Portal container={listHostRef}>
<Autocomplete.Positioner align="start" anchor={boxRef} className="z-40" sideOffset={4}>
<Autocomplete.Popup className="octane-hk-popover w-[260px] overflow-hidden rounded border border-zinc-200 bg-white shadow-lg">
{parsed.kind === 'empty' && rows.length > 0 && (
<div className="border-b border-zinc-100 px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-zinc-400">
{LocalizeText('housekeeping.user.recent')}
</div>
)}
{rows.length === 0 && <div className="px-2 py-1.5 text-[11px] italic text-zinc-500">{LocalizeText('housekeeping.search.no_results')}</div>}
<Autocomplete.List>
{(key: string) => {
const row = rowByKey.get(key);
if (!row) return null;
return (
<Autocomplete.Item
key={key}
className="flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left text-xs data-[highlighted]:bg-sky-50"
value={key}
>
{row.kind === 'user' ? (
<FaUser className="shrink-0 text-sky-500" size={9} />
) : (
<FaHome className="shrink-0 text-violet-500" size={10} />
)}
<span className="grow truncate font-medium">{row.label}</span>
{row.online !== undefined && <FaCircle className={row.online ? 'text-emerald-500' : 'text-zinc-300'} size={6} />}
{row.hint && <span className="max-w-[80px] shrink-0 truncate text-[10px] text-zinc-500">{row.hint}</span>}
</Autocomplete.Item>
);
}}
</Autocomplete.List>
</Autocomplete.Popup>
</Autocomplete.Positioner>
</Autocomplete.Portal>
</Autocomplete.Root>
</div>
</NativeTextTree>
);
};
