Merge pull request #534 from erzabytyci/user-tiers

Improve: Free exercise, UI
This commit is contained in:
erzabytyci
2026-06-10 00:00:16 +03:00
committed by GitHub
27 changed files with 273 additions and 170 deletions
@@ -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);
+2 -2
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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}
/>
);
}