From 704a5ae4043707a83581b4724496cf9fa3d07170 Mon Sep 17 00:00:00 2001 From: Alex Mickelson Date: Tue, 22 Jul 2025 14:23:40 -0600 Subject: [PATCH] can add existing courses --- globalSettings.yml | 10 ++- src/app/CourseList.tsx | 8 +- .../AddExistingCourseToGlobalSettings.tsx | 84 +++++++++++++++++-- .../course/[courseName]/CourseNavigation.tsx | 2 +- .../[courseName]/calendar/CalendarMonth.tsx | 5 +- .../context/CalendarItemsContextProvider.tsx | 2 +- .../[moduleName]/page/[pageName]/EditPage.tsx | 2 +- src/app/todaysLectures/OneCourseLectures.tsx | 2 +- src/components/form/StoragePathSelector.tsx | 28 +++---- src/components/form/TextInput.tsx | 3 + src/hooks/localCourse/globalSettingsHooks.ts | 28 +++++++ .../localCourse/storageDirectoryHooks.ts | 7 ++ .../fileStorage/fileStorageService.ts | 7 ++ .../globalSettingsFileStorageService.ts | 10 ++- .../fileStorage/settingsFileStorageService.ts | 7 ++ src/services/serverFunctions/router/app.ts | 2 + .../router/directoriesRouter.ts | 9 ++ .../router/globalSettingsRouter.ts | 23 +++++ 18 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 src/hooks/localCourse/globalSettingsHooks.ts create mode 100644 src/services/serverFunctions/router/globalSettingsRouter.ts diff --git a/globalSettings.yml b/globalSettings.yml index 2344fb2..7832364 100644 --- a/globalSettings.yml +++ b/globalSettings.yml @@ -1 +1,9 @@ -courses: [] +courses: + - path: ./4850_AdvancedFE/2025-fall-alex/modules/ + name: Distributed 2025 + - path: ./1420/2025-fall-alex/modules/ + name: "1420" + - path: ./1810/2025-fall-alex/modules/ + name: Web Intro + - path: ./1430/2025-fall-alex/modules/ + name: UX diff --git a/src/app/CourseList.tsx b/src/app/CourseList.tsx index 2eb6832..5166b28 100644 --- a/src/app/CourseList.tsx +++ b/src/app/CourseList.tsx @@ -1,6 +1,10 @@ "use client"; import { useLocalCoursesSettingsQuery } from "@/hooks/localCourse/localCoursesHooks"; -import { getDateKey, getTermName, groupByStartDate } from "@/models/local/utils/timeUtils"; +import { + getDateKey, + getTermName, + groupByStartDate, +} from "@/models/local/utils/timeUtils"; import { getCourseUrl } from "@/services/urlUtils"; import Link from "next/link"; @@ -38,4 +42,4 @@ export default function CourseList() { ))} ); -} \ No newline at end of file +} diff --git a/src/app/addCourse/AddExistingCourseToGlobalSettings.tsx b/src/app/addCourse/AddExistingCourseToGlobalSettings.tsx index 29a9774..f0cb99e 100644 --- a/src/app/addCourse/AddExistingCourseToGlobalSettings.tsx +++ b/src/app/addCourse/AddExistingCourseToGlobalSettings.tsx @@ -1,8 +1,14 @@ "use client"; import ClientOnly from "@/components/ClientOnly"; import { StoragePathSelector } from "@/components/form/StoragePathSelector"; +import TextInput from "@/components/form/TextInput"; import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling"; -import { FC, useState } from "react"; +import { + useGlobalSettingsQuery, + useUpdateGlobalSettingsMutation, +} from "@/hooks/localCourse/globalSettingsHooks"; +import { useDirectoryIsCourseQuery } from "@/hooks/localCourse/storageDirectoryHooks"; +import { FC, useEffect, useRef, useState } from "react"; export const AddExistingCourseToGlobalSettings = () => { const [showForm, setShowForm] = useState(false); @@ -27,14 +33,82 @@ export const AddExistingCourseToGlobalSettings = () => { const ExistingCourseForm: FC<{}> = () => { const [path, setPath] = useState("./"); + const [name, setName] = useState(""); + const nameInputRef = useRef(null); + const directoryIsCourseQuery = useDirectoryIsCourseQuery(path); + const { data: globalSettings } = useGlobalSettingsQuery(); + const updateSettingsMutation = useUpdateGlobalSettingsMutation(); + + // Focus name input when directory becomes a valid course + useEffect(() => { + console.log("Checking directory:", directoryIsCourseQuery.data); + if (directoryIsCourseQuery.data) { + console.log("Focusing name input"); + nameInputRef.current?.focus(); + } + }, [directoryIsCourseQuery.data]); + return ( -
+
{ + e.preventDefault(); + console.log(path); + + await updateSettingsMutation.mutateAsync({ + globalSettings: { + ...globalSettings, + courses: [ + ...globalSettings.courses, + { + name, + path, + }, + ], + }, + }); + setName(""); + setPath("./"); + }} + className="min-w-3xl" + >

Add Existing Course

