- api/names.ts: suggestNames() + buildSuggestPath() + NAMEGEN_USAGES (norn/japa/mythg/ger/arb) - de.ts: namegen-Sektion (button, panelTitle, letter/usages/load/states) - NameSuggestPanel.tsx: inline-Panel mit Buchstabe/Herkunftskultur-Multiselect, Vorschlagsliste; 503 NamesKeyMissing → freundlicher Hinweis - GerbilFormPage: 'Name vorschlagen'-Button neben Namensfeld (htmlFor-Pattern), Panel schließt+befüllt bei Auswahl - vitest: 11 Tests (buildSuggestPath URL-Logik + NAMEGEN_USAGES-Struktur) - e2e: mock /names/suggest + namesConfigured-Flag; 4 Specs (Panel öffnen, Klick befüllt, Buchstabe-Filter, 503-Fallback) Gate: vitest 104/104, e2e 156/156, tsc clean
133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
import { useState } from 'react'
|
|
import { de } from '../strings/de'
|
|
import { ApiError, errorCode } from '../api/client'
|
|
import { NAMEGEN_USAGES, suggestNames, type NamegenUsageCode, type NameSuggestion } from '../api/names'
|
|
import './NameSuggestPanel.css'
|
|
|
|
interface NameSuggestPanelProps {
|
|
gender: string
|
|
onPick: (name: string) => void
|
|
onClose: () => void
|
|
}
|
|
|
|
const ALL_CODES = NAMEGEN_USAGES.map((u) => u.code) as NamegenUsageCode[]
|
|
|
|
export default function NameSuggestPanel({ gender, onPick, onClose }: NameSuggestPanelProps) {
|
|
const t = de.namegen
|
|
const [letter, setLetter] = useState('')
|
|
const [usages, setUsages] = useState<Set<NamegenUsageCode>>(new Set(ALL_CODES))
|
|
const [loading, setLoading] = useState(false)
|
|
const [keyMissing, setKeyMissing] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [suggestions, setSuggestions] = useState<NameSuggestion[]>([])
|
|
const [fetched, setFetched] = useState(false)
|
|
|
|
function toggleUsage(code: NamegenUsageCode) {
|
|
setUsages((prev) => {
|
|
const next = new Set(prev)
|
|
if (next.has(code)) next.delete(code)
|
|
else next.add(code)
|
|
return next
|
|
})
|
|
}
|
|
|
|
async function load() {
|
|
setLoading(true)
|
|
setError(null)
|
|
setKeyMissing(false)
|
|
setSuggestions([])
|
|
try {
|
|
const result = await suggestNames({
|
|
letter: letter.trim() || undefined,
|
|
gender: gender || undefined,
|
|
usages: [...usages] as NamegenUsageCode[],
|
|
count: 6,
|
|
})
|
|
setSuggestions(result)
|
|
setFetched(true)
|
|
} catch (err) {
|
|
if (errorCode(err) === 'NamesKeyMissing') {
|
|
setKeyMissing(true)
|
|
} else {
|
|
setError(err instanceof ApiError ? err.message : de.api.errors.unknown)
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="namegen-panel" role="region" aria-label={t.panelTitle}>
|
|
<div className="namegen-panel__header">
|
|
<span>{t.panelTitle}</span>
|
|
<button type="button" className="btn" onClick={onClose} aria-label={t.close}>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="namegen-panel__filters">
|
|
<div className="namegen-panel__letter">
|
|
<label htmlFor="namegen-letter">{t.letterLabel}</label>
|
|
<input
|
|
id="namegen-letter"
|
|
className="input namegen-panel__letter-input"
|
|
value={letter}
|
|
onChange={(e) => setLetter(e.target.value)}
|
|
placeholder={t.letterPlaceholder}
|
|
maxLength={1}
|
|
/>
|
|
</div>
|
|
|
|
<div className="namegen-panel__usages">
|
|
<span>{t.usagesLabel}</span>
|
|
<div className="namegen-usages-grid">
|
|
{NAMEGEN_USAGES.map(({ code, label }) => (
|
|
<label key={code}>
|
|
<input
|
|
type="checkbox"
|
|
checked={usages.has(code)}
|
|
onChange={() => toggleUsage(code)}
|
|
/>
|
|
{label}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="btn btn--primary"
|
|
onClick={load}
|
|
disabled={loading || usages.size === 0}
|
|
>
|
|
{loading ? t.loading : t.loadButton}
|
|
</button>
|
|
|
|
{keyMissing && <p className="muted">{t.keyMissing}</p>}
|
|
{error && <p className="error-text">{error}</p>}
|
|
{fetched && !loading && !keyMissing && !error && suggestions.length === 0 && (
|
|
<p className="muted">{t.empty}</p>
|
|
)}
|
|
|
|
{suggestions.length > 0 && (
|
|
<ul className="namegen-suggestions" aria-label={t.panelTitle}>
|
|
{suggestions.map((s, i) => (
|
|
<li key={`${s.name}-${i}`} className="namegen-suggestion">
|
|
<button
|
|
type="button"
|
|
className="namegen-suggestion__pick"
|
|
onClick={() => onPick(s.name)}
|
|
>
|
|
{s.name}
|
|
</button>
|
|
<span className="namegen-suggestion__meaning">{s.meaning}</span>
|
|
<span className="namegen-suggestion__origin">{s.origin}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|