From e78be430c706d2bee6a733ce07e97ef1f90b2b9f Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 12 Jun 2026 22:43:46 +0200 Subject: [PATCH] feat: Placeholder farbig markieren in Freigabe-View und Pair-Modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Objekt-Placeholder indigo, Wort-Placeholder grün, geleakte ⟦PHn⟧-Tokens rot. Co-Authored-By: Claude Fable 5 --- src/components/PairReviewModal.jsx | 3 ++- src/components/PlaceholderText.jsx | 29 +++++++++++++++++++++++++++++ src/lib/pairRows.js | 25 +++++++++++++++++++++++-- src/pages/Publish.jsx | 3 ++- 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 src/components/PlaceholderText.jsx diff --git a/src/components/PairReviewModal.jsx b/src/components/PairReviewModal.jsx index 778979b..89e90e4 100644 --- a/src/components/PairReviewModal.jsx +++ b/src/components/PairReviewModal.jsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { apiPost, apiPatch } from '../lib/api'; import { buildRows } from '../lib/pairRows'; +import PlaceholderText from './PlaceholderText'; const LANGS = [ { code: 'de', flag: '🇩🇪' }, @@ -130,7 +131,7 @@ function Row({ row }) { const val = row.cell(l.code); return (
- {val || 'fehlt'} + {val ? : 'fehlt'}
); })} diff --git a/src/components/PlaceholderText.jsx b/src/components/PlaceholderText.jsx new file mode 100644 index 0000000..64c0390 --- /dev/null +++ b/src/components/PlaceholderText.jsx @@ -0,0 +1,29 @@ +import { parsePlaceholderSegments } from '../lib/pairRows'; + +// Rendert Satztext mit farbig markierten Placeholdern: +// {{label.o:id}} → indigo (Objekt-Verknüpfung, passend zu den 🔗-Buttons) +// {{label.w:id}} → emerald (Wort-Verknüpfung) +// ⟦PHn:label⟧ → rot (geleaktes Übersetzungs-Token, Datenfehler) +const KIND_STYLES = { + object: { className: 'bg-indigo-100 text-indigo-800 rounded px-0.5', title: 'Objekt-Placeholder' }, + word: { className: 'bg-emerald-100 text-emerald-800 rounded px-0.5', title: 'Wort-Placeholder' }, + broken: { className: 'bg-red-100 text-red-700 rounded px-0.5 line-through', title: 'Kaputtes Übersetzungs-Token — bitte reparieren' }, +}; + +export default function PlaceholderText({ text }) { + const segs = parsePlaceholderSegments(text); + return ( + <> + {segs.map((s, i) => { + const style = s.kind && KIND_STYLES[s.kind]; + if (!style) return {s.text}; + return ( + + {s.text} + + ); + })} + + ); +} diff --git a/src/lib/pairRows.js b/src/lib/pairRows.js index afba87d..0c1eb27 100644 --- a/src/lib/pairRows.js +++ b/src/lib/pairRows.js @@ -10,6 +10,26 @@ export function strip(text) { }); } +// Zerlegt einen Satz in Segmente für die farbige Placeholder-Anzeige: +// { text, kind: 'object' | 'word' | 'broken' | null, id? } +// 'broken' = geleakte ⟦PHn:label⟧-Tokens aus der Übersetzung (sollten nicht vorkommen). +const SEGMENT_RE = /\{\{([^.{}]+)\.(w|o):([0-9a-f-]{36})\}\}|⟦PH\d+:([^⟧]*)⟧/g; + +export function parsePlaceholderSegments(text) { + if (!text) return []; + const str = String(text); + const segs = []; + let last = 0; + for (const m of str.matchAll(SEGMENT_RE)) { + if (m.index > last) segs.push({ text: str.slice(last, m.index), kind: null }); + if (m[1] !== undefined) segs.push({ text: m[1], kind: m[2] === 'o' ? 'object' : 'word', id: m[3] }); + else segs.push({ text: m[4], kind: 'broken' }); + last = m.index + m[0].length; + } + if (last < str.length) segs.push({ text: str.slice(last), kind: null }); + return segs; +} + // Baut die Anzeigezeilen je nach answer_type. Jede Zeile ist entweder // - { kind: 'lang', label, color, cell(l) } → eine Zelle pro Sprache (übersetzbar) // - { kind: 'single', label, color, value } → ein einzelner Wert (nicht sprachabhängig) @@ -19,13 +39,14 @@ export function buildRows(content) { const rows = []; const wordsCell = (stmt) => (l) => (stmt?.words || []).map(w => w[`titel_${l}`] || '—').join(', '); + // Roher Text inkl. {{…}}-Placeholder — die Anzeige läuft über . const sentenceCell = (stmt, prefix) => (l) => - strip(stmt?.sentence?.[`${prefix}_${l}`] || ''); + stmt?.sentence?.[`${prefix}_${l}`] || ''; // Frage (yes_no / question / word) if (content.question) { rows.push({ kind: 'lang', label: 'Frage', color: 'text-slate-700', - cell: l => strip(content.question[`sentence_${l}`] || '') }); + cell: l => content.question[`sentence_${l}`] || '' }); } if (type === 'yes_no') { diff --git a/src/pages/Publish.jsx b/src/pages/Publish.jsx index 215c29d..4442aed 100644 --- a/src/pages/Publish.jsx +++ b/src/pages/Publish.jsx @@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback } from 'react'; import Layout from '../components/Layout'; import { apiFetch, apiPost } from '../lib/api'; import { buildRows } from '../lib/pairRows'; +import PlaceholderText from '../components/PlaceholderText'; const LANGS = [ { code: 'de', flag: '🇩🇪' }, @@ -124,7 +125,7 @@ function PairRow({ pair, flagged, onToggleFlag, objIndexById, onAssign, assignin const isQuestion = row.label === 'Frage'; return (
- {val || — {row.label} fehlt —} + {val ? : — {row.label} fehlt —}
); })}