From 8242dab18c15abacc9700c3206bf75934bf19f5c Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 25 May 2026 17:28:16 +0200 Subject: [PATCH] feat: unified ContentCreation tool (Object + Statement creation merged) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New ContentCreation.jsx: 3-column layout Left: object list + "+ Objekt hinzufügen" button Center: dual-mode canvas (draw OR highlight) Right: ObjectAddPanel (mode=add) or PairsPanel (mode=objectId) - After saving object → auto-switches to PairsPanel for that object - All ObjectCreation + StatementCreation logic merged into one page - All pictures loaded (no objects_created filter) - "Objekte abgeschlossen" button marks picture (visual badge) - ContentHub: 2 tiles (Content Erstellen + Veröffentlichen placeholder) - App.jsx: /content/creation route, old /content/objects + /content/statements removed Co-Authored-By: Claude Sonnet 4.6 --- src/App.jsx | 6 +- src/pages/ContentCreation.jsx | 1162 +++++++++++++++++++++++++++++++++ src/pages/ContentHub.jsx | 34 +- 3 files changed, 1177 insertions(+), 25 deletions(-) create mode 100644 src/pages/ContentCreation.jsx diff --git a/src/App.jsx b/src/App.jsx index 5817c6e..c6d0669 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,8 +5,7 @@ import Dashboard from './pages/Dashboard'; import DatabaseAdmin from './pages/DatabaseAdmin'; import TableView from './pages/TableView'; import ContentHub from './pages/ContentHub'; -import ObjectCreation from './pages/ObjectCreation'; -import StatementCreation from './pages/StatementCreation'; +import ContentCreation from './pages/ContentCreation'; function RequireAuth({ children }) { const user = getUser(); @@ -23,8 +22,7 @@ export default function App() { } /> } /> } /> - } /> - } /> + } /> } /> diff --git a/src/pages/ContentCreation.jsx b/src/pages/ContentCreation.jsx new file mode 100644 index 0000000..c7f0b38 --- /dev/null +++ b/src/pages/ContentCreation.jsx @@ -0,0 +1,1162 @@ +import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; +import Layout from '../components/Layout'; +import { apiFetch, apiPost, apiPatch, apiLink, apiUnlink, getUserLang } from '../lib/api'; +import { STATUS_COLORS } from '../lib/tables'; + +// ─── Word / placeholder helpers ─────────────────────────────────────────────── + +function tokenize(text, wordMap) { + const titles = Object.keys(wordMap); + if (!titles.length || !text) return [{ text }]; + const escaped = titles.sort((a, b) => b.length - a.length) + .map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + const re = new RegExp(`(${escaped.join('|')})`, 'gi'); + return text.split(re).filter(s => s !== '').map(part => ({ + text: part, + word: wordMap[part.toLowerCase()] || null, + })); +} + +function withPlaceholders(text, wordMap, objectAssignments = {}) { + if (!text) return ''; + let result = text; + Object.entries(wordMap).sort((a, b) => b[0].length - a[0].length).forEach(([title, w]) => { + const re = new RegExp(`\\b${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi'); + result = result.replace(re, `{{${objectAssignments[w.id] || w.id}}}`); + }); + return result; +} + +async function resolvePlaceholders(text, allObjects) { + if (!text) return ''; + const matches = [...text.matchAll(/\{\{([^}]+)\}\}/g)]; + if (!matches.length) return text; + const uuids = [...new Set(matches.map(m => m[1]))]; + const idMap = {}; + allObjects.forEach(obj => { + if (uuids.includes(obj.id)) { + const label = (obj._words || []).slice(0, 2).map(w => w.titel_de).filter(Boolean).join('/') || 'Objekt'; + idMap[obj.id] = label; + } + }); + await Promise.all(uuids.filter(id => !idMap[id]).map(async id => { + try { const w = await apiFetch(`/words/${id}`); if (w) idMap[id] = w.titel_de || w.titel_en || id; } + catch { idMap[id] = id; } + })); + return text.replace(/\{\{([^}]+)\}\}/g, (_, id) => idMap[id] || id); +} + +// ─── HighlightedTextarea ────────────────────────────────────────────────────── + +function HighlightedTextarea({ value, onChange, wordMap, rows = 2, placeholder, onSelectionChange }) { + const taRef = useRef(null); + const tokens = useMemo(() => tokenize(value, wordMap), [value, wordMap]); + function handleSelect() { + const ta = taRef.current; if (!ta) return; + onSelectionChange?.(ta.value.slice(ta.selectionStart, ta.selectionEnd).trim()); + } + return ( +
+
+ {tokens.map((tok, i) => tok.word + ? {tok.text} + : {tok.text})} + {'\n'} +
+