Add autocomplete suggestions for Author, Chapter, and Reviewer fields

Adds a GET /api/jobs/suggestions endpoint that returns distinct values for
author, chapter, and reviewer_name from the database, and wires them into
HTML datalist elements on the New Job, result view, and Browse Jobs pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Roberts
2026-06-09 18:24:49 +01:00
parent 1d15b5f0c1
commit 5ea18d76d6
5 changed files with 88 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { useSuggestions } from '../hooks/useSuggestions'
import { motion, AnimatePresence } from 'framer-motion'
import {
Search, ChevronLeft, ChevronRight, CheckCircle2, Clock,
@@ -30,7 +31,7 @@ function StatusBadge({ status }) {
// ─────────────────────────────────────────────────────────────
// Full-screen Job Detail
// ─────────────────────────────────────────────────────────────
function JobDetail({ jobId, onClose, onReviewed }) {
function JobDetail({ jobId, onClose, onReviewed, suggestions = {} }) {
const [job, setJob] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -177,10 +178,19 @@ function JobDetail({ jobId, onClose, onReviewed }) {
{/* Metadata + reviewer row */}
<div className="glass p-4 rounded-2xl flex-shrink-0">
<datalist id="jd-authors">
{(suggestions.authors || []).map(a => <option key={a} value={a} />)}
</datalist>
<datalist id="jd-chapters">
{(suggestions.chapters || []).map(c => <option key={c} value={c} />)}
</datalist>
<datalist id="jd-reviewers">
{(suggestions.reviewers || []).map(r => <option key={r} value={r} />)}
</datalist>
<div className="grid grid-cols-6 gap-4">
<div>
<label className="text-xs text-gray-400 mb-1 block">Author</label>
<input type="text" value={editAuthor} onChange={e => setEditAuthor(e.target.value)} placeholder="Author" className={INPUT_CLASS} />
<input type="text" list="jd-authors" value={editAuthor} onChange={e => setEditAuthor(e.target.value)} placeholder="Author" className={INPUT_CLASS} />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Book</label>
@@ -188,7 +198,7 @@ function JobDetail({ jobId, onClose, onReviewed }) {
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Chapter</label>
<input type="text" value={editChapter} onChange={e => setEditChapter(e.target.value)} placeholder="Chapter" className={INPUT_CLASS} />
<input type="text" list="jd-chapters" value={editChapter} onChange={e => setEditChapter(e.target.value)} placeholder="Chapter" className={INPUT_CLASS} />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Page</label>
@@ -196,7 +206,7 @@ function JobDetail({ jobId, onClose, onReviewed }) {
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Reviewer</label>
<input type="text" value={reviewerName} onChange={e => setReviewerName(e.target.value)} placeholder="Your name" className={INPUT_CLASS} />
<input type="text" list="jd-reviewers" value={reviewerName} onChange={e => setReviewerName(e.target.value)} placeholder="Your name" className={INPUT_CLASS} />
</div>
<div className="flex flex-col justify-end">
<motion.button
@@ -255,6 +265,7 @@ function JobDetail({ jobId, onClose, onReviewed }) {
// Search / List view
// ─────────────────────────────────────────────────────────────
export default function JobsPanel() {
const suggestions = useSuggestions()
const [search, setSearch] = useState('')
const [filterStatus, setFilterStatus] = useState('')
const [filterAuthor, setFilterAuthor] = useState('')
@@ -308,6 +319,7 @@ export default function JobsPanel() {
jobId={selectedJobId}
onClose={() => setSelectedJobId(null)}
onReviewed={handleReviewed}
suggestions={suggestions}
/>
</AnimatePresence>
)
@@ -340,13 +352,16 @@ export default function JobsPanel() {
</motion.button>
</form>
<datalist id="jp-authors">
{suggestions.authors.map(a => <option key={a} value={a} />)}
</datalist>
<div className="grid grid-cols-3 gap-2">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className={INPUT_CLASS}>
<option value="">All statuses</option>
<option value="unreviewed">Unreviewed</option>
<option value="reviewed">Reviewed</option>
</select>
<input type="text" value={filterAuthor} onChange={e => setFilterAuthor(e.target.value)} placeholder="Author..." className={INPUT_CLASS} />
<input type="text" list="jp-authors" value={filterAuthor} onChange={e => setFilterAuthor(e.target.value)} placeholder="Author..." className={INPUT_CLASS} />
<input type="text" value={filterBook} onChange={e => setFilterBook(e.target.value)} placeholder="Book..." className={INPUT_CLASS} />
</div>