From 5d16cae417f32c087d8e956b68905b529238e4b0 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Mon, 26 Aug 2024 13:37:23 -0600 Subject: [PATCH] handling iso date strings --- .../course/[courseName]/calendarMonthUtils.ts | 66 +++++++++++++++++++ nextjs/src/app/course/[courseName]/page.tsx | 27 ++++++++ nextjs/src/app/layout.tsx | 5 +- nextjs/src/app/page.tsx | 3 +- .../components/LoadingAndErrorHandling.tsx | 1 + nextjs/src/hooks/localCoursesHooks.ts | 17 +++++ nextjs/src/models/local/localCourse.ts | 25 ++++++- .../src/models/local/tests/timeUtils.test.ts | 5 ++ nextjs/src/models/local/timeUtils.ts | 35 +++++++--- .../fileStorage/utils/couresMarkdownLoader.ts | 1 + .../services/utils/MyQueryClientProvider.tsx | 7 +- 11 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 nextjs/src/app/course/[courseName]/calendarMonthUtils.ts create mode 100644 nextjs/src/app/course/[courseName]/page.tsx 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} ); };