diff --git a/nextjs/src/app/api/canvas/[...rest]/route.ts b/nextjs/src/app/api/canvas/[...rest]/route.ts index 3e90c88..d30e48b 100644 --- a/nextjs/src/app/api/canvas/[...rest]/route.ts +++ b/nextjs/src/app/api/canvas/[...rest]/route.ts @@ -44,7 +44,7 @@ const proxyResponseHeaders = (response: any) => { }; export async function GET( - req: NextRequest, + _req: NextRequest, { params }: { params: { rest: string[] } } ) { return withErrorHandling(async () => { @@ -58,7 +58,12 @@ export async function GET( url.toString() ); - var returnData = firstData ? [firstData] : []; + if(!Array.isArray(firstData)) + { + return NextResponse.json(firstData) + } + + var returnData = firstData; var nextUrl = getNextUrl(firstHeaders); while (nextUrl) { @@ -77,6 +82,7 @@ export async function GET( } return NextResponse.json(returnData); + } catch (error: any) { return new NextResponse( JSON.stringify({ error: error.message || "Canvas GET request failed" }), diff --git a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx index 968a678..0f16c19 100644 --- a/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx +++ b/nextjs/src/app/course/[courseName]/modules/ExpandableModule.tsx @@ -1,3 +1,4 @@ +"use client"; import { useAssignmentNamesQuery, useAssignmentsQueries, @@ -12,9 +13,15 @@ import { } from "@/hooks/localCourse/quizHooks"; import { IModuleItem } from "@/models/local/IModuleItem"; import { getDateFromStringOrThrow } from "@/models/local/timeUtils"; -import { useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import Modal from "../../../../components/Modal"; import NewItemForm from "./NewItemForm"; +import { useCanvasModulesQuery } from "@/hooks/canvas/canvasModuleHooks"; +import { Spinner } from "@/components/Spinner"; +import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"; +import { isServer } from "@tanstack/react-query"; +import { ModuleCanvasStatus } from "./ModuleCanvasStatus"; +import ClientOnly from "@/components/ClientOnly"; export default function ExpandableModule({ moduleName, @@ -66,11 +73,14 @@ export default function ExpandableModule({ return (
setExpanded((e) => !e)} > - {moduleName} +
{moduleName}
+ + +
c.name === moduleName); + + return ( +
+ {!canvasModule &&
Not in Canvas
} + {!canvasModule && ( + + )} + {canvasModule && !canvasModule.published &&
Not Published
} + {canvasModule && canvasModule.published &&
Published
} +
+ ); +} diff --git a/nextjs/src/app/newCourse/AddNewCourse.tsx b/nextjs/src/app/newCourse/AddNewCourse.tsx index 18bbecb..4feb587 100644 --- a/nextjs/src/app/newCourse/AddNewCourse.tsx +++ b/nextjs/src/app/newCourse/AddNewCourse.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"; import NewCourseForm from "./NewCourseForm"; +import ClientOnly from "@/components/ClientOnly"; export default function AddNewCourse() { const [showForm, setShowForm] = useState(false); @@ -13,7 +14,7 @@ export default function AddNewCourse() {
- {showForm && } + {showForm && }
diff --git a/nextjs/src/app/page.tsx b/nextjs/src/app/page.tsx index 70d6b06..0724e01 100644 --- a/nextjs/src/app/page.tsx +++ b/nextjs/src/app/page.tsx @@ -8,8 +8,7 @@ export default async function Home() {

- - +
); diff --git a/nextjs/src/components/ClientOnly.tsx b/nextjs/src/components/ClientOnly.tsx new file mode 100644 index 0000000..a1eb0a7 --- /dev/null +++ b/nextjs/src/components/ClientOnly.tsx @@ -0,0 +1,13 @@ +"use client" +import React, { ReactNode, useEffect, useState } from "react"; + +export default function ClientOnly({ children }: { children: ReactNode }) { + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + if (!isClient) return <>; + return <>{children}; +} diff --git a/nextjs/src/hooks/canvas/canvasHooks.ts b/nextjs/src/hooks/canvas/canvasHooks.ts index 68b2ab0..929d45e 100644 --- a/nextjs/src/hooks/canvas/canvasHooks.ts +++ b/nextjs/src/hooks/canvas/canvasHooks.ts @@ -1,5 +1,6 @@ import { canvasService } from "@/services/canvas/canvasService"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { QueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { canvasCourseModuleKeys } from "./canvasModuleHooks"; export const canvasKeys = { allTerms: ["all canvas terms"] as const, @@ -34,4 +35,4 @@ export const useCanvasTermsQuery = (queryDate: Date) => { return currentTerms; }, }); -}; +}; \ No newline at end of file diff --git a/nextjs/src/hooks/canvas/canvasModuleHooks.ts b/nextjs/src/hooks/canvas/canvasModuleHooks.ts new file mode 100644 index 0000000..f628109 --- /dev/null +++ b/nextjs/src/hooks/canvas/canvasModuleHooks.ts @@ -0,0 +1,34 @@ +import { canvasModuleService } from "@/services/canvas/canvasModuleService"; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; +import { useLocalCourseSettingsQuery } from "../localCourse/localCoursesHooks"; + +export const canvasCourseModuleKeys = { + modules: (canvasId: number) => ["canvas", canvasId, "module list"] as const, +}; + +export const useCanvasModulesQuery = () => { + const { data: settings } = useLocalCourseSettingsQuery(); + return useSuspenseQuery({ + queryKey: canvasCourseModuleKeys.modules(settings.canvasId), + queryFn: async () => + await canvasModuleService.getCourseModules(settings.canvasId), + }); +}; + +export const useAddCanvasModuleMutation = () => { + const { data: settings } = useLocalCourseSettingsQuery(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (moduleName: string) => + await canvasModuleService.createModule(settings.canvasId, moduleName), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: canvasCourseModuleKeys.modules(settings.canvasId), + }); + }, + }); +}; diff --git a/nextjs/src/services/canvas/canvasAssignmentGroupService.ts b/nextjs/src/services/canvas/canvasAssignmentGroupService.ts index 3ceb243..7fc285a 100644 --- a/nextjs/src/services/canvas/canvasAssignmentGroupService.ts +++ b/nextjs/src/services/canvas/canvasAssignmentGroupService.ts @@ -1,10 +1,9 @@ -import { canvasServiceUtils } from "./canvasServiceUtils"; +import { baseCanvasUrl, canvasServiceUtils } from "./canvasServiceUtils"; import { axiosClient } from "../axiosUtils"; import { CanvasAssignmentGroup } from "@/models/canvas/assignments/canvasAssignmentGroup"; import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup"; import { rateLimitAwareDelete } from "./canvasWebRequestor"; -const baseCanvasUrl = "https://snow.instructure.com/api/v1"; export const canvasAssignmentGroupService = { async getAll(courseId: number): Promise { diff --git a/nextjs/src/services/canvas/canvasModuleService.ts b/nextjs/src/services/canvas/canvasModuleService.ts new file mode 100644 index 0000000..cd40fd3 --- /dev/null +++ b/nextjs/src/services/canvas/canvasModuleService.ts @@ -0,0 +1,65 @@ +import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems"; +import { CanvasPage } from "@/models/canvas/pages/canvasPageModel"; +import { axiosClient } from "../axiosUtils"; +import { baseCanvasUrl } from "./canvasServiceUtils"; +import { CanvasModule } from "@/models/canvas/modules/canvasModule"; + +export const canvasModuleService = { + async updateModuleItem( + canvasCourseId: number, + canvasModuleId: number, + item: CanvasModuleItem + ) { + console.log(`Updating module item ${item.title}`); + const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules/${canvasModuleId}/items/${item.id}`; + const body = { + module_item: { title: item.title, position: item.position }, + }; + const { data } = await axiosClient.put(url, body); + + if (!data) throw new Error("Something went wrong updating module item"); + }, + + async createModuleItem( + canvasCourseId: number, + canvasModuleId: number, + title: string, + type: string, + contentId: number | string + ): Promise { + console.log(`Creating new module item ${title}`); + const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`; + const body = { module_item: { title, type, content_id: contentId } }; + await axiosClient.post(url, body); + }, + + async createPageModuleItem( + canvasCourseId: number, + canvasModuleId: number, + title: string, + canvasPage: CanvasPage + ): Promise { + console.log(`Creating new module item ${title}`); + const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`; + const body = { + module_item: { title, type: "Page", page_url: canvasPage.url }, + }; + await axiosClient.post(url, body); + }, + + async getCourseModules(canvasCourseId: number) { + const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules`; + const response = await axiosClient.get(url); + return response.data; + }, + + async createModule(canvasCourseId: number, moduleName: string) { + const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules`; + const body = { + module: { + name: moduleName, + }, + }; + await axiosClient.post(url, body); + }, +}; diff --git a/nextjs/src/services/canvas/canvasPageService.ts b/nextjs/src/services/canvas/canvasPageService.ts index 9837c28..6fdadc1 100644 --- a/nextjs/src/services/canvas/canvasPageService.ts +++ b/nextjs/src/services/canvas/canvasPageService.ts @@ -1,11 +1,10 @@ import { CanvasPage } from "@/models/canvas/pages/canvasPageModel"; import { LocalCoursePage } from "@/models/local/page/localCoursePage"; -import { canvasServiceUtils } from "./canvasServiceUtils"; +import { baseCanvasUrl, canvasServiceUtils } from "./canvasServiceUtils"; import { markdownToHTMLSafe } from "../htmlMarkdownUtils"; import { axiosClient } from "../axiosUtils"; import { rateLimitAwareDelete } from "./canvasWebRequestor"; -const baseCanvasUrl = "https://snow.instructure.com/api/v1"; export const canvasPageService = { async getAll(courseId: number): Promise { diff --git a/nextjs/src/services/canvas/canvasService.ts b/nextjs/src/services/canvas/canvasService.ts index a42c4e0..18a0461 100644 --- a/nextjs/src/services/canvas/canvasService.ts +++ b/nextjs/src/services/canvas/canvasService.ts @@ -1,12 +1,11 @@ import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel"; -import { canvasServiceUtils } from "./canvasServiceUtils"; +import { baseCanvasUrl, canvasServiceUtils } from "./canvasServiceUtils"; import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel"; import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems"; import { CanvasPage } from "@/models/canvas/pages/canvasPageModel"; import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel"; import { axiosClient } from "../axiosUtils"; -const baseCanvasUrl = "https://snow.instructure.com/api/v1"; const getAllTerms = async () => { const url = `${baseCanvasUrl}/accounts/10/terms`; @@ -23,7 +22,7 @@ export const canvasService = { getAllTerms, async getCourses(termId: number) { const url = `${baseCanvasUrl}/courses`; - const response = await axiosClient.get(url); + const response = await axiosClient.get(url); const allCourses = response.data; const coursesInTerm = allCourses .flatMap((l) => l) @@ -57,47 +56,6 @@ export const canvasService = { return currentTerms; }, - async updateModuleItem( - canvasCourseId: number, - canvasModuleId: number, - item: CanvasModuleItem - ) { - console.log(`Updating module item ${item.title}`); - const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules/${canvasModuleId}/items/${item.id}`; - const body = { - module_item: { title: item.title, position: item.position }, - }; - const { data } = await axiosClient.put(url, body); - - if (!data) throw new Error("Something went wrong updating module item"); - }, - - async createModuleItem( - canvasCourseId: number, - canvasModuleId: number, - title: string, - type: string, - contentId: number | string - ): Promise { - console.log(`Creating new module item ${title}`); - const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`; - const body = { module_item: { title, type, content_id: contentId } }; - const response = await axiosClient.post(url, body); - }, - - async createPageModuleItem( - canvasCourseId: number, - canvasModuleId: number, - title: string, - canvasPage: CanvasPage - ): Promise { - console.log(`Creating new module item ${title}`); - const url = `${baseCanvasUrl}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`; - const body = { - module_item: { title, type: "Page", page_url: canvasPage.url }, - }; - await axiosClient.post(url, body); - }, async getEnrolledStudents(canvasCourseId: number) { console.log(`Getting students for course ${canvasCourseId}`); diff --git a/nextjs/src/services/canvas/canvasServiceUtils.ts b/nextjs/src/services/canvas/canvasServiceUtils.ts index acf8a27..360b971 100644 --- a/nextjs/src/services/canvas/canvasServiceUtils.ts +++ b/nextjs/src/services/canvas/canvasServiceUtils.ts @@ -3,6 +3,9 @@ import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios"; import { axiosClient } from "../axiosUtils"; + +export const baseCanvasUrl = "https://snow.instructure.com/api/v1"; + const getNextUrl = ( headers: AxiosResponseHeaders | RawAxiosResponseHeaders ): string | undefined => {