feat: Placeholder farbig markieren in Freigabe-View und Pair-Modal
Objekt-Placeholder indigo, Wort-Placeholder grün, geleakte ⟦PHn⟧-Tokens rot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { apiPost, apiPatch } from '../lib/api';
|
import { apiPost, apiPatch } from '../lib/api';
|
||||||
import { buildRows } from '../lib/pairRows';
|
import { buildRows } from '../lib/pairRows';
|
||||||
|
import PlaceholderText from './PlaceholderText';
|
||||||
|
|
||||||
const LANGS = [
|
const LANGS = [
|
||||||
{ code: 'de', flag: '🇩🇪' },
|
{ code: 'de', flag: '🇩🇪' },
|
||||||
@@ -130,7 +131,7 @@ function Row({ row }) {
|
|||||||
const val = row.cell(l.code);
|
const val = row.cell(l.code);
|
||||||
return (
|
return (
|
||||||
<div key={l.code} className={`text-sm rounded-lg border px-2.5 py-1.5 ${val ? `bg-slate-50 border-slate-200 ${row.color}` : 'bg-red-50 border-red-200 text-red-400 italic'}`}>
|
<div key={l.code} className={`text-sm rounded-lg border px-2.5 py-1.5 ${val ? `bg-slate-50 border-slate-200 ${row.color}` : 'bg-red-50 border-red-200 text-red-400 italic'}`}>
|
||||||
{val || 'fehlt'}
|
{val ? <PlaceholderText text={val} /> : 'fehlt'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
29
src/components/PlaceholderText.jsx
Normal file
29
src/components/PlaceholderText.jsx
Normal file
@@ -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 <span key={i}>{s.text}</span>;
|
||||||
|
return (
|
||||||
|
<mark key={i} className={style.className}
|
||||||
|
title={`${style.title}${s.id ? ` · ${s.id.slice(0, 8)}…` : ''}`}>
|
||||||
|
{s.text}
|
||||||
|
</mark>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
// Baut die Anzeigezeilen je nach answer_type. Jede Zeile ist entweder
|
||||||
// - { kind: 'lang', label, color, cell(l) } → eine Zelle pro Sprache (übersetzbar)
|
// - { kind: 'lang', label, color, cell(l) } → eine Zelle pro Sprache (übersetzbar)
|
||||||
// - { kind: 'single', label, color, value } → ein einzelner Wert (nicht sprachabhängig)
|
// - { kind: 'single', label, color, value } → ein einzelner Wert (nicht sprachabhängig)
|
||||||
@@ -19,13 +39,14 @@ export function buildRows(content) {
|
|||||||
const rows = [];
|
const rows = [];
|
||||||
const wordsCell = (stmt) => (l) =>
|
const wordsCell = (stmt) => (l) =>
|
||||||
(stmt?.words || []).map(w => w[`titel_${l}`] || '—').join(', ');
|
(stmt?.words || []).map(w => w[`titel_${l}`] || '—').join(', ');
|
||||||
|
// Roher Text inkl. {{…}}-Placeholder — die Anzeige läuft über <PlaceholderText>.
|
||||||
const sentenceCell = (stmt, prefix) => (l) =>
|
const sentenceCell = (stmt, prefix) => (l) =>
|
||||||
strip(stmt?.sentence?.[`${prefix}_${l}`] || '');
|
stmt?.sentence?.[`${prefix}_${l}`] || '';
|
||||||
|
|
||||||
// Frage (yes_no / question / word)
|
// Frage (yes_no / question / word)
|
||||||
if (content.question) {
|
if (content.question) {
|
||||||
rows.push({ kind: 'lang', label: 'Frage', color: 'text-slate-700',
|
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') {
|
if (type === 'yes_no') {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback } from 'react';
|
|||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
import { apiFetch, apiPost } from '../lib/api';
|
import { apiFetch, apiPost } from '../lib/api';
|
||||||
import { buildRows } from '../lib/pairRows';
|
import { buildRows } from '../lib/pairRows';
|
||||||
|
import PlaceholderText from '../components/PlaceholderText';
|
||||||
|
|
||||||
const LANGS = [
|
const LANGS = [
|
||||||
{ code: 'de', flag: '🇩🇪' },
|
{ code: 'de', flag: '🇩🇪' },
|
||||||
@@ -124,7 +125,7 @@ function PairRow({ pair, flagged, onToggleFlag, objIndexById, onAssign, assignin
|
|||||||
const isQuestion = row.label === 'Frage';
|
const isQuestion = row.label === 'Frage';
|
||||||
return (
|
return (
|
||||||
<div key={row.label} className={`${row.color} ${isQuestion ? 'italic' : ''} break-words`}>
|
<div key={row.label} className={`${row.color} ${isQuestion ? 'italic' : ''} break-words`}>
|
||||||
{val || <span className="text-red-400 italic not-italic">— {row.label} fehlt —</span>}
|
{val ? <PlaceholderText text={val} /> : <span className="text-red-400 italic not-italic">— {row.label} fehlt —</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user