+ +
+ {directoryIsCourseQuery.isLoading ? ( + <> + + Checking directory... + + ) : directoryIsCourseQuery.data ? ( + <> + + This is a valid course directory. + + ) : ( + <> + + Not a course directory. + + )} +
-
+ {directoryIsCourseQuery.data && ( + <> + +
+ +
+ + )} + ); }; diff --git a/src/app/course/[courseName]/CourseNavigation.tsx b/src/app/course/[courseName]/CourseNavigation.tsx index 97d50bb..25705cb 100644 --- a/src/app/course/[courseName]/CourseNavigation.tsx +++ b/src/app/course/[courseName]/CourseNavigation.tsx @@ -96,4 +96,4 @@ function getSemesterName(startDate: string) { } else { return `Fall ${year}`; } -} \ No newline at end of file +} diff --git a/src/app/course/[courseName]/calendar/CalendarMonth.tsx b/src/app/course/[courseName]/calendar/CalendarMonth.tsx index 3c19f6e..13df28a 100644 --- a/src/app/course/[courseName]/calendar/CalendarMonth.tsx +++ b/src/app/course/[courseName]/calendar/CalendarMonth.tsx @@ -18,7 +18,7 @@ export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => { ); const isPastSemester = Date.now() > new Date(settings.endDate).getTime(); - + const pastWeekNumber = getWeekNumber( startDate, new Date(Date.now() - four_days_in_milliseconds) @@ -29,7 +29,8 @@ export const CalendarMonth = ({ month }: { month: CalendarMonthModel }) => { new Date(month.year, month.month, 1) ); - const shouldCollapse = (pastWeekNumber >= startOfMonthWeekNumber) && !isPastSemester; + const shouldCollapse = + pastWeekNumber >= startOfMonthWeekNumber && !isPastSemester; const monthName = new Date(month.year, month.month - 1, 1).toLocaleString( "default", diff --git a/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx b/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx index 6af6b7b..2d21415 100644 --- a/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx +++ b/src/app/course/[courseName]/context/CalendarItemsContextProvider.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { ReactNode } from "react"; import { CalendarItemsContext, diff --git a/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx b/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx index 9bb6b80..78e144d 100644 --- a/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx +++ b/src/app/course/[courseName]/modules/[moduleName]/page/[pageName]/EditPage.tsx @@ -125,5 +125,5 @@ export default function EditPage({ } /> - ) + ); } diff --git a/src/app/todaysLectures/OneCourseLectures.tsx b/src/app/todaysLectures/OneCourseLectures.tsx index 4f0fdbb..518aeb0 100644 --- a/src/app/todaysLectures/OneCourseLectures.tsx +++ b/src/app/todaysLectures/OneCourseLectures.tsx @@ -9,7 +9,7 @@ import { getDateOnlyMarkdownString } from "@/models/local/utils/timeUtils"; export default function OneCourseLectures() { const { courseName } = useCourseContext(); - const {data: weeks} = useLecturesQuery(); + const { data: weeks } = useLecturesQuery(); const dayAsDate = new Date(); const dayAsString = getDateOnlyMarkdownString(dayAsDate); diff --git a/src/components/form/StoragePathSelector.tsx b/src/components/form/StoragePathSelector.tsx index 5fa6a11..2d0f504 100644 --- a/src/components/form/StoragePathSelector.tsx +++ b/src/components/form/StoragePathSelector.tsx @@ -3,26 +3,28 @@ import { useState, useRef, useEffect } from "react"; import { createPortal } from "react-dom"; export function StoragePathSelector({ - startingValue, - submitValue, + value, + setValue, label, className, }: { - startingValue: string; - submitValue: (newValue: string) => void; + value: string; + setValue: (newValue: string) => void; label: string; className?: string; }) { - const [path, setPath] = useState(startingValue); - const [lastCorrectPath, setLastCorrectPath] = useState(startingValue); - const { data: directoryContents } = - useDirectoryContentsQuery(lastCorrectPath); + const [path, setPath] = useState(value); + const { data: directoryContents } = useDirectoryContentsQuery(value); const [isFocused, setIsFocused] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const [arrowUsed, setArrowUsed] = useState(false); const inputRef = useRef(null); const dropdownRef = useRef(null); + useEffect(() => { + setPath(value); + }, [value]); + const handleKeyDown = (e: React.KeyboardEvent) => { if (!isFocused || filteredFolders.length === 0) return; if (e.key === "ArrowDown") { @@ -88,13 +90,12 @@ export function StoragePathSelector({ newPath += "/"; } setPath(newPath); - setLastCorrectPath(newPath); + setValue(newPath); setArrowUsed(false); setHighlightedIndex(-1); if (shouldFocus) { setTimeout(() => inputRef.current?.focus(), 0); } - submitValue(newPath); }; // Scroll highlighted option into view when it changes @@ -116,12 +117,12 @@ export function StoragePathSelector({
{ setPath(e.target.value); if (e.target.value.endsWith("/")) { - setLastCorrectPath(e.target.value); + setValue(e.target.value); setTimeout(() => inputRef.current?.focus(), 0); } }} @@ -130,9 +131,6 @@ export function StoragePathSelector({ onKeyDown={handleKeyDown} autoComplete="off" /> -
- Last valid path: {lastCorrectPath} -
{isFocused && createPortal(
diff --git a/src/components/form/TextInput.tsx b/src/components/form/TextInput.tsx index d961279..39c5683 100644 --- a/src/components/form/TextInput.tsx +++ b/src/components/form/TextInput.tsx @@ -6,12 +6,14 @@ export default function TextInput({ label, className, isTextArea = false, + inputRef = undefined, }: { value: string; setValue: (newValue: string) => void; label: string; className?: string; isTextArea?: boolean; + inputRef?: React.RefObject; }) { return (