101 lines
4.0 KiB
JavaScript
101 lines
4.0 KiB
JavaScript
/* Global search — App.search from js/app.js:171-191. Same sources and the same
|
|
4/4/3 caps. The inline onclick="App.searchGo(...)" strings become navigate()
|
|
calls, and the setTimeout(cb, 120) hack that waited for the old router to
|
|
swap innerHTML is gone: the target screen reads `state.open` instead. */
|
|
|
|
import { useMemo, useRef, useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { seedQuery } from '../data/seedQueries'
|
|
import { Avatar, Icon } from '../ui/primitives'
|
|
|
|
export default function GlobalSearch({ inputRef }) {
|
|
const navigate = useNavigate()
|
|
const [q, setQ] = useState('')
|
|
const [open, setOpen] = useState(false)
|
|
const boxRef = useRef(null)
|
|
|
|
const { data: jobs = [] } = useQuery(seedQuery('jobs'))
|
|
const { data: candidates = [] } = useQuery(seedQuery('candidates'))
|
|
const { data: managers = [] } = useQuery(seedQuery('managers'))
|
|
|
|
const results = useMemo(() => {
|
|
const term = q.trim().toLowerCase()
|
|
if (!term) return null
|
|
return {
|
|
jobs: jobs.filter((j) => (j.title + j.id + j.department).toLowerCase().includes(term)).slice(0, 4),
|
|
candidates: candidates.filter((c) => (c.name + c.email + c.jobTitle).toLowerCase().includes(term)).slice(0, 4),
|
|
managers: managers.filter((m) => m.name.toLowerCase().includes(term)).slice(0, 3),
|
|
}
|
|
}, [q, jobs, candidates, managers])
|
|
|
|
function go(path, state) {
|
|
setQ('')
|
|
setOpen(false)
|
|
navigate(path, { state })
|
|
}
|
|
|
|
const empty = results && !results.jobs.length && !results.candidates.length && !results.managers.length
|
|
|
|
return (
|
|
<div className="topbar-search" onClick={(e) => e.stopPropagation()}>
|
|
<Icon name="search" />
|
|
<input
|
|
type="text"
|
|
ref={inputRef}
|
|
value={q}
|
|
placeholder="Search jobs, candidates, managers…"
|
|
autoComplete="off"
|
|
onChange={(e) => {
|
|
setQ(e.target.value)
|
|
setOpen(Boolean(e.target.value.trim()))
|
|
}}
|
|
onFocus={() => setOpen(Boolean(q.trim()))}
|
|
/>
|
|
<div className={`search-results${open && results ? ' open' : ''}`} ref={boxRef}>
|
|
{results && (
|
|
<>
|
|
{results.jobs.length > 0 && <div className="search-group-label">Jobs</div>}
|
|
{results.jobs.map((j) => (
|
|
<div key={j.id} className="search-item" onClick={() => go('/jobs', { openJob: j.id })}>
|
|
<span className="kpi-icn i-indigo" style={{ width: 32, height: 32, borderRadius: 8 }}>
|
|
<Icon name="briefcase" />
|
|
</span>
|
|
<div>
|
|
<div className="si-title">{j.title}</div>
|
|
<div className="si-sub">{j.id} · {j.department}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{results.candidates.length > 0 && <div className="search-group-label">Candidates</div>}
|
|
{results.candidates.map((c) => (
|
|
<div key={c.id} className="search-item" onClick={() => go('/candidates', { openCandidate: c.id })}>
|
|
<Avatar name={c.name} initials={c.initials} color={c.color} />
|
|
<div>
|
|
<div className="si-title">{c.name}</div>
|
|
<div className="si-sub">{c.jobTitle}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{results.managers.length > 0 && <div className="search-group-label">Hiring Managers</div>}
|
|
{results.managers.map((m) => (
|
|
<div key={m.id} className="search-item" onClick={() => go('/managers', { openManager: m.id })}>
|
|
<Avatar name={m.name} initials={m.initials} color={m.color} />
|
|
<div>
|
|
<div className="si-title">{m.name}</div>
|
|
<div className="si-sub">{m.title}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{empty && <div className="search-empty">No results for “{q}”</div>}
|
|
</>
|
|
)}
|
|
</div>
|
|
<kbd className="search-kbd">⌘K</kbd>
|
|
</div>
|
|
)
|
|
}
|