115 lines
4.1 KiB
React
115 lines
4.1 KiB
React
|
|
import { useEffect, useRef, useState } from 'react'
|
||
|
|
import { api } from '../utils/api'
|
||
|
|
|
||
|
|
export default function FoodSearchModal({ title, initialQuery, onSelect, onClose }) {
|
||
|
|
const [q, setQ] = useState(initialQuery || '')
|
||
|
|
const [hits, setHits] = useState([])
|
||
|
|
const [loading, setLoading] = useState(false)
|
||
|
|
const [error, setError] = useState(null)
|
||
|
|
const inputRef = useRef(null)
|
||
|
|
const timer = useRef(null)
|
||
|
|
|
||
|
|
const runSearch = async (term) => {
|
||
|
|
const query = (term || '').trim()
|
||
|
|
if (query.length < 2) {
|
||
|
|
setHits([])
|
||
|
|
return
|
||
|
|
}
|
||
|
|
setLoading(true)
|
||
|
|
setError(null)
|
||
|
|
try {
|
||
|
|
setHits(await api.searchBlsFoods(query, 30))
|
||
|
|
} catch (e) {
|
||
|
|
setError(e.message)
|
||
|
|
setHits([])
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
inputRef.current?.focus()
|
||
|
|
inputRef.current?.select()
|
||
|
|
if ((initialQuery || '').trim().length >= 2) runSearch(initialQuery)
|
||
|
|
const onKey = (e) => { if (e.key === 'Escape') onClose() }
|
||
|
|
window.addEventListener('keydown', onKey)
|
||
|
|
return () => {
|
||
|
|
window.removeEventListener('keydown', onKey)
|
||
|
|
clearTimeout(timer.current)
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const onChange = (value) => {
|
||
|
|
setQ(value)
|
||
|
|
clearTimeout(timer.current)
|
||
|
|
timer.current = setTimeout(() => runSearch(value), 250)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)',
|
||
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 20000,
|
||
|
|
padding: 16,
|
||
|
|
}}
|
||
|
|
onClick={onClose}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
role="dialog"
|
||
|
|
aria-modal="true"
|
||
|
|
aria-labelledby="food-search-title"
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
style={{
|
||
|
|
width: '100%', maxWidth: 520, maxHeight: 'min(88vh, 640px)',
|
||
|
|
background: 'var(--surface)', borderRadius: 16,
|
||
|
|
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
||
|
|
display: 'flex', flexDirection: 'column',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 16px', borderBottom: '1px solid var(--border)' }}>
|
||
|
|
<h2 id="food-search-title" className="card-title" style={{ margin: 0, fontSize: 16 }}>{title || 'Lebensmittel suchen'}</h2>
|
||
|
|
<button type="button" className="btn btn-secondary" onClick={onClose}>Schließen</button>
|
||
|
|
</div>
|
||
|
|
<div style={{ padding: '12px 16px' }}>
|
||
|
|
<input
|
||
|
|
ref={inputRef}
|
||
|
|
className="form-input"
|
||
|
|
style={{ width: '100%', textAlign: 'left' }}
|
||
|
|
placeholder="Name eingeben, z. B. Haferflocken"
|
||
|
|
value={q}
|
||
|
|
onChange={(e) => onChange(e.target.value)}
|
||
|
|
/>
|
||
|
|
<p style={{ fontSize: 12, color: 'var(--text3)', margin: '8px 0 0' }}>
|
||
|
|
Suche nach dem Namen. Den BLS-Code brauchst du nicht.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div style={{ overflowY: 'auto', padding: '0 16px 16px', flex: 1 }}>
|
||
|
|
{loading && <p style={{ fontSize: 13, color: 'var(--text2)' }}>Suche…</p>}
|
||
|
|
{error && <p style={{ color: 'var(--danger)', fontSize: 13 }}>{error}</p>}
|
||
|
|
{!loading && q.trim().length >= 2 && hits.length === 0 && (
|
||
|
|
<p style={{ fontSize: 13, color: 'var(--text3)' }}>Kein Treffer. Anderen Suchbegriff versuchen.</p>
|
||
|
|
)}
|
||
|
|
{hits.map((h) => (
|
||
|
|
<button
|
||
|
|
key={h.id}
|
||
|
|
type="button"
|
||
|
|
className="btn btn-secondary btn-full"
|
||
|
|
style={{ marginTop: 8, justifyContent: 'flex-start', textAlign: 'left', height: 'auto', padding: '10px 12px' }}
|
||
|
|
onClick={() => onSelect(h)}
|
||
|
|
>
|
||
|
|
<span>
|
||
|
|
<strong style={{ display: 'block' }}>{h.name_de}</strong>
|
||
|
|
<span style={{ fontSize: 12, color: 'var(--text3)' }}>
|
||
|
|
{h.bls_code ? `BLS ${h.bls_code}` : 'ohne Code'}
|
||
|
|
{h.food_group ? ` · Gruppe ${h.food_group}` : ''}
|
||
|
|
{h.catalog_kind !== 'official_bls' ? ' · manuell' : ''}
|
||
|
|
</span>
|
||
|
|
</span>
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|