From 5cc4db08bb822b0567399fa2d047b246dad5fcaf Mon Sep 17 00:00:00 2001 From: Erza Bytyci Date: Mon, 25 May 2026 07:59:20 +0000 Subject: [PATCH] improve jumbled exercise: rename, add feedback option, functionality --- ...00047_add_feedback_to_jumbled_sentence.sql | 4 + ...correct_alternates_to_jumbled_sentence.sql | 2 + gradesa/data/schema.sql | 6 +- .../jumbled-sentence.module.css | 71 +++- .../create-exercise/jumbled-sentence/page.js | 306 ++++++++++++++++-- gradesa/src/app/admin/create-exercise/page.js | 2 +- .../exercises/jumbled-sentence/[id]/route.js | 20 +- .../admin/exercises/jumbled-sentence/route.js | 14 +- .../exercises/jumbled-sentence/route.test.js | 18 +- .../exercises/jumbled-sentence/[id]/route.js | 18 +- .../exercises/jumbled-sentence/[id]/page.js | 110 +++++-- .../exercises/jumbled-sentence/page.js | 4 +- gradesa/src/app/grammar/exercises/page.js | 2 +- .../schemas/jumbled-sentence.schemas.js | 49 +++ gradesa/test-results/.last-run.json | 4 +- 15 files changed, 559 insertions(+), 71 deletions(-) create mode 100644 gradesa/data/migrations/00047_add_feedback_to_jumbled_sentence.sql create mode 100644 gradesa/data/migrations/00048_add_incorrect_alternates_to_jumbled_sentence.sql diff --git a/gradesa/data/migrations/00047_add_feedback_to_jumbled_sentence.sql b/gradesa/data/migrations/00047_add_feedback_to_jumbled_sentence.sql new file mode 100644 index 0000000..006e7d9 --- /dev/null +++ b/gradesa/data/migrations/00047_add_feedback_to_jumbled_sentence.sql @@ -0,0 +1,4 @@ +ALTER TABLE public.jumbled_sentence_sentences + ADD COLUMN IF NOT EXISTS correct_feedback text, + ADD COLUMN IF NOT EXISTS alternate_feedbacks text[] NOT NULL DEFAULT ARRAY[]::text[], + ADD COLUMN IF NOT EXISTS incorrect_feedbacks text[] NOT NULL DEFAULT ARRAY[]::text[]; diff --git a/gradesa/data/migrations/00048_add_incorrect_alternates_to_jumbled_sentence.sql b/gradesa/data/migrations/00048_add_incorrect_alternates_to_jumbled_sentence.sql new file mode 100644 index 0000000..3e3fab9 --- /dev/null +++ b/gradesa/data/migrations/00048_add_incorrect_alternates_to_jumbled_sentence.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.jumbled_sentence_sentences + ADD COLUMN IF NOT EXISTS incorrect_alternates text[] NOT NULL DEFAULT ARRAY[]::text[]; \ No newline at end of file diff --git a/gradesa/data/schema.sql b/gradesa/data/schema.sql index af29070..5df3d7c 100644 --- a/gradesa/data/schema.sql +++ b/gradesa/data/schema.sql @@ -423,7 +423,11 @@ CREATE TABLE public.jumbled_sentence_sentences ( id bigint PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY, jumbled_exercise_id bigint NOT NULL, sentence text NOT NULL, - alternates text[] DEFAULT ARRAY[]::text[] + alternates text[] DEFAULT ARRAY[]::text[], + correct_feedback text, + alternate_feedbacks text[] NOT NULL DEFAULT ARRAY[]::text[], + incorrect_feedbacks text[] NOT NULL DEFAULT ARRAY[]::text[], + incorrect_alternates text[] NOT NULL DEFAULT ARRAY[]::text[] ); CREATE TABLE public.jumbled_sentence_exercises ( diff --git a/gradesa/src/app/admin/create-exercise/jumbled-sentence/jumbled-sentence.module.css b/gradesa/src/app/admin/create-exercise/jumbled-sentence/jumbled-sentence.module.css index 9467ef1..bcf03f0 100644 --- a/gradesa/src/app/admin/create-exercise/jumbled-sentence/jumbled-sentence.module.css +++ b/gradesa/src/app/admin/create-exercise/jumbled-sentence/jumbled-sentence.module.css @@ -23,6 +23,18 @@ box-sizing: border-box; } +.sentenceHint { + font-size: 0.9rem; + font-style: italic; + font-weight: 500; + color: var(--fg4); +} + +.sentenceTextarea { + resize: vertical; + line-height: 1.4; +} + .sentenceCard { border: 1px solid #ccc; padding: 0.5rem; @@ -30,12 +42,33 @@ width: 40rem; } +.alternateGroup { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-top: 0; +} + +.alternateGroup+.alternateGroup { + margin-top: 0.4rem; +} + .alternateRow { position: relative; width: min(100%, var(--jumbled-input-max-width)); margin-top: 0.5rem; } +.alternateGroup>.alternateRow { + margin-top: 0; +} + +.alternateRowNoGapTop { + position: relative; + width: min(100%, var(--jumbled-input-max-width)); + margin-top: 0; +} + .alternateInput { padding-right: 2rem; } @@ -56,15 +89,51 @@ } .addAlternateButton { - min-width: 14rem; + width: fit-content; white-space: nowrap; } +.fitContentButton { + width: auto; + min-width: 18rem; + white-space: nowrap; + justify-content: center; +} + .inlineActionsRow { width: min(100%, var(--jumbled-input-max-width)); margin-top: 0.5rem; } +.feedbackSection { + width: min(100%, var(--jumbled-input-max-width)); + margin-top: 0.75rem; + display: flex; + flex-direction: column; + gap: 0; +} + +.sectionTitle { + margin-bottom: 0; + font-weight: 600; +} + +.sectionTitle+.alternateRowNoGapTop { + margin-top: 0; +} + +.alternateRowNoGapTop+.alternateRowNoGapTop { + margin-top: 0.4rem; +} + +.feedbackSection>.fitContentButton { + margin-top: 0.5rem; +} + +.feedbackSection>.sectionTitle+.sectionTitle { + margin-top: 0.5rem; +} + .removeSentenceButton { background-color: #f8d7da; border-color: #f5c2c7; diff --git a/gradesa/src/app/admin/create-exercise/jumbled-sentence/page.js b/gradesa/src/app/admin/create-exercise/jumbled-sentence/page.js index d98ccb8..ef4284c 100644 --- a/gradesa/src/app/admin/create-exercise/jumbled-sentence/page.js +++ b/gradesa/src/app/admin/create-exercise/jumbled-sentence/page.js @@ -9,13 +9,25 @@ import { withBasePath } from "@/shared/utils/basePath"; import useQuery from "@/shared/hooks/useQuery"; import styles from "./jumbled-sentence.module.css"; +function toWordPerLine(value) { + if (typeof value !== "string") return ""; + return value.trim().split(/\s+/).filter(Boolean).join("\n"); +} + export default function CreateJumbledSentenceExercise() { const router = useRouter(); const { exercise_id } = useParams(); const isEditMode = Boolean(exercise_id); const [title, setTitle] = useState(""); const [sentences, setSentences] = useState([ - { sentence: "", alternates: [""] }, + { + sentence: "", + alternates: [""], + alternateFeedbacks: [""], + correctSentenceFeedback: "", + incorrectAlternates: [""], + incorrectFeedbacks: [""], + }, ]); const [error, setError] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); @@ -38,13 +50,47 @@ export default function CreateJumbledSentenceExercise() { setSentences( (loadedExercise.sentences || []).length ? loadedExercise.sentences.map((item) => ({ - sentence: item.sentence || "", + sentence: toWordPerLine(item.sentence || ""), alternates: item.alternates && item.alternates.length > 0 ? item.alternates : [""], + alternateFeedbacks: + item.alternateFeedbacks && + item.alternateFeedbacks.length === item.alternates?.length + ? item.alternateFeedbacks + : new Array( + item.alternates && item.alternates.length > 0 + ? item.alternates.length + : 1 + ).fill(""), + correctSentenceFeedback: item.correctSentenceFeedback || "", + incorrectAlternates: + item.incorrectAlternates && item.incorrectAlternates.length > 0 + ? item.incorrectAlternates + : [""], + incorrectFeedbacks: + item.incorrectFeedbacks && + item.incorrectFeedbacks.length === + (item.incorrectAlternates?.length || 0) + ? item.incorrectFeedbacks + : new Array( + item.incorrectAlternates && + item.incorrectAlternates.length > 0 + ? item.incorrectAlternates.length + : 1 + ).fill(""), })) - : [{ sentence: "", alternates: [""] }] + : [ + { + sentence: "", + alternates: [""], + alternateFeedbacks: [""], + correctSentenceFeedback: "", + incorrectAlternates: [""], + incorrectFeedbacks: [""], + }, + ] ); }, [isEditMode, data]); @@ -60,13 +106,62 @@ export default function CreateJumbledSentenceExercise() { setSentences(updated); }; + const handleAlternateFeedbackChange = (idx, altIdx, value) => { + const updated = [...sentences]; + updated[idx].alternateFeedbacks[altIdx] = value; + setSentences(updated); + }; + + const handleCorrectFeedbackChange = (idx, value) => { + const updated = [...sentences]; + updated[idx].correctSentenceFeedback = value; + setSentences(updated); + }; + + const handleIncorrectFeedbackChange = (idx, fbIdx, value) => { + const updated = [...sentences]; + updated[idx].incorrectFeedbacks[fbIdx] = value; + setSentences(updated); + }; + + const handleIncorrectAlternateChange = (idx, altIdx, value) => { + const updated = [...sentences]; + updated[idx].incorrectAlternates[altIdx] = value; + setSentences(updated); + }; + const addSentence = () => { - setSentences([...sentences, { sentence: "", alternates: [""] }]); + setSentences([ + ...sentences, + { + sentence: "", + alternates: [""], + alternateFeedbacks: [""], + correctSentenceFeedback: "", + incorrectAlternates: [""], + incorrectFeedbacks: [""], + }, + ]); }; const addAlternate = (idx) => { const updated = [...sentences]; updated[idx].alternates.push(""); + updated[idx].alternateFeedbacks.push(""); + setSentences(updated); + }; + + const addIncorrectFeedback = (idx) => { + const updated = [...sentences]; + updated[idx].incorrectAlternates.push(""); + updated[idx].incorrectFeedbacks.push(""); + setSentences(updated); + }; + + const addIncorrectAlternate = (idx) => { + const updated = [...sentences]; + updated[idx].incorrectAlternates.push(""); + updated[idx].incorrectFeedbacks.push(""); setSentences(updated); }; @@ -79,15 +174,87 @@ export default function CreateJumbledSentenceExercise() { updated[idx].alternates = updated[idx].alternates.filter( (_, i) => i !== altIdx ); + updated[idx].alternateFeedbacks = updated[idx].alternateFeedbacks.filter( + (_, i) => i !== altIdx + ); + if (updated[idx].alternates.length === 0) { + updated[idx].alternates = [""]; + updated[idx].alternateFeedbacks = [""]; + } + setSentences(updated); + }; + + const removeIncorrectAlternate = (idx, altIdx) => { + const updated = [...sentences]; + updated[idx].incorrectAlternates = updated[idx].incorrectAlternates.filter( + (_, i) => i !== altIdx + ); + updated[idx].incorrectFeedbacks = updated[idx].incorrectFeedbacks.filter( + (_, i) => i !== altIdx + ); + if (updated[idx].incorrectAlternates.length === 0) { + updated[idx].incorrectAlternates = [""]; + updated[idx].incorrectFeedbacks = [""]; + } setSentences(updated); }; const handleSubmit = async (e) => { e.preventDefault(); setError(""); + + const normalizedSentences = sentences.map((item) => ({ + ...item, + sentence: toWordPerLine(item.sentence), + alternates: item.alternates + .map((alt, index) => ({ + alt: typeof alt === "string" ? alt.trim() : "", + feedback: + typeof item.alternateFeedbacks?.[index] === "string" + ? item.alternateFeedbacks[index].trim() + : "", + })) + .filter((entry) => entry.alt.length > 0) + .map((entry) => entry.alt), + alternateFeedbacks: item.alternates + .map((alt, index) => ({ + alt: typeof alt === "string" ? alt.trim() : "", + feedback: + typeof item.alternateFeedbacks?.[index] === "string" + ? item.alternateFeedbacks[index].trim() + : "", + })) + .filter((entry) => entry.alt.length > 0) + .map((entry) => entry.feedback), + correctSentenceFeedback: + typeof item.correctSentenceFeedback === "string" + ? item.correctSentenceFeedback.trim() + : "", + incorrectAlternates: (item.incorrectAlternates || []) + .map((alt, index) => ({ + alt: typeof alt === "string" ? alt.trim() : "", + feedback: + typeof item.incorrectFeedbacks?.[index] === "string" + ? item.incorrectFeedbacks[index].trim() + : "", + })) + .filter((entry) => entry.alt.length > 0) + .map((entry) => entry.alt), + incorrectFeedbacks: (item.incorrectAlternates || []) + .map((alt, index) => ({ + alt: typeof alt === "string" ? alt.trim() : "", + feedback: + typeof item.incorrectFeedbacks?.[index] === "string" + ? item.incorrectFeedbacks[index].trim() + : "", + })) + .filter((entry) => entry.alt.length > 0) + .map((entry) => entry.feedback), + })); + const parsed = jumbledSentenceExerciseSchema.safeParse({ title, - sentences, + sentences: normalizedSentences, }); if (!parsed.success) { setError( @@ -105,7 +272,7 @@ export default function CreateJumbledSentenceExercise() { const res = await fetch(endpoint, { method: isEditMode ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title, sentences }), + body: JSON.stringify({ title, sentences: normalizedSentences }), }); const data = await res.json(); if (!res.ok) throw new Error(data?.error || "Fehler beim Speichern."); @@ -133,9 +300,7 @@ export default function CreateJumbledSentenceExercise() {

- {isEditMode - ? "Jumbled Sentence Übung bearbeiten" - : "Jumbled Sentence Übung erstellen"} + {isEditMode ? "Satzmix-Übung bearbeiten" : "Satzmix-Übung erstellen"}

- Trage den Satz in der richtigen Reihenfolge ein und optional - alternative Reihenfolgen mit

denselben Wörtern. Für die - Lernenden werden die Wörter anschließend gemischt angezeigt. + Geben Sie den Satz in der richtigen Reihenfolge und optional in + alternativen Reihenfolgen

mit denselben Elementen ein. Die + Elemente werden den Lernenden anschließend in zufälliger

+ Reihenfolge angezeigt.

{sentences.map((s, idx) => (