diff --git a/gradesa/playwright-report/index.html b/gradesa/playwright-report/index.html index 184f4b6..45a6579 100644 --- a/gradesa/playwright-report/index.html +++ b/gradesa/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/gradesa/src/app/error.js b/gradesa/src/app/error.js new file mode 100644 index 0000000..026f817 --- /dev/null +++ b/gradesa/src/app/error.js @@ -0,0 +1,13 @@ +"use client"; + +export default function Error({ error, reset }) { + return ( +
+

Etwas ist schiefgelaufen / Something went wrong

+

+ Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut. +

+ +
+ ); +} diff --git a/gradesa/src/app/pages/[type]/[slug]/page.js b/gradesa/src/app/pages/[type]/[slug]/page.js index f8c93a6..0339449 100644 --- a/gradesa/src/app/pages/[type]/[slug]/page.js +++ b/gradesa/src/app/pages/[type]/[slug]/page.js @@ -45,13 +45,21 @@ export default async function Chapters({ params }) { - {previousLink && Zurück} + {previousLink && ( + + Zurück + + )} {showLearningFormLink && ( Go to form )} - {nextLink && Weiter} + {nextLink && ( + + Weiter + + )} diff --git a/gradesa/src/components/ui/editor/ClientEditor.js b/gradesa/src/components/ui/editor/ClientEditor.js index 7f23999..159a175 100644 --- a/gradesa/src/components/ui/editor/ClientEditor.js +++ b/gradesa/src/components/ui/editor/ClientEditor.js @@ -134,6 +134,40 @@ function setHtmlContent(quill, html) { quill.clipboard.dangerouslyPasteHTML(0, content, "api"); } +const MAX_IMAGE_DIMENSION = 1600; +const IMAGE_COMPRESSION_QUALITY = 0.75; + +// Downscales/re-encodes images client-side so pasted base64 stays well under proxy body-size limits. +function compressImageFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(reader.error); + reader.onload = () => { + const img = new Image(); + img.onerror = () => reject(new Error("Could not read image")); + img.onload = () => { + const scale = Math.min( + 1, + MAX_IMAGE_DIMENSION / Math.max(img.width, img.height) + ); + const width = Math.round(img.width * scale); + const height = Math.round(img.height * scale); + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + canvas.getContext("2d").drawImage(img, 0, 0, width, height); + + const outputType = + file.type === "image/png" ? "image/png" : "image/jpeg"; + resolve(canvas.toDataURL(outputType, IMAGE_COMPRESSION_QUALITY)); + }; + img.src = reader.result; + }; + reader.readAsDataURL(file); + }); +} + function isNodeEffectivelyEmptyText(node) { if (!(node instanceof HTMLElement)) { return false; @@ -492,8 +526,34 @@ const ClientEditor = (props) => { [{ script: "sub" }, { script: "super" }], [{ indent: "-1" }, { indent: "+1" }], ]; + const imageHandler = () => { + const input = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("accept", "image/*"); + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) { + return; + } + try { + const dataUrl = await compressImageFile(file); + const range = quill.getSelection(true); + quill.insertEmbed(range.index, "image", dataUrl, "user"); + quill.setSelection(range.index + 1, 0, "silent"); + } catch (error) { + console.warn("Image compression failed", error); + } + }; + input.click(); + }; + const modules = { - toolbar: toolbarOptions, + toolbar: { + container: toolbarOptions, + handlers: { + image: imageHandler, + }, + }, table: false, "table-better": { language: "en_US", diff --git a/gradesa/src/components/ui/linkbutton/index.js b/gradesa/src/components/ui/linkbutton/index.js index 0d7b974..4090fb4 100644 --- a/gradesa/src/components/ui/linkbutton/index.js +++ b/gradesa/src/components/ui/linkbutton/index.js @@ -1,6 +1,7 @@ "use client"; import Link from "next/link"; import styles from "@/components/ui/button/button.module.css"; +import { withBasePath } from "@/shared/utils/basePath"; export const LinkButton = ({ href, @@ -9,6 +10,7 @@ export const LinkButton = ({ size = "md", width = "fit", className, + hardNavigation = false, }) => { const classes = [ styles.baseButton, @@ -19,13 +21,21 @@ export const LinkButton = ({ ] .filter(Boolean) .join(" "); + const style = { display: "inline-block", textDecoration: "none" }; + + // Bypass Next.js soft-navigation (RSC fetch) for pages whose HTML may be too + // large to stream reliably, avoiding client-side crashes on partial responses. + // next/link auto-prefixes basePath; a plain does not, so add it manually. + if (hardNavigation) { + return ( + + {children} + + ); + } return ( - + {children} ); diff --git a/gradesa/test-results/.last-run.json b/gradesa/test-results/.last-run.json index f854b86..cbcc1fb 100644 --- a/gradesa/test-results/.last-run.json +++ b/gradesa/test-results/.last-run.json @@ -1,13 +1,4 @@ { - "status": "failed", - "failedTests": [ - "e05c51b8ad1b3eb66010-0e43830ad8f2dfd8d1e2", - "c5b6e4261f23dff4104e-cb5330f948ccf9c8b61e", - "c5b6e4261f23dff4104e-fd730098ea5d4ab30d2a", - "c5b6e4261f23dff4104e-4c06fad2b2a291767cec", - "18e1443b7169cebccd21-4a0da125f055d790cf05", - "cf4ffee829137744e3cd-e6406448e73a28c0ddd9", - "d936625e1207b3e19d4b-67eb4c2dd9e8efb88e3d", - "d8918302922cfb4b5ab2-53975d1f9ee0854667fa" - ] + "status": "passed", + "failedTests": [] } \ No newline at end of file