mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
Refactor learning boxes UI, rename headers/buttons, add footer credits, improve KPL 5 result sorting, support line breaks in MC exercises, and update sidebar
This commit is contained in:
@@ -7,23 +7,37 @@ test.describe("Learning Page", () => {
|
||||
await expect(page.getByRole("main").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show language dropdown button", async ({ page }) => {
|
||||
test("should show language toggle options", async ({ page }) => {
|
||||
await page.goto("/learning", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", { name: /English|Deutsch/i })
|
||||
page.getByRole("group", { name: "Choose language" })
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
await expect(page.getByRole("button", { name: "English" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Deutsch" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should have language selection functionality", async ({ page }) => {
|
||||
test("should switch language selection", async ({ page }) => {
|
||||
await page.goto("/learning");
|
||||
|
||||
const languageButton = page.getByRole("button", {
|
||||
name: /English|Deutsch/i,
|
||||
});
|
||||
await expect(languageButton).toBeVisible();
|
||||
const englishButton = page.getByRole("button", { name: "English" });
|
||||
const deutschButton = page.getByRole("button", { name: "Deutsch" });
|
||||
|
||||
await languageButton.click();
|
||||
await expect(englishButton).toBeVisible();
|
||||
await expect(deutschButton).toBeVisible();
|
||||
|
||||
const englishPressed =
|
||||
(await englishButton.getAttribute("aria-pressed")) === "true";
|
||||
|
||||
if (englishPressed) {
|
||||
await deutschButton.click();
|
||||
await expect(deutschButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(englishButton).toHaveAttribute("aria-pressed", "false");
|
||||
} else {
|
||||
await englishButton.click();
|
||||
await expect(englishButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(deutschButton).toHaveAttribute("aria-pressed", "false");
|
||||
}
|
||||
|
||||
await expect(page).toHaveURL(/.*learning/);
|
||||
});
|
||||
|
||||
@@ -8,14 +8,14 @@ test.describe("Navigation", () => {
|
||||
});
|
||||
|
||||
test("should navigate to learning resources page", async ({ page }) => {
|
||||
await page.goto("/resources");
|
||||
await expect(page).toHaveURL(/.*resources/);
|
||||
await page.goto("/pages/resources");
|
||||
await expect(page).toHaveURL(/.*pages\/resources/);
|
||||
await expect(page.locator("main")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should navigate to a specific chapter", async ({ page }) => {
|
||||
await page.goto("/resources/1");
|
||||
await expect(page).toHaveURL(/.*resources\/1/);
|
||||
await page.goto("/pages/resources/1");
|
||||
await expect(page).toHaveURL(/.*pages\/resources\/1/);
|
||||
await expect(page.locator("main")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { takeScreenshot, elementExists } = require("./utils/helpers");
|
||||
const { test } = require("@playwright/test");
|
||||
const { elementExists } = require("./utils/helpers");
|
||||
|
||||
test.describe("Page Structure Analysis", () => {
|
||||
test("analyze homepage structure", async ({ page }) => {
|
||||
@@ -40,8 +40,11 @@ test.describe("Page Structure Analysis", () => {
|
||||
const hasH1 = await elementExists(page, "h1");
|
||||
console.log("Has h1:", hasH1);
|
||||
|
||||
const hasTabs = await elementExists(page, '[role="tablist"]');
|
||||
console.log("Has tablist:", hasTabs);
|
||||
const hasLanguageToggle = await elementExists(
|
||||
page,
|
||||
'[role="group"][aria-label="Choose language"]'
|
||||
);
|
||||
console.log("Has language toggle group:", hasLanguageToggle);
|
||||
|
||||
const hasButtons = await elementExists(page, "button");
|
||||
console.log("Has buttons:", hasButtons);
|
||||
|
||||
@@ -36,7 +36,7 @@ test.describe("Resources Detail Pages", () => {
|
||||
await page.goto("/pages/resources/1", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const resourcesLink = page.getByRole("link", {
|
||||
name: "Richtig Online Lernen",
|
||||
name: "Effektiv online lernen",
|
||||
});
|
||||
|
||||
if (await resourcesLink.isVisible()) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
.error-message {
|
||||
color: var(--fg1);
|
||||
background: var(--tertiary1);
|
||||
background: var(--tertiary2);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--tertiary2);
|
||||
@@ -38,6 +38,18 @@
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-input.input-error {
|
||||
border-color: var(--tertiary2, #ef4444);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.field-error-hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--tertiary2, #ef4444);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--fg4);
|
||||
}
|
||||
@@ -77,6 +89,18 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.linebreak-block {
|
||||
width: 100%;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.linebreak-indicator {
|
||||
font-size: 1.1rem;
|
||||
color: var(--fg4);
|
||||
padding: 0 6px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* The text input in the sentence flow (The "..." lines) */
|
||||
.text-block-input {
|
||||
border: none;
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function CreateMultichoicePage() {
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [titleError, setTitleError] = useState(false);
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
@@ -65,6 +66,10 @@ export default function CreateMultichoicePage() {
|
||||
setActiveSectionIndex(newIndex);
|
||||
};
|
||||
|
||||
const addLineBreakSection = () => {
|
||||
setSections([...sections, { id: crypto.randomUUID(), type: "linebreak" }]);
|
||||
};
|
||||
|
||||
const handleTextChange = (index, newValue) => {
|
||||
const newSections = [...sections];
|
||||
newSections[index].value = newValue;
|
||||
@@ -83,6 +88,7 @@ export default function CreateMultichoicePage() {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setError("");
|
||||
setTitleError(false);
|
||||
|
||||
// --- FIX 1: Convert Index to Text for correct answer ---
|
||||
const contentToSave = sections.map((section) => {
|
||||
@@ -107,6 +113,7 @@ export default function CreateMultichoicePage() {
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
if (res.status === 409) setTitleError(true);
|
||||
throw new Error(data.error || "Fehler beim Speichern");
|
||||
}
|
||||
|
||||
@@ -128,11 +135,19 @@ export default function CreateMultichoicePage() {
|
||||
<div className="form-group">
|
||||
<label>Titel</label>
|
||||
<input
|
||||
className="form-input"
|
||||
className={`form-input${titleError ? " input-error" : ""}`}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
setTitleError(false);
|
||||
}}
|
||||
placeholder="Titel der Übung"
|
||||
/>
|
||||
{titleError && (
|
||||
<span className="field-error-hint">
|
||||
Dieser Titel existiert bereits. Bitte wähle einen anderen.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<hr className="divider" />
|
||||
|
||||
@@ -160,6 +175,22 @@ export default function CreateMultichoicePage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (section.type === "linebreak") {
|
||||
return (
|
||||
<div
|
||||
key={section.id}
|
||||
className="inline-block-wrapper linebreak-block"
|
||||
>
|
||||
<span className="linebreak-indicator">↵</span>
|
||||
<button
|
||||
onClick={() => removeSection(index)}
|
||||
className="delete-x"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (section.type === "gap") {
|
||||
const isActive = activeSectionIndex === index;
|
||||
|
||||
@@ -209,10 +240,13 @@ export default function CreateMultichoicePage() {
|
||||
+ Text
|
||||
</Button>
|
||||
<Button size="sm" onClick={addMultiChoiceSection}>
|
||||
+ Dropdown
|
||||
+ Runterfallen
|
||||
</Button>
|
||||
<Button size="sm" onClick={addGapSection}>
|
||||
+ Gap
|
||||
+ Lücke
|
||||
</Button>
|
||||
<Button size="sm" onClick={addLineBreakSection}>
|
||||
+ Zeilenumbruch
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,6 +392,8 @@ export default function CreateMultichoicePage() {
|
||||
placeholder="..."
|
||||
disabled
|
||||
/>
|
||||
) : s.type === "linebreak" ? (
|
||||
<br key={i} />
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -388,6 +388,7 @@ h2 {
|
||||
|
||||
ul {
|
||||
list-style-type: circle;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -489,7 +490,7 @@ label {
|
||||
|
||||
.rendered-html ul,
|
||||
.rendered-html ol {
|
||||
padding-left: 1.5rem;
|
||||
padding-left: 3rem;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ export default function FreeformFeedback() {
|
||||
<Container mt="xl">
|
||||
<Container mb="md">
|
||||
<Button variant="outline" onClick={toggleFeedback}>
|
||||
{showAllFeedback ? "Hide Feedback" : "Show correct answers"}
|
||||
{showAllFeedback
|
||||
? "Feedback ausblenden"
|
||||
: "Richtige Antworten anzeigen"}
|
||||
</Button>
|
||||
</Container>
|
||||
|
||||
@@ -38,15 +40,15 @@ export default function FreeformFeedback() {
|
||||
<>
|
||||
{isLoading && (
|
||||
<Container p="md" bg="var(--bg2)" br="md" mb="md">
|
||||
Loading feedback...
|
||||
Feedback wird geladen...
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Container p="md" bg="var(--tertiary1)" br="md" mb="md">
|
||||
Error: {error}
|
||||
Fehler: {error}
|
||||
<Button onClick={refetch} variant="secondary" size="sm" mt="sm">
|
||||
Try Again
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
</Container>
|
||||
)}
|
||||
@@ -57,7 +59,7 @@ export default function FreeformFeedback() {
|
||||
|
||||
{feedback.userAnswers && feedback.userAnswers.length > 0 ? (
|
||||
<Container mt="md">
|
||||
<h3>Your Answer</h3>
|
||||
<h3>Ihre Antwort</h3>
|
||||
{feedback.userAnswers.map((answer, index) => (
|
||||
<Container
|
||||
key={index}
|
||||
@@ -69,11 +71,11 @@ export default function FreeformFeedback() {
|
||||
br="md"
|
||||
>
|
||||
<Row justify="space-between" w="100%">
|
||||
<strong>Answer: {answer.answer}</strong>
|
||||
{answer.is_correct ? "Correct" : "Incorrect"}
|
||||
<strong>Antwort: {answer.answer}</strong>
|
||||
{answer.is_correct ? "Richtig" : "Falsch"}
|
||||
</Row>
|
||||
<div>
|
||||
<strong>Attempted:</strong>{" "}
|
||||
<strong>Versucht:</strong>{" "}
|
||||
{new Date(answer.updated_at).toLocaleString()}
|
||||
</div>
|
||||
</Container>
|
||||
@@ -84,7 +86,7 @@ export default function FreeformFeedback() {
|
||||
{feedback.possibleAnswers &&
|
||||
feedback.possibleAnswers.length > 0 ? (
|
||||
<Container mt="md">
|
||||
<h3>Possible Answers</h3>
|
||||
<h3>Mögliche Antworten</h3>
|
||||
{feedback.possibleAnswers
|
||||
.filter((answer) => answer.is_correct)
|
||||
.map((answer, index) => (
|
||||
@@ -100,12 +102,12 @@ export default function FreeformFeedback() {
|
||||
br="md"
|
||||
>
|
||||
<div>
|
||||
<strong>Answer {index + 1}:</strong> {answer.answer}
|
||||
<strong>Antwort {index + 1}:</strong> {answer.answer}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Feedback:</strong>{" "}
|
||||
{answer.feedback ||
|
||||
(answer.is_correct ? "Correct" : "Incorrect")}
|
||||
(answer.is_correct ? "Richtig" : "Falsch")}
|
||||
</div>
|
||||
</Container>
|
||||
))}
|
||||
@@ -116,7 +118,7 @@ export default function FreeformFeedback() {
|
||||
!feedback.possibleAnswers.length) &&
|
||||
(!feedback.userAnswers || !feedback.userAnswers.length) && (
|
||||
<Container>
|
||||
No feedback available for this exercise.
|
||||
Für diese Übung ist kein Feedback verfügbar.
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@@ -49,6 +49,30 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.languageToggleWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--u-sm);
|
||||
}
|
||||
|
||||
.languageLabel {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--fg3);
|
||||
}
|
||||
|
||||
.languageToggle {
|
||||
display: inline-flex;
|
||||
gap: var(--u-sm);
|
||||
padding: var(--u-xs);
|
||||
border: 1px solid var(--fg4);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg2);
|
||||
}
|
||||
|
||||
.languageButton {
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
.learningAdvice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -80,3 +104,20 @@
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.learningAnswersHeader {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--u-md);
|
||||
}
|
||||
|
||||
.languageToggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.languageButton {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,15 @@
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
import styles from "./learning-answers.module.css";
|
||||
import { FORM_LANGUAGE_OPTIONS } from "../page";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
getLanguageField,
|
||||
getLanguageTitle,
|
||||
} from "@/components/ui/learning-form";
|
||||
import { Dropdown } from "@/components/ui/dropdown";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import layout from "@/shared/styles/layout.module.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
import useLocalStorage from "@/shared/utils/useLocalStorage";
|
||||
|
||||
export default function LearningAnswersPage() {
|
||||
const { data: answers } = useQuery("/forms/learning_type/answer");
|
||||
const { data: form } = useQuery("/forms/learning_type");
|
||||
@@ -20,8 +19,16 @@ export default function LearningAnswersPage() {
|
||||
"language",
|
||||
FORM_LANGUAGE_OPTIONS[0]
|
||||
);
|
||||
|
||||
const partsByHighestScore = [...(form?.parts || [])].sort((partA, partB) => {
|
||||
const scoreA = answers?.[partA.id] ?? 0;
|
||||
const scoreB = answers?.[partB.id] ?? 0;
|
||||
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
const renderAverages = () => {
|
||||
return form?.parts.map((part, index) => {
|
||||
return partsByHighestScore.map((part) => {
|
||||
const average = answers?.[part.id];
|
||||
return (
|
||||
<LearningAverage
|
||||
@@ -35,7 +42,7 @@ export default function LearningAnswersPage() {
|
||||
};
|
||||
|
||||
const renderAdvices = () => {
|
||||
return form?.parts
|
||||
return [...(form?.parts || [])]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.advice_threshold < answers?.[b.id]) -
|
||||
@@ -56,9 +63,32 @@ export default function LearningAnswersPage() {
|
||||
return (
|
||||
<div className={layout.view}>
|
||||
<div className={styles.learningAnswersHeader}>
|
||||
<Dropdown options={FORM_LANGUAGE_OPTIONS} onSelect={setLanguage}>
|
||||
<Button>{language.label}</Button>
|
||||
</Dropdown>
|
||||
<div className={styles.languageToggleWrap}>
|
||||
<span className={styles.languageLabel}>Language / Sprache</span>
|
||||
<div
|
||||
className={styles.languageToggle}
|
||||
role="group"
|
||||
aria-label="Choose language"
|
||||
>
|
||||
{FORM_LANGUAGE_OPTIONS.map((option) => {
|
||||
const isActive = language.value === option.value;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.value}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={isActive ? "primary" : "outline"}
|
||||
className={styles.languageButton}
|
||||
onClick={() => setLanguage(option)}
|
||||
aria-pressed={isActive}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => router.push("/learning")}>
|
||||
{language.value === "de" ? "Test neu machen" : "Retake Test"}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.languageToggleWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--u-sm);
|
||||
}
|
||||
|
||||
.languageLabel {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--fg3);
|
||||
}
|
||||
|
||||
.languageToggle {
|
||||
display: inline-flex;
|
||||
gap: var(--u-sm);
|
||||
padding: var(--u-xs);
|
||||
border: 1px solid var(--fg4);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg2);
|
||||
}
|
||||
|
||||
.languageButton {
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.languageToggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.languageButton {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Dropdown } from "@/components/ui/dropdown";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useQuery from "@/shared/hooks/useQuery";
|
||||
import layout from "@/shared/styles/layout.module.css";
|
||||
import styles from "./learning.module.css";
|
||||
import { useRequest } from "@/shared/hooks/useRequest";
|
||||
import useLocalStorage from "@/shared/utils/useLocalStorage";
|
||||
import { LearningForm } from "@/components/ui/learning-form";
|
||||
@@ -50,9 +50,32 @@ export default function Learning() {
|
||||
return (
|
||||
<div className={layout.view}>
|
||||
<Row justify="space-between">
|
||||
<Dropdown options={FORM_LANGUAGE_OPTIONS} onSelect={setLanguage}>
|
||||
<Button>{language.label}</Button>
|
||||
</Dropdown>
|
||||
<div className={styles.languageToggleWrap}>
|
||||
<span className={styles.languageLabel}>Language / Sprache</span>
|
||||
<div
|
||||
className={styles.languageToggle}
|
||||
role="group"
|
||||
aria-label="Choose language"
|
||||
>
|
||||
{FORM_LANGUAGE_OPTIONS.map((option) => {
|
||||
const isActive = language.value === option.value;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={option.value}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={isActive ? "primary" : "outline"}
|
||||
className={styles.languageButton}
|
||||
onClick={() => setLanguage(option)}
|
||||
aria-pressed={isActive}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{error && <span className="error">{error}</span>}
|
||||
</Row>
|
||||
{data && (
|
||||
|
||||
+56
-13
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { LinkButton } from "@/components/ui/linkbutton";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@@ -16,27 +17,69 @@ export default function Home() {
|
||||
|
||||
<div className="features">
|
||||
<div className="feature-card">
|
||||
<h3>Lernplan</h3>
|
||||
<h3>Lerntechniken</h3>
|
||||
<p>Wie lernst du online am besten?</p>
|
||||
<LinkButton href="/learning">
|
||||
Entdecke deine Lernstrategien
|
||||
</LinkButton>
|
||||
<p>Finde den passenden Übungstyp für dich!</p>
|
||||
<div className="feature-card">
|
||||
<LinkButton href="/pages/resources/1" size="md">
|
||||
Effektiv online lernen
|
||||
</LinkButton>
|
||||
<LinkButton href="/learning" size="md">
|
||||
Entdecke deine Lernstrategien
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="feature-card">
|
||||
<h3>Grammatik</h3>
|
||||
<p>Selbständig Grammatik lernen mit kommunikativen Situationen!</p>
|
||||
<LinkButton href="/pages/communications" size="md">
|
||||
Kommunikationssituationen
|
||||
</LinkButton>
|
||||
<LinkButton href="/grammar/themes" size="md">
|
||||
Themen der Grammatik
|
||||
</LinkButton>
|
||||
<LinkButton href="/grammar/exercises" size="md">
|
||||
Übungen
|
||||
</LinkButton>
|
||||
<div className="feature-card">
|
||||
<LinkButton href="/pages/communications" size="md">
|
||||
Kommunikationssituationen
|
||||
</LinkButton>
|
||||
<LinkButton href="/grammar/themes" size="md">
|
||||
Themen der Grammatik
|
||||
</LinkButton>
|
||||
<LinkButton href="/grammar/exercises" size="md">
|
||||
Alle Übungen anzeigen
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<section className={styles.creditsSection}>
|
||||
<div className={styles.creditsList}>
|
||||
<p className={styles.creditsItem}>
|
||||
Diese Webseiten entstanden in einem Autorenkollektiv der
|
||||
Universitäten Helsinki, Oulu und Turku unter der Leitung von{" "}
|
||||
<a href="mailto:michi@dlc.fi" className={styles.creditsLink}>
|
||||
Dr. Michael Möbius
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<br></br>
|
||||
<p className={styles.creditsItem}>
|
||||
Die Entwicklung wurde gefördert durch die{" "}
|
||||
<a
|
||||
href="https://foundationhollo.fi/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.creditsLink}
|
||||
>
|
||||
Stiftung Erkki-J-Hollo
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className={styles.creditsItem}>
|
||||
Programmierung durch IT-Studierende der Universität Helsinki
|
||||
</p>
|
||||
<p className={styles.creditsItem}>
|
||||
Technischer Support und Hosting: Universität Helsinki
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -53,3 +53,53 @@
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.creditsSection {
|
||||
width: min(100%, 1080px);
|
||||
margin: 0 auto;
|
||||
padding: 1.75rem 2rem;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.creditsTitle {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--fg2);
|
||||
font-size: clamp(1.25rem, 2vw, 1.7rem);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.creditsList {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.creditsItem {
|
||||
color: var(--fg2);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.7;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.creditsLink {
|
||||
color: var(--blue);
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.creditsLink:hover {
|
||||
color: var(--fg1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.creditsSection {
|
||||
padding: 1.25rem;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.creditsItem {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@
|
||||
|
||||
.baseButton.sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
width: 8rem;
|
||||
/* width: 8rem; , it aligns better with auto */
|
||||
width: auto;
|
||||
/* height: 1.5rem; , it aligns better with auto */
|
||||
height: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
@@ -66,6 +66,10 @@ export default function RenderText({
|
||||
);
|
||||
}
|
||||
|
||||
if (item.content_type === "linebreak") {
|
||||
return <br key={index} />;
|
||||
}
|
||||
|
||||
return null; // Fallback for unsupported types
|
||||
};
|
||||
return <p className="task-sentence">{exerciseData.map(renderItem)}</p>;
|
||||
|
||||
@@ -95,13 +95,17 @@ function StudentSideBar() {
|
||||
return (
|
||||
<>
|
||||
<SidebarGroup
|
||||
title="Richtig Online Lernen"
|
||||
title="Effektiv online lernen"
|
||||
sublinks={chapters}
|
||||
topLink="/pages/resources"
|
||||
collapsible
|
||||
/>
|
||||
<Link className={styles.sidebarLink} href="/pages/communications">
|
||||
Kommunikations-situationen
|
||||
</Link>
|
||||
<Link className={styles.sidebarLink} href="/grammar/themes">
|
||||
Themen der Grammatik
|
||||
</Link>
|
||||
<Link className={styles.sidebarLink} href="/talkback">
|
||||
Rückmeldekanal-Feedback geben
|
||||
</Link>
|
||||
@@ -211,8 +215,54 @@ function AdminSideBar({ showUsers }) {
|
||||
return <SidebarGroup title="Admin" sublinks={links} topLink="/admin" />;
|
||||
}
|
||||
|
||||
function SidebarGroup({ title, sublinks, topLink }) {
|
||||
function SidebarGroup({ title, sublinks, topLink, collapsible = false }) {
|
||||
const pathname = usePathname();
|
||||
const isUnderSection =
|
||||
pathname === topLink || pathname.startsWith(topLink + "/");
|
||||
const [open, setOpen] = useState(isUnderSection);
|
||||
|
||||
if (collapsible) {
|
||||
return (
|
||||
<div className={styles.sidebarGroup}>
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
styles.sidebarLink,
|
||||
styles.sidebarGroupToggle,
|
||||
isUnderSection ? styles.active : "",
|
||||
].join(" ")}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
{title}
|
||||
<span
|
||||
className={[styles.chevron, open ? styles.chevronOpen : ""].join(
|
||||
" "
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className={styles.sublinkContainer}>
|
||||
{sublinks.map((sublink) => (
|
||||
<Link
|
||||
className={[
|
||||
styles.sublink,
|
||||
pathname === sublink.link ? styles.active : "",
|
||||
].join(" ")}
|
||||
key={sublink.link}
|
||||
href={sublink.link}
|
||||
>
|
||||
{sublink.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.sidebarGroup}>
|
||||
|
||||
@@ -95,6 +95,30 @@
|
||||
gap: var(--u-md);
|
||||
}
|
||||
|
||||
.sidebarGroupToggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
display: inline-block;
|
||||
font-size: var(--font-md);
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chevronOpen {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.navItem {
|
||||
width: 100%;
|
||||
padding: 12px 12px;
|
||||
@@ -117,4 +141,4 @@
|
||||
.sidebar {
|
||||
background-color: var(--bg1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user