From ff520233d65ac7ab007c2f377e778cd5a879fc19 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Tue, 10 Sep 2024 14:20:46 -0600 Subject: [PATCH] updating settings --- .../courses/[courseName]/settings/route.ts | 11 +++ .../[courseName]/settings/DefaultDueTime.tsx | 28 ++++++ .../[courseName]/settings/SettingsHeader.tsx | 8 ++ .../[courseName]/settings/StartAndEndDate.tsx | 16 +++ .../app/course/[courseName]/settings/page.tsx | 14 ++- nextjs/src/app/globals.css | 8 +- nextjs/src/components/TimePicker.tsx | 99 +++++++++++++++++++ .../hooks/localCourse/localCoursesHooks.ts | 50 +++++++--- nextjs/src/models/local/localCourse.ts | 14 ++- .../fileStorage/fileStorageService.ts | 15 +++ 10 files changed, 238 insertions(+), 25 deletions(-) create mode 100644 nextjs/src/app/course/[courseName]/settings/DefaultDueTime.tsx create mode 100644 nextjs/src/app/course/[courseName]/settings/SettingsHeader.tsx create mode 100644 nextjs/src/app/course/[courseName]/settings/StartAndEndDate.tsx create mode 100644 nextjs/src/components/TimePicker.tsx diff --git a/nextjs/src/app/api/courses/[courseName]/settings/route.ts b/nextjs/src/app/api/courses/[courseName]/settings/route.ts index 82a3fcf..a153ddb 100644 --- a/nextjs/src/app/api/courses/[courseName]/settings/route.ts +++ b/nextjs/src/app/api/courses/[courseName]/settings/route.ts @@ -12,3 +12,14 @@ export const GET = async ( const settings = await fileStorageService.getCourseSettings(courseName); return Response.json(settings); }); +export const PUT = async ( + request: Request, + { params: { courseName } }: { params: { courseName: string } } +) => + await withErrorHandling(async () => { + const settings = await request.json(); + + await fileStorageService.updateCourseSettings(courseName, settings); + + return Response.json({}); + }); diff --git a/nextjs/src/app/course/[courseName]/settings/DefaultDueTime.tsx b/nextjs/src/app/course/[courseName]/settings/DefaultDueTime.tsx new file mode 100644 index 0000000..8123cfc --- /dev/null +++ b/nextjs/src/app/course/[courseName]/settings/DefaultDueTime.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { + useLocalCourseSettingsQuery, + useUpdateLocalCourseSettingsMutation, +} from "@/hooks/localCourse/localCoursesHooks"; +import { TimePicker } from "../../../../components/TimePicker"; + +export default function DefaultDueTime() { + const { data: settings } = useLocalCourseSettingsQuery(); + const updateSettings = useUpdateLocalCourseSettingsMutation(); + return ( +
+
Default Assignment Due Time
+
+ { + console.log(simpleTime); + updateSettings.mutate({ + ...settings, + defaultDueTime: simpleTime, + }); + }} + /> +
+ ); +} diff --git a/nextjs/src/app/course/[courseName]/settings/SettingsHeader.tsx b/nextjs/src/app/course/[courseName]/settings/SettingsHeader.tsx new file mode 100644 index 0000000..a3304aa --- /dev/null +++ b/nextjs/src/app/course/[courseName]/settings/SettingsHeader.tsx @@ -0,0 +1,8 @@ +"use client" +import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; +import React from "react"; + +export default function SettingsHeader() { + const { data: settings } = useLocalCourseSettingsQuery(); + return
Settings for {settings.name}
; +} diff --git a/nextjs/src/app/course/[courseName]/settings/StartAndEndDate.tsx b/nextjs/src/app/course/[courseName]/settings/StartAndEndDate.tsx new file mode 100644 index 0000000..d97a75c --- /dev/null +++ b/nextjs/src/app/course/[courseName]/settings/StartAndEndDate.tsx @@ -0,0 +1,16 @@ +"use client"; +import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; +import { getDateOnlyMarkdownString } from "@/models/local/timeUtils"; +import React from "react"; + +export default function StartAndEndDate() { + const { data: settings } = useLocalCourseSettingsQuery(); + const startDate = new Date(settings.startDate); + const endDate = new Date(settings.endDate); + return ( +
+
Start: {getDateOnlyMarkdownString(startDate)}
+
End: {getDateOnlyMarkdownString(endDate)}
+
+ ); +} diff --git a/nextjs/src/app/course/[courseName]/settings/page.tsx b/nextjs/src/app/course/[courseName]/settings/page.tsx index cb2be10..db73499 100644 --- a/nextjs/src/app/course/[courseName]/settings/page.tsx +++ b/nextjs/src/app/course/[courseName]/settings/page.tsx @@ -1,7 +1,15 @@ -import React from 'react' +import React from "react"; +import { useCourseContext } from "../context/courseContext"; +import StartAndEndDate from "./StartAndEndDate"; +import SettingsHeader from "./SettingsHeader"; +import DefaultDueTime from "./DefaultDueTime"; export default function page() { return ( -
page
- ) +
+ + + +
+ ); } diff --git a/nextjs/src/app/globals.css b/nextjs/src/app/globals.css index 2d0c127..ee74d87 100644 --- a/nextjs/src/app/globals.css +++ b/nextjs/src/app/globals.css @@ -59,7 +59,7 @@ ol { padding-left: 1.5rem; } hr { - @apply border-t border-gray-200 my-4; + @apply border-t border-gray-500 my-4; } blockquote { @@ -76,3 +76,9 @@ p { button { @apply bg-blue-900 hover:bg-blue-700 text-blue-50 font-bold py-1 px-3 rounded transition-all duration-200; } + +select { + @apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm sm:text-sm; + @apply focus:outline-none focus:ring-blue-500 focus:border-blue-500 ; + @apply bg-slate-800; +} diff --git a/nextjs/src/components/TimePicker.tsx b/nextjs/src/components/TimePicker.tsx new file mode 100644 index 0000000..be164f4 --- /dev/null +++ b/nextjs/src/components/TimePicker.tsx @@ -0,0 +1,99 @@ +"use client"; +import { SimpleTimeOnly } from "@/models/local/localCourse"; +import { FC, useState, useEffect } from "react"; + +export const TimePicker: FC<{ + setChosenTime: (simpleTime: SimpleTimeOnly) => void; + time: SimpleTimeOnly; +}> = ({ setChosenTime, time }) => { + const adjustedHour = time.hour % 12 === 0 ? 12 : time.hour % 12; + const partOfDay = time.hour < 12 ? "AM" : "PM"; + + // useEffect(() => { + // const newtime = { + // hour: partOfDay === "PM" ? hour + 12 : hour, + // minute: minute, + // }; + // if ( + // newtime.hour != startingTime.hour || + // newtime.minute != startingTime.minute + // ) { + // setChosenTime(newtime); + // } + // }, [ + // hour, + // minute, + // partOfDay, + // setChosenTime, + // startingTime.hour, + // startingTime.minute, + // ]); + + return ( + <> +
e.preventDefault()} + className="flex flex-row gap-3" + > +
+ +
+
+ +
+
+ +
+
+ + ); +}; diff --git a/nextjs/src/hooks/localCourse/localCoursesHooks.ts b/nextjs/src/hooks/localCourse/localCoursesHooks.ts index fbbbd14..87e7049 100644 --- a/nextjs/src/hooks/localCourse/localCoursesHooks.ts +++ b/nextjs/src/hooks/localCourse/localCoursesHooks.ts @@ -1,7 +1,9 @@ "use client"; import { LocalCourseSettings } from "@/models/local/localCourse"; import { + useMutation, useQueries, + useQueryClient, useSuspenseQueries, useSuspenseQuery, } from "@tanstack/react-query"; @@ -14,9 +16,18 @@ import { useAssignmentNamesQuery, useAssignmentsQueries, } from "./assignmentHooks"; -import { getPageNamesQueryConfig, getPageQueryConfig, usePageNamesQuery, usePagesQueries } from "./pageHooks"; -import { getQuizNamesQueryConfig, getQuizQueryConfig, useQuizNamesQuery, useQuizzesQueries } from "./quizHooks"; -import { useMemo } from "react"; +import { + getPageNamesQueryConfig, + getPageQueryConfig, + usePageNamesQuery, + usePagesQueries, +} from "./pageHooks"; +import { + getQuizNamesQueryConfig, + getQuizQueryConfig, + useQuizNamesQuery, + useQuizzesQueries, +} from "./quizHooks"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; export const useLocalCourseNamesQuery = () => @@ -41,6 +52,22 @@ export const useLocalCourseSettingsQuery = () => { }); }; +export const useUpdateLocalCourseSettingsMutation = () => { + const { courseName } = useCourseContext(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (updatedSettings: LocalCourseSettings) => { + const url = `/api/courses/${courseName}/settings`; + await axiosClient.put(url, updatedSettings); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: localCourseKeys.settings(courseName), + }); + }, + }); +}; + export const useModuleNamesQuery = () => { const { courseName } = useCourseContext(); return useSuspenseQuery({ @@ -116,7 +143,7 @@ export const useAllCourseDataQuery = () => { }), }); - const {data: quizNamesAndModules } = useSuspenseQueries({ + const { data: quizNamesAndModules } = useSuspenseQueries({ queries: moduleNames.map((moduleName) => getQuizNamesQueryConfig(courseName, moduleName) ), @@ -132,9 +159,8 @@ export const useAllCourseDataQuery = () => { }); const { data: quizzesAndModules } = useSuspenseQueries({ - queries: quizNamesAndModules.map( - ({ moduleName, quizName }, i) => - getQuizQueryConfig(courseName, moduleName, quizName) + queries: quizNamesAndModules.map(({ moduleName, quizName }, i) => + getQuizQueryConfig(courseName, moduleName, quizName) ), combine: (results) => ({ data: results.flatMap((r, i) => ({ @@ -145,7 +171,7 @@ export const useAllCourseDataQuery = () => { }), }); - const {data: pageNamesAndModules } = useSuspenseQueries({ + const { data: pageNamesAndModules } = useSuspenseQueries({ queries: moduleNames.map((moduleName) => getPageNamesQueryConfig(courseName, moduleName) ), @@ -161,9 +187,8 @@ export const useAllCourseDataQuery = () => { }); const { data: pagesAndModules } = useSuspenseQueries({ - queries: pageNamesAndModules.map( - ({ moduleName, pageName }, i) => - getPageQueryConfig(courseName, moduleName, pageName) + queries: pageNamesAndModules.map(({ moduleName, pageName }, i) => + getPageQueryConfig(courseName, moduleName, pageName) ), combine: (results) => ({ data: results.flatMap((r, i) => ({ @@ -174,8 +199,7 @@ export const useAllCourseDataQuery = () => { }), }); - - return { assignmentsAndModules, quizzesAndModules, pagesAndModules }; + return { assignmentsAndModules, quizzesAndModules, pagesAndModules }; }; // export const useUpdateCourseMutation = (courseName: string) => { diff --git a/nextjs/src/models/local/localCourse.ts b/nextjs/src/models/local/localCourse.ts index b3e7cc4..6e47b71 100644 --- a/nextjs/src/models/local/localCourse.ts +++ b/nextjs/src/models/local/localCourse.ts @@ -34,26 +34,24 @@ export enum DayOfWeek { export const localCourseYamlUtils = { parseSettingYaml: (settingsString: string): LocalCourseSettings => { const settings = parse(settingsString); - return lowercaseFirstLetter(settings) + return lowercaseFirstLetter(settings); }, - settingsToYaml: (settings: LocalCourseSettings) => { + settingsToYaml: (settings: Omit) => { return stringify(settings); }, }; function lowercaseFirstLetter(obj: T): T { - if (obj === null || typeof obj !== 'object') - return obj as T; + if (obj === null || typeof obj !== "object") return obj as T; - if (Array.isArray(obj)) - return obj.map(lowercaseFirstLetter) as unknown 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') { + if (value && typeof value === "object") { result[newKey] = lowercaseFirstLetter(value); } else { result[newKey] = value; @@ -61,4 +59,4 @@ function lowercaseFirstLetter(obj: T): T { }); return result as T; -} \ No newline at end of file +} diff --git a/nextjs/src/services/fileStorage/fileStorageService.ts b/nextjs/src/services/fileStorage/fileStorageService.ts index 1c4df66..9ad5f7e 100644 --- a/nextjs/src/services/fileStorage/fileStorageService.ts +++ b/nextjs/src/services/fileStorage/fileStorageService.ts @@ -75,6 +75,21 @@ export const fileStorageService = { return { ...settings, name: folderName }; }, + async updateCourseSettings( + courseName: string, + settings: LocalCourseSettings + ) { + const courseDirectory = path.join(basePath, courseName); + const settingsPath = path.join(courseDirectory, "settings.yml"); + + const { name, ...settingsWithoutName } = settings; + const settingsMarkdown = + localCourseYamlUtils.settingsToYaml(settingsWithoutName); + + console.log(`Saving settings ${settingsPath}`); + await fs.writeFile(settingsPath, settingsMarkdown); + }, + async getModuleNames(courseName: string) { const courseDirectory = path.join(basePath, courseName); const moduleDirectories = await fs.readdir(courseDirectory, {