diff --git a/nextjs/src/app/CourseDetailsWrapper.tsx b/nextjs/src/app/CourseDetailsWrapper.tsx new file mode 100644 index 0000000..4680818 --- /dev/null +++ b/nextjs/src/app/CourseDetailsWrapper.tsx @@ -0,0 +1,18 @@ +"use client"; +import { useLocalCourseDetailsQuery } from "@/hooks/localCoursesHooks"; +import { CourseContext } from "./course/[courseName]/courseContext"; +import CourseDetails from "./course/[courseName]/CourseDetails"; + +export default function CourseDetailsWrapper({ + courseName, +}: { + courseName: string; +}) { + const { data: course } = useLocalCourseDetailsQuery(courseName); + console.log("courseName", courseName); + return ( + + + + ); +} diff --git a/nextjs/src/app/CourseList.tsx b/nextjs/src/app/CourseList.tsx new file mode 100644 index 0000000..2d52137 --- /dev/null +++ b/nextjs/src/app/CourseList.tsx @@ -0,0 +1,16 @@ +"use client" +import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks"; +import Link from "next/link"; + +export default function CourseList() { + const { data: courses } = useLocalCoursesQuery(); + return ( +
+ {courses.map((c) => ( + + {c.settings.name}{" "} + + ))} +
+ ); +} diff --git a/nextjs/src/app/course/[courseName]/calendarMonth.tsx b/nextjs/src/app/course/[courseName]/CalendarMonth.tsx similarity index 94% rename from nextjs/src/app/course/[courseName]/calendarMonth.tsx rename to nextjs/src/app/course/[courseName]/CalendarMonth.tsx index bba1e24..f5bec1d 100644 --- a/nextjs/src/app/course/[courseName]/calendarMonth.tsx +++ b/nextjs/src/app/course/[courseName]/CalendarMonth.tsx @@ -1,7 +1,8 @@ +"use client" import { useState } from "react"; import { CalendarMonthModel } from "./calendarMonthUtils"; import { DayOfWeek, LocalCourse } from "@/models/local/localCourse"; -import Day from "./day"; +import Day from "./Day"; export default function CalendarMonth({ month, @@ -55,7 +56,7 @@ export default function CalendarMonth({ {month.daysByWeek.map((week, weekIndex) => (
{week.map((day, dayIndex) => ( - + ))}
))} diff --git a/nextjs/src/app/course/[courseName]/CourseContextProvider.tsx b/nextjs/src/app/course/[courseName]/CourseContextProvider.tsx new file mode 100644 index 0000000..df2c4e0 --- /dev/null +++ b/nextjs/src/app/course/[courseName]/CourseContextProvider.tsx @@ -0,0 +1,19 @@ +"use client" +import { ReactNode } from "react"; +import { CourseContext } from "./courseContext"; +import { useLocalCourseDetailsQuery } from "@/hooks/localCoursesHooks"; + +export default function CourseContextProvider({ + localCourseName, + children, +}: { + children: ReactNode; + localCourseName: string; +}) { + const { data: course } = useLocalCourseDetailsQuery(localCourseName); + return ( + + {children} + + ); +} diff --git a/nextjs/src/app/course/[courseName]/CourseDetails.tsx b/nextjs/src/app/course/[courseName]/CourseDetails.tsx new file mode 100644 index 0000000..4234ca5 --- /dev/null +++ b/nextjs/src/app/course/[courseName]/CourseDetails.tsx @@ -0,0 +1,42 @@ +"use client"; +import { getDateFromStringOrThrow } from "@/models/local/timeUtils"; +import { useCourseContext } from "./courseContext"; +import { getMonthsBetweenDates } from "./calendarMonthUtils"; +import CalendarMonth from "./CalendarMonth"; + +export default function CourseDetails() { + const context = useCourseContext(); + + const startDate = getDateFromStringOrThrow( + context.localCourse.settings.startDate, + "course start date" + ); + const endDate = getDateFromStringOrThrow( + context.localCourse.settings.endDate, + "course end date" + ); + + const months = getMonthsBetweenDates(startDate, endDate); + return ( +
+ {context.localCourse.settings.name} +
+
+ {months.map((month) => ( + + ))} +
+
+
+ first module +
here are the module items
+
+
+
+
+ ); +} diff --git a/nextjs/src/app/course/[courseName]/Day.tsx b/nextjs/src/app/course/[courseName]/Day.tsx new file mode 100644 index 0000000..417fb1b --- /dev/null +++ b/nextjs/src/app/course/[courseName]/Day.tsx @@ -0,0 +1,13 @@ +"use client" +export default function Day({ day, month }: { day: Date; month: number }) { + const classes = "border rounded rounded-3 p-2 pb-4 m-1 "; + + const backgroundClass = day.getMonth() + 1 != month ? "" : "bg-slate-900"; + + return ( +
+ {day.getDate()} + {/*
{day.getMonth()}
*/} +
+ ); +} diff --git a/nextjs/src/app/course/[courseName]/ModuleList.tsx b/nextjs/src/app/course/[courseName]/ModuleList.tsx new file mode 100644 index 0000000..628fc69 --- /dev/null +++ b/nextjs/src/app/course/[courseName]/ModuleList.tsx @@ -0,0 +1,4 @@ +"use client" +export default function ModuleList() { + return
+} \ No newline at end of file diff --git a/nextjs/src/app/course/[courseName]/calendarMonthUtils.ts b/nextjs/src/app/course/[courseName]/calendarMonthUtils.ts index a2ae318..8e039f1 100644 --- a/nextjs/src/app/course/[courseName]/calendarMonthUtils.ts +++ b/nextjs/src/app/course/[courseName]/calendarMonthUtils.ts @@ -1,8 +1,9 @@ +"use client" export interface CalendarMonthModel { year: number; month: number; - weeks: (number | undefined)[][]; - daysByWeek: (Date | undefined)[][]; + weeks: number[][]; + daysByWeek: (Date)[][]; } function weeksInMonth(year: number, month: number): number { @@ -17,42 +18,27 @@ function weeksInMonth(year: number, month: number): number { } function createCalendarMonth(year: number, month: number): CalendarMonthModel { - const daysByWeek: (Date | undefined)[][] = []; const weeksNumber = weeksInMonth(year, month); const daysInMonth = new Date(year, month, 0).getDate(); let currentDay = 1; const firstDayOfMonth = new Date(year, month - 1, 1).getDay(); - for (let i = 0; i < weeksNumber; i++) { - const thisWeek: (Date | undefined)[] = []; - if (i === 0 && firstDayOfMonth !== 0) { - // 0 is Sunday in JavaScript - for (let j = 0; j < 7; j++) { - if (j < firstDayOfMonth) { - thisWeek.push(undefined); - } else { - thisWeek.push(new Date(year, month - 1, currentDay)); - currentDay++; - } + const daysByWeek = Array.from({ length: weeksNumber }).map((_, weekIndex) => + Array.from({ length: 7 }).map((_, dayIndex) => { + if (weekIndex === 0 && dayIndex < firstDayOfMonth) { + return new Date(year, month - 1, dayIndex - firstDayOfMonth + 1); + } else if (currentDay <= daysInMonth) { + return new Date(year, month - 1, currentDay++); + } else { + currentDay++; + return new Date(year, month, currentDay - daysInMonth); } - } else { - for (let j = 0; j < 7; j++) { - if (currentDay <= daysInMonth) { - thisWeek.push(new Date(year, month - 1, currentDay)); - currentDay++; - } else { - thisWeek.push(undefined); - } - } - } - daysByWeek.push(thisWeek); - } - - const weeks = daysByWeek.map((week) => - week.map((day) => (day ? day.getDate() : undefined)) + }) ); + const weeks = daysByWeek.map((week) => week.map((day) => day.getDate())); + return { year, month, weeks, daysByWeek }; } diff --git a/nextjs/src/app/course/[courseName]/courseContext.ts b/nextjs/src/app/course/[courseName]/courseContext.ts new file mode 100644 index 0000000..e298bda --- /dev/null +++ b/nextjs/src/app/course/[courseName]/courseContext.ts @@ -0,0 +1,31 @@ +"use client"; +import { LocalCourse } from "@/models/local/localCourse"; +import { createContext, useContext } from "react"; + +export interface CourseContextInterface { + localCourse: LocalCourse; +} + +const defaultValue: CourseContextInterface = { + localCourse: { + modules: [], + settings: { + name: "", + assignmentGroups: [], + daysOfWeek: [], + startDate: "", + endDate: "", + defaultDueTime: { + hour: 0, + minute: 0, + }, + }, + }, +}; + +export const CourseContext = + createContext(defaultValue); + +export function useCourseContext() { + return useContext(CourseContext); +} diff --git a/nextjs/src/app/course/[courseName]/day.tsx b/nextjs/src/app/course/[courseName]/day.tsx deleted file mode 100644 index cd4db52..0000000 --- a/nextjs/src/app/course/[courseName]/day.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export default function Day({ day }: { day?: Date }) { - const classes = "border rounded rounded-3 p-2 pb-4 m-1 "; - if (!day) return
; - - return
{day.getDate()}
; -} diff --git a/nextjs/src/app/course/[courseName]/page.tsx b/nextjs/src/app/course/[courseName]/page.tsx index 9fae88b..6ef89bc 100644 --- a/nextjs/src/app/course/[courseName]/page.tsx +++ b/nextjs/src/app/course/[courseName]/page.tsx @@ -1,29 +1,16 @@ -"use client"; -import { useLocalCourseDetailsQuery } from "@/hooks/localCoursesHooks"; -import { getDateFromStringOrThrow } from "@/models/local/timeUtils"; -import { getMonthsBetweenDates } from "./calendarMonthUtils"; -import CalendarMonth from "./calendarMonth"; +import CourseDetailsWrapper from "@/app/CourseDetailsWrapper"; +import { getDehydratedClient } from "@/app/layout"; +import { HydrationBoundary } from "@tanstack/react-query"; - -export default function Page({ +export default async function CoursePage({ params: { courseName }, }: { params: { courseName: string }; }) { - const { data: course } = useLocalCourseDetailsQuery(courseName); - - const startDate = getDateFromStringOrThrow(course.settings.startDate); - const endDate = getDateFromStringOrThrow(course.settings.endDate); - - const months = getMonthsBetweenDates(startDate, endDate); + const dehydratedState = await getDehydratedClient(); return ( -
- {course.settings.name} -
- {months.map((month) => ( - - ))} -
-
+ + + ); } diff --git a/nextjs/src/app/getDehydratedState.ts b/nextjs/src/app/getDehydratedState.ts new file mode 100644 index 0000000..209bf06 --- /dev/null +++ b/nextjs/src/app/getDehydratedState.ts @@ -0,0 +1,11 @@ +import { hydrateCourses } from "@/hooks/hookHydration"; +import { createQueryClientForServer } from "@/services/utils/queryClientServer"; +import { dehydrate } from "@tanstack/react-query"; + +export async function getDehydratedState() { + const queryClient = createQueryClientForServer(); + + await hydrateCourses(queryClient); + const dehydratedState = dehydrate(queryClient); + return dehydratedState; +} diff --git a/nextjs/src/app/globals.css b/nextjs/src/app/globals.css index 875c01e..a88506d 100644 --- a/nextjs/src/app/globals.css +++ b/nextjs/src/app/globals.css @@ -18,12 +18,7 @@ body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + } @layer utilities { diff --git a/nextjs/src/app/layout.tsx b/nextjs/src/app/layout.tsx index 1c01047..dd51ed9 100644 --- a/nextjs/src/app/layout.tsx +++ b/nextjs/src/app/layout.tsx @@ -3,8 +3,7 @@ import "./globals.css"; import { dehydrate } from "@tanstack/react-query"; import { hydrateCourses } from "@/hooks/hookHydration"; import { createQueryClientForServer } from "@/services/utils/queryClientServer"; -import MyQueryClientProvider from "@/services/utils/MyQueryClientProvider"; - +import Providers from "./providers"; export const metadata: Metadata = { title: "Canvas Manager 2.0", @@ -18,18 +17,16 @@ export async function getDehydratedClient() { return dehydratedState; } -export default async function RootLayout({ +export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - const dehydratedState = await getDehydratedClient(); - return ( - - {children} - + + {children} + ); } diff --git a/nextjs/src/app/page.tsx b/nextjs/src/app/page.tsx index 67f1a01..7d51850 100644 --- a/nextjs/src/app/page.tsx +++ b/nextjs/src/app/page.tsx @@ -1,14 +1,14 @@ -"use client" -import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks"; -import Link from "next/link"; +import { HydrationBoundary } from "@tanstack/react-query"; +import CourseList from "./CourseList"; +import { getDehydratedClient } from "./layout"; -export default function Home() { - const { data: courses } = useLocalCoursesQuery(); +export default async function Home() { + const dehydratedState = await getDehydratedClient(); return (
- {courses.map((c) => ( - {c.settings.name} - ))} + + +
); } diff --git a/nextjs/src/app/providers.tsx b/nextjs/src/app/providers.tsx new file mode 100644 index 0000000..968f494 --- /dev/null +++ b/nextjs/src/app/providers.tsx @@ -0,0 +1,47 @@ +"use client"; +import { + isServer, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; +import { ReactNode } from "react"; + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }); +} + +let browserQueryClient: QueryClient | undefined = undefined; + +function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } +} + +export default function Providers({ children }: { children: ReactNode }) { + // NOTE: Avoid useState when initializing the query client if you don't + // have a suspense boundary between this and the code that may + // suspend because React will throw away the client on the initial + // render if it suspends and there is no boundary + const queryClient = getQueryClient(); + + return ( + {children} + ); +} diff --git a/nextjs/src/models/local/timeUtils.ts b/nextjs/src/models/local/timeUtils.ts index 0e3176c..06dc7e8 100644 --- a/nextjs/src/models/local/timeUtils.ts +++ b/nextjs/src/models/local/timeUtils.ts @@ -56,9 +56,9 @@ export const getDateFromString = (value: string): Date | undefined => { } }; -export const getDateFromStringOrThrow = (value: string): Date => { +export const getDateFromStringOrThrow = (value: string, labelForError: string): Date => { const d = getDateFromString(value); - if (!d) throw Error(`Invalid date format, ${value}`); + if (!d) throw Error(`Invalid date format for ${labelForError}, ${value}`); return d; }; diff --git a/nextjs/src/services/canvas/canvasServiceUtils.ts b/nextjs/src/services/canvas/canvasServiceUtils.ts index db2c537..f0444d1 100644 --- a/nextjs/src/services/canvas/canvasServiceUtils.ts +++ b/nextjs/src/services/canvas/canvasServiceUtils.ts @@ -23,9 +23,8 @@ export const canvasServiceUtils = { const url = new URL(request.url!, BASE_URL); url.searchParams.set("per_page", "100"); - const [firstData, firstResponse] = await webRequestor.get( - url.toString() - ); + const { data: firstData, response: firstResponse } = + await webRequestor.get(url.toString()); if (!firstResponse.ok) { console.error( @@ -41,7 +40,7 @@ export const canvasServiceUtils = { while (nextUrl) { requestCount += 1; - const [data, response] = await webRequestor.get(nextUrl); + const {data, response} = await webRequestor.get(nextUrl); if (data) { returnData = [...returnData, data]; } diff --git a/nextjs/src/services/utils/MyQueryClientProvider.tsx b/nextjs/src/services/utils/MyQueryClientProvider.tsx index e771645..320692d 100644 --- a/nextjs/src/services/utils/MyQueryClientProvider.tsx +++ b/nextjs/src/services/utils/MyQueryClientProvider.tsx @@ -1,12 +1,11 @@ -"use client"; - +"use client" import { DehydratedState, hydrate, QueryClientProvider, } from "@tanstack/react-query"; import React from "react"; -import { FC, ReactNode, useState } from "react"; +import { ReactNode, useState } from "react"; import { createQueryClient } from "./queryClient"; export default function MyQueryClientProvider({