mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
+2
-1
@@ -38,7 +38,8 @@ ENV SESSION_SECRET=$SESSION_SECRET \
|
|||||||
DB_USER=$DB_USER \
|
DB_USER=$DB_USER \
|
||||||
DB_PASSWORD=$DB_PASSWORD \
|
DB_PASSWORD=$DB_PASSWORD \
|
||||||
DB_NAME=$DB_NAME \
|
DB_NAME=$DB_NAME \
|
||||||
NODE_ENV=$NODE_ENV
|
NODE_ENV=$NODE_ENV \
|
||||||
|
NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
|
||||||
|
|
||||||
# Install ALL dependencies (including dev) for build
|
# Install ALL dependencies (including dev) for build
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig =
|
const nextConfig =
|
||||||
{
|
{
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
|
||||||
|
basePath,
|
||||||
|
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
@@ -14,7 +18,8 @@ const nextConfig =
|
|||||||
DB_PORT: process.env.DB_PORT || '5432',
|
DB_PORT: process.env.DB_PORT || '5432',
|
||||||
DB_USER: process.env.DB_USER || 'postgres',
|
DB_USER: process.env.DB_USER || 'postgres',
|
||||||
DB_PASSWORD: process.env.DB_PASSWORD || 'password',
|
DB_PASSWORD: process.env.DB_PASSWORD || 'password',
|
||||||
DB_NAME: process.env.DB_NAME || 'postgres'
|
DB_NAME: process.env.DB_NAME || 'postgres',
|
||||||
|
NEXT_PUBLIC_BASE_PATH: basePath
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import "./click.css";
|
|||||||
import WordSelectionExercise from "@/components/ui/click/click.js";
|
import WordSelectionExercise from "@/components/ui/click/click.js";
|
||||||
import Editor from "@/components/ui/editor";
|
import Editor from "@/components/ui/editor";
|
||||||
import { htmlToPlainText } from "@/shared/utils/normalizeEditorText";
|
import { htmlToPlainText } from "@/shared/utils/normalizeEditorText";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
@@ -90,17 +91,20 @@ export default function CreateExercise() {
|
|||||||
const handleSaveExercise = async () => {
|
const handleSaveExercise = async () => {
|
||||||
try {
|
try {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
const response = await fetch(`/api/admin/exercises/click/${click_id}`, {
|
const response = await fetch(
|
||||||
method: "PUT",
|
withBasePath(`/api/admin/exercises/click/${click_id}`),
|
||||||
headers: { "Content-Type": "application/json" },
|
{
|
||||||
body: JSON.stringify({
|
method: "PUT",
|
||||||
title,
|
headers: { "Content-Type": "application/json" },
|
||||||
targetCategory,
|
body: JSON.stringify({
|
||||||
targetWords: selectedWords,
|
title,
|
||||||
allWords,
|
targetCategory,
|
||||||
sourceHtml: allWordsHtml,
|
targetWords: selectedWords,
|
||||||
}),
|
allWords,
|
||||||
});
|
sourceHtml: allWordsHtml,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Row } from "@/components/ui/layout/container";
|
import { Row } from "@/components/ui/layout/container";
|
||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import "./admin.css";
|
import "./admin.css";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
const EMPTY_PAIR = () => ({ leftItem: "", rightItem: "" });
|
const EMPTY_PAIR = () => ({ leftItem: "", rightItem: "" });
|
||||||
|
|
||||||
@@ -90,20 +91,26 @@ export default function DndMatchAdminPage() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
const res = await fetch(`/api/admin/exercises/dnd-match/${match_id}`, {
|
const res = await fetch(
|
||||||
method: "PUT",
|
withBasePath(`/api/admin/exercises/dnd-match/${match_id}`),
|
||||||
headers: { "Content-Type": "application/json" },
|
{
|
||||||
body: JSON.stringify(payload),
|
method: "PUT",
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}
|
||||||
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || "Fehler beim Speichern.");
|
if (!res.ok) throw new Error(data.error || "Fehler beim Speichern.");
|
||||||
router.push("/grammar/exercises/dnd-match");
|
router.push("/grammar/exercises/dnd-match");
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(`/api/admin/exercises/dnd-match`, {
|
const res = await fetch(
|
||||||
method: "POST",
|
withBasePath(`/api/admin/exercises/dnd-match`),
|
||||||
headers: { "Content-Type": "application/json" },
|
{
|
||||||
body: JSON.stringify(payload),
|
method: "POST",
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}
|
||||||
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || "Fehler beim Erstellen.");
|
if (!res.ok) throw new Error(data.error || "Fehler beim Erstellen.");
|
||||||
router.push(`/grammar/exercises/dnd-match/${data.id}`);
|
router.push(`/grammar/exercises/dnd-match/${data.id}`);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useRequest } from "@/shared/hooks/useRequest";
|
|||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import PreviewDragDrop from "@/components/ui/dragdrop/dragdrop_preview";
|
import PreviewDragDrop from "@/components/ui/dragdrop/dragdrop_preview";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function DragdropAdminPage() {
|
export default function DragdropAdminPage() {
|
||||||
const { dnd_id } = useParams();
|
const { dnd_id } = useParams();
|
||||||
@@ -85,7 +86,7 @@ export default function DragdropAdminPage() {
|
|||||||
|
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/dragdrop/${dnd_id}`,
|
withBasePath(`/api/admin/exercises/dragdrop/${dnd_id}`),
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useRouter, useParams } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import "../../../multichoice/multichoice.css";
|
import "../../../multichoice/multichoice.css";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function EditFreeFormExercisePage() {
|
export default function EditFreeFormExercisePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -130,7 +131,7 @@ export default function EditFreeFormExercisePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/free-form/${exercise_id}`,
|
withBasePath(`/api/admin/exercises/free-form/${exercise_id}`),
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRouter, useSearchParams, useParams } from "next/navigation";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Column, Row } from "@/components/ui/layout/container";
|
import { Column, Row } from "@/components/ui/layout/container";
|
||||||
import { useRequest } from "@/shared/hooks/useRequest";
|
import { useRequest } from "@/shared/hooks/useRequest";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
const emptyPair = { left_item: "", right_item: "" };
|
const emptyPair = { left_item: "", right_item: "" };
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export default function CreateMemoryGame() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/memory-game/${gameId}`
|
withBasePath(`/api/admin/exercises/memory-game/${gameId}`)
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useParams, useRouter } from "next/navigation";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import "./multichoice.css";
|
import "./multichoice.css";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function CreateMultichoicePage() {
|
export default function CreateMultichoicePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -173,8 +174,8 @@ export default function CreateMultichoicePage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const endpoint = isEditMode
|
const endpoint = isEditMode
|
||||||
? `/api/admin/exercises/multichoice/${exercise_id}`
|
? withBasePath(`/api/admin/exercises/multichoice/${exercise_id}`)
|
||||||
: `/api/admin/exercises/multichoice`;
|
: withBasePath(`/api/admin/exercises/multichoice`);
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
method: isEditMode ? "PUT" : "POST",
|
method: isEditMode ? "PUT" : "POST",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import Editor from "@/components/ui/editor";
|
import Editor from "@/components/ui/editor";
|
||||||
import layout from "@/shared/styles/layout.module.css";
|
import layout from "@/shared/styles/layout.module.css";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function EditorSection({
|
export default function EditorSection({
|
||||||
initialContent,
|
initialContent,
|
||||||
@@ -49,16 +50,19 @@ export default function EditorSection({
|
|||||||
const jsonData = JSON.stringify(requestBody);
|
const jsonData = JSON.stringify(requestBody);
|
||||||
|
|
||||||
if (!pageExists && type === "grammar") {
|
if (!pageExists && type === "grammar") {
|
||||||
const createRes = await fetch(`/api/admin/pages/${type}/${slug}`, {
|
const createRes = await fetch(
|
||||||
headers: {
|
withBasePath(`/api/admin/pages/${type}/${slug}`),
|
||||||
Accept: "application/json",
|
{
|
||||||
"Content-Type": "application/json",
|
headers: {
|
||||||
},
|
Accept: "application/json",
|
||||||
method: "POST",
|
"Content-Type": "application/json",
|
||||||
body: JSON.stringify({
|
},
|
||||||
title: titleInput,
|
method: "POST",
|
||||||
}),
|
body: JSON.stringify({
|
||||||
});
|
title: titleInput,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!createRes.ok && createRes.status !== 200) {
|
if (!createRes.ok && createRes.status !== 200) {
|
||||||
setEditorMessage({
|
setEditorMessage({
|
||||||
@@ -68,7 +72,7 @@ export default function EditorSection({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await fetch(`/api/admin/pages/${type}/${slug}`, {
|
const res = await fetch(withBasePath(`/api/admin/pages/${type}/${slug}`), {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRequest } from "@/shared/hooks/useRequest";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { zodErrorToFormErrors } from "@/shared/schemas/schema-utils";
|
import { zodErrorToFormErrors } from "@/shared/schemas/schema-utils";
|
||||||
import Editor from "@/components/ui/editor";
|
import Editor from "@/components/ui/editor";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
const defaultFormErrors = {
|
const defaultFormErrors = {
|
||||||
word: "",
|
word: "",
|
||||||
@@ -31,7 +32,9 @@ export default function CreateGlossaryEntry() {
|
|||||||
const loadEntry = async () => {
|
const loadEntry = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoadingEntry(true);
|
setIsLoadingEntry(true);
|
||||||
const response = await fetch(`/api/admin/glossary?id=${entryId}`);
|
const response = await fetch(
|
||||||
|
withBasePath(`/api/admin/glossary?id=${entryId}`)
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to load glossary entry");
|
throw new Error("Failed to load glossary entry");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import layout from "@/shared/styles/layout.module.css";
|
import layout from "@/shared/styles/layout.module.css";
|
||||||
import styles from "./new-page.module.css";
|
import styles from "./new-page.module.css";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function NewPage() {
|
export default function NewPage() {
|
||||||
const [type, setType] = useState("resources");
|
const [type, setType] = useState("resources");
|
||||||
@@ -17,7 +18,7 @@ export default function NewPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type !== "grammar") return;
|
if (type !== "grammar") return;
|
||||||
fetch("/api/admin/grammar-topics")
|
fetch(withBasePath("/api/admin/grammar-topics"))
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setGrammarTopics(data);
|
setGrammarTopics(data);
|
||||||
@@ -49,7 +50,7 @@ export default function NewPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/admin/pages`, {
|
const response = await fetch(withBasePath(`/api/admin/pages`), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Container } from "@/components/ui/layout/container";
|
import { Container } from "@/components/ui/layout/container";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function EditEmail() {
|
export default function EditEmail() {
|
||||||
const [user, setUser] = useState("");
|
const [user, setUser] = useState("");
|
||||||
@@ -16,7 +17,7 @@ export default function EditEmail() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCurrentUser = async () => {
|
const fetchCurrentUser = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/edit_email", {
|
const res = await fetch(withBasePath("/api/edit_email"), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -43,7 +44,7 @@ export default function EditEmail() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch("/api/edit_email", {
|
const res = await fetch(withBasePath("/api/edit_email"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Container } from "@/components/ui/layout/container";
|
import { Container } from "@/components/ui/layout/container";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function EditPassword() {
|
export default function EditPassword() {
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
@@ -24,7 +25,7 @@ export default function EditPassword() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch("/api/edit_password", {
|
const res = await fetch(withBasePath("/api/edit_password"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import styles from "../page.module.css";
|
|||||||
import { Container } from "@/components/ui/layout/container";
|
import { Container } from "@/components/ui/layout/container";
|
||||||
import { LinkButton } from "@/components/ui/linkbutton";
|
import { LinkButton } from "@/components/ui/linkbutton";
|
||||||
import { Grid } from "@radix-ui/themes";
|
import { Grid } from "@radix-ui/themes";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function EditInfo() {
|
export default function EditInfo() {
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
@@ -13,7 +14,7 @@ export default function EditInfo() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getUser = async () => {
|
const getUser = async () => {
|
||||||
const res = await fetch("/api/edit_info");
|
const res = await fetch(withBasePath("/api/edit_info"));
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
router.replace("/auth/login");
|
router.replace("/auth/login");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Container, Row, Column } from "@/components/ui/layout/container";
|
|||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function ClickExercisesPage() {
|
export default function ClickExercisesPage() {
|
||||||
const {
|
const {
|
||||||
@@ -20,9 +21,12 @@ export default function ClickExercisesPage() {
|
|||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/admin/exercises/click/${exerciseId}`, {
|
const response = await fetch(
|
||||||
method: "DELETE",
|
withBasePath(`/api/admin/exercises/click/${exerciseId}`),
|
||||||
});
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Fehler beim Löschen der Übung.");
|
throw new Error("Fehler beim Löschen der Übung.");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Container, Row, Column } from "@/components/ui/layout/container";
|
|||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function DndMatchExercisesPage() {
|
export default function DndMatchExercisesPage() {
|
||||||
const {
|
const {
|
||||||
@@ -20,7 +21,7 @@ export default function DndMatchExercisesPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/dnd-match/${exerciseId}`,
|
withBasePath(`/api/admin/exercises/dnd-match/${exerciseId}`),
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Container, Row, Column } from "@/components/ui/layout/container";
|
|||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function DragdropExercisesPage() {
|
export default function DragdropExercisesPage() {
|
||||||
const {
|
const {
|
||||||
@@ -21,7 +22,7 @@ export default function DragdropExercisesPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/dragdrop/${exerciseId}`,
|
withBasePath(`/api/admin/exercises/dragdrop/${exerciseId}`),
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Container, Row, Column } from "@/components/ui/layout/container";
|
|||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function FillInTheGapExercisesPage() {
|
export default function FillInTheGapExercisesPage() {
|
||||||
const {
|
const {
|
||||||
@@ -21,7 +22,7 @@ export default function FillInTheGapExercisesPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/fillinthegap/${exerciseId}`,
|
withBasePath(`/api/admin/exercises/fillinthegap/${exerciseId}`),
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Container, Row, Column } from "@/components/ui/layout/container";
|
|||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function FreeFormExercisesPage() {
|
export default function FreeFormExercisesPage() {
|
||||||
const {
|
const {
|
||||||
@@ -21,7 +22,7 @@ export default function FreeFormExercisesPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/free-form/${exerciseId}`,
|
withBasePath(`/api/admin/exercises/free-form/${exerciseId}`),
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import useQuery from "@/shared/hooks/useQuery";
|
|||||||
import { Column, Container, Row } from "@/components/ui/layout/container";
|
import { Column, Container, Row } from "@/components/ui/layout/container";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { MemoryGame } from "@/components/ui/memory-game/MemoryGame";
|
import { MemoryGame } from "@/components/ui/memory-game/MemoryGame";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function MemoryGamePlayerPage() {
|
export default function MemoryGamePlayerPage() {
|
||||||
const { memory_game_id } = useParams();
|
const { memory_game_id } = useParams();
|
||||||
@@ -27,7 +28,7 @@ export default function MemoryGamePlayerPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/exercises/memory-game/${memory_game_id}/answers`,
|
withBasePath(`/api/exercises/memory-game/${memory_game_id}/answers`),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Container, Column, Row } from "@/components/ui/layout/container";
|
|||||||
import useQuery from "@/shared/hooks/useQuery";
|
import useQuery from "@/shared/hooks/useQuery";
|
||||||
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
import { ExerciseLinkButton } from "@/components/ui/button/exercise-link-button";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function MultichoiceExercisesPage() {
|
export default function MultichoiceExercisesPage() {
|
||||||
const {
|
const {
|
||||||
@@ -21,7 +22,7 @@ export default function MultichoiceExercisesPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/admin/exercises/multichoice/${exerciseId}`,
|
withBasePath(`/api/admin/exercises/multichoice/${exerciseId}`),
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useUser } from "@/context/user.context";
|
import { useUser } from "@/context/user.context";
|
||||||
import { Row } from "@/components/ui/layout/container";
|
import { Row } from "@/components/ui/layout/container";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export default function AdminButtons({ type, slug, pageExists = true }) {
|
export default function AdminButtons({ type, slug, pageExists = true }) {
|
||||||
const { setActAs } = useUser();
|
const { setActAs } = useUser();
|
||||||
@@ -17,7 +18,7 @@ export default function AdminButtons({ type, slug, pageExists = true }) {
|
|||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
if (!confirm("Möchten Sie den Inhalt dieser Seite wirklich leeren?"))
|
if (!confirm("Möchten Sie den Inhalt dieser Seite wirklich leeren?"))
|
||||||
return;
|
return;
|
||||||
const res = await fetch(`/api/admin/pages/${type}/${slug}`, {
|
const res = await fetch(withBasePath(`/api/admin/pages/${type}/${slug}`), {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ export default function AdminButtons({ type, slug, pageExists = true }) {
|
|||||||
} else if (type === "resources") {
|
} else if (type === "resources") {
|
||||||
redirectPath = "/learning"; // fallback
|
redirectPath = "/learning"; // fallback
|
||||||
try {
|
try {
|
||||||
const listRes = await fetch(`/api/pages/${type}`);
|
const listRes = await fetch(withBasePath(`/api/pages/${type}`));
|
||||||
if (listRes.ok) {
|
if (listRes.ok) {
|
||||||
const data = await listRes.json();
|
const data = await listRes.json();
|
||||||
const pages = Array.isArray(data?.pages) ? data.pages : [];
|
const pages = Array.isArray(data?.pages) ? data.pages : [];
|
||||||
@@ -82,7 +83,7 @@ export default function AdminButtons({ type, slug, pageExists = true }) {
|
|||||||
redirectPath = getFallbackPath(type);
|
redirectPath = getFallbackPath(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/admin/pages/${type}/${slug}`, {
|
const res = await fetch(withBasePath(`/api/admin/pages/${type}/${slug}`), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function TalkbackPage() {
|
|||||||
try {
|
try {
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
|
|
||||||
const response = await fetch("/api/talkback", {
|
const response = await fetch(withBasePath("/api/talkback"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
export function ExerciseLinkButton({ href, children, ...props }) {
|
export function ExerciseLinkButton({ href, children, ...props }) {
|
||||||
const copyToClipboard = async (e) => {
|
const copyToClipboard = async (e) => {
|
||||||
@@ -7,7 +8,7 @@ export function ExerciseLinkButton({ href, children, ...props }) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
const origin = window.location.origin;
|
const origin = window.location.origin;
|
||||||
const url = `${origin}${href}`;
|
const url = `${origin}${withBasePath(href)}`;
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
alert("Link kopiert!");
|
alert("Link kopiert!");
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MatchCard } from "./MatchCard";
|
|||||||
import { MatchSlot } from "./MatchSlot";
|
import { MatchSlot } from "./MatchSlot";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import "./dnd-match.css";
|
import "./dnd-match.css";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
function shuffle(arr) {
|
function shuffle(arr) {
|
||||||
const a = [...arr];
|
const a = [...arr];
|
||||||
@@ -109,7 +110,7 @@ const MatchAreaInner = ({ exercise, exerciseId }) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/api/exercises/dnd-match/${exerciseId}/answers`,
|
withBasePath(`/api/exercises/dnd-match/${exerciseId}/answers`),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { memo, useCallback, useState, useEffect } from "react";
|
|||||||
import { WordBox } from "./wordbox.js";
|
import { WordBox } from "./wordbox.js";
|
||||||
import { Dustbin } from "./dustbin.js";
|
import { Dustbin } from "./dustbin.js";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath.js";
|
||||||
|
|
||||||
const Area = ({ exerciseID }) => {
|
const Area = ({ exerciseID }) => {
|
||||||
const [dustbins, setDustbins] = useState([]);
|
const [dustbins, setDustbins] = useState([]);
|
||||||
@@ -22,7 +23,9 @@ const Area = ({ exerciseID }) => {
|
|||||||
if (!exerciseID) return;
|
if (!exerciseID) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/exercises/dragdrop/${exerciseID}`);
|
const response = await fetch(
|
||||||
|
withBasePath(`/api/exercises/dragdrop/${exerciseID}`)
|
||||||
|
);
|
||||||
if (!response.ok) throw new Error("Fehler beim Laden der Übung.");
|
if (!response.ok) throw new Error("Fehler beim Laden der Übung.");
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
GlossaryParagraph,
|
GlossaryParagraph,
|
||||||
GlossaryListItem,
|
GlossaryListItem,
|
||||||
} from "@/components/ui/glossary/GlossaryText";
|
} from "@/components/ui/glossary/GlossaryText";
|
||||||
|
import { BASE_PATH, withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize HTML attributes for use in React elements.
|
* Normalize HTML attributes for use in React elements.
|
||||||
@@ -34,6 +35,15 @@ const normalizeAttribs = (attributes = {}) => {
|
|||||||
return normalized;
|
return normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const prefixRootRelativePath = (value) => {
|
||||||
|
if (typeof value !== "string") return value;
|
||||||
|
if (!value.startsWith("/") || value.startsWith("//")) return value;
|
||||||
|
if (BASE_PATH && (value === BASE_PATH || value.startsWith(`${BASE_PATH}/`))) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return withBasePath(value);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RenderHTML
|
* RenderHTML
|
||||||
*
|
*
|
||||||
@@ -84,11 +94,27 @@ export default function RenderHTML({ data, disableGlossary = false }) {
|
|||||||
// Legacy DB content can contain <Container> tags from old React markup.
|
// Legacy DB content can contain <Container> tags from old React markup.
|
||||||
// Render only children to preserve previous look without invalid tag warnings.
|
// Render only children to preserve previous look without invalid tag warnings.
|
||||||
container: (_attributes, children) => <Fragment>{children}</Fragment>,
|
container: (_attributes, children) => <Fragment>{children}</Fragment>,
|
||||||
|
|
||||||
|
a: (attributes, children) => {
|
||||||
|
const safeAttribs = { ...attributes };
|
||||||
|
if (safeAttribs.href) {
|
||||||
|
safeAttribs.href = prefixRootRelativePath(safeAttribs.href);
|
||||||
|
}
|
||||||
|
return <a {...safeAttribs}>{children}</a>;
|
||||||
|
},
|
||||||
|
img: (attributes) => {
|
||||||
|
const safeAttribs = { ...attributes };
|
||||||
|
if (safeAttribs.src) {
|
||||||
|
safeAttribs.src = prefixRootRelativePath(safeAttribs.src);
|
||||||
|
}
|
||||||
|
return <img {...safeAttribs} />;
|
||||||
|
},
|
||||||
|
|
||||||
anchor: (attributes, children) => {
|
anchor: (attributes, children) => {
|
||||||
const safeAttribs = { ...attributes };
|
const safeAttribs = { ...attributes };
|
||||||
const href = safeAttribs.href || safeAttribs["data-href"];
|
const href = safeAttribs.href || safeAttribs["data-href"];
|
||||||
if (href) {
|
if (href) {
|
||||||
safeAttribs["data-href"] = href;
|
safeAttribs["data-href"] = prefixRootRelativePath(href);
|
||||||
delete safeAttribs.href;
|
delete safeAttribs.href;
|
||||||
}
|
}
|
||||||
safeAttribs.className = [safeAttribs.className, "glossary-anchor"]
|
safeAttribs.className = [safeAttribs.className, "glossary-anchor"]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useIsMounted } from "@/shared/hooks/useIsMounted";
|
import { useIsMounted } from "@/shared/hooks/useIsMounted";
|
||||||
|
import { withBasePath } from "@/shared/utils/basePath";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const { auth, setActAs, actAs } = useUser();
|
const { auth, setActAs, actAs } = useUser();
|
||||||
@@ -79,7 +80,7 @@ function StudentSideBar() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/pages/resources")
|
fetch(withBasePath("/api/pages/resources"))
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setChapters(
|
setChapters(
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { useState, useEffect, useRef, useMemo, memo } from "react";
|
|||||||
import { useIsMounted } from "./useIsMounted";
|
import { useIsMounted } from "./useIsMounted";
|
||||||
import { useUser } from "@/context/user.context";
|
import { useUser } from "@/context/user.context";
|
||||||
import { useSessionId } from "./useSessionId";
|
import { useSessionId } from "./useSessionId";
|
||||||
|
import { withBasePath } from "../utils/basePath";
|
||||||
|
|
||||||
export const axiosInstance = axios.create({
|
export const axiosInstance = axios.create({
|
||||||
baseURL: "/api",
|
baseURL: withBasePath("/api"),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||||
|
|
||||||
|
export function withBasePath(path) {
|
||||||
|
if (!path.startsWith("/")) return path;
|
||||||
|
return `${BASE_PATH}${path}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user