fix: blurhash cache miss, design dropdown visibility + add Hauptwörter panel

- DrawCanvas: call onImageLoad for cached images (fixes permanent blur)
- DrawIt: reset imageLoaded immediately on navigation (shows blur right away)
- Design dropdown: always visible when picture loaded, no longer gated on designOptions or objects count
- Fertigstellen: always visible when picture loaded, disabled when no objects
- Hauptwörter: new panel above object list for picture-level words (db_words_db_pictures)
- Backend: DELETE /api/directus/db-pictures/<id>/words/<junction_id>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 18:47:14 +02:00
parent 17918a414b
commit 5424fea8e1
4 changed files with 114 additions and 6 deletions

View File

@@ -498,6 +498,33 @@ export async function deleteDbObjectWord(
return res.json()
}
// Add a single word to a picture
export async function addDbPictureWord(
pictureId: string,
word: { titel_de: string; level: number },
token: string
) {
const res = await fetch(`/api/directus/db-pictures/${pictureId}/words`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: token },
body: JSON.stringify({ words: [word] }),
})
return res.json()
}
// Remove a single word from a picture by junction ID
export async function deleteDbPictureWord(
pictureId: string,
junctionId: string | number,
token: string
) {
const res = await fetch(`/api/directus/db-pictures/${pictureId}/words/${junctionId}`, {
method: 'DELETE',
headers: { Authorization: token },
})
return res.json()
}
export async function updateDbPair(
pairId: string,
payload: {

View File

@@ -272,6 +272,7 @@ export default forwardRef<DrawCanvasHandle, Props>(function DrawCanvas(
const cached = imageCache.get(imageSrc)
if (cached) {
applyImage(cached)
onImageLoad?.() // ← notify parent so blurhash hides for cached images
return
}

View File

@@ -12,6 +12,9 @@ import {
getDbObjectWords,
addDbObjectWord,
deleteDbObjectWord,
getDbPictureWords,
addDbPictureWord,
deleteDbPictureWord,
directusAssetUrl,
getDesignOptions,
} from '../api'
@@ -59,6 +62,8 @@ export default function DrawIt() {
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)
@@ -68,6 +73,11 @@ export default function DrawIt() {
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
@@ -93,13 +103,15 @@ export default function DrawIt() {
getDesignOptions(token).then(setDesignOptions).catch(console.error)
}, [token])
// Load objects when picture changes, then load each object's word
// 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)
@@ -118,6 +130,10 @@ export default function DrawIt() {
})
})
.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) => {
@@ -233,6 +249,26 @@ export default function DrawIt() {
}
}
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 {
@@ -270,6 +306,44 @@ export default function DrawIt() {
<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 }}>
<input
type="text"
value={pictureWordInput}
onChange={e => setPictureWordInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleAddPictureWord() }}
placeholder="Hauptwort hinzufügen…"
style={{ 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 }}
/>
<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
@@ -338,14 +412,13 @@ export default function DrawIt() {
)}
</div>
{currentPicture && designOptions.length > 0 && (
{currentPicture && (
<div className="sidebar-panel">
<h3 className="sidebar-heading">Design</h3>
<select
value={currentPicture.design || ''}
onChange={async e => {
const value = e.target.value
// Optimistic update
setPictureList(prev => prev.map(p => p.id === currentPicture.id ? { ...p, design: value || null } : p))
try {
await fetch(`/api/directus/db-pictures/${currentPicture.id}`, {
@@ -369,13 +442,13 @@ export default function DrawIt() {
</div>
)}
{objects.length > 0 && (
{currentPicture && (
<div className="sidebar-panel">
<button
className="btn-primary btn-sm btn-block"
onClick={finishPicture}
disabled={finishing}
style={{ background: 'var(--success, #16a34a)' }}
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>