Merge pull request #502 from erzabytyci/user-tiers

Add BasePath
This commit is contained in:
erzabytyci
2026-05-13 14:23:18 +03:00
committed by GitHub
30 changed files with 147 additions and 65 deletions
+2 -1
View File
@@ -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
+6 -1
View File
@@ -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");
} }
+3 -2
View File
@@ -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",
+3 -2
View File
@@ -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",
+2 -1
View File
@@ -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",
}); });
+1 -1
View File
@@ -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" },
+4 -1
View File
@@ -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"]
+2 -1
View File
@@ -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(
+2 -1
View File
@@ -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"),
}); });
/** /**
+6
View File
@@ -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}`;
}