feat: Objekt inline bearbeiten in GenerateIt (Notizen + Wörter)

✏️ Button pro Objekt in der linken Sidebar öffnet Edit-Panel:
- Notizen-Textarea bearbeitbar
- Wörter: Chips mit × + neues Wort hinzufügen (Enter oder +)
- Speichern / Abbrechen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 20:50:40 +02:00
parent 5a7777f555
commit 9a32e4a39b

View File

@@ -7,6 +7,9 @@ import {
getDbObjects, getDbObjects,
getDbObjectPairs, getDbObjectPairs,
getDbObjectWords, getDbObjectWords,
updateDbObject,
addDbObjectWord,
deleteDbObjectWord,
createDbPair, createDbPair,
updateDbPair, updateDbPair,
deleteDbPair, deleteDbPair,
@@ -417,6 +420,11 @@ export default function GenerateIt() {
const [pairsLoading, setPairsLoading] = useState(false) const [pairsLoading, setPairsLoading] = useState(false)
// per-object words: objectId → DbWord[] // per-object words: objectId → DbWord[]
const [objectWords, setObjectWords] = useState<Record<string, DbWord[]>>({}) const [objectWords, setObjectWords] = useState<Record<string, DbWord[]>>({})
// inline object edit
const [editingObjId, setEditingObjId] = useState<string | null>(null)
const [editNotes, setEditNotes] = useState('')
const [editWordInput, setEditWordInput] = useState('')
const [savingObj, setSavingObj] = useState(false)
const currentPicture: DbPicture | null = const currentPicture: DbPicture | null =
currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null
@@ -491,6 +499,44 @@ export default function GenerateIt() {
.finally(() => setPairsLoading(false)) .finally(() => setPairsLoading(false))
} }
const startEditObj = (obj: DbObject) => {
setEditingObjId(obj.id)
setEditNotes(obj.user_notes ?? '')
setEditWordInput('')
}
const cancelEditObj = () => { setEditingObjId(null); setEditWordInput('') }
const saveEditObj = async (objId: string) => {
if (!token) return
setSavingObj(true)
try {
await updateDbObject(objId, { user_notes: editNotes.trim() || null }, token)
setDbObjects(prev => prev.map(o => o.id === objId ? { ...o, user_notes: editNotes.trim() || null } : o))
setEditingObjId(null)
} finally {
setSavingObj(false)
}
}
const addWordToObj = async (objId: string) => {
if (!token) return
const titel_de = editWordInput.trim()
if (!titel_de) return
const result = await addDbObjectWord(objId, { titel_de, level: 50 }, token)
if (result.ok) {
const words = await getDbObjectWords(objId, token)
setObjectWords(prev => ({ ...prev, [objId]: words }))
setEditWordInput('')
}
}
const removeWordFromObj = async (objId: string, junctionId: string | number) => {
if (!token) return
await deleteDbObjectWord(objId, junctionId, token)
setObjectWords(prev => ({ ...prev, [objId]: (prev[objId] || []).filter(w => w.junction_id !== junctionId) }))
}
const imageNav = ( const imageNav = (
<div className="image-nav"> <div className="image-nav">
<button className="btn-icon" onClick={() => setCurrentIndex(i => i - 1)} disabled={currentIndex <= 0}> <button className="btn-icon" onClick={() => setCurrentIndex(i => i - 1)} disabled={currentIndex <= 0}>
@@ -546,12 +592,62 @@ export default function GenerateIt() {
<span>{obj.selections?.length ? `${obj.selections.length} Auswahl(en)` : ''}</span> <span>{obj.selections?.length ? `${obj.selections.length} Auswahl(en)` : ''}</span>
)} )}
</div> </div>
<button
onClick={e => { e.stopPropagation(); editingObjId === obj.id ? cancelEditObj() : startEditObj(obj) }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, color: 'var(--text-2)', padding: '0 3px', flexShrink: 0 }}
title="Bearbeiten"
></button>
</div> </div>
{obj.user_notes && (
{obj.user_notes && editingObjId !== obj.id && (
<div style={{ padding: '4px 8px 6px 12px', fontSize: 11.5, color: 'var(--text-2)', lineHeight: 1.4, borderTop: '1px solid var(--border)' }}> <div style={{ padding: '4px 8px 6px 12px', fontSize: 11.5, color: 'var(--text-2)', lineHeight: 1.4, borderTop: '1px solid var(--border)' }}>
{obj.user_notes} {obj.user_notes}
</div> </div>
)} )}
{/* Inline edit panel */}
{editingObjId === obj.id && (
<div style={{ padding: '8px', borderTop: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 6 }} onClick={e => e.stopPropagation()}>
{/* Notizen */}
<textarea
value={editNotes}
onChange={e => setEditNotes(e.target.value)}
rows={3}
placeholder="Notizen…"
style={{ width: '100%', resize: 'vertical', padding: '5px 7px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12, boxSizing: 'border-box' }}
/>
{/* Wörter */}
<div>
<div style={{ fontSize: 10, color: 'var(--text-2)', marginBottom: 3 }}>Wörter</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3, marginBottom: 4 }}>
{(objectWords[obj.id] || []).map(w => (
<span key={w.junction_id} style={{ display: 'flex', alignItems: 'center', gap: 2, padding: '2px 7px', background: '#e0e7ff', color: '#3730a3', borderRadius: 9999, fontSize: 11 }}>
{w.titel_de}
<button onClick={() => removeWordFromObj(obj.id, w.junction_id!)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#818cf8', padding: 0, fontSize: 13, lineHeight: 1 }}>×</button>
</span>
))}
</div>
<div style={{ display: 'flex', gap: 4 }}>
<input
type="text"
value={editWordInput}
onChange={e => setEditWordInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') addWordToObj(obj.id) }}
placeholder="Wort hinzufügen…"
style={{ flex: 1, padding: '3px 7px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12 }}
/>
<button onClick={() => addWordToObj(obj.id)} style={{ padding: '3px 8px', borderRadius: 'var(--r-sm)', background: '#6366f1', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 12 }}>+</button>
</div>
</div>
{/* Aktionen */}
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn-primary btn-sm" style={{ flex: 1 }} onClick={() => saveEditObj(obj.id)} disabled={savingObj}>
{savingObj ? 'Speichere…' : 'Speichern'}
</button>
<button className="btn-ghost btn-sm" onClick={cancelEditObj}>Abbrechen</button>
</div>
</div>
)}
</div> </div>
))} ))}
</div> </div>