diff --git a/nextjs/src/app/course/[courseName]/calendarMonthUtils.ts b/nextjs/src/app/course/[courseName]/calendarMonthUtils.ts
new file mode 100644
index 0000000..81c62af
--- /dev/null
+++ b/nextjs/src/app/course/[courseName]/calendarMonthUtils.ts
@@ -0,0 +1,66 @@
+interface CalendarMonth {
+ year: number;
+ month: number;
+ weeks: (number | null)[][];
+ daysByWeek: (Date | null)[][];
+}
+
+const calendarMonthUtils = {
+ weeksInMonth: (year: number, month: number): number => {
+ const firstDayOfMonth = new Date(year, month - 1, 1).getDay();
+ const daysInMonth = new Date(year, month, 0).getDate();
+ const longDaysInMonth = daysInMonth + firstDayOfMonth;
+ let weeks = Math.floor(longDaysInMonth / 7);
+ if (longDaysInMonth % 7 > 0) {
+ weeks += 1;
+ }
+ return weeks;
+ },
+
+ createCalendarMonth: (year: number, month: number): CalendarMonth => {
+ const daysByWeek: (Date | null)[][] = [];
+ const weeksInMonth = calendarMonthUtils.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 < weeksInMonth; i++) {
+ const thisWeek: (Date | null)[] = [];
+ if (i === 0 && firstDayOfMonth !== 0) { // 0 is Sunday in JavaScript
+ for (let j = 0; j < 7; j++) {
+ if (j < firstDayOfMonth) {
+ thisWeek.push(null);
+ } else {
+ thisWeek.push(new Date(year, month - 1, currentDay));
+ currentDay++;
+ }
+ }
+ } else {
+ for (let j = 0; j < 7; j++) {
+ if (currentDay <= daysInMonth) {
+ thisWeek.push(new Date(year, month - 1, currentDay));
+ currentDay++;
+ } else {
+ thisWeek.push(null);
+ }
+ }
+ }
+ daysByWeek.push(thisWeek);
+ }
+
+ const weeks = daysByWeek.map(week => week.map(day => day ? day.getDate() : null));
+
+ return { year, month, weeks, daysByWeek };
+ },
+
+ getMonthsBetweenDates: (startDate: Date, endDate: Date): CalendarMonth[] => {
+ const monthsInTerm = 1 + (endDate.getFullYear() - startDate.getFullYear()) * 12 + endDate.getMonth() - startDate.getMonth();
+
+ return Array.from({ length: monthsInTerm }, (_, monthDiff) => {
+ const month = ((startDate.getMonth() + monthDiff) % 12) + 1;
+ const year = startDate.getFullYear() + Math.floor((startDate.getMonth() + monthDiff) / 12);
+ return calendarMonthUtils.createCalendarMonth(year, month);
+ });
+ }
+};
diff --git a/nextjs/src/app/course/[courseName]/page.tsx b/nextjs/src/app/course/[courseName]/page.tsx
new file mode 100644
index 0000000..2c0f68d
--- /dev/null
+++ b/nextjs/src/app/course/[courseName]/page.tsx
@@ -0,0 +1,27 @@
+"use client";
+import { useLocalCourseDetailsQuery } from "@/hooks/localCoursesHooks";
+import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
+
+export default function Page({
+ params: { courseName },
+}: {
+ params: { courseName: string };
+}) {
+ const { data: course } = useLocalCourseDetailsQuery(courseName);
+ console.log(course);
+
+ const startDate = getDateFromStringOrThrow(course.settings.startDate);
+ const endDate = getDateFromStringOrThrow(course.settings.endDate);
+
+ const months = calendarMonthUtils.getMonthsBetweenDates(startDate, endDate);
+ return (
+
+ {course.settings.name}
+
+ {months.map((month) => (
+
{month.month}
+ ))}
+
+
+ );
+}
diff --git a/nextjs/src/app/layout.tsx b/nextjs/src/app/layout.tsx
index 77d82fe..528ab33 100644
--- a/nextjs/src/app/layout.tsx
+++ b/nextjs/src/app/layout.tsx
@@ -4,7 +4,6 @@ import "./globals.css";
import { dehydrate } from "@tanstack/react-query";
import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider";
import { hydrateCourses } from "@/hooks/hookHydration";
-import { LoadingAndErrorHandling } from "@/components/LoadingAndErrorHandling";
import { createQueryClientForServer } from "@/services/utils/queryClientServer";
const inter = Inter({ subsets: ["latin"] });
@@ -31,9 +30,9 @@ export default async function RootLayout({
return (
-
+ {/* */}
{children}
-
+ {/* */}
);
diff --git a/nextjs/src/app/page.tsx b/nextjs/src/app/page.tsx
index e8fdbfc..be336cb 100644
--- a/nextjs/src/app/page.tsx
+++ b/nextjs/src/app/page.tsx
@@ -1,12 +1,13 @@
"use client"
import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks";
+import Link from "next/link";
export default function Home() {
const { data: courses } = useLocalCoursesQuery();
return (
{courses.map((c) => (
- {c.settings.name}
+ {c.settings.name}
))}
);
diff --git a/nextjs/src/components/LoadingAndErrorHandling.tsx b/nextjs/src/components/LoadingAndErrorHandling.tsx
index 9984576..e52654b 100644
--- a/nextjs/src/components/LoadingAndErrorHandling.tsx
+++ b/nextjs/src/components/LoadingAndErrorHandling.tsx
@@ -2,6 +2,7 @@ import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { FC, ReactNode, Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
+// not at top level?
export const LoadingAndErrorHandling: FC<{ children: ReactNode }> = ({
children,
}) => {
diff --git a/nextjs/src/hooks/localCoursesHooks.ts b/nextjs/src/hooks/localCoursesHooks.ts
index a9fef95..97a54c0 100644
--- a/nextjs/src/hooks/localCoursesHooks.ts
+++ b/nextjs/src/hooks/localCoursesHooks.ts
@@ -4,6 +4,7 @@ import axios from "axios";
export const localCourseKeys = {
allCourses: ["all courses"] as const,
+ courseDetail: (courseName: string) => ["all courses", courseName] as const,
};
export const useLocalCoursesQuery = () =>
@@ -15,3 +16,19 @@ export const useLocalCoursesQuery = () =>
return response.data;
},
});
+
+export const useLocalCourseDetailsQuery = (courseName: string) => {
+ const { data: courses } = useLocalCoursesQuery();
+ return useSuspenseQuery({
+ queryKey: localCourseKeys.courseDetail(courseName),
+ queryFn: () => {
+ const course = courses.find((c) => c.settings.name === courseName);
+ if (!course) {
+ console.log(courses);
+ console.log(courseName);
+ throw Error(`Could not find course with name ${courseName}`);
+ }
+ return course;
+ },
+ });
+};
diff --git a/nextjs/src/models/local/localCourse.ts b/nextjs/src/models/local/localCourse.ts
index c506526..077371a 100644
--- a/nextjs/src/models/local/localCourse.ts
+++ b/nextjs/src/models/local/localCourse.ts
@@ -33,9 +33,32 @@ export enum DayOfWeek {
}
export const localCourseYamlUtils = {
parseSettingYaml: (settingsString: string): LocalCourseSettings => {
- return parse(settingsString);
+ const settings = parse(settingsString);
+ return lowercaseFirstLetter(settings)
},
settingsToYaml: (settings: LocalCourseSettings) => {
return stringify(settings);
},
};
+
+function lowercaseFirstLetter(obj: T): T {
+ if (obj === null || typeof obj !== 'object')
+ return obj as T;
+
+ if (Array.isArray(obj))
+ return obj.map(lowercaseFirstLetter) as unknown as T;
+
+ const result: Record = {};
+ Object.keys(obj).forEach((key) => {
+ const value = (obj as Record)[key];
+ const newKey = key.charAt(0).toLowerCase() + key.slice(1);
+
+ if (value && typeof value === 'object') {
+ result[newKey] = lowercaseFirstLetter(value);
+ } else {
+ result[newKey] = value;
+ }
+ });
+
+ return result as T;
+}
\ No newline at end of file
diff --git a/nextjs/src/models/local/tests/timeUtils.test.ts b/nextjs/src/models/local/tests/timeUtils.test.ts
index 2582c0c..45e9cb8 100644
--- a/nextjs/src/models/local/tests/timeUtils.test.ts
+++ b/nextjs/src/models/local/tests/timeUtils.test.ts
@@ -15,4 +15,9 @@ describe("Can properly handle expected date formats", () => {
const dateObject = getDateFromString(dateString)
expect(dateObject).not.toBeUndefined()
})
+ it("can use ISO format", () =>{
+ const dateString = "2024-08-26T00:00:00.0000000"
+ const dateObject = getDateFromString(dateString)
+ expect(dateObject).not.toBeUndefined()
+ })
})
\ No newline at end of file
diff --git a/nextjs/src/models/local/timeUtils.ts b/nextjs/src/models/local/timeUtils.ts
index a161bdf..0e3176c 100644
--- a/nextjs/src/models/local/timeUtils.ts
+++ b/nextjs/src/models/local/timeUtils.ts
@@ -1,5 +1,7 @@
-
-const _getDateFromAMPM = (datePart: string, timePartWithMeridian: string): Date | undefined => {
+const _getDateFromAMPM = (
+ datePart: string,
+ timePartWithMeridian: string
+): Date | undefined => {
const [day, month, year] = datePart.split("/").map(Number);
const [timePart, meridian] = timePartWithMeridian.split(" ");
const [hours, minutes, seconds] = timePart.split(":").map(Number);
@@ -18,7 +20,10 @@ const _getDateFromAMPM = (datePart: string, timePartWithMeridian: string): Date
return isNaN(date.getTime()) ? undefined : date;
};
-const _getDateFromMilitary = (datePart: string, timePart: string): Date | undefined => {
+const _getDateFromMilitary = (
+ datePart: string,
+ timePart: string
+): Date | undefined => {
const [day, month, year] = datePart.split("/").map(Number);
const [hours, minutes, seconds] = timePart.split(":").map(Number);
@@ -26,14 +31,20 @@ const _getDateFromMilitary = (datePart: string, timePart: string): Date | undefi
return isNaN(date.getTime()) ? undefined : date;
};
+const _getDateFromISO = (value: string): Date | undefined => {
+ const date = new Date(value);
+ return isNaN(date.getTime()) ? undefined : date;
+};
+
export const getDateFromString = (value: string): Date | undefined => {
- // Regex for AM/PM format: "M/D/YYYY h:mm:ss AM/PM"
- const ampmDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}\s{1}[APap][Mm]$/;
+ const ampmDateRegex =
+ /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}\s{1}[APap][Mm]$/; //"M/D/YYYY h:mm:ss AM/PM"
+ const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/; //"MM/DD/YYYY HH:mm:ss"
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)$/; //"2024-08-26T00:00:00.0000000"
- // Regex for military time format: "MM/DD/YYYY HH:mm:ss"
- const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/;
-
- if (ampmDateRegex.test(value)) {
+ if (isoDateRegex.test(value)) {
+ return _getDateFromISO(value);
+ } else if (ampmDateRegex.test(value)) {
const [datePart, timePartWithMeridian] = value.split(/[\s\u202F]+/);
return _getDateFromAMPM(datePart, timePartWithMeridian);
} else if (militaryDateRegex.test(value)) {
@@ -45,7 +56,11 @@ export const getDateFromString = (value: string): Date | undefined => {
}
};
-
+export const getDateFromStringOrThrow = (value: string): Date => {
+ const d = getDateFromString(value);
+ if (!d) throw Error(`Invalid date format, ${value}`);
+ return d;
+};
export const verifyDateStringOrUndefined = (
value: string
diff --git a/nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts b/nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts
index 40a83c5..95b95d5 100644
--- a/nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts
+++ b/nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts
@@ -73,6 +73,7 @@ export const courseMarkdownLoader = {
const settingsString = await fs.readFile(settingsPath, "utf-8");
const settings = localCourseYamlUtils.parseSettingYaml(settingsString);
+ console.log(settingsString, settings);
const folderName = path.basename(courseDirectory);
return { ...settings, name: folderName };
diff --git a/nextjs/src/services/utils/MyQueryClientProvider.tsx b/nextjs/src/services/utils/MyQueryClientProvider.tsx
index 21b9358..4e025e7 100644
--- a/nextjs/src/services/utils/MyQueryClientProvider.tsx
+++ b/nextjs/src/services/utils/MyQueryClientProvider.tsx
@@ -3,7 +3,6 @@
import {
DehydratedState,
hydrate,
- HydrationBoundary,
QueryClientProvider,
} from "@tanstack/react-query";
import React from "react";
@@ -16,9 +15,9 @@ export const MyQueryClientProvider: FC<{
}> = ({ children, dehydratedState }) => {
const [queryClient] = useState(createQueryClient());
+ hydrate(queryClient, dehydratedState);
+
return (
-
- {children}
-
+ {children}
);
};