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'} +
+