mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
Merge pull request #534 from erzabytyci/user-tiers
Improve: Free exercise, UI
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE free_form_user_answers
|
||||
DROP CONSTRAINT IF EXISTS free_form_user_answers_free_form_exercise_id_user_id_key;
|
||||
|
||||
ALTER TABLE free_form_user_answers
|
||||
ADD CONSTRAINT free_form_user_answers_user_exercise_question_key
|
||||
UNIQUE (user_id, free_form_exercise_id, free_form_question_id);
|
||||
@@ -546,8 +546,8 @@ CREATE TABLE public.free_form_user_answers (
|
||||
);
|
||||
|
||||
ALTER TABLE public.free_form_user_answers
|
||||
ADD CONSTRAINT free_form_user_answers_free_form_exercise_id_user_id_key
|
||||
UNIQUE (free_form_exercise_id, user_id);
|
||||
ADD CONSTRAINT free_form_user_answers_user_exercise_question_key
|
||||
UNIQUE (user_id, free_form_exercise_id, free_form_question_id);
|
||||
|
||||
ALTER TABLE public.free_form_user_answers
|
||||
ADD CONSTRAINT free_form_user_answers_free_form_exercise_id_fkey
|
||||
|
||||
+8
-3
@@ -228,17 +228,22 @@ CREATE TABLE public.free_form_user_answers (
|
||||
answer text NOT NULL,
|
||||
free_form_exercise_id bigint NOT NULL,
|
||||
is_correct boolean DEFAULT false,
|
||||
user_id bigint NOT NULL
|
||||
user_id bigint NOT NULL,
|
||||
free_form_question_id bigint
|
||||
);
|
||||
|
||||
ALTER TABLE public.free_form_user_answers
|
||||
ADD CONSTRAINT free_form_user_answers_free_form_exercise_id_user_id_key
|
||||
UNIQUE (free_form_exercise_id, user_id);
|
||||
ADD CONSTRAINT free_form_user_answers_user_exercise_question_key
|
||||
UNIQUE (user_id, free_form_exercise_id, free_form_question_id);
|
||||
|
||||
ALTER TABLE public.free_form_user_answers
|
||||
ADD CONSTRAINT free_form_user_answers_free_form_exercise_id_fkey
|
||||
FOREIGN KEY (free_form_exercise_id) REFERENCES free_form_exercises(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.free_form_user_answers
|
||||
ADD CONSTRAINT free_form_user_answers_free_form_question_id_fkey
|
||||
FOREIGN KEY (free_form_question_id) REFERENCES free_form_questions(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE public.free_form_user_answers
|
||||
ADD CONSTRAINT free_form_user_answers_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -18,33 +18,23 @@ export const GET = withAuth(async (request, { params }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const { rows: answerRows } = await DB.pool(
|
||||
`SELECT
|
||||
id,
|
||||
answer,
|
||||
is_correct,
|
||||
feedback
|
||||
FROM free_form_answers
|
||||
WHERE free_form_exercise_id = $1
|
||||
ORDER BY is_correct DESC, id ASC`,
|
||||
[exercise_id]
|
||||
);
|
||||
|
||||
const userId = request.user.id;
|
||||
const { rows: userAnswerRows } = await DB.pool(
|
||||
`SELECT
|
||||
answer,
|
||||
is_correct,
|
||||
free_form_question_id,
|
||||
ffq.question,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM free_form_user_answers
|
||||
LEFT JOIN free_form_questions ffq ON ffq.id = free_form_user_answers.free_form_question_id
|
||||
WHERE free_form_exercise_id = $1 AND user_id = $2
|
||||
ORDER BY updated_at DESC`,
|
||||
[exercise_id, userId]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
possibleAnswers: answerRows,
|
||||
userAnswers: userAnswerRows,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -181,6 +181,56 @@ describe("freeform answers", () => {
|
||||
String(questionId)
|
||||
);
|
||||
});
|
||||
|
||||
it("should store answers separately per question within same exercise", async () => {
|
||||
const user = await TestFactory.user();
|
||||
const { mockPost } = useTestRequest(user);
|
||||
const { exerciseId, questionId } = await setupExercise();
|
||||
|
||||
const secondQuestion = await DB.pool(
|
||||
`INSERT INTO free_form_questions (free_form_exercise_id, question, question_order)
|
||||
VALUES ($1, $2, $3) RETURNING id`,
|
||||
[exerciseId, "Second test question", 2]
|
||||
);
|
||||
const secondQuestionId = secondQuestion.rows[0].id;
|
||||
|
||||
await TestFactory.freeFormAnswer({
|
||||
free_form_exercise_id: exerciseId,
|
||||
free_form_question_id: secondQuestionId,
|
||||
answer: "Second question correct answer",
|
||||
is_correct: true,
|
||||
feedback: "Good second answer",
|
||||
});
|
||||
|
||||
await POST(
|
||||
mockPost("/api/exercises/freeform/answers", {
|
||||
freeFormExerciseId: exerciseId,
|
||||
freeFormQuestionId: questionId,
|
||||
answer: "This is the correct answer",
|
||||
})
|
||||
);
|
||||
|
||||
await POST(
|
||||
mockPost("/api/exercises/freeform/answers", {
|
||||
freeFormExerciseId: exerciseId,
|
||||
freeFormQuestionId: secondQuestionId,
|
||||
answer: "Second question correct answer",
|
||||
})
|
||||
);
|
||||
|
||||
const result = await DB.pool(
|
||||
`SELECT free_form_question_id, answer
|
||||
FROM free_form_user_answers
|
||||
WHERE user_id = $1 AND free_form_exercise_id = $2
|
||||
ORDER BY free_form_question_id ASC`,
|
||||
[user.id, exerciseId]
|
||||
);
|
||||
|
||||
expect(result.rows.length).toBe(2);
|
||||
expect(result.rows.map((row) => String(row.free_form_question_id))).toEqual(
|
||||
[String(questionId), String(secondQuestionId)].sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("text processing utility functions", () => {
|
||||
|
||||
@@ -261,7 +261,7 @@ export const POST = withAuth(
|
||||
return NextResponse.json({
|
||||
is_correct: false,
|
||||
feedback:
|
||||
"Your answer doesn't match any of the expected answers. Please try again.",
|
||||
"Deine Antwort passt zu keiner erwarteten Antwort. Bitte versuche es erneut.",
|
||||
similarity: bestMatch.similarity,
|
||||
});
|
||||
}
|
||||
@@ -289,18 +289,15 @@ export const POST = withAuth(
|
||||
|
||||
// Generate feedback based on comparison
|
||||
let feedbackMessage =
|
||||
bestMatch.feedback ||
|
||||
(isAnswerCorrect
|
||||
? "Correct answer!"
|
||||
: "Incorrect answer. Please try again.");
|
||||
bestMatch.feedback || (isAnswerCorrect ? "Richtig" : "Falsch");
|
||||
|
||||
if (comparisonDetails) {
|
||||
if (comparisonDetails.wordOrderErrors > 0 && isAnswerCorrect) {
|
||||
feedbackMessage =
|
||||
"Your answer has the right words, but some are in the wrong order.";
|
||||
"Deine Antwort hat die richtigen Wörter, aber teilweise in falscher Reihenfolge.";
|
||||
} else if (comparisonDetails.hasErrors && isAnswerCorrect) {
|
||||
feedbackMessage =
|
||||
"Your answer is correct, but there are some minor errors.";
|
||||
"Deine Antwort ist richtig, enthält aber kleine Fehler.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +331,7 @@ async function recordUserAnswer(
|
||||
`INSERT INTO free_form_user_answers
|
||||
(answer, free_form_exercise_id, free_form_question_id, is_correct, user_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (user_id, free_form_exercise_id)
|
||||
ON CONFLICT (user_id, free_form_exercise_id, free_form_question_id)
|
||||
DO UPDATE SET
|
||||
answer = $1,
|
||||
is_correct = $4,
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import WordSelectionExercise from "@/components/ui/click/click.js";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
import { useRequest } from "@/shared/hooks/useRequest";
|
||||
|
||||
@@ -55,6 +56,9 @@ export default function StudentExercisePage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/click/${click_id}`}
|
||||
/>
|
||||
<WordSelectionExercise
|
||||
title={exercise.title}
|
||||
targetCategory={exercise.category}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function ClickExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Klick-Übungen</h1>
|
||||
<h2>Klick-Übungen</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Übungen verfügbar.
|
||||
@@ -74,7 +74,7 @@ export default function ClickExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Klick-Übungen</h1>
|
||||
<h2>Klick-Übungen</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -5,6 +5,7 @@ import DndMatchLayout from "../layout";
|
||||
import MatchArea from "@/components/ui/dnd-match/MatchArea";
|
||||
import { LinkButton } from "@/components/ui/linkbutton";
|
||||
import { Container } from "@/components/ui/layout/container";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
|
||||
export default function DndMatchExercisePage() {
|
||||
@@ -23,7 +24,10 @@ export default function DndMatchExercisePage() {
|
||||
return (
|
||||
<DndMatchLayout>
|
||||
<Container p="md">
|
||||
<h1>{exercise.title}</h1>
|
||||
<h2>{exercise.title}</h2>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/dnd-match/${match_id}`}
|
||||
/>
|
||||
{exercise.description && (
|
||||
<p style={{ color: "var(--fg2)", marginBottom: "var(--u-lg)" }}>
|
||||
{exercise.description}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function DndMatchExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Zuordnungs-Übungen</h1>
|
||||
<h2>Zuordnungs-Übungen</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Übungen verfügbar.
|
||||
@@ -68,7 +68,7 @@ export default function DndMatchExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Zuordnungs-Übungen</h1>
|
||||
<h2>Zuordnungs-Übungen</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -9,6 +9,7 @@ import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import "../../../../globals.css";
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
import { LinkButton } from "@/components/ui/linkbutton";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
|
||||
export default function Dragdrop({}) {
|
||||
const params = useParams();
|
||||
@@ -38,7 +39,10 @@ export default function Dragdrop({}) {
|
||||
<DragdropLayout>
|
||||
<div className={styles.page}>
|
||||
<div className="exercise-container">
|
||||
<h1>{exercise.title}</h1>
|
||||
<h2>{exercise.title}</h2>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/dragdrop/${dnd_id}`}
|
||||
/>
|
||||
{!!exercise.description && <p>{exercise.description}</p>}
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Area exerciseID={dnd_id} />
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function DragdropExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Sortieren/Gruppieren-Übungen</h1>
|
||||
<h2>Sortieren/Gruppieren-Übungen</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Übungen verfügbar.
|
||||
@@ -74,7 +74,7 @@ export default function DragdropExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Sortieren/Gruppieren-Übungen</h1>
|
||||
<h2>Sortieren/Gruppieren-Übungen</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Container, Column, Row } from "@/components/ui/layout/container";
|
||||
import parse from "html-react-parser";
|
||||
import { useRequest } from "@/shared/hooks/useRequest";
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
import "../fillinthegap.css";
|
||||
|
||||
function tokenizeText(text) {
|
||||
@@ -204,7 +205,10 @@ export default function FillInTheGapExercisePage() {
|
||||
return (
|
||||
<Container maxW="900px" m="0 auto" p="md">
|
||||
<Column gap="md">
|
||||
<h1>{exercise.title}</h1>
|
||||
<h2>{exercise.title}</h2>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/fillinthegap/${exercise_id}`}
|
||||
/>
|
||||
|
||||
{!!exercise.source_html && (
|
||||
<Container
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function FillInTheGapExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Lückentext-Übungen</h1>
|
||||
<h2>Lückentext-Übungen</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Übungen verfügbar.
|
||||
@@ -74,7 +74,7 @@ export default function FillInTheGapExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Lückentext-Übungen</h1>
|
||||
<h2>Lückentext-Übungen</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -1,129 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Container, Row } from "@/components/ui/layout/container";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
|
||||
export default function FreeformFeedback() {
|
||||
const { exercise_id } = useParams();
|
||||
export default function FreeformFeedback({
|
||||
questions = [],
|
||||
submissionsByQuestionId = {},
|
||||
}) {
|
||||
const [showAllFeedback, setShowAllFeedback] = useState(false);
|
||||
|
||||
const {
|
||||
data: feedback,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery(`/exercises/freeform/answers/${exercise_id}`, undefined, {
|
||||
enabled: showAllFeedback,
|
||||
});
|
||||
|
||||
const toggleFeedback = () => {
|
||||
setShowAllFeedback(!showAllFeedback);
|
||||
if (!showAllFeedback) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container mt="xl">
|
||||
<Container mb="md">
|
||||
<Container mt="0">
|
||||
<Container mb={showAllFeedback ? "md" : "0"}>
|
||||
<Button variant="outline" onClick={toggleFeedback}>
|
||||
{showAllFeedback
|
||||
? "Feedback ausblenden"
|
||||
: "Richtige Antworten anzeigen"}
|
||||
? "Meine Antworten ausblenden"
|
||||
: "Meine Antworten anzeigen"}
|
||||
</Button>
|
||||
</Container>
|
||||
|
||||
{showAllFeedback && (
|
||||
<>
|
||||
{isLoading && (
|
||||
<Container p="md" bg="var(--bg2)" br="md" mb="md">
|
||||
Feedback wird geladen...
|
||||
</Container>
|
||||
<Container p="md" bg="var(--bg2)" br="md" mb="md">
|
||||
<h2>Meine Antworten</h2>
|
||||
|
||||
{questions.length === 0 && (
|
||||
<Container>Für diese Übung sind keine Fragen verfügbar.</Container>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Container p="md" bg="var(--tertiary1)" br="md" mb="md">
|
||||
Fehler: {error}
|
||||
<Button onClick={refetch} variant="secondary" size="sm" mt="sm">
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
</Container>
|
||||
)}
|
||||
{questions.map((question, index) => {
|
||||
const submission = submissionsByQuestionId[question.id];
|
||||
|
||||
{!isLoading && !error && feedback && (
|
||||
<Container p="md" bg="var(--bg2)" br="md" mb="md">
|
||||
<h2>Feedback</h2>
|
||||
|
||||
{feedback.userAnswers && feedback.userAnswers.length > 0 ? (
|
||||
<Container mt="md">
|
||||
<h3>Ihre Antwort</h3>
|
||||
{feedback.userAnswers.map((answer, index) => (
|
||||
<Container
|
||||
key={index}
|
||||
p="sm"
|
||||
mb="sm"
|
||||
bg={
|
||||
answer.is_correct ? "var(--green1)" : "var(--tertiary1)"
|
||||
}
|
||||
br="md"
|
||||
>
|
||||
<Row justify="space-between" w="100%">
|
||||
<strong>Antwort: {answer.answer}</strong>
|
||||
{answer.is_correct ? "Richtig" : "Falsch"}
|
||||
</Row>
|
||||
<div>
|
||||
<strong>Versucht:</strong>{" "}
|
||||
{new Date(answer.updated_at).toLocaleString()}
|
||||
</div>
|
||||
</Container>
|
||||
))}
|
||||
if (!submission) {
|
||||
return (
|
||||
<Container
|
||||
key={question.id}
|
||||
p="sm"
|
||||
mb="sm"
|
||||
bg="var(--bg1)"
|
||||
br="md"
|
||||
>
|
||||
<div>
|
||||
<strong>Frage {index + 1}:</strong> {question.question}
|
||||
</div>
|
||||
<div>Noch nicht beantwortet.</div>
|
||||
</Container>
|
||||
) : null}
|
||||
);
|
||||
}
|
||||
|
||||
{feedback.possibleAnswers &&
|
||||
feedback.possibleAnswers.length > 0 ? (
|
||||
<Container mt="md">
|
||||
<h3>Mögliche Antworten</h3>
|
||||
{feedback.possibleAnswers
|
||||
.filter((answer) => answer.is_correct)
|
||||
.map((answer, index) => (
|
||||
<Container
|
||||
key={answer.id}
|
||||
p="sm"
|
||||
mb="sm"
|
||||
bg={
|
||||
answer.is_correct
|
||||
? "var(--green1)"
|
||||
: "var(--tertiary1)"
|
||||
}
|
||||
br="md"
|
||||
>
|
||||
<div>
|
||||
<strong>Antwort {index + 1}:</strong> {answer.answer}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Feedback:</strong>{" "}
|
||||
{answer.feedback ||
|
||||
(answer.is_correct ? "Richtig" : "Falsch")}
|
||||
</div>
|
||||
</Container>
|
||||
))}
|
||||
</Container>
|
||||
) : null}
|
||||
const fallbackFeedback = submission.is_correct
|
||||
? "Richtig"
|
||||
: "Falsch";
|
||||
|
||||
{(!feedback.possibleAnswers ||
|
||||
!feedback.possibleAnswers.length) &&
|
||||
(!feedback.userAnswers || !feedback.userAnswers.length) && (
|
||||
<Container>
|
||||
Für diese Übung ist kein Feedback verfügbar.
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
return (
|
||||
<Container
|
||||
key={question.id}
|
||||
p="sm"
|
||||
mb="sm"
|
||||
bg={
|
||||
submission.is_correct ? "var(--green1)" : "var(--tertiary1)"
|
||||
}
|
||||
br="md"
|
||||
>
|
||||
<Row justify="space-between" w="100%">
|
||||
<strong>Antwort: {submission.answer}</strong>
|
||||
{submission.is_correct ? "Richtig" : "Falsch"}
|
||||
</Row>
|
||||
<div>
|
||||
<strong>Feedback:</strong>{" "}
|
||||
{submission.feedback || fallbackFeedback}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import useQuery from "@/shared/hooks/useQuery";
|
||||
import { useRequest } from "@/shared/hooks/useRequest";
|
||||
import FreeformFeedback from "./feedback";
|
||||
import { LinkButton } from "@/components/ui/linkbutton";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
|
||||
const styles = {
|
||||
errorWord: {
|
||||
@@ -131,7 +132,7 @@ const AnswerComparison = ({ comparisonDetails }) => {
|
||||
export default function FreeFormExercisePage() {
|
||||
const { exercise_id } = useParams();
|
||||
const [answer, setAnswer] = useState("");
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
const [submissionsByQuestionId, setSubmissionsByQuestionId] = useState({});
|
||||
const [error, setError] = useState(null);
|
||||
const makeRequest = useRequest();
|
||||
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
||||
@@ -151,6 +152,11 @@ export default function FreeFormExercisePage() {
|
||||
currentExerciseIndex !== -1 &&
|
||||
allExercises?.[currentExerciseIndex + 1];
|
||||
|
||||
const currentQuestion = exercise?.questions?.[currentQuestionIndex] || null;
|
||||
const currentSubmission = currentQuestion
|
||||
? submissionsByQuestionId[currentQuestion.id]
|
||||
: null;
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -162,7 +168,16 @@ export default function FreeFormExercisePage() {
|
||||
});
|
||||
const data = response.data;
|
||||
if (data) {
|
||||
setFeedback(data);
|
||||
setSubmissionsByQuestionId((prev) => ({
|
||||
...prev,
|
||||
[exercise.questions[currentQuestionIndex].id]: {
|
||||
answer,
|
||||
is_correct: Boolean(data.is_correct),
|
||||
feedback: data.feedback || "",
|
||||
comparisonDetails: data.comparisonDetails || null,
|
||||
submittedAt: new Date().toISOString(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
@@ -195,7 +210,10 @@ export default function FreeFormExercisePage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>{exercise.title}</h1>
|
||||
<h2>{exercise.title}</h2>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/free-form/${exercise_id}`}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
<Container mb="xl" p="md" bg="var(--bg2)" br="md">
|
||||
@@ -203,13 +221,13 @@ export default function FreeFormExercisePage() {
|
||||
Frage {currentQuestionIndex + 1} von {exercise.questions.length}
|
||||
</Container>
|
||||
<Container>
|
||||
<h2 style={{ fontSize: "2rem", lineHeight: "1.3", margin: 0 }}>
|
||||
<h2 style={{ fontSize: "1rem", lineHeight: "1.3", margin: 0 }}>
|
||||
{exercise.questions[currentQuestionIndex]?.question}
|
||||
</h2>
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form id="freeform-answer-form" onSubmit={handleSubmit}>
|
||||
<Column gap="lg">
|
||||
<Container>
|
||||
<label>
|
||||
@@ -225,17 +243,18 @@ export default function FreeFormExercisePage() {
|
||||
/>
|
||||
</label>
|
||||
</Container>
|
||||
|
||||
<Row justify="center">
|
||||
<Button type="submit" variant="secondary" width="fit">
|
||||
Antwort abschicken
|
||||
</Button>
|
||||
</Row>
|
||||
</Column>
|
||||
</form>
|
||||
|
||||
<Row justify="space-between" align="center" mt="lg" mb="md">
|
||||
<FreeformFeedback />
|
||||
<Button
|
||||
type="submit"
|
||||
form="freeform-answer-form"
|
||||
variant="secondary"
|
||||
width="fit"
|
||||
>
|
||||
Antwort abschicken
|
||||
</Button>
|
||||
<Row justify="end" gap="md">
|
||||
{currentQuestionIndex > 0 && (
|
||||
<Button
|
||||
@@ -244,7 +263,6 @@ export default function FreeFormExercisePage() {
|
||||
onClick={() => {
|
||||
setCurrentQuestionIndex((prev) => prev - 1);
|
||||
setAnswer("");
|
||||
setFeedback(null);
|
||||
}}
|
||||
>
|
||||
Vorherige
|
||||
@@ -257,7 +275,6 @@ export default function FreeFormExercisePage() {
|
||||
onClick={() => {
|
||||
setCurrentQuestionIndex((prev) => prev + 1);
|
||||
setAnswer("");
|
||||
setFeedback(null);
|
||||
}}
|
||||
>
|
||||
Nächste
|
||||
@@ -266,33 +283,43 @@ export default function FreeFormExercisePage() {
|
||||
</Row>
|
||||
</Row>
|
||||
|
||||
{feedback && (
|
||||
<FreeformFeedback
|
||||
questions={exercise.questions || []}
|
||||
submissionsByQuestionId={submissionsByQuestionId}
|
||||
/>
|
||||
|
||||
{currentSubmission && (
|
||||
<Row
|
||||
p="md"
|
||||
br="md"
|
||||
mb="md"
|
||||
mt="lg"
|
||||
justify="space-between"
|
||||
bg={feedback.is_correct ? "var(--green1)" : "var(--tertiary1)"}
|
||||
bg={
|
||||
currentSubmission.is_correct ? "var(--green1)" : "var(--tertiary1)"
|
||||
}
|
||||
>
|
||||
<Column>
|
||||
<span>Feedback des Lehrers:</span>
|
||||
{feedback.feedback}
|
||||
{currentSubmission.feedback ||
|
||||
(currentSubmission.is_correct ? "Richtig" : "Falsch")}
|
||||
</Column>
|
||||
<Container mb="sm" fontWeight="600">
|
||||
{feedback.is_correct ? "Richtig!" : "Falsch"}
|
||||
{currentSubmission.is_correct ? "Richtig!" : "Falsch"}
|
||||
</Container>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{feedback && feedback.comparisonDetails && (
|
||||
<AnswerComparison comparisonDetails={feedback.comparisonDetails} />
|
||||
{currentSubmission && currentSubmission.comparisonDetails && (
|
||||
<AnswerComparison
|
||||
comparisonDetails={currentSubmission.comparisonDetails}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Row gap="md" mt="xl" justify="space-between">
|
||||
<LinkButton href="/grammar/exercises">Zurück zu den Übungen</LinkButton>
|
||||
|
||||
{feedback?.is_correct && nextExercise && (
|
||||
{currentSubmission?.is_correct && nextExercise && (
|
||||
<LinkButton
|
||||
variant="secondary"
|
||||
href={`/grammar/exercises/freeform/${nextExercise.id}`}
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function FreeFormExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Freie Übungen</h1>
|
||||
<h2>Freie Übungen</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Übungen verfügbar.
|
||||
@@ -73,7 +73,7 @@ export default function FreeFormExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Freie Übungen</h1>
|
||||
<h2>Freie Übungen</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Column, Row } from "@/components/ui/layout/container";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
import { withBasePath } from "@/shared/utils/basePath";
|
||||
|
||||
function shuffle(array) {
|
||||
@@ -286,6 +287,9 @@ export default function JumbledSentenceExercisePage() {
|
||||
return (
|
||||
<Column gap="lg">
|
||||
<h2>{exercise.title}</h2>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/jumbled-sentence/${id}`}
|
||||
/>
|
||||
|
||||
{exercise.sentences.map((_, sentenceIdx) => (
|
||||
<Column
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function JumbledSentenceExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Satzmix-Übungen</h1>
|
||||
<h2>Satzmix-Übungen</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Übungen verfügbar.
|
||||
@@ -71,7 +71,7 @@ export default function JumbledSentenceExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Satzmix-Übungen</h1>
|
||||
<h2>Satzmix-Übungen</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -7,6 +7,7 @@ import useQuery from "@/shared/hooks/useQuery";
|
||||
import { Column, Container, Row } from "@/components/ui/layout/container";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MemoryGame } from "@/components/ui/memory-game/MemoryGame";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
import { withBasePath } from "@/shared/utils/basePath";
|
||||
|
||||
export default function MemoryGamePlayerPage() {
|
||||
@@ -83,7 +84,10 @@ export default function MemoryGamePlayerPage() {
|
||||
return (
|
||||
<Container maxW="1000px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>{exercise.title}</h1>
|
||||
<h2>{exercise.title}</h2>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/memory-game/${memory_game_id}`}
|
||||
/>
|
||||
<p>{exercise.description}</p>
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function MemoryGameExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Memory Game</h1>
|
||||
<h2>Memory Game</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Memory Games verfügbar.
|
||||
@@ -71,7 +71,7 @@ export default function MemoryGameExercisesPage() {
|
||||
return (
|
||||
<Container maxW="1000px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Memory Game</h1>
|
||||
<h2>Memory Game</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MultichoicePage from "@/components/ui/multichoice/multichoicepage";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
import styles from "../../../../page.module.css";
|
||||
|
||||
export default async function Multichoice({ params }) {
|
||||
@@ -6,7 +7,10 @@ export default async function Multichoice({ params }) {
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1>Multiple-Choice-Übung</h1>
|
||||
<h2>Multiple-Choice-Übung</h2>
|
||||
<AdminVisibleLastModified
|
||||
endpoint={`/admin/exercises/multichoice/${id}`}
|
||||
/>
|
||||
<MultichoicePage exerciseId={id} /> {/* Pass the id to MultichoicePage */}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function MultichoiceExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Multichoice Übungen</h1>
|
||||
<h2>Multichoice Übungen</h2>
|
||||
</Container>
|
||||
<Container color="var(--fg4)">
|
||||
Zurzeit sind keine Übungen verfügbar.
|
||||
@@ -74,7 +74,7 @@ export default function MultichoiceExercisesPage() {
|
||||
return (
|
||||
<Container maxW="800px" m="0 auto" p="md">
|
||||
<Container mb="lg">
|
||||
<h1>Multiple-Choice-Übungen</h1>
|
||||
<h2>Multiple-Choice-Übungen</h2>
|
||||
</Container>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function ExercisePage({}) {
|
||||
return (
|
||||
<LessonsLayout>
|
||||
<Column>
|
||||
<h1>Übungstypen</h1>
|
||||
<h2>Übungstypen</h2>
|
||||
|
||||
<Column gap="md">
|
||||
{exerciseTypes.map((type, index) => (
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getPageData,
|
||||
} from "@/backend/html-services";
|
||||
import AdminButtons from "@/app/pages/[type]/[slug]/admin-buttons";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
import { LinkButton } from "@/components/ui/linkbutton";
|
||||
import "../lessons.css";
|
||||
|
||||
@@ -120,6 +121,10 @@ export default async function GrammarPage({ params, searchParams }) {
|
||||
return (
|
||||
<Column>
|
||||
<AdminButtons type="grammar" slug={decodedSlug} />
|
||||
<AdminVisibleLastModified
|
||||
updatedAt={pageData.updated_at}
|
||||
updatedBy={pageData.updated_by_username}
|
||||
/>
|
||||
<h1>{pageData.title}</h1>
|
||||
<RenderHTML data={pageData.content} />
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getPageData, getPageList } from "@/backend/html-services";
|
||||
import { transformHtmlToGlossaryTags } from "@/backend/html-transform";
|
||||
import AdminButtons from "./admin-buttons";
|
||||
import { LinkButton } from "@/components/ui/linkbutton";
|
||||
import AdminVisibleLastModified from "@/components/ui/admin-visible-last-modified";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -32,6 +33,10 @@ export default async function Chapters({ params }) {
|
||||
return (
|
||||
<Column className={layout.viewContent}>
|
||||
<AdminButtons type={type} slug={slug} />
|
||||
<AdminVisibleLastModified
|
||||
updatedAt={pageData.updated_at}
|
||||
updatedBy={pageData.updated_by_username}
|
||||
/>
|
||||
<h1 className="chapter-page-title">{pageData.title}</h1>
|
||||
|
||||
<Container className="chapter-rendered-content">
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import AdminLastModified from "@/components/ui/admin-last-modified";
|
||||
import { checkIsAdmin } from "@/context/user.context";
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
|
||||
export default function AdminVisibleLastModified({
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
endpoint,
|
||||
className,
|
||||
}) {
|
||||
const isAdmin = checkIsAdmin();
|
||||
const shouldFetch = isAdmin && !!endpoint;
|
||||
|
||||
const { data } = useQuery(endpoint || "", null, {
|
||||
enabled: shouldFetch,
|
||||
});
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedUpdatedAt =
|
||||
updatedAt || data?.last_modified_at || data?.exercise?.last_modified_at;
|
||||
const resolvedUpdatedBy =
|
||||
updatedBy || data?.last_modified_by || data?.exercise?.last_modified_by;
|
||||
|
||||
return (
|
||||
<AdminLastModified
|
||||
updatedAt={resolvedUpdatedAt}
|
||||
updatedBy={resolvedUpdatedBy}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user