Files
hejyou_content_creation/frontend/src/pages/DrawIt.tsx
admin d02788bd0e feat: CRM-Dashboard, Content-Verwaltung und Wort-Autocomplete
- Home-Seite nach Login mit Begrüßung und 3 Kacheln (Content erstellen, Content verwalten, User verwalten)
- AuthContext speichert User-Profil + Rolle; AdminRoute blockt Nicht-Admins
- Content verwalten (admin-only): Status-Dashboard pro Collection, Liste/Kachel-View, generisches Edit-Formular
- Nur aktive db_-Collections im Dashboard (alte pictures/objects/words/questions entfernt)
- Wort-Autocomplete in DrawIt: ab dem ersten Buchstaben Vorschläge aus db_words, Tastatur-Navigation, Duplikat-Filter
- Backend: /users/me Proxy, db-words/search Endpoint, generische Collection-Endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 23:37:48 +02:00

683 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback, useRef } from 'react'
import DrawCanvas, { type DrawCanvasHandle } from '../components/DrawCanvas'
import BlurhashCanvas from '../components/BlurhashCanvas'
import Topbar from '../components/Topbar'
import WordAutocomplete from '../components/WordAutocomplete'
import {
getDbPictures,
updateDbPicture,
updateDbPictureStatus,
deleteDbPicture,
getDbObjects,
createDbObject,
updateDbObject,
deleteDbObject,
getDbObjectWords,
addDbObjectWord,
deleteDbObjectWord,
getDbPictureWords,
addDbPictureWord,
deleteDbPictureWord,
directusAssetUrl,
getDesignOptions,
} from '../api'
import { useAuth } from '../context/AuthContext'
import type { DbPicture, DbObject, DbWord, Selection, CanvasObject } from '../types'
const ChevronLeftIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6" />
</svg>
)
const ChevronRightIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6" />
</svg>
)
const TrashIcon = () => (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14H6L5 6" /><path d="M10 11v6M14 11v6" /><path d="M9 6V4h6v2" />
</svg>
)
export default function DrawIt() {
const { token } = useAuth()
const [pictureList, setPictureList] = useState<DbPicture[]>([])
const [currentIndex, setCurrentIndex] = useState(-1)
const [debouncedIndex, setDebouncedIndex] = useState(-1)
const [objects, setObjects] = useState<DbObject[]>([])
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null)
const [currentSelections, setCurrentSelections] = useState<Selection[]>([])
const [userNotes, setUserNotes] = useState('')
// per-object words: objectId → DbWord[]
const [objectWords, setObjectWords] = useState<Record<string, DbWord[]>>({})
// per-object word input values: objectId → current input text
const [wordInputs, setWordInputs] = useState<Record<string, string>>({})
const [pendingWords, setPendingWords] = useState<string[]>([])
const [newWordInput, setNewWordInput] = useState('')
const [designOptions, setDesignOptions] = useState<{ text: string; value: string }[]>([])
const [editingNotes, setEditingNotes] = useState<{ id: string; notes: string } | null>(null)
const [mode, setMode] = useState<'rect' | 'polygon'>('polygon')
const [hasSelection, setHasSelection] = useState(false)
const [saving, setSaving] = useState(false)
const [finishing, setFinishing] = useState(false)
const [deleting, setDeleting] = useState(false)
const [statusMsg, setStatusMsg] = useState('')
const [statusError, setStatusError] = useState(false)
const [imageLoaded, setImageLoaded] = useState(false)
const [pictureWords, setPictureWords] = useState<DbWord[]>([])
const [pictureWordInput, setPictureWordInput] = useState('')
const canvasRef = useRef<DrawCanvasHandle>(null)
// Debounce: only load picture data after 350ms of no navigation
useEffect(() => {
const t = setTimeout(() => setDebouncedIndex(currentIndex), 350)
return () => clearTimeout(t)
}, [currentIndex])
// Reset imageLoaded immediately on navigation so blurhash shows right away
useEffect(() => {
setImageLoaded(false)
}, [currentIndex])
const currentPicture: DbPicture | null =
debouncedIndex >= 0 && debouncedIndex < pictureList.length ? pictureList[debouncedIndex] : null
const canvasObjects: CanvasObject[] = objects.map((obj, i) => ({
id: obj.id,
visible: obj.visible !== false,
selections: obj.selections,
index: i + 1,
hierarchy: 1,
}))
// Load db_pictures with status=draft
useEffect(() => {
if (!token) return
getDbPictures(token, 'draft')
.then(pics => { setPictureList(pics); setCurrentIndex(pics.length > 0 ? 0 : -1) })
.catch(console.error)
}, [token])
// Load design options once on mount
useEffect(() => {
if (!token) return
getDesignOptions(token).then(setDesignOptions).catch(console.error)
}, [token])
// Load objects when picture changes, then load each object's words and picture words
useEffect(() => {
if (!currentPicture || !token) {
setObjects([]); setSelectedObjectId(null)
setObjectWords({})
setWordInputs({})
setImageLoaded(false)
setPictureWords([])
setPictureWordInput('')
return
}
getDbObjects(currentPicture.id, token)
.then(objs => {
setObjects(objs.map(o => ({ ...o, visible: true })))
setSelectedObjectId(null)
// Load words for each object
const newWords: Record<string, DbWord[]> = {}
const promises = objs.map(obj =>
getDbObjectWords(obj.id, token)
.then(words => { newWords[obj.id] = words })
.catch(() => { newWords[obj.id] = [] })
)
Promise.all(promises).then(() => {
setObjectWords(newWords)
})
})
.catch(console.error)
// Load picture-level words
getDbPictureWords(currentPicture.id, token)
.then(words => setPictureWords(words))
.catch(() => setPictureWords([]))
}, [currentPicture?.id, token])
const showStatus = (msg: string, isError = false) => {
setStatusMsg(msg); setStatusError(isError)
}
const handleHasSelection = useCallback((has: boolean) => setHasSelection(has), [])
const handleAddObjectWord = async (objId: string) => {
if (!token) return
const titel_de = (wordInputs[objId] || '').trim()
if (!titel_de) return
try {
const result = await addDbObjectWord(objId, { titel_de, level: 50 }, token)
if (result.ok) {
const words = await getDbObjectWords(objId, token)
setObjectWords(prev => ({ ...prev, [objId]: words }))
setWordInputs(prev => ({ ...prev, [objId]: '' }))
}
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler beim Hinzufügen des Worts.', true)
}
}
const handleAddPendingWord = () => {
const w = newWordInput.trim()
if (!w || pendingWords.includes(w)) return
setPendingWords(prev => [...prev, w])
setNewWordInput('')
}
const handleRemoveObjectWord = async (objId: string, junctionId: string | number) => {
if (!token) return
try {
await deleteDbObjectWord(objId, junctionId, token)
setObjectWords(prev => ({
...prev,
[objId]: (prev[objId] || []).filter(w => w.junction_id !== junctionId)
}))
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler beim Entfernen des Worts.', true)
}
}
const addSelection = () => {
const sel = canvasRef.current?.getCurrentSelection()
if (!sel) { showStatus('Bitte zuerst einen Bereich auswählen.', true); return }
setCurrentSelections(prev => { const next = [...prev, sel]; showStatus(`Auswahl ${next.length} hinzugefügt.`); return next })
canvasRef.current?.resetSelection()
setHasSelection(false)
}
const saveObject = async () => {
if (!currentPicture || !token || currentSelections.length === 0) return
setSaving(true)
try {
const obj = await createDbObject({
picture: currentPicture.id,
selections: currentSelections,
user_notes: userNotes.trim() || null,
}, token)
// Save pending words to the new object
const savedWords: DbWord[] = []
for (const w of pendingWords) {
try {
const result = await addDbObjectWord(obj.id, { titel_de: w, level: 50 }, token)
if (result.ok) {
savedWords.push({ junction_id: result.junction_id, word_id: result.word_id, titel_de: w, level: 50, status: 'draft' })
}
} catch {}
}
setObjects(prev => [...prev, { ...obj, visible: true }])
setObjectWords(prev => ({ ...prev, [obj.id]: savedWords }))
setCurrentSelections([])
setUserNotes('')
setPendingWords([])
setNewWordInput('')
canvasRef.current?.resetSelection()
showStatus('Objekt gespeichert.')
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler beim Speichern.', true)
} finally {
setSaving(false)
}
}
// Mark picture as objects_created and remove from list
const finishPicture = async () => {
if (!currentPicture || !token) return
setFinishing(true)
try {
// Save status + current design in one call to avoid race conditions
await updateDbPicture(currentPicture.id, {
status: 'objects_created',
design: currentPicture.design ?? null,
}, token)
setPictureList(prev => prev.filter(p => p.id !== currentPicture.id))
setCurrentIndex(i => Math.max(0, i - 1))
setObjects([])
showStatus('Bild fertiggestellt.')
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler.', true)
} finally {
setFinishing(false)
}
}
const saveNoteEdit = async () => {
if (!editingNotes || !token) return
try {
await updateDbObject(editingNotes.id, { user_notes: editingNotes.notes }, token)
setObjects(prev => prev.map(o => o.id === editingNotes.id ? { ...o, user_notes: editingNotes.notes } : o))
setEditingNotes(null)
showStatus('Notizen gespeichert.')
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler.', true)
}
}
const handleAddPictureWord = async () => {
if (!token || !currentPicture) return
const titel_de = pictureWordInput.trim()
if (!titel_de) return
try {
await addDbPictureWord(currentPicture.id, { titel_de, level: 50 }, token)
const words = await getDbPictureWords(currentPicture.id, token)
setPictureWords(words)
setPictureWordInput('')
} catch (e) { showStatus('Fehler beim Hinzufügen.', true) }
}
const handleRemovePictureWord = async (junctionId: string | number) => {
if (!token || !currentPicture) return
try {
await deleteDbPictureWord(currentPicture.id, junctionId, token)
setPictureWords(prev => prev.filter(w => w.junction_id !== junctionId))
} catch (e) { showStatus('Fehler beim Entfernen.', true) }
}
const deleteObject = async (objId: string) => {
if (!token) return
try {
await deleteDbObject(objId, token)
setObjects(prev => prev.filter(o => o.id !== objId))
setObjectWords(prev => { const n = { ...prev }; delete n[objId]; return n })
setWordInputs(prev => { const n = { ...prev }; delete n[objId]; return n })
if (selectedObjectId === objId) setSelectedObjectId(null)
showStatus('Objekt gelöscht.')
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler beim Löschen.', true)
}
}
const deleteCurrentPicture = async () => {
if (!token || !currentPicture) return
if (!window.confirm('Bild wirklich löschen? Der Eintrag und die Datei werden dauerhaft aus Directus entfernt.')) return
setDeleting(true)
try {
await deleteDbPicture(currentPicture.id, token)
const newList = pictureList.filter(p => p.id !== currentPicture.id)
setPictureList(newList)
const nextIndex = Math.min(currentIndex, newList.length - 1)
setCurrentIndex(nextIndex)
showStatus('Bild gelöscht.')
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler beim Löschen.', true)
} finally {
setDeleting(false)
}
}
const imageNav = (
<div className="image-nav">
<button className="btn-icon" onClick={() => setCurrentIndex(i => i - 1)} disabled={currentIndex <= 0}>
<ChevronLeftIcon />
</button>
<span className="image-counter">
{pictureList.length > 0
? <><span className="image-counter-num">{currentIndex + 1}</span><span className="image-counter-sep">/</span><span className="image-counter-total">{pictureList.length}</span></>
: <span className="image-counter-empty">Keine Bilder</span>}
</span>
<button className="btn-icon" onClick={() => setCurrentIndex(i => i + 1)} disabled={currentIndex >= pictureList.length - 1}>
<ChevronRightIcon />
</button>
<button
className="btn-icon"
onClick={deleteCurrentPicture}
disabled={deleting || !currentPicture}
title="Bild löschen"
style={{ color: 'var(--error, #dc2626)', marginLeft: 8 }}
>
<TrashIcon />
</button>
</div>
)
return (
<div className="app-shell">
<Topbar page="draw" center={imageNav} />
<div className="workspace">
{/* Left sidebar: saved objects */}
<aside className="sidebar">
{currentPicture && (
<div className="sidebar-panel">
<h3 className="sidebar-heading">Hauptwörter</h3>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 6 }}>
{pictureWords.length === 0 && (
<span style={{ fontSize: 11, color: 'var(--text-2)' }}>Noch keine Hauptwörter</span>
)}
{pictureWords.map(w => (
<span key={w.junction_id} style={{
display: 'flex', alignItems: 'center', gap: 3,
padding: '2px 8px', background: '#dcfce7', color: '#166534',
borderRadius: 9999, fontSize: 11,
}}>
{w.titel_de}
<button
onClick={() => handleRemovePictureWord(w.junction_id!)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#16a34a', padding: 0, fontSize: 13 }}
>×</button>
</span>
))}
</div>
<div style={{ display: 'flex', gap: 4 }}>
<WordAutocomplete
value={pictureWordInput}
onChange={setPictureWordInput}
onSubmit={handleAddPictureWord}
token={token}
placeholder="Hauptwort hinzufügen…"
excludeTitles={pictureWords.map(w => w.titel_de)}
inputStyle={{ flex: 1, padding: '4px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12, width: '100%' }}
/>
<button
onClick={handleAddPictureWord}
style={{ padding: '4px 10px', borderRadius: 'var(--r-sm)', background: '#16a34a', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 12 }}
>+</button>
</div>
</div>
)}
<div className="sidebar-panel" style={{ flex: 1 }}>
<h3 className="sidebar-heading">
Objekte
{objects.length > 0 && <span className="badge">{objects.length}</span>}
</h3>
{objects.length === 0 ? (
<div className="empty-state">Noch keine Objekte für dieses Bild.</div>
) : (
<div className="objects-list">
{objects.map((obj, i) => (
<div
key={obj.id}
className={`object-item${selectedObjectId === obj.id ? ' selected' : ''}`}
onClick={() => setSelectedObjectId(obj.id === selectedObjectId ? null : obj.id)}
>
<div className="object-item-header">
<input
type="checkbox"
checked={obj.visible !== false}
onClick={e => e.stopPropagation()}
onChange={e => setObjects(prev => prev.map(o => o.id === obj.id ? { ...o, visible: e.target.checked } : o))}
/>
<div className="object-item-text">
<strong>Objekt {i + 1}</strong>
<span>{obj.selections?.length ? `${obj.selections.length} Auswahl(en)` : ''}</span>
</div>
<button
className="object-icon-button"
onClick={e => { e.stopPropagation(); setEditingNotes({ id: obj.id, notes: obj.user_notes ?? '' }) }}
title="Notizen bearbeiten"
></button>
<button
className="object-icon-button"
onClick={e => { e.stopPropagation(); deleteObject(obj.id) }}
title="Löschen"
><TrashIcon /></button>
</div>
{obj.user_notes && (
<div style={{ padding: '4px 8px 6px 32px', fontSize: 11.5, color: 'var(--text-2)', lineHeight: 1.4, borderTop: '1px solid var(--border)' }}>
{obj.user_notes}
</div>
)}
{editingNotes?.id === obj.id && (
<div style={{ padding: '8px', borderTop: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 6 }} onClick={e => e.stopPropagation()}>
<textarea
value={editingNotes.notes}
onChange={e => setEditingNotes({ ...editingNotes, notes: e.target.value })}
rows={3}
style={{ width: '100%', resize: 'vertical', padding: '6px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12 }}
placeholder="Notizen zum Objekt…"
autoFocus
/>
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn-primary btn-sm" style={{ flex: 1 }} onClick={saveNoteEdit}>Speichern</button>
<button className="btn-ghost btn-sm" onClick={() => setEditingNotes(null)}>Abbrechen</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
{currentPicture && (
<div className="sidebar-panel">
<h3 className="sidebar-heading">Design</h3>
<select
value={currentPicture.design || ''}
onChange={async e => {
const value = e.target.value
const design = value || null
setPictureList(prev => prev.map(p => p.id === currentPicture.id ? { ...p, design } : p))
try {
await updateDbPicture(currentPicture.id, { design }, token!)
showStatus('Design gespeichert.')
} catch (e) {
showStatus('Fehler beim Speichern des Designs.', true)
}
}}
style={{
width: '100%', padding: '6px 8px', borderRadius: 'var(--r-sm)',
border: '1px solid var(--border)', background: 'var(--surface-2)',
color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 13, cursor: 'pointer',
}}
>
<option value=""> kein Design </option>
{designOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.text}</option>
))}
</select>
</div>
)}
{currentPicture && (
<div className="sidebar-panel">
<button
className="btn-primary btn-sm btn-block"
onClick={finishPicture}
disabled={finishing || objects.length === 0}
style={{ background: objects.length > 0 ? 'var(--success, #16a34a)' : undefined, opacity: objects.length === 0 ? 0.5 : 1 }}
>
{finishing ? 'Wird fertiggestellt…' : '✅ Fertigstellen'}
</button>
</div>
)}
</aside>
{/* Center: Canvas */}
<main className="canvas-area">
<div
className="canvas-frame"
style={{ position: 'relative', background: 'var(--surface-2)' }}
>
{/* Blurhash-Platzhalter: sichtbar solange das echte Bild noch lädt */}
{currentPicture?.blurhash && !imageLoaded && (
<BlurhashCanvas
hash={currentPicture.blurhash}
width={32}
height={32}
style={{ zIndex: 1 }}
/>
)}
<DrawCanvas
ref={canvasRef}
imageSrc={currentPicture && token ? directusAssetUrl(currentPicture.picture, token) : null}
objects={canvasObjects}
selectedObjectId={selectedObjectId}
mode={mode}
onHasSelection={handleHasSelection}
onImageLoad={() => setImageLoaded(true)}
/>
</div>
</main>
{/* Right sidebar: drawing tools */}
<aside className="sidebar sidebar--right">
<div className="sidebar-panel">
<h3 className="sidebar-heading">Modus</h3>
<div className="mode-group">
<label className={`mode-btn${mode === 'rect' ? ' active' : ''}`}>
<input type="radio" name="mode" value="rect" checked={mode === 'rect'} onChange={() => setMode('rect')} />
<span>Rechteck</span>
</label>
<label className={`mode-btn${mode === 'polygon' ? ' active' : ''}`}>
<input type="radio" name="mode" value="polygon" checked={mode === 'polygon'} onChange={() => setMode('polygon')} />
<span>Polygon</span>
</label>
</div>
<div className="action-group">
{mode === 'polygon' && (
<button
className="btn-primary btn-sm btn-block"
onClick={() => canvasRef.current?.closePolygon()}
disabled={!hasSelection}
>
Polygon schließen
</button>
)}
<button className="btn-ghost btn-sm btn-block" onClick={() => canvasRef.current?.resetSelection()}>
Auswahl zurücksetzen
</button>
</div>
</div>
<div className="sidebar-panel">
<h3 className="sidebar-heading">Notizen zum Objekt</h3>
<textarea
value={userNotes}
onChange={e => setUserNotes(e.target.value)}
rows={4}
placeholder="Beschreibung, Besonderheiten, Kontext…"
style={{
width: '100%', resize: 'vertical', padding: '7px 10px',
borderRadius: 'var(--r-md)', border: '1px solid var(--border)',
background: 'var(--surface-2)', color: 'var(--text-1)',
fontFamily: 'var(--font)', fontSize: 13, lineHeight: 1.5,
transition: 'border-color .15s, box-shadow .15s', outline: 'none',
}}
onFocus={e => { e.target.style.borderColor = 'var(--primary)'; e.target.style.boxShadow = '0 0 0 3px rgba(92,108,246,.12)' }}
onBlur={e => { e.target.style.borderColor = 'var(--border)'; e.target.style.boxShadow = 'none' }}
/>
</div>
<div className="sidebar-panel">
<h3 className="sidebar-heading">
Auswahlen
{currentSelections.length > 0 && <span className="badge">{currentSelections.length}</span>}
</h3>
<div className="selections-list">
{currentSelections.length === 0
? <div className="empty-state">Noch keine Auswahlen</div>
: currentSelections.map((sel, i) => (
<div className="selection-chip" key={i}>
<span className="selection-chip-num">{i + 1}</span>
<span className="selection-chip-type">{sel.mode === 'rect' ? 'Rect' : 'Poly'}</span>
<span className="selection-chip-info">
{sel.mode === 'rect' && sel.bbox
? `${Math.round(sel.bbox.width)}×${Math.round(sel.bbox.height)}`
: `${sel.polygon?.length ?? 0} Pkt.`}
</span>
</div>
))}
</div>
<div className="action-group">
<button className="btn-primary btn-sm btn-block" onClick={addSelection} disabled={!hasSelection || !currentPicture}>
+ Auswahl hinzufügen
</button>
<button
className="btn-primary btn-sm btn-block"
onClick={saveObject}
disabled={!currentPicture || currentSelections.length === 0 || saving}
>
{saving ? 'Speichere…' : `${currentSelections.length > 1 ? currentSelections.length + ' Objekte' : 'Objekt'} speichern`}
</button>
<button
className="btn-ghost btn-sm btn-block btn-danger"
onClick={() => { setCurrentSelections([]); canvasRef.current?.resetSelection(); showStatus('Alle Auswahlen gelöscht.') }}
>
Alle löschen
</button>
</div>
{statusMsg && <div className={`status-msg ${statusError ? 'error' : 'ok'}`}>{statusMsg}</div>}
</div>
<div className="sidebar-panel">
<h3 className="sidebar-heading">
Wörter
{selectedObjectId
? ` (Objekt ${objects.findIndex(o => o.id === selectedObjectId) + 1})`
: ' (neues Objekt)'}
</h3>
{/* Existing word chips for selected object */}
{selectedObjectId && (
<div style={{ display:'flex', flexWrap:'wrap', gap:4, marginBottom:6 }}>
{(objectWords[selectedObjectId] || []).map(w => (
<span key={w.junction_id} style={{
display:'flex', alignItems:'center', gap:3,
padding:'2px 8px', background:'#e0e7ff', color:'#3730a3',
borderRadius:9999, fontSize:11,
}}>
{w.titel_de}
<button onClick={() => handleRemoveObjectWord(selectedObjectId, w.junction_id!)}
style={{ background:'none', border:'none', cursor:'pointer', color:'#818cf8', padding:0, fontSize:13 }}>×</button>
</span>
))}
{(objectWords[selectedObjectId] || []).length === 0 && (
<span style={{ fontSize:11, color:'var(--text-2)' }}>Noch keine Wörter</span>
)}
</div>
)}
{/* Pending words for new object */}
{!selectedObjectId && pendingWords.length > 0 && (
<div style={{ display:'flex', flexWrap:'wrap', gap:4, marginBottom:6 }}>
{pendingWords.map((w, i) => (
<span key={i} style={{
display:'flex', alignItems:'center', gap:3,
padding:'2px 8px', background:'#fef3c7', color:'#92400e',
borderRadius:9999, fontSize:11,
}}>
{w}
<button onClick={() => setPendingWords(prev => prev.filter((_, j) => j !== i))}
style={{ background:'none', border:'none', cursor:'pointer', color:'#d97706', padding:0, fontSize:13 }}>×</button>
</span>
))}
</div>
)}
{/* Add word input */}
<div style={{ display:'flex', gap:4 }}>
<WordAutocomplete
value={selectedObjectId ? (wordInputs[selectedObjectId] || '') : newWordInput}
onChange={v => selectedObjectId
? setWordInputs(prev => ({ ...prev, [selectedObjectId]: v }))
: setNewWordInput(v)}
onSubmit={() => selectedObjectId ? handleAddObjectWord(selectedObjectId) : handleAddPendingWord()}
token={token}
placeholder="Wort hinzufügen…"
excludeTitles={selectedObjectId
? (objectWords[selectedObjectId] || []).map(w => w.titel_de)
: pendingWords}
inputStyle={{ flex:1, padding:'4px 8px', borderRadius:'var(--r-sm)', border:'1px solid var(--border)', background:'var(--surface-2)', color:'var(--text-1)', fontFamily:'var(--font)', fontSize:12, width: '100%' }}
/>
<button
onClick={() => selectedObjectId ? handleAddObjectWord(selectedObjectId) : handleAddPendingWord()}
style={{ padding:'4px 10px', borderRadius:'var(--r-sm)', background:'#6366f1', color:'#fff', border:'none', cursor:'pointer', fontSize:12 }}
>+</button>
</div>
</div>
</aside>
</div>
</div>
)
}