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 = () => ( ) const ChevronRightIcon = () => ( ) const TrashIcon = () => ( ) export default function DrawIt() { const { token } = useAuth() const [pictureList, setPictureList] = useState([]) const [currentIndex, setCurrentIndex] = useState(-1) const [debouncedIndex, setDebouncedIndex] = useState(-1) const [objects, setObjects] = useState([]) const [selectedObjectId, setSelectedObjectId] = useState(null) const [currentSelections, setCurrentSelections] = useState([]) const [userNotes, setUserNotes] = useState('') // per-object words: objectId → DbWord[] const [objectWords, setObjectWords] = useState>({}) // per-object word input values: objectId → current input text const [wordInputs, setWordInputs] = useState>({}) const [pendingWords, setPendingWords] = useState([]) 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([]) const [pictureWordInput, setPictureWordInput] = useState('') const canvasRef = useRef(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 = {} 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 = (
{pictureList.length > 0 ? <>{currentIndex + 1}/{pictureList.length} : Keine Bilder}
) return (
{/* Left sidebar: saved objects */}