mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
fix: prevent 413 errors and navigation crashes from large embedded images
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
export default function Error({ error, reset }) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Etwas ist schiefgelaufen / Something went wrong</h2>
|
||||
<p>
|
||||
Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.
|
||||
</p>
|
||||
<button onClick={() => reset()}>Erneut versuchen / Try again</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -45,13 +45,21 @@ export default async function Chapters({ params }) {
|
||||
|
||||
<Row pb="xl" className="chapter-nav-row" justify="space-between">
|
||||
<Container className="chapter-nav-left">
|
||||
{previousLink && <LinkButton href={previousLink}>Zurück</LinkButton>}
|
||||
{previousLink && (
|
||||
<LinkButton href={previousLink} hardNavigation>
|
||||
Zurück
|
||||
</LinkButton>
|
||||
)}
|
||||
</Container>
|
||||
<Container className="chapter-nav-right">
|
||||
{showLearningFormLink && (
|
||||
<LinkButton href="/learning">Go to form</LinkButton>
|
||||
)}
|
||||
{nextLink && <LinkButton href={nextLink}>Weiter</LinkButton>}
|
||||
{nextLink && (
|
||||
<LinkButton href={nextLink} hardNavigation>
|
||||
Weiter
|
||||
</LinkButton>
|
||||
)}
|
||||
</Container>
|
||||
</Row>
|
||||
</Column>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <a> does not, so add it manually.
|
||||
if (hardNavigation) {
|
||||
return (
|
||||
<a href={withBasePath(href)} className={classes} style={style}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={classes}
|
||||
style={{ display: "inline-block", textDecoration: "none" }}
|
||||
>
|
||||
<Link href={href} className={classes} style={style}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
Reference in New Issue
Block a user