refactor: PairForm — single type dropdown, 4 variants, carry-over behavior
- answer_type is now a single value (text/yes_no/question/word) - Dropdown replaces checkboxes, per-type field sections - Text: only positive statement - Yes/No: optional question + answer picker - Question: question* + positive* + optional negative - Word: optional question + word pickers - After save: texts/words carry over, type resets for next pair - Fix: closed missing div in PairsPanel scroll container Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -156,16 +156,22 @@ function WordTag({ word, onRemove }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── PairForm ─────────────────────────────────────────────────────────────────
|
// ─── PairForm ─────────────────────────────────────────────────────────────────
|
||||||
|
const PAIR_TYPES = [
|
||||||
|
{ value: 'text', label: 'Text', hint: 'Nur positives Statement' },
|
||||||
|
{ value: 'yes_no', label: 'Ja / Nein', hint: 'Frage + Ja/Nein Antwort' },
|
||||||
|
{ value: 'question', label: 'Frage', hint: 'Frage + Positiv + Negativ' },
|
||||||
|
{ value: 'word', label: 'Wort', hint: 'Frage + Positive/Negative Wörter' },
|
||||||
|
];
|
||||||
|
|
||||||
function PairForm({ objectId, onPairSaved }) {
|
function PairForm({ objectId, onPairSaved }) {
|
||||||
const lang = getUserLang();
|
const lang = getUserLang();
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [savedFlash, setSavedFlash] = useState(false);
|
||||||
|
|
||||||
// Type checkboxes
|
// Single type dropdown
|
||||||
const [typeText, setTypeText] = useState(false);
|
const [type, setType] = useState('');
|
||||||
const [typeYesNo, setTypeYesNo] = useState(false);
|
|
||||||
const [typeWord, setTypeWord] = useState(false);
|
|
||||||
|
|
||||||
// Text fields
|
// Text fields — carried over between saves
|
||||||
const [question, setQuestion] = useState('');
|
const [question, setQuestion] = useState('');
|
||||||
const [positive, setPositive] = useState('');
|
const [positive, setPositive] = useState('');
|
||||||
const [negative, setNegative] = useState('');
|
const [negative, setNegative] = useState('');
|
||||||
@@ -174,16 +180,17 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
const [creatingWord, setCreatingWord] = useState(false);
|
const [creatingWord, setCreatingWord] = useState(false);
|
||||||
|
|
||||||
// Yes/No answer
|
// Yes/No answer
|
||||||
const [yesNoAnswer, setYesNoAnswer] = useState(null); // null | true | false
|
const [yesNoAnswer, setYesNoAnswer] = useState(null);
|
||||||
|
|
||||||
// Word pickers
|
// Word pickers — carried over between saves
|
||||||
const [positiveWords, setPositiveWords] = useState([]);
|
const [positiveWords, setPositiveWords] = useState([]);
|
||||||
const [negativeWords, setNegativeWords] = useState([]);
|
const [negativeWords, setNegativeWords] = useState([]);
|
||||||
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// Auto-detect words from text fields (only when Text type active)
|
// Auto-detect words in sentence fields
|
||||||
const allText = typeText ? `${question} ${positive} ${negative}` : '';
|
const needsWordDetection = type === 'text' || type === 'question';
|
||||||
|
const allText = needsWordDetection ? `${question} ${positive} ${negative}` : '';
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!allText.trim()) { setWordMap({}); return; }
|
if (!allText.trim()) { setWordMap({}); return; }
|
||||||
const t = setTimeout(async () => {
|
const t = setTimeout(async () => {
|
||||||
@@ -212,40 +219,32 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
finally { setCreatingWord(false); }
|
finally { setCreatingWord(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation
|
function canSave() {
|
||||||
const atLeastOne = typeText || typeYesNo || typeWord;
|
if (!type) return false;
|
||||||
const textOk = !typeText || positive.trim().length > 0;
|
if (type === 'text') return positive.trim().length > 0;
|
||||||
const negativeOk = !negative.trim() || (question.trim().length > 0 && positive.trim().length > 0);
|
if (type === 'yes_no') return true;
|
||||||
const canSave = atLeastOne && textOk && negativeOk;
|
if (type === 'question') return question.trim().length > 0 && positive.trim().length > 0;
|
||||||
|
if (type === 'word') return positiveWords.length > 0 || negativeWords.length > 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function validationHint() {
|
function validationHint() {
|
||||||
if (!atLeastOne) return 'Wähle mindestens einen Typ (Text / Ja·Nein / Wort).';
|
if (!type) return 'Bitte einen Typ wählen.';
|
||||||
if (!textOk) return 'Text: Positive Aussage ist Pflicht.';
|
if (type === 'text' && !positive.trim()) return 'Statement ist Pflicht.';
|
||||||
if (!negativeOk) return 'Negative Aussage erfordert Frage + Positive Aussage.';
|
if (type === 'question' && !question.trim()) return 'Frage ist Pflicht.';
|
||||||
|
if (type === 'question' && !positive.trim()) return 'Positive Aussage ist Pflicht.';
|
||||||
|
if (type === 'word' && !positiveWords.length && !negativeWords.length)
|
||||||
|
return 'Mindestens ein Wort erforderlich.';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
|
||||||
setTypeText(false); setTypeYesNo(false); setTypeWord(false);
|
|
||||||
setQuestion(''); setPositive(''); setNegative('');
|
|
||||||
setWordMap({}); setYesNoAnswer(null);
|
|
||||||
setPositiveWords([]); setNegativeWords([]);
|
|
||||||
setShowForm(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!canSave) return;
|
if (!canSave()) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const answerTypes = [
|
// Question record — for yes_no, question, word (if text filled)
|
||||||
...(typeText ? ['text'] : []),
|
|
||||||
...(typeYesNo ? ['yes_no'] : []),
|
|
||||||
...(typeWord ? ['word'] : []),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Question (only if Text + question text filled)
|
|
||||||
let questionId = null;
|
let questionId = null;
|
||||||
if (typeText && question.trim()) {
|
if (type !== 'text' && question.trim()) {
|
||||||
const q = await apiPost('/questions', {
|
const q = await apiPost('/questions', {
|
||||||
[`sentence_${lang}`]: withPlaceholders(question, wordMap),
|
[`sentence_${lang}`]: withPlaceholders(question, wordMap),
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
@@ -254,87 +253,97 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Positive statement
|
// Positive statement
|
||||||
const posBody = { status: 'draft' };
|
let posStmtId = null;
|
||||||
if (typeText && positive.trim())
|
if (type === 'text' || type === 'question') {
|
||||||
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap);
|
const posBody = { status: 'draft' };
|
||||||
if (typeYesNo && yesNoAnswer !== null)
|
if (positive.trim())
|
||||||
posBody.answer = yesNoAnswer;
|
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap);
|
||||||
|
const s = await apiPost('/statements', posBody);
|
||||||
const posStmt = await apiPost('/statements', posBody);
|
posStmtId = s.id;
|
||||||
|
} else if (type === 'yes_no') {
|
||||||
// Link positive words to positive statement
|
const posBody = { status: 'draft' };
|
||||||
if (typeWord && positiveWords.length)
|
if (yesNoAnswer !== null) posBody.answer = yesNoAnswer;
|
||||||
await Promise.all(positiveWords.map(w => apiLink(`/statements/${posStmt.id}/positive-words/${w.id}`)));
|
const s = await apiPost('/statements', posBody);
|
||||||
|
posStmtId = s.id;
|
||||||
// Negative statement (Text + negative text, OR Word + negativeWords)
|
} else if (type === 'word' && positiveWords.length) {
|
||||||
let negStmtId = null;
|
const s = await apiPost('/statements', { status: 'draft' });
|
||||||
const hasNegText = typeText && negative.trim();
|
posStmtId = s.id;
|
||||||
const hasNegWords = typeWord && negativeWords.length > 0;
|
await Promise.all(positiveWords.map(w => apiLink(`/statements/${posStmtId}/positive-words/${w.id}`)));
|
||||||
|
|
||||||
if (hasNegText || hasNegWords) {
|
|
||||||
const negBody = { status: 'draft' };
|
|
||||||
if (hasNegText)
|
|
||||||
negBody[`negative_sentence_${lang}`] = withPlaceholders(negative, wordMap);
|
|
||||||
const negStmt = await apiPost('/statements', negBody);
|
|
||||||
negStmtId = negStmt.id;
|
|
||||||
if (hasNegWords)
|
|
||||||
await Promise.all(negativeWords.map(w => apiLink(`/statements/${negStmt.id}/negative-words/${w.id}`)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pair
|
// Negative statement
|
||||||
|
let negStmtId = null;
|
||||||
|
if (type === 'question' && negative.trim()) {
|
||||||
|
const negBody = { status: 'draft', [`negative_sentence_${lang}`]: withPlaceholders(negative, wordMap) };
|
||||||
|
const s = await apiPost('/statements', negBody);
|
||||||
|
negStmtId = s.id;
|
||||||
|
} else if (type === 'word' && negativeWords.length) {
|
||||||
|
const s = await apiPost('/statements', { status: 'draft' });
|
||||||
|
negStmtId = s.id;
|
||||||
|
await Promise.all(negativeWords.map(w => apiLink(`/statements/${negStmtId}/negative-words/${w.id}`)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create pair
|
||||||
const pair = await apiPost('/pairs', {
|
const pair = await apiPost('/pairs', {
|
||||||
answer_type: answerTypes,
|
answer_type: type,
|
||||||
question_id: questionId,
|
question_id: questionId,
|
||||||
positive_statement_id: posStmt.id,
|
positive_statement_id: posStmtId,
|
||||||
negative_statement_id: negStmtId,
|
negative_statement_id: negStmtId,
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
});
|
});
|
||||||
|
|
||||||
await apiLink(`/objects/${objectId}/pairs/${pair.id}`);
|
await apiLink(`/objects/${objectId}/pairs/${pair.id}`);
|
||||||
|
|
||||||
reset();
|
// Carry-over: keep all text/word values, only reset type to prompt re-selection
|
||||||
onPairSaved({ ...pair, question: questionId ? { [`sentence_${lang}`]: question } : null,
|
setType('');
|
||||||
positive_statement: posStmt, negative_statement: null });
|
setYesNoAnswer(null);
|
||||||
|
setSavedFlash(true);
|
||||||
|
setTimeout(() => setSavedFlash(false), 2000);
|
||||||
|
onPairSaved(pair);
|
||||||
} catch (e) { alert('Fehler: ' + e.message); }
|
} catch (e) { alert('Fehler: ' + e.message); }
|
||||||
finally { setSaving(false); }
|
finally { setSaving(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* Toggle button */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowForm(s => !s)}
|
onClick={() => setShowForm(s => !s)}
|
||||||
className="w-full py-1.5 text-xs font-medium rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
|
className="w-full py-1.5 text-xs font-medium rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
|
||||||
>
|
>
|
||||||
{showForm ? '✕ Abbrechen' : '+ Add new pair'}
|
{showForm ? '✕ Abbrechen' : '+ Pair hinzufügen'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="border border-indigo-100 rounded-xl p-3 bg-indigo-50/30 space-y-3">
|
<div className="border border-indigo-100 rounded-xl p-3 bg-indigo-50/30 space-y-3">
|
||||||
|
|
||||||
{/* ── Type checkboxes ── */}
|
{/* ── Saved flash ── */}
|
||||||
<div className="flex items-center gap-4">
|
{savedFlash && (
|
||||||
{[['typeText', typeText, setTypeText, 'Text'],
|
<div className="text-xs text-green-700 bg-green-50 border border-green-200 rounded-lg px-3 py-1.5 font-medium">
|
||||||
['typeYesNo', typeYesNo, setTypeYesNo, 'Ja / Nein'],
|
✓ Pair gespeichert — Texte übernommen
|
||||||
['typeWord', typeWord, setTypeWord, 'Wort'],
|
</div>
|
||||||
].map(([key, val, setter, label]) => (
|
)}
|
||||||
<label key={key} className="flex items-center gap-1.5 text-xs font-medium text-slate-700 cursor-pointer select-none">
|
|
||||||
<input
|
{/* ── Type dropdown ── */}
|
||||||
type="checkbox"
|
<div>
|
||||||
checked={val}
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Typ</label>
|
||||||
onChange={e => setter(e.target.checked)}
|
<select
|
||||||
className="w-3.5 h-3.5 accent-indigo-600"
|
value={type}
|
||||||
/>
|
onChange={e => setType(e.target.value)}
|
||||||
{label}
|
className="w-full border border-slate-300 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-white"
|
||||||
</label>
|
>
|
||||||
))}
|
<option value="">— Typ wählen —</option>
|
||||||
|
{PAIR_TYPES.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>{opt.label} — {opt.hint}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Text section ── */}
|
{/* ── Per-type fields ── */}
|
||||||
{typeText && (
|
{type && (
|
||||||
<div className="space-y-2.5 pt-1 border-t border-indigo-100">
|
<div className="space-y-2.5 pt-1 border-t border-indigo-100">
|
||||||
{/* "Als Wort erstellen" banner */}
|
|
||||||
{selection && (
|
{/* Word creation banner (text/question) */}
|
||||||
|
{(type === 'text' || type === 'question') && selection && (
|
||||||
<div className="flex items-center gap-2 bg-white border border-indigo-200 rounded-lg px-2.5 py-1.5">
|
<div className="flex items-center gap-2 bg-white border border-indigo-200 rounded-lg px-2.5 py-1.5">
|
||||||
<span className="text-xs text-slate-600 flex-1 truncate">
|
<span className="text-xs text-slate-600 flex-1 truncate">
|
||||||
„<strong className="text-indigo-700">{selection}</strong>"
|
„<strong className="text-indigo-700">{selection}</strong>"
|
||||||
@@ -346,35 +355,115 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
{/* TEXT — only positive statement */}
|
||||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
{type === 'text' && (
|
||||||
Positive Aussage <span className="text-red-400">*</span>
|
<div>
|
||||||
</label>
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||||
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap}
|
Statement <span className="text-red-400">*</span>
|
||||||
rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
</label>
|
||||||
</div>
|
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap}
|
||||||
|
rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
{/* YES_NO — question + answer picker */}
|
||||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
{type === 'yes_no' && (
|
||||||
Frage <span className="text-slate-300">(optional)</span>
|
<>
|
||||||
</label>
|
<div>
|
||||||
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||||
rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
Frage <span className="text-slate-400 font-normal normal-case">(optional)</span>
|
||||||
</div>
|
</label>
|
||||||
|
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
||||||
|
rows={2} placeholder="Ist das ein Hund?" onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Antwort</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[
|
||||||
|
[null, '— Offen', 'bg-slate-100 text-slate-600 border-slate-200'],
|
||||||
|
[true, '✓ Ja', 'bg-green-50 text-green-700 border-green-300'],
|
||||||
|
[false, '✗ Nein', 'bg-red-50 text-red-600 border-red-300'],
|
||||||
|
].map(([val, label, cls]) => (
|
||||||
|
<button key={String(val)} onClick={() => setYesNoAnswer(val)}
|
||||||
|
className={`flex-1 py-1.5 text-xs font-medium rounded-lg border transition-all
|
||||||
|
${yesNoAnswer === val
|
||||||
|
? `${cls} ring-2 ring-offset-1 ring-indigo-400`
|
||||||
|
: 'bg-white text-slate-500 border-slate-200 hover:border-slate-300'}`}
|
||||||
|
>{label}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
{/* QUESTION — question + positive + negative */}
|
||||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
{type === 'question' && (
|
||||||
Negative Aussage
|
<>
|
||||||
<span className="text-slate-300 ml-1">(optional — braucht Frage + Positiv)</span>
|
<div>
|
||||||
</label>
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||||
<HighlightedTextarea value={negative} onChange={setNegative} wordMap={wordMap}
|
Frage <span className="text-red-400">*</span>
|
||||||
rows={2} placeholder="Das ist keine Katze." onSelectionChange={setSelection} />
|
</label>
|
||||||
{negative.trim() && !negativeOk && (
|
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
||||||
<p className="text-xs text-red-500 mt-0.5">Frage + Positive Aussage erforderlich.</p>
|
rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||||
|
Positive Aussage <span className="text-red-400">*</span>
|
||||||
|
</label>
|
||||||
|
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap}
|
||||||
|
rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||||
|
Negative Aussage <span className="text-slate-400 font-normal normal-case">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<HighlightedTextarea value={negative} onChange={setNegative} wordMap={wordMap}
|
||||||
|
rows={2} placeholder="Das ist keine Katze." onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.keys(wordMap).length > 0 && (
|
{/* WORD — question + positive/negative word pickers */}
|
||||||
|
{type === 'word' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||||
|
Frage <span className="text-slate-400 font-normal normal-case">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
||||||
|
rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Positive Wörter</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||||
|
{positiveWords.map(w => (
|
||||||
|
<WordTag key={w.id} word={w} onRemove={id => setPositiveWords(prev => prev.filter(x => x.id !== id))} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<WordSearch
|
||||||
|
existingIds={positiveWords.map(w => w.id)}
|
||||||
|
onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
||||||
|
placeholder="Positives Wort suchen…"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Negative Wörter</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||||
|
{negativeWords.map(w => (
|
||||||
|
<WordTag key={w.id} word={w} onRemove={id => setNegativeWords(prev => prev.filter(x => x.id !== id))} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<WordSearch
|
||||||
|
existingIds={negativeWords.map(w => w.id)}
|
||||||
|
onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
||||||
|
placeholder="Negatives Wort suchen…"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Detected words indicator */}
|
||||||
|
{(type === 'text' || type === 'question') && Object.keys(wordMap).length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<span className="text-xs text-slate-400 w-full">Erkannte Wörter:</span>
|
<span className="text-xs text-slate-400 w-full">Erkannte Wörter:</span>
|
||||||
{Object.entries(wordMap).map(([title, w]) => (
|
{Object.entries(wordMap).map(([title, w]) => (
|
||||||
@@ -385,67 +474,14 @@ function PairForm({ objectId, onPairSaved }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Ja/Nein section ── */}
|
|
||||||
{typeYesNo && (
|
|
||||||
<div className="pt-2 border-t border-indigo-100">
|
|
||||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">Antwort</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{[
|
|
||||||
[null, '— Offen', 'bg-slate-100 text-slate-600 border-slate-200'],
|
|
||||||
[true, '✓ Ja', 'bg-green-50 text-green-700 border-green-300'],
|
|
||||||
[false, '✗ Nein', 'bg-red-50 text-red-600 border-red-300'],
|
|
||||||
].map(([val, label, cls]) => (
|
|
||||||
<button key={String(val)}
|
|
||||||
onClick={() => setYesNoAnswer(val)}
|
|
||||||
className={`flex-1 py-1.5 text-xs font-medium rounded-lg border transition-all
|
|
||||||
${yesNoAnswer === val ? `${cls} ring-2 ring-offset-1 ring-indigo-400` : 'bg-white text-slate-500 border-slate-200 hover:border-slate-300'}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Wort section ── */}
|
|
||||||
{typeWord && (
|
|
||||||
<div className="pt-2 border-t border-indigo-100 space-y-2.5">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Positive Wörter</p>
|
|
||||||
<div className="flex flex-wrap gap-1 mb-1.5">
|
|
||||||
{positiveWords.map(w => (
|
|
||||||
<WordTag key={w.id} word={w} onRemove={id => setPositiveWords(prev => prev.filter(x => x.id !== id))} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<WordSearch
|
|
||||||
existingIds={positiveWords.map(w => w.id)}
|
|
||||||
onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
|
||||||
placeholder="Positives Wort suchen…"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Negative Wörter</p>
|
|
||||||
<div className="flex flex-wrap gap-1 mb-1.5">
|
|
||||||
{negativeWords.map(w => (
|
|
||||||
<WordTag key={w.id} word={w} onRemove={id => setNegativeWords(prev => prev.filter(x => x.id !== id))} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<WordSearch
|
|
||||||
existingIds={negativeWords.map(w => w.id)}
|
|
||||||
onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
|
||||||
placeholder="Negatives Wort suchen…"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Save ── */}
|
{/* ── Save ── */}
|
||||||
<div className="pt-2 border-t border-indigo-100 space-y-1.5">
|
<div className="pt-2 border-t border-indigo-100 space-y-1.5">
|
||||||
{!canSave && <p className="text-xs text-amber-600">{validationHint()}</p>}
|
{type && !canSave() && (
|
||||||
|
<p className="text-xs text-amber-600">{validationHint()}</p>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving || !canSave}
|
disabled={saving || !canSave()}
|
||||||
className="w-full py-2 text-xs font-medium rounded-lg bg-green-600 hover:bg-green-700 disabled:opacity-40 text-white transition-colors"
|
className="w-full py-2 text-xs font-medium rounded-lg bg-green-600 hover:bg-green-700 disabled:opacity-40 text-white transition-colors"
|
||||||
>
|
>
|
||||||
{saving ? 'Speichern…' : '✓ Pair speichern'}
|
{saving ? 'Speichern…' : '✓ Pair speichern'}
|
||||||
@@ -527,6 +563,7 @@ function PairsPanel({ selectedObject, objectPairs, loadingPairs, onPairSaved })
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user