limited latex support

This commit is contained in:
2024-09-18 21:50:40 -06:00
parent 31f39b8193
commit 395e9934e6
12 changed files with 195 additions and 50 deletions

View File

@@ -12,7 +12,9 @@
"@tanstack/react-query": "^5.54.1", "@tanstack/react-query": "^5.54.1",
"axios": "^1.7.5", "axios": "^1.7.5",
"isomorphic-dompurify": "^2.15.0", "isomorphic-dompurify": "^2.15.0",
"marked": "^14.1.0", "katex": "^0.16.11",
"marked": "^14.1.2",
"marked-katex-extension": "^5.1.2",
"next": "^14.2.7", "next": "^14.2.7",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
@@ -1751,6 +1753,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/katex": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.5.2", "version": "22.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz",
@@ -5196,6 +5204,31 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/katex": {
"version": "0.16.11",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz",
"integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5337,9 +5370,9 @@
} }
}, },
"node_modules/marked": { "node_modules/marked": {
"version": "14.1.0", "version": "14.1.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.0.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.2.tgz",
"integrity": "sha512-P93GikH/Pde0hM5TAXEd8I4JAYi8IB03n8qzW8Bh1BIEFpEyBoYxi/XWZA53LSpTeLBiMQOoSMj0u5E/tiVYTA==", "integrity": "sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked.js"
@@ -5348,6 +5381,19 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/marked-katex-extension": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/marked-katex-extension/-/marked-katex-extension-5.1.2.tgz",
"integrity": "sha512-jRtacvDAPULKBWArDno0IGpzzpUw12yb8OaEsv3dTlvcIr21+mF9kD+Bxo2m/ErX/2ZIml6zFVMnpxCpqx3stw==",
"license": "MIT",
"dependencies": {
"@types/katex": "^0.16.7"
},
"peerDependencies": {
"katex": ">=0.16 <0.17",
"marked": ">=4 <15"
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",

View File

@@ -14,7 +14,9 @@
"@tanstack/react-query": "^5.54.1", "@tanstack/react-query": "^5.54.1",
"axios": "^1.7.5", "axios": "^1.7.5",
"isomorphic-dompurify": "^2.15.0", "isomorphic-dompurify": "^2.15.0",
"marked": "^14.1.0", "katex": "^0.16.11",
"marked": "^14.1.2",
"marked-katex-extension": "^5.1.2",
"next": "^14.2.7", "next": "^14.2.7",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",

View File

@@ -1,7 +1,11 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { axiosClient } from "@/services/axiosUtils"; import { axiosClient } from "@/services/axiosUtils";
import { withErrorHandling } from "@/services/withErrorHandling"; import { withErrorHandling } from "@/services/withErrorHandling";
import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios"; import {
AxiosResponseHeaders,
isAxiosError,
RawAxiosResponseHeaders,
} from "axios";
const getUrl = (params: { rest: string[] }) => { const getUrl = (params: { rest: string[] }) => {
const { rest } = params; const { rest } = params;
@@ -27,7 +31,9 @@ const getNextUrl = (
const nextLink = links.find((link) => link.includes('rel="next"')); const nextLink = links.find((link) => link.includes('rel="next"'));
if (!nextLink) { if (!nextLink) {
console.log("could not find next url in link header, reached end of pagination"); console.log(
"could not find next url in link header, reached end of pagination"
);
return undefined; return undefined;
} }
@@ -58,9 +64,8 @@ export async function GET(
url.toString() url.toString()
); );
if(!Array.isArray(firstData)) if (!Array.isArray(firstData)) {
{ return NextResponse.json(firstData);
return NextResponse.json(firstData)
} }
var returnData = firstData; var returnData = firstData;
@@ -82,7 +87,6 @@ export async function GET(
} }
return NextResponse.json(returnData); return NextResponse.json(returnData);
} catch (error: any) { } catch (error: any) {
return new NextResponse( return new NextResponse(
JSON.stringify({ error: error.message || "Canvas GET request failed" }), JSON.stringify({ error: error.message || "Canvas GET request failed" }),
@@ -97,18 +101,24 @@ export async function POST(
{ params }: { params: { rest: string[] } } { params }: { params: { rest: string[] } }
) { ) {
return withErrorHandling(async () => { return withErrorHandling(async () => {
try {
const url = getUrl(params); const url = getUrl(params);
const body = await req.json(); const body = await req.json();
const response = await axiosClient.post(url.toString(), body); let response;
try {
response = await axiosClient.post(url.toString(), body);
const headers = proxyResponseHeaders(response); const headers = proxyResponseHeaders(response);
return new NextResponse(JSON.stringify(response.data), { headers }); return new NextResponse(JSON.stringify(response.data), { headers });
} catch (error: any) { } catch (error: any) {
return new NextResponse( if (isAxiosError(error)) {
JSON.stringify({ console.log(url.toString(), body);
console.log("response data", JSON.stringify( error.response?.data));
console.log("is axios error");
}
return NextResponse.json(
{
error: error.message || "Canvas POST request failed", error: error.message || "Canvas POST request failed",
}), },
{ status: error.response?.status || 500 } { status: error.response?.status || 500 }
); );
} }

View File

@@ -135,7 +135,7 @@ function DraggableListItem({
href={getModuleItemUrl(courseName, moduleName, type, item.name)} href={getModuleItemUrl(courseName, moduleName, type, item.name)}
shallow={true} shallow={true}
className={ className={
" border rounded-sm px-1 mx-1 break-all " + " border rounded-sm px-1 mx-1 break-all mb-1 " +
" border-slate-600 bg-slate-800 " + " border-slate-600 bg-slate-800 " +
" block " " block "
} }

View File

@@ -15,9 +15,11 @@ import {
useAddAssignmentToCanvasMutation, useAddAssignmentToCanvasMutation,
useCanvasAssignmentsQuery, useCanvasAssignmentsQuery,
useDeleteAssignmentFromCanvasMutation, useDeleteAssignmentFromCanvasMutation,
useUpdateAssignmentInCanvasMutation,
} from "@/hooks/canvas/canvasAssignmentHooks"; } from "@/hooks/canvas/canvasAssignmentHooks";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils"; import { baseCanvasUrl } from "@/services/canvas/canvasServiceUtils";
import ClientOnly from "@/components/ClientOnly";
export default function EditAssignment({ export default function EditAssignment({
moduleName, moduleName,
@@ -79,10 +81,12 @@ export default function EditAssignment({
<AssignmentPreview assignment={assignment} /> <AssignmentPreview assignment={assignment} />
</div> </div>
</div> </div>
<ClientOnly>
<AssignmentButtons <AssignmentButtons
moduleName={moduleName} moduleName={moduleName}
assignmentName={assignmentName} assignmentName={assignmentName}
/> />
</ClientOnly>
</div> </div>
); );
} }
@@ -100,13 +104,16 @@ function AssignmentButtons({
const { data: assignment } = useAssignmentQuery(moduleName, assignmentName); const { data: assignment } = useAssignmentQuery(moduleName, assignmentName);
const addToCanvas = useAddAssignmentToCanvasMutation(); const addToCanvas = useAddAssignmentToCanvasMutation();
const deleteFromCanvas = useDeleteAssignmentFromCanvasMutation(); const deleteFromCanvas = useDeleteAssignmentFromCanvasMutation();
const updateAssignment = useUpdateAssignmentInCanvasMutation();
const assignmentInCanvas = canvasAssignments.find( const assignmentInCanvas = canvasAssignments.find(
(a) => a.name === assignmentName (a) => a.name === assignmentName
); );
return ( return (
<div className="p-5 flex flex-row justify-end gap-3"> <div className="p-5 flex flex-row justify-end gap-3">
{(addToCanvas.isPending || deleteFromCanvas.isPending) && <Spinner />} {(addToCanvas.isPending ||
deleteFromCanvas.isPending ||
updateAssignment.isPending) && <Spinner />}
{assignmentInCanvas && !assignmentInCanvas.published && ( {assignmentInCanvas && !assignmentInCanvas.published && (
<div className="text-rose-300 my-auto">Not Published</div> <div className="text-rose-300 my-auto">Not Published</div>
)} )}
@@ -115,7 +122,7 @@ function AssignmentButtons({
disabled={addToCanvas.isPending} disabled={addToCanvas.isPending}
onClick={() => addToCanvas.mutate(assignment)} onClick={() => addToCanvas.mutate(assignment)}
> >
Add to canvas.... Add to canvas
</button> </button>
)} )}
{assignmentInCanvas && ( {assignmentInCanvas && (
@@ -127,6 +134,20 @@ function AssignmentButtons({
View in Canvas View in Canvas
</a> </a>
)} )}
{assignmentInCanvas && (
<button
className=""
disabled={deleteFromCanvas.isPending}
onClick={() =>
updateAssignment.mutate({
canvasAssignmentId: assignmentInCanvas.id,
assignment,
})
}
>
Update in Canvas
</button>
)}
{assignmentInCanvas && ( {assignmentInCanvas && (
<button <button
className="btn-danger" className="btn-danger"

View File

@@ -13,7 +13,6 @@ import {
useCanvasPagesQuery, useCanvasPagesQuery,
useCreateCanvasPageMutation, useCreateCanvasPageMutation,
} from "@/hooks/canvas/canvasPageHooks"; } from "@/hooks/canvas/canvasPageHooks";
import { Spinner } from "@/components/Spinner";
import EditPageButtons from "./EditPageButtons"; import EditPageButtons from "./EditPageButtons";
export default function EditPage({ export default function EditPage({

View File

@@ -18,7 +18,8 @@
/* monaco editor */ /* monaco editor */
.monaco-editor-background, .monaco-editor-background,
.monaco-editor .margin { .monaco-editor .margin {
background-color: #18181b !important; background-color: #020617 !important;
/* background-color: #18181b !important; */
} }
.monaco-editor { position: absolute !important; } .monaco-editor { position: absolute !important; }
@@ -59,6 +60,19 @@ ol {
list-style-type: decimal; list-style-type: decimal;
padding-left: 1.5rem; padding-left: 1.5rem;
} }
table {
@apply border-collapse border border-gray-700;
}
thead {
@apply text-lg ;
}
th, td {
@apply px-2 py-1 border border-gray-700;
}
hr { hr {
@apply border-t border-gray-500 my-4; @apply border-t border-gray-500 my-4;
} }
@@ -70,6 +84,7 @@ blockquote {
code { code {
@apply font-mono text-sm bg-gray-800 px-2 py-1 rounded-md leading-tight inline-block; @apply font-mono text-sm bg-gray-800 px-2 py-1 rounded-md leading-tight inline-block;
} }
p { p {
@apply mb-3; @apply mb-3;
} }

View File

@@ -5,8 +5,8 @@ import { Suspense } from "react";
import { getQueryClient } from "./providersQueryClientUtils"; import { getQueryClient } from "./providersQueryClientUtils";
import { hydrateCourses } from "@/hooks/hookHydration"; import { hydrateCourses } from "@/hooks/hookHydration";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { ToastBar, Toaster } from "react-hot-toast";
import { MyToaster } from "./MyToaster"; import { MyToaster } from "./MyToaster";
import "katex/dist/katex.min.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Canvas Manager 2.0", title: "Canvas Manager 2.0",

View File

@@ -46,13 +46,43 @@ export const useAddAssignmentToCanvasMutation = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (assignmnet: LocalAssignment) => { mutationFn: async (assignment: LocalAssignment) => {
const assignmentGroup = settings.assignmentGroups.find( const assignmentGroup = settings.assignmentGroups.find(
(g) => g.name === assignmnet.localAssignmentGroupName (g) => g.name === assignment.localAssignmentGroupName
); );
await canvasAssignmentService.create( await canvasAssignmentService.create(
settings.canvasId, settings.canvasId,
assignmnet, assignment,
assignmentGroup?.canvasId
);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: canvasAssignmentKeys.assignments(settings.canvasId),
});
},
});
};
export const useUpdateAssignmentInCanvasMutation = () => {
const { data: settings } = useLocalCourseSettingsQuery();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
assignment,
canvasAssignmentId,
}: {
assignment: LocalAssignment;
canvasAssignmentId: number;
}) => {
const assignmentGroup = settings.assignmentGroups.find(
(g) => g.name === assignment.localAssignmentGroupName
);
await canvasAssignmentService.update(
settings.canvasId,
canvasAssignmentId,
assignment,
assignmentGroup?.canvasId assignmentGroup?.canvasId
); );
}, },

View File

@@ -31,21 +31,27 @@ axiosClient.interceptors.response.use(
(response) => response, (response) => response,
(error: AxiosError) => { (error: AxiosError) => {
if (error.response) { if (error.response) {
console.log("response error", error.response); // console.log("response error", error.response);
const responseErrorText = const responseErrorText =
typeof error.response.data === "object" typeof error.response.data === "object"
? (error.response.data as any).error ? (error.response.data as any).error
: error.response.data; : error.response.data;
if (!isServer) {
toast.error( toast.error(
`Error: ${error.response.status} - ${responseErrorText}, ${decodeURI( `Error: ${error.response.status} - ${responseErrorText}, ${decodeURI(
error.response.config.url ?? "" error.response.config.url ?? ""
)}` )}`
); );
}
} else if (error.request) { } else if (error.request) {
if (!isServer) {
toast.error("Error: No response from server"); toast.error("Error: No response from server");
}
} else { } else {
if (!isServer) {
toast.error(`Error: ${error.message}`); toast.error(`Error: ${error.message}`);
} }
}
return Promise.reject(error); return Promise.reject(error);
} }
); );

View File

@@ -5,6 +5,10 @@ import { axiosClient } from "../axiosUtils";
import { markdownToHTMLSafe } from "../htmlMarkdownUtils"; import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
import { CanvasRubricCreationResponse } from "@/models/canvas/assignments/canvasRubricCreationResponse"; import { CanvasRubricCreationResponse } from "@/models/canvas/assignments/canvasRubricCreationResponse";
import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils"; import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
import {
getDateFromString,
getDateFromStringOrThrow,
} from "@/models/local/timeUtils";
const createRubric = async ( const createRubric = async (
courseId: number, courseId: number,
@@ -77,18 +81,22 @@ export const canvasAssignmentService = {
console.log(`Creating assignment: ${localAssignment.name}`); console.log(`Creating assignment: ${localAssignment.name}`);
const url = `${canvasApi}/courses/${canvasCourseId}/assignments`; const url = `${canvasApi}/courses/${canvasCourseId}/assignments`;
const body = { const body = {
assignment: {
name: localAssignment.name, name: localAssignment.name,
submission_types: localAssignment.submissionTypes.map((t) => submission_types: localAssignment.submissionTypes.map((t) =>
t.toString() t.toString()
), ),
allowed_extensions: localAssignment.allowedFileUploadExtensions.map((e) => allowed_extensions: localAssignment.allowedFileUploadExtensions.map(
e.toString() (e) => e.toString()
), ),
description: markdownToHTMLSafe(localAssignment.description), description: markdownToHTMLSafe(localAssignment.description),
due_at: localAssignment.dueAt, due_at: getDateFromString(localAssignment.dueAt)?.toISOString(),
lock_at: localAssignment.lockAt, lock_at:
localAssignment.lockAt &&
getDateFromString(localAssignment.lockAt)?.toISOString(),
points_possible: assignmentPoints(localAssignment), points_possible: assignmentPoints(localAssignment),
assignment_group_id: canvasAssignmentGroupId, assignment_group_id: canvasAssignmentGroupId,
},
}; };
const response = await axiosClient.post<CanvasAssignment>(url, body); const response = await axiosClient.post<CanvasAssignment>(url, body);

View File

@@ -1,8 +1,16 @@
"use client"; "use client";
import { marked } from "marked"; import { marked } from "marked";
import markedKatex from "marked-katex-extension";
import * as DOMPurify from "isomorphic-dompurify"; import * as DOMPurify from "isomorphic-dompurify";
export function markdownToHTMLSafe(markdownString: string) { export function markdownToHTMLSafe(markdownString: string) {
const options = {
throwOnError: false,
nonStandard: true
};
marked.use(markedKatex(options));
const clean = DOMPurify.sanitize( const clean = DOMPurify.sanitize(
marked.parse(markdownString, { async: false, pedantic: false, gfm: true }) marked.parse(markdownString, { async: false, pedantic: false, gfm: true })
); );