mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
handling iso date strings
This commit is contained in:
66
nextjs/src/app/course/[courseName]/calendarMonthUtils.ts
Normal file
66
nextjs/src/app/course/[courseName]/calendarMonthUtils.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
27
nextjs/src/app/course/[courseName]/page.tsx
Normal file
27
nextjs/src/app/course/[courseName]/page.tsx
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
{course.settings.name}
|
||||||
|
<div>
|
||||||
|
{months.map((month) => (
|
||||||
|
<div key={month.month + "" + month.year}>{month.month}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import "./globals.css";
|
|||||||
import { dehydrate } from "@tanstack/react-query";
|
import { dehydrate } from "@tanstack/react-query";
|
||||||
import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider";
|
import { MyQueryClientProvider } from "@/services/utils/MyQueryClientProvider";
|
||||||
import { hydrateCourses } from "@/hooks/hookHydration";
|
import { hydrateCourses } from "@/hooks/hookHydration";
|
||||||
import { LoadingAndErrorHandling } from "@/components/LoadingAndErrorHandling";
|
|
||||||
import { createQueryClientForServer } from "@/services/utils/queryClientServer";
|
import { createQueryClientForServer } from "@/services/utils/queryClientServer";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
@@ -31,9 +30,9 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<MyQueryClientProvider dehydratedState={dehydratedState}>
|
<MyQueryClientProvider dehydratedState={dehydratedState}>
|
||||||
<LoadingAndErrorHandling>
|
{/* <LoadingAndErrorHandling> */}
|
||||||
<body className={inter.className}>{children}</body>
|
<body className={inter.className}>{children}</body>
|
||||||
</LoadingAndErrorHandling>
|
{/* </LoadingAndErrorHandling> */}
|
||||||
</MyQueryClientProvider>
|
</MyQueryClientProvider>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks";
|
import { useLocalCoursesQuery } from "@/hooks/localCoursesHooks";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { data: courses } = useLocalCoursesQuery();
|
const { data: courses } = useLocalCoursesQuery();
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||||
{courses.map((c) => (
|
{courses.map((c) => (
|
||||||
<div key={c.settings.name}>{c.settings.name} </div>
|
<Link href={`/course/${c.settings.name}`} key={c.settings.name}>{c.settings.name} </Link>
|
||||||
))}
|
))}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
|||||||
import { FC, ReactNode, Suspense } from "react";
|
import { FC, ReactNode, Suspense } from "react";
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
|
||||||
|
// not at top level?
|
||||||
export const LoadingAndErrorHandling: FC<{ children: ReactNode }> = ({
|
export const LoadingAndErrorHandling: FC<{ children: ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import axios from "axios";
|
|||||||
|
|
||||||
export const localCourseKeys = {
|
export const localCourseKeys = {
|
||||||
allCourses: ["all courses"] as const,
|
allCourses: ["all courses"] as const,
|
||||||
|
courseDetail: (courseName: string) => ["all courses", courseName] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLocalCoursesQuery = () =>
|
export const useLocalCoursesQuery = () =>
|
||||||
@@ -15,3 +16,19 @@ export const useLocalCoursesQuery = () =>
|
|||||||
return response.data;
|
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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -33,9 +33,32 @@ export enum DayOfWeek {
|
|||||||
}
|
}
|
||||||
export const localCourseYamlUtils = {
|
export const localCourseYamlUtils = {
|
||||||
parseSettingYaml: (settingsString: string): LocalCourseSettings => {
|
parseSettingYaml: (settingsString: string): LocalCourseSettings => {
|
||||||
return parse(settingsString);
|
const settings = parse(settingsString);
|
||||||
|
return lowercaseFirstLetter(settings)
|
||||||
},
|
},
|
||||||
settingsToYaml: (settings: LocalCourseSettings) => {
|
settingsToYaml: (settings: LocalCourseSettings) => {
|
||||||
return stringify(settings);
|
return stringify(settings);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function lowercaseFirstLetter<T>(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<string, any> = {};
|
||||||
|
Object.keys(obj).forEach((key) => {
|
||||||
|
const value = (obj as Record<string, any>)[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;
|
||||||
|
}
|
||||||
@@ -15,4 +15,9 @@ describe("Can properly handle expected date formats", () => {
|
|||||||
const dateObject = getDateFromString(dateString)
|
const dateObject = getDateFromString(dateString)
|
||||||
expect(dateObject).not.toBeUndefined()
|
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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
const _getDateFromAMPM = (
|
||||||
const _getDateFromAMPM = (datePart: string, timePartWithMeridian: string): Date | undefined => {
|
datePart: string,
|
||||||
|
timePartWithMeridian: string
|
||||||
|
): Date | undefined => {
|
||||||
const [day, month, year] = datePart.split("/").map(Number);
|
const [day, month, year] = datePart.split("/").map(Number);
|
||||||
const [timePart, meridian] = timePartWithMeridian.split(" ");
|
const [timePart, meridian] = timePartWithMeridian.split(" ");
|
||||||
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
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;
|
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 [day, month, year] = datePart.split("/").map(Number);
|
||||||
const [hours, minutes, seconds] = timePart.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;
|
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 => {
|
export const getDateFromString = (value: string): Date | undefined => {
|
||||||
// Regex for AM/PM format: "M/D/YYYY h:mm:ss AM/PM"
|
const ampmDateRegex =
|
||||||
const ampmDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}\s{1}[APap][Mm]$/;
|
/^\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"
|
if (isoDateRegex.test(value)) {
|
||||||
const militaryDateRegex = /^\d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{2}:\d{2}$/;
|
return _getDateFromISO(value);
|
||||||
|
} else if (ampmDateRegex.test(value)) {
|
||||||
if (ampmDateRegex.test(value)) {
|
|
||||||
const [datePart, timePartWithMeridian] = value.split(/[\s\u202F]+/);
|
const [datePart, timePartWithMeridian] = value.split(/[\s\u202F]+/);
|
||||||
return _getDateFromAMPM(datePart, timePartWithMeridian);
|
return _getDateFromAMPM(datePart, timePartWithMeridian);
|
||||||
} else if (militaryDateRegex.test(value)) {
|
} 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 = (
|
export const verifyDateStringOrUndefined = (
|
||||||
value: string
|
value: string
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export const courseMarkdownLoader = {
|
|||||||
|
|
||||||
const settingsString = await fs.readFile(settingsPath, "utf-8");
|
const settingsString = await fs.readFile(settingsPath, "utf-8");
|
||||||
const settings = localCourseYamlUtils.parseSettingYaml(settingsString);
|
const settings = localCourseYamlUtils.parseSettingYaml(settingsString);
|
||||||
|
console.log(settingsString, settings);
|
||||||
|
|
||||||
const folderName = path.basename(courseDirectory);
|
const folderName = path.basename(courseDirectory);
|
||||||
return { ...settings, name: folderName };
|
return { ...settings, name: folderName };
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import {
|
import {
|
||||||
DehydratedState,
|
DehydratedState,
|
||||||
hydrate,
|
hydrate,
|
||||||
HydrationBoundary,
|
|
||||||
QueryClientProvider,
|
QueryClientProvider,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -16,9 +15,9 @@ export const MyQueryClientProvider: FC<{
|
|||||||
}> = ({ children, dehydratedState }) => {
|
}> = ({ children, dehydratedState }) => {
|
||||||
const [queryClient] = useState(createQueryClient());
|
const [queryClient] = useState(createQueryClient());
|
||||||
|
|
||||||
|
hydrate(queryClient, dehydratedState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user