Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
244 lines
7.7 KiB
TypeScript
244 lines
7.7 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* ThemenSuche - Autocomplete search for Abitur themes
|
|
* Features debounced API calls, suggestion display, and keyboard navigation
|
|
*/
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
import { Search, X, Loader2 } from 'lucide-react'
|
|
import type { ThemaSuggestion } from '@/lib/education/abitur-archiv-types'
|
|
import { POPULAR_THEMES } from '@/lib/education/abitur-archiv-types'
|
|
|
|
interface ThemenSucheProps {
|
|
onSearch: (query: string) => void
|
|
onClear: () => void
|
|
placeholder?: string
|
|
}
|
|
|
|
export function ThemenSuche({
|
|
onSearch,
|
|
onClear,
|
|
placeholder = 'Thema suchen (z.B. Gedichtanalyse, Eroerterung, Drama...)'
|
|
}: ThemenSucheProps) {
|
|
const [query, setQuery] = useState('')
|
|
const [suggestions, setSuggestions] = useState<ThemaSuggestion[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [showDropdown, setShowDropdown] = useState(false)
|
|
const [selectedIndex, setSelectedIndex] = useState(-1)
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
|
|
// Debounced API call for suggestions
|
|
useEffect(() => {
|
|
const timer = setTimeout(async () => {
|
|
if (query.length >= 2) {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch(`/api/education/abitur-archiv/suggest?q=${encodeURIComponent(query)}`)
|
|
const data = await res.json()
|
|
setSuggestions(data.suggestions || [])
|
|
setShowDropdown(true)
|
|
} catch (error) {
|
|
console.error('Suggest error:', error)
|
|
// Fallback to popular themes
|
|
setSuggestions(POPULAR_THEMES.filter(t =>
|
|
t.label.toLowerCase().includes(query.toLowerCase())
|
|
))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
} else if (query.length === 0) {
|
|
setSuggestions(POPULAR_THEMES)
|
|
} else {
|
|
setSuggestions([])
|
|
}
|
|
}, 300)
|
|
|
|
return () => clearTimeout(timer)
|
|
}, [query])
|
|
|
|
// Close dropdown when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(e.target as Node) &&
|
|
inputRef.current &&
|
|
!inputRef.current.contains(e.target as Node)
|
|
) {
|
|
setShowDropdown(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
}, [])
|
|
|
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
if (!showDropdown || suggestions.length === 0) return
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault()
|
|
setSelectedIndex(prev => Math.min(prev + 1, suggestions.length - 1))
|
|
break
|
|
case 'ArrowUp':
|
|
e.preventDefault()
|
|
setSelectedIndex(prev => Math.max(prev - 1, -1))
|
|
break
|
|
case 'Enter':
|
|
e.preventDefault()
|
|
if (selectedIndex >= 0) {
|
|
handleSelectSuggestion(suggestions[selectedIndex])
|
|
} else if (query.trim()) {
|
|
handleSearch()
|
|
}
|
|
break
|
|
case 'Escape':
|
|
setShowDropdown(false)
|
|
setSelectedIndex(-1)
|
|
break
|
|
}
|
|
}, [showDropdown, suggestions, selectedIndex, query])
|
|
|
|
const handleSelectSuggestion = (suggestion: ThemaSuggestion) => {
|
|
setQuery(suggestion.label)
|
|
setShowDropdown(false)
|
|
setSelectedIndex(-1)
|
|
onSearch(suggestion.label)
|
|
}
|
|
|
|
const handleSearch = () => {
|
|
if (query.trim()) {
|
|
onSearch(query.trim())
|
|
setShowDropdown(false)
|
|
}
|
|
}
|
|
|
|
const handleClear = () => {
|
|
setQuery('')
|
|
setSuggestions(POPULAR_THEMES)
|
|
setShowDropdown(false)
|
|
setSelectedIndex(-1)
|
|
onClear()
|
|
inputRef.current?.focus()
|
|
}
|
|
|
|
const handleFocus = () => {
|
|
if (query.length === 0) {
|
|
setSuggestions(POPULAR_THEMES)
|
|
}
|
|
setShowDropdown(true)
|
|
}
|
|
|
|
return (
|
|
<div className="relative">
|
|
{/* Search Input */}
|
|
<div className="relative flex items-center">
|
|
<div className="absolute left-4 text-slate-400">
|
|
{loading ? (
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
) : (
|
|
<Search className="w-5 h-5" />
|
|
)}
|
|
</div>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => {
|
|
setQuery(e.target.value)
|
|
setSelectedIndex(-1)
|
|
}}
|
|
onKeyDown={handleKeyDown}
|
|
onFocus={handleFocus}
|
|
placeholder={placeholder}
|
|
className="w-full pl-12 pr-24 py-3 text-lg border border-slate-300 rounded-xl
|
|
focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
|
bg-white shadow-sm"
|
|
/>
|
|
<div className="absolute right-2 flex items-center gap-2">
|
|
{query && (
|
|
<button
|
|
onClick={handleClear}
|
|
className="p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg"
|
|
title="Suche loeschen"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={handleSearch}
|
|
disabled={!query.trim()}
|
|
className="px-4 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700
|
|
disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
|
|
>
|
|
Suchen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Suggestions Dropdown */}
|
|
{showDropdown && suggestions.length > 0 && (
|
|
<div
|
|
ref={dropdownRef}
|
|
className="absolute top-full left-0 right-0 mt-2 bg-white rounded-xl border border-slate-200
|
|
shadow-lg z-50 max-h-80 overflow-y-auto"
|
|
>
|
|
<div className="p-2">
|
|
{query.length === 0 && (
|
|
<div className="px-3 py-2 text-xs font-medium text-slate-500 uppercase tracking-wide">
|
|
Beliebte Themen
|
|
</div>
|
|
)}
|
|
{suggestions.map((suggestion, index) => (
|
|
<button
|
|
key={`${suggestion.aufgabentyp}-${suggestion.label}`}
|
|
onClick={() => handleSelectSuggestion(suggestion)}
|
|
className={`w-full px-3 py-2.5 text-left rounded-lg flex items-center justify-between
|
|
transition-colors ${
|
|
index === selectedIndex
|
|
? 'bg-blue-50 text-blue-700'
|
|
: 'hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Search className="w-4 h-4 text-slate-400" />
|
|
<div>
|
|
<div className="font-medium text-slate-800">{suggestion.label}</div>
|
|
{suggestion.kategorie && (
|
|
<div className="text-xs text-slate-500">{suggestion.kategorie}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<span className="text-sm text-slate-400">
|
|
{suggestion.count} Dokumente
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Quick Theme Tags */}
|
|
{!showDropdown && query.length === 0 && (
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
<span className="text-sm text-slate-500">Vorschlaege:</span>
|
|
{POPULAR_THEMES.slice(0, 5).map((theme) => (
|
|
<button
|
|
key={theme.aufgabentyp}
|
|
onClick={() => handleSelectSuggestion(theme)}
|
|
className="px-3 py-1 text-sm bg-slate-100 text-slate-700 rounded-full
|
|
hover:bg-slate-200 transition-colors"
|
|
>
|
|
{theme.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|