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:
7
app.py
7
app.py
@@ -1814,6 +1814,13 @@ def directus_db_picture_words(pic_id):
|
|||||||
return jsonify({"ok": True, "saved": saved})
|
return jsonify({"ok": True, "saved": saved})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/directus/db-pictures/<pic_id>/words/<junction_id>", methods=["DELETE"])
|
||||||
|
def directus_db_picture_word_delete(pic_id, junction_id):
|
||||||
|
token = request.headers.get("Authorization", "")
|
||||||
|
_directus("DELETE", f"/items/db_words_db_pictures/{junction_id}", token)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/directus/db-objects/<obj_id>/pairs", methods=["GET", "POST"])
|
@app.route("/api/directus/db-objects/<obj_id>/pairs", methods=["GET", "POST"])
|
||||||
def directus_db_object_pairs(obj_id):
|
def directus_db_object_pairs(obj_id):
|
||||||
token = request.headers.get("Authorization", "")
|
token = request.headers.get("Authorization", "")
|
||||||
|
|||||||
@@ -498,6 +498,33 @@ export async function deleteDbObjectWord(
|
|||||||
return res.json()
|
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(
|
export async function updateDbPair(
|
||||||
pairId: string,
|
pairId: string,
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ export default forwardRef<DrawCanvasHandle, Props>(function DrawCanvas(
|
|||||||
const cached = imageCache.get(imageSrc)
|
const cached = imageCache.get(imageSrc)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
applyImage(cached)
|
applyImage(cached)
|
||||||
|
onImageLoad?.() // ← notify parent so blurhash hides for cached images
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
getDbObjectWords,
|
getDbObjectWords,
|
||||||
addDbObjectWord,
|
addDbObjectWord,
|
||||||
deleteDbObjectWord,
|
deleteDbObjectWord,
|
||||||
|
getDbPictureWords,
|
||||||
|
addDbPictureWord,
|
||||||
|
deleteDbPictureWord,
|
||||||
directusAssetUrl,
|
directusAssetUrl,
|
||||||
getDesignOptions,
|
getDesignOptions,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
@@ -59,6 +62,8 @@ export default function DrawIt() {
|
|||||||
const [statusMsg, setStatusMsg] = useState('')
|
const [statusMsg, setStatusMsg] = useState('')
|
||||||
const [statusError, setStatusError] = useState(false)
|
const [statusError, setStatusError] = useState(false)
|
||||||
const [imageLoaded, setImageLoaded] = useState(false)
|
const [imageLoaded, setImageLoaded] = useState(false)
|
||||||
|
const [pictureWords, setPictureWords] = useState<DbWord[]>([])
|
||||||
|
const [pictureWordInput, setPictureWordInput] = useState('')
|
||||||
|
|
||||||
const canvasRef = useRef<DrawCanvasHandle>(null)
|
const canvasRef = useRef<DrawCanvasHandle>(null)
|
||||||
|
|
||||||
@@ -68,6 +73,11 @@ export default function DrawIt() {
|
|||||||
return () => clearTimeout(t)
|
return () => clearTimeout(t)
|
||||||
}, [currentIndex])
|
}, [currentIndex])
|
||||||
|
|
||||||
|
// Reset imageLoaded immediately on navigation so blurhash shows right away
|
||||||
|
useEffect(() => {
|
||||||
|
setImageLoaded(false)
|
||||||
|
}, [currentIndex])
|
||||||
|
|
||||||
const currentPicture: DbPicture | null =
|
const currentPicture: DbPicture | null =
|
||||||
debouncedIndex >= 0 && debouncedIndex < pictureList.length ? pictureList[debouncedIndex] : null
|
debouncedIndex >= 0 && debouncedIndex < pictureList.length ? pictureList[debouncedIndex] : null
|
||||||
|
|
||||||
@@ -93,13 +103,15 @@ export default function DrawIt() {
|
|||||||
getDesignOptions(token).then(setDesignOptions).catch(console.error)
|
getDesignOptions(token).then(setDesignOptions).catch(console.error)
|
||||||
}, [token])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!currentPicture || !token) {
|
if (!currentPicture || !token) {
|
||||||
setObjects([]); setSelectedObjectId(null)
|
setObjects([]); setSelectedObjectId(null)
|
||||||
setObjectWords({})
|
setObjectWords({})
|
||||||
setWordInputs({})
|
setWordInputs({})
|
||||||
setImageLoaded(false)
|
setImageLoaded(false)
|
||||||
|
setPictureWords([])
|
||||||
|
setPictureWordInput('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getDbObjects(currentPicture.id, token)
|
getDbObjects(currentPicture.id, token)
|
||||||
@@ -118,6 +130,10 @@ export default function DrawIt() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
// Load picture-level words
|
||||||
|
getDbPictureWords(currentPicture.id, token)
|
||||||
|
.then(words => setPictureWords(words))
|
||||||
|
.catch(() => setPictureWords([]))
|
||||||
}, [currentPicture?.id, token])
|
}, [currentPicture?.id, token])
|
||||||
|
|
||||||
const showStatus = (msg: string, isError = false) => {
|
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) => {
|
const deleteObject = async (objId: string) => {
|
||||||
if (!token) return
|
if (!token) return
|
||||||
try {
|
try {
|
||||||
@@ -270,6 +306,44 @@ export default function DrawIt() {
|
|||||||
<div className="workspace">
|
<div className="workspace">
|
||||||
{/* Left sidebar: saved objects */}
|
{/* Left sidebar: saved objects */}
|
||||||
<aside className="sidebar">
|
<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 }}>
|
<div className="sidebar-panel" style={{ flex: 1 }}>
|
||||||
<h3 className="sidebar-heading">
|
<h3 className="sidebar-heading">
|
||||||
Objekte
|
Objekte
|
||||||
@@ -338,14 +412,13 @@ export default function DrawIt() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentPicture && designOptions.length > 0 && (
|
{currentPicture && (
|
||||||
<div className="sidebar-panel">
|
<div className="sidebar-panel">
|
||||||
<h3 className="sidebar-heading">Design</h3>
|
<h3 className="sidebar-heading">Design</h3>
|
||||||
<select
|
<select
|
||||||
value={currentPicture.design || ''}
|
value={currentPicture.design || ''}
|
||||||
onChange={async e => {
|
onChange={async e => {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
// Optimistic update
|
|
||||||
setPictureList(prev => prev.map(p => p.id === currentPicture.id ? { ...p, design: value || null } : p))
|
setPictureList(prev => prev.map(p => p.id === currentPicture.id ? { ...p, design: value || null } : p))
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/directus/db-pictures/${currentPicture.id}`, {
|
await fetch(`/api/directus/db-pictures/${currentPicture.id}`, {
|
||||||
@@ -369,13 +442,13 @@ export default function DrawIt() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{objects.length > 0 && (
|
{currentPicture && (
|
||||||
<div className="sidebar-panel">
|
<div className="sidebar-panel">
|
||||||
<button
|
<button
|
||||||
className="btn-primary btn-sm btn-block"
|
className="btn-primary btn-sm btn-block"
|
||||||
onClick={finishPicture}
|
onClick={finishPicture}
|
||||||
disabled={finishing}
|
disabled={finishing || objects.length === 0}
|
||||||
style={{ background: 'var(--success, #16a34a)' }}
|
style={{ background: objects.length > 0 ? 'var(--success, #16a34a)' : undefined, opacity: objects.length === 0 ? 0.5 : 1 }}
|
||||||
>
|
>
|
||||||
{finishing ? 'Wird fertiggestellt…' : '✅ Fertigstellen'}
|
{finishing ? 'Wird fertiggestellt…' : '✅ Fertigstellen'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user