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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user