From 0c816d3f2dfcde90b81103612ea89b327e6e64f7 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 25 May 2026 16:11:02 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20PairForm=20=E2=80=94=20single=20typ?= =?UTF-8?q?e=20dropdown,=204=20variants,=20carry-over=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/pages/StatementCreation.jsx | 377 ++++++++++++++++++-------------- 1 file changed, 207 insertions(+), 170 deletions(-) diff --git a/src/pages/StatementCreation.jsx b/src/pages/StatementCreation.jsx index 21477af..cb464fb 100644 --- a/src/pages/StatementCreation.jsx +++ b/src/pages/StatementCreation.jsx @@ -156,16 +156,22 @@ function WordTag({ word, onRemove }) { } // ─── 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 }) { const lang = getUserLang(); const [showForm, setShowForm] = useState(false); + const [savedFlash, setSavedFlash] = useState(false); - // Type checkboxes - const [typeText, setTypeText] = useState(false); - const [typeYesNo, setTypeYesNo] = useState(false); - const [typeWord, setTypeWord] = useState(false); + // Single type dropdown + const [type, setType] = useState(''); - // Text fields + // Text fields — carried over between saves const [question, setQuestion] = useState(''); const [positive, setPositive] = useState(''); const [negative, setNegative] = useState(''); @@ -174,16 +180,17 @@ function PairForm({ objectId, onPairSaved }) { const [creatingWord, setCreatingWord] = useState(false); // 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 [negativeWords, setNegativeWords] = useState([]); const [saving, setSaving] = useState(false); - // Auto-detect words from text fields (only when Text type active) - const allText = typeText ? `${question} ${positive} ${negative}` : ''; + // Auto-detect words in sentence fields + const needsWordDetection = type === 'text' || type === 'question'; + const allText = needsWordDetection ? `${question} ${positive} ${negative}` : ''; useEffect(() => { if (!allText.trim()) { setWordMap({}); return; } const t = setTimeout(async () => { @@ -212,40 +219,32 @@ function PairForm({ objectId, onPairSaved }) { finally { setCreatingWord(false); } } - // Validation - const atLeastOne = typeText || typeYesNo || typeWord; - const textOk = !typeText || positive.trim().length > 0; - const negativeOk = !negative.trim() || (question.trim().length > 0 && positive.trim().length > 0); - const canSave = atLeastOne && textOk && negativeOk; + function canSave() { + if (!type) return false; + if (type === 'text') return positive.trim().length > 0; + if (type === 'yes_no') return true; + 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() { - if (!atLeastOne) return 'Wähle mindestens einen Typ (Text / Ja·Nein / Wort).'; - if (!textOk) return 'Text: Positive Aussage ist Pflicht.'; - if (!negativeOk) return 'Negative Aussage erfordert Frage + Positive Aussage.'; + if (!type) return 'Bitte einen Typ wählen.'; + if (type === 'text' && !positive.trim()) return 'Statement ist Pflicht.'; + 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 ''; } - function reset() { - setTypeText(false); setTypeYesNo(false); setTypeWord(false); - setQuestion(''); setPositive(''); setNegative(''); - setWordMap({}); setYesNoAnswer(null); - setPositiveWords([]); setNegativeWords([]); - setShowForm(false); - } - async function handleSave() { - if (!canSave) return; + if (!canSave()) return; setSaving(true); try { - const answerTypes = [ - ...(typeText ? ['text'] : []), - ...(typeYesNo ? ['yes_no'] : []), - ...(typeWord ? ['word'] : []), - ]; - - // Question (only if Text + question text filled) + // Question record — for yes_no, question, word (if text filled) let questionId = null; - if (typeText && question.trim()) { + if (type !== 'text' && question.trim()) { const q = await apiPost('/questions', { [`sentence_${lang}`]: withPlaceholders(question, wordMap), status: 'draft', @@ -254,87 +253,97 @@ function PairForm({ objectId, onPairSaved }) { } // Positive statement - const posBody = { status: 'draft' }; - if (typeText && positive.trim()) - posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap); - if (typeYesNo && yesNoAnswer !== null) - posBody.answer = yesNoAnswer; - - const posStmt = await apiPost('/statements', posBody); - - // Link positive words to positive statement - if (typeWord && positiveWords.length) - await Promise.all(positiveWords.map(w => apiLink(`/statements/${posStmt.id}/positive-words/${w.id}`))); - - // Negative statement (Text + negative text, OR Word + negativeWords) - let negStmtId = null; - const hasNegText = typeText && negative.trim(); - const hasNegWords = typeWord && negativeWords.length > 0; - - 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}`))); + let posStmtId = null; + if (type === 'text' || type === 'question') { + const posBody = { status: 'draft' }; + if (positive.trim()) + posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap); + const s = await apiPost('/statements', posBody); + posStmtId = s.id; + } else if (type === 'yes_no') { + const posBody = { status: 'draft' }; + if (yesNoAnswer !== null) posBody.answer = yesNoAnswer; + const s = await apiPost('/statements', posBody); + posStmtId = s.id; + } else if (type === 'word' && positiveWords.length) { + const s = await apiPost('/statements', { status: 'draft' }); + posStmtId = s.id; + await Promise.all(positiveWords.map(w => apiLink(`/statements/${posStmtId}/positive-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', { - answer_type: answerTypes, + answer_type: type, question_id: questionId, - positive_statement_id: posStmt.id, + positive_statement_id: posStmtId, negative_statement_id: negStmtId, status: 'draft', }); await apiLink(`/objects/${objectId}/pairs/${pair.id}`); - reset(); - onPairSaved({ ...pair, question: questionId ? { [`sentence_${lang}`]: question } : null, - positive_statement: posStmt, negative_statement: null }); + // Carry-over: keep all text/word values, only reset type to prompt re-selection + setType(''); + setYesNoAnswer(null); + setSavedFlash(true); + setTimeout(() => setSavedFlash(false), 2000); + onPairSaved(pair); } catch (e) { alert('Fehler: ' + e.message); } finally { setSaving(false); } } return (
- {/* Toggle button */} {showForm && (
- {/* ── Type checkboxes ── */} -
- {[['typeText', typeText, setTypeText, 'Text'], - ['typeYesNo', typeYesNo, setTypeYesNo, 'Ja / Nein'], - ['typeWord', typeWord, setTypeWord, 'Wort'], - ].map(([key, val, setter, label]) => ( - - ))} + {/* ── Saved flash ── */} + {savedFlash && ( +
+ ✓ Pair gespeichert — Texte übernommen +
+ )} + + {/* ── Type dropdown ── */} +
+ +
- {/* ── Text section ── */} - {typeText && ( + {/* ── Per-type fields ── */} + {type && (
- {/* "Als Wort erstellen" banner */} - {selection && ( + + {/* Word creation banner (text/question) */} + {(type === 'text' || type === 'question') && selection && (
{selection}" @@ -346,35 +355,115 @@ function PairForm({ objectId, onPairSaved }) {
)} -
- - -
+ {/* TEXT — only positive statement */} + {type === 'text' && ( +
+ + +
+ )} -
- - -
+ {/* YES_NO — question + answer picker */} + {type === 'yes_no' && ( + <> +
+ + +
+
+

Antwort

+
+ {[ + [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]) => ( + + ))} +
+
+ + )} -
- - - {negative.trim() && !negativeOk && ( -

Frage + Positive Aussage erforderlich.

- )} -
+ {/* QUESTION — question + positive + negative */} + {type === 'question' && ( + <> +
+ + +
+
+ + +
+
+ + +
+ + )} - {Object.keys(wordMap).length > 0 && ( + {/* WORD — question + positive/negative word pickers */} + {type === 'word' && ( + <> +
+ + +
+
+

Positive Wörter

+
+ {positiveWords.map(w => ( + setPositiveWords(prev => prev.filter(x => x.id !== id))} /> + ))} +
+ w.id)} + onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} + placeholder="Positives Wort suchen…" + /> +
+
+

Negative Wörter

+
+ {negativeWords.map(w => ( + setNegativeWords(prev => prev.filter(x => x.id !== id))} /> + ))} +
+ w.id)} + onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} + placeholder="Negatives Wort suchen…" + /> +
+ + )} + + {/* Detected words indicator */} + {(type === 'text' || type === 'question') && Object.keys(wordMap).length > 0 && (
Erkannte Wörter: {Object.entries(wordMap).map(([title, w]) => ( @@ -385,67 +474,14 @@ function PairForm({ objectId, onPairSaved }) {
)} - {/* ── Ja/Nein section ── */} - {typeYesNo && ( -
-

Antwort

-
- {[ - [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]) => ( - - ))} -
-
- )} - - {/* ── Wort section ── */} - {typeWord && ( -
-
-

Positive Wörter

-
- {positiveWords.map(w => ( - setPositiveWords(prev => prev.filter(x => x.id !== id))} /> - ))} -
- w.id)} - onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} - placeholder="Positives Wort suchen…" - /> -
- -
-

Negative Wörter

-
- {negativeWords.map(w => ( - setNegativeWords(prev => prev.filter(x => x.id !== id))} /> - ))} -
- w.id)} - onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])} - placeholder="Negatives Wort suchen…" - /> -
-
- )} - {/* ── Save ── */}
- {!canSave &&

{validationHint()}

} + {type && !canSave() && ( +

{validationHint()}

+ )}
))} +
);