moving v2 to top level

This commit is contained in:
2024-12-17 09:19:21 -07:00
parent 5f0b3554dc
commit 576ee02afb
468 changed files with 79 additions and 15430 deletions

View File

@@ -0,0 +1,25 @@
"use client"
import AssignmentGroupManagement from "./AssignmentGroupManagement";
import DaysOfWeekSettings from "./DaysOfWeekSettings";
import DefaultDueTime from "./DefaultDueTime";
import DefaultFileUploadTypes from "./DefaultFileUploadTypes";
import HolidayConfig from "./HolidayConfig";
import SettingsHeader from "./SettingsHeader";
import StartAndEndDate from "./StartAndEndDate";
import SubmissionDefaults from "./SubmissionDefaults";
export default function AllSettings() {
return (
<>
<SettingsHeader />
<DaysOfWeekSettings />
<StartAndEndDate />
<SubmissionDefaults />
<DefaultFileUploadTypes />
<DefaultDueTime />
<AssignmentGroupManagement />
<HolidayConfig />
</>
);
}

View File

@@ -0,0 +1,167 @@
"use client";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import { LocalAssignmentGroup } from "@/models/local/assignment/localAssignmentGroup";
import { useEffect, useState } from "react";
import TextInput from "../../../../components/form/TextInput";
import { useSetAssignmentGroupsMutation } from "@/hooks/canvas/canvasCourseHooks";
import { settingsBox } from "./sharedSettings";
import { Spinner } from "@/components/Spinner";
export default function AssignmentGroupManagement() {
const [settings, { isPending }] = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
const applyInCanvas = useSetAssignmentGroupsMutation(settings.canvasId);
const [assignmentGroups, setAssignmentGroups] = useState<
LocalAssignmentGroup[]
>(settings.assignmentGroups);
useEffect(() => {
const delay = 1000;
const handler = setTimeout(() => {
if (
!areAssignmentGroupsEqual(assignmentGroups, settings.assignmentGroups)
) {
console.log(
"updating",
assignmentGroups,
updateSettings.isPending,
isPending
);
updateSettings.mutate({
settings: {
...settings,
assignmentGroups,
},
});
}
}, delay);
return () => {
clearTimeout(handler);
};
}, [assignmentGroups, isPending, settings, updateSettings]);
return (
<div className={settingsBox}>
{assignmentGroups.map((group) => (
<div key={group.id} className="flex flex-row gap-3">
<TextInput
value={group.name}
setValue={(newValue) =>
setAssignmentGroups((oldGroups) =>
oldGroups.map((g) =>
g.id === group.id ? { ...g, name: newValue } : g
)
)
}
label={"Group Name"}
/>
<TextInput
value={group.weight.toString()}
setValue={(newValue) =>
setAssignmentGroups((oldGroups) =>
oldGroups.map((g) =>
g.id === group.id
? { ...g, weight: parseInt(newValue || "0") }
: g
)
)
}
label={"Weight"}
/>
</div>
))}
<div className="flex gap-3 mt-3">
<button
className="btn-danger"
onClick={() => {
setAssignmentGroups((oldGroups) => oldGroups.slice(0, -1));
}}
>
Remove Assignment Group
</button>
<button
onClick={() => {
setAssignmentGroups((oldGroups) => [
...oldGroups,
{
id: Date.now().toString(),
name: "",
weight: 0,
},
]);
}}
>
Add Assignment Group
</button>
</div>
<br />
<div className="flex justify-end">
<button
onClick={async () => {
const newSettings = await applyInCanvas.mutateAsync(settings);
// prevent debounce from resetting
if (newSettings) setAssignmentGroups(newSettings.assignmentGroups);
}}
disabled={applyInCanvas.isPending}
>
Update Assignment Groups In Canvas
</button>
</div>
{applyInCanvas.isPending && <Spinner />}
{applyInCanvas.isSuccess && (
<div>
{
"You will need to go to your course assignments page > settings > Assignment Group Weights"
}
<br />
{"and check the 'Weight final grade based on assignment groups' box"}
</div>
)}
</div>
);
}
function areAssignmentGroupsEqual(
list1: LocalAssignmentGroup[],
list2: LocalAssignmentGroup[]
): boolean {
// Check if lists have the same length
if (list1.length !== list2.length) return false;
// Sort both lists by the unique 'id' or 'canvasId' as a fallback
const sortedList1 = [...list1].sort((a, b) => {
if (a.id !== b.id) return a.id > b.id ? 1 : -1;
if (a.canvasId !== b.canvasId) return (a.canvasId || 0) - (b.canvasId || 0);
return 0;
});
const sortedList2 = [...list2].sort((a, b) => {
if (a.id !== b.id) return a.id > b.id ? 1 : -1;
if (a.canvasId !== b.canvasId) return (a.canvasId || 0) - (b.canvasId || 0);
return 0;
});
// Deep compare each object in the sorted lists
for (let i = 0; i < sortedList1.length; i++) {
const group1 = sortedList1[i];
const group2 = sortedList2[i];
if (
group1.id !== group2.id ||
group1.name !== group2.name ||
group1.weight !== group2.weight ||
group1.canvasId !== group2.canvasId
) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,34 @@
"use client";
import { DayOfWeekInput } from "@/components/form/DayOfWeekInput";
import { Spinner } from "@/components/Spinner";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import React from "react";
export default function DaysOfWeekSettings() {
const [settings] = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
return (
<>
<DayOfWeekInput
selectedDays={settings.daysOfWeek}
updateSettings={(day) => {
const hasDay = settings.daysOfWeek.includes(day);
updateSettings.mutate({
settings: {
...settings,
daysOfWeek: hasDay
? settings.daysOfWeek.filter((d) => d !== day)
: [day, ...settings.daysOfWeek],
},
});
}}
/>
{updateSettings.isPending && <Spinner />}
</>
);
}

View File

@@ -0,0 +1,71 @@
"use client";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import { TimePicker } from "../../../../components/TimePicker";
import { useState } from "react";
import DefaultLockOffset from "./DefaultLockOffset";
import { settingsBox } from "./sharedSettings";
export default function DefaultDueTime() {
const [settings] = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
const [haveLockOffset, setHaveLockOffset] = useState(
typeof settings.defaultLockHoursOffset !== "undefined"
);
return (
<div className={settingsBox}>
<div className="text-center">Default Assignment Due Time</div>
<hr className="m-1 p-0" />
<TimePicker
time={settings.defaultDueTime}
setChosenTime={(simpleTime) => {
console.log(simpleTime);
updateSettings.mutate({
settings: {
...settings,
defaultDueTime: simpleTime,
},
});
}}
/>
<hr />
{!haveLockOffset && (
<button
onClick={async () => {
await updateSettings.mutateAsync({
settings: {
...settings,
defaultLockHoursOffset: 0,
},
});
setHaveLockOffset(true);
}}
>
have a default Lock Offset?
</button>
)}
{haveLockOffset && <DefaultLockOffset />}
<br />
{haveLockOffset && (
<button
className="btn-danger"
onClick={async () => {
await updateSettings.mutateAsync({
settings: {
...settings,
defaultLockHoursOffset: undefined,
},
});
setHaveLockOffset(false);
}}
>
remove default Lock Offset?
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,68 @@
"use client";
import TextInput from "@/components/form/TextInput";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import { useState, useEffect } from "react";
import { settingsBox } from "./sharedSettings";
export default function DefaultFileUploadTypes() {
const [settings] = useLocalCourseSettingsQuery();
const [defaultFileUploadTypes, setDefaultFileUploadTypes] = useState<
string[]
>(settings.defaultFileUploadTypes);
const updateSettings = useUpdateLocalCourseSettingsMutation();
useEffect(() => {
const id = setTimeout(() => {
if (
JSON.stringify(settings.defaultFileUploadTypes) !==
JSON.stringify(defaultFileUploadTypes)
) {
updateSettings.mutate({
settings: {
...settings,
defaultFileUploadTypes: defaultFileUploadTypes,
},
});
}
}, 500);
return () => clearTimeout(id);
}, [defaultFileUploadTypes, settings, updateSettings]);
return (
<div className={settingsBox}>
<div className="text-center">Default File Upload Types</div>
{defaultFileUploadTypes.map((type, index) => (
<div key={index} className="flex flex-row gap-3">
<TextInput
value={type}
setValue={(newValue) =>
setDefaultFileUploadTypes((oldTypes) =>
oldTypes.map((t, i) => (i === index ? newValue : t))
)
}
label={"Default Type " + index}
/>
</div>
))}
<div className="flex gap-3 mt-3">
<button
className="btn-danger"
onClick={() => {
setDefaultFileUploadTypes((old) => old.slice(0, -1));
}}
>
Remove Default File Upload Type
</button>
<button
onClick={() => setDefaultFileUploadTypes((old) => [...old, ""])}
>
Add Default File Upload Type
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import TextInput from "@/components/form/TextInput";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import { useEffect, useState } from "react";
export default function DefaultLockOffset() {
const [settings] = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
const [hoursOffset, setHoursOffset] = useState(
settings.defaultLockHoursOffset?.toString() ?? "0"
);
useEffect(() => {
const id = setTimeout(() => {
try {
const hoursNumber = parseInt(hoursOffset);
if (
!Number.isNaN(hoursNumber) &&
hoursNumber !== settings.defaultLockHoursOffset
) {
updateSettings.mutate({
settings: {
...settings,
defaultLockHoursOffset: hoursNumber,
},
});
}
} catch {}
}, 500);
return () => clearTimeout(id);
}, [hoursOffset, settings, settings.defaultLockHoursOffset, updateSettings]);
return (
<div>
<div className="text-center">Default Assignment Due Time</div>
<hr className="m-1 p-0" />
<TextInput
value={hoursOffset}
setValue={(n) => setHoursOffset(n)}
label={"Hours Offset"}
/>
</div>
);
}

View File

@@ -0,0 +1,160 @@
"use client";
import TextInput from "@/components/form/TextInput";
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import { getDateFromString } from "@/models/local/utils/timeUtils";
import { useEffect, useState } from "react";
import {
holidaysToString,
parseHolidays,
} from "../../../../models/local/utils/settingsUtils";
import { settingsBox } from "./sharedSettings";
const exampleString = `springBreak:
- 10/12/2024
- 10/13/2024
- 10/14/2024
laborDay:
- 9/1/2024`;
export const holidaysAreEqual = (
holidays1: {
name: string;
days: string[];
}[],
holidays2: {
name: string;
days: string[];
}[]
): boolean => {
if (holidays1.length !== holidays2.length) return false;
const sortedObj1 = [...holidays1].sort((a, b) => a.name.localeCompare(b.name));
const sortedObj2 = [...holidays2].sort((a, b) => a.name.localeCompare(b.name));
for (let i = 0; i < sortedObj1.length; i++) {
const holiday1 = sortedObj1[i];
const holiday2 = sortedObj2[i];
if (holiday1.name !== holiday2.name) return false;
const sortedDays1 = [...holiday1.days].sort();
const sortedDays2 = [...holiday2.days].sort();
if (sortedDays1.length !== sortedDays2.length) return false;
for (let j = 0; j < sortedDays1.length; j++) {
if (sortedDays1[j] !== sortedDays2[j]) return false;
}
}
return true;
};
export default function HolidayConfig() {
return (
<SuspenseAndErrorHandling>
<InnerHolidayConfig />
</SuspenseAndErrorHandling>
);
}
function InnerHolidayConfig() {
const [settings] = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
const [rawText, setRawText] = useState(holidaysToString(settings.holidays));
useEffect(() => {
const id = setTimeout(() => {
try {
const parsed = parseHolidays(rawText);
if (!holidaysAreEqual(settings.holidays, parsed)) {
console.log("different holiday configs", settings.holidays, parsed);
updateSettings.mutate({
settings: {
...settings,
holidays: parsed,
},
});
}
} catch (error: any) {}
}, 500);
return () => clearTimeout(id);
}, [rawText, settings.holidays, settings, updateSettings]);
return (
<div className={settingsBox}>
<div className="flex flex-row gap-3">
<TextInput
value={rawText}
setValue={setRawText}
label={"Holiday Days"}
isTextArea={true}
/>
<div>
Format your holidays like so:
<pre>
<code>{exampleString}</code>
</pre>
</div>
</div>
<div>
<SuspenseAndErrorHandling>
<ParsedHolidaysDisplay value={rawText} />
</SuspenseAndErrorHandling>
</div>
</div>
);
}
function ParsedHolidaysDisplay({ value }: { value: string }) {
const [parsedHolidays, setParsedHolidays] = useState<
{
name: string;
days: string[];
}[]
>([]);
const [error, setError] = useState("");
useEffect(() => {
try {
const parsed = parseHolidays(value);
setParsedHolidays(parsed);
setError("");
} catch (error: any) {
setError(error + "");
}
}, [value]);
return (
<div>
<div className="text-rose-500">{error}</div>
{parsedHolidays.map((holiday) => (
<div key={holiday.name}>
<div>{holiday.name}</div>
<div>
{holiday.days.map((day) => {
const date = getDateFromString(day);
return (
<div key={day}>
{date?.toLocaleDateString("en-us", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})}
</div>
);
})}
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getCourseUrl } from "@/services/urlUtils";
import Link from "next/link";
import React from "react";
import { useCourseContext } from "../context/courseContext";
export default function SettingsHeader() {
const { courseName } = useCourseContext();
const [settings] = useLocalCourseSettingsQuery();
return (
<>
<div className="flex flex-row justify-between">
<div className="my-auto">
<Link className="btn" href={getCourseUrl(courseName)}>
Back To Course
</Link>
</div>
<h3 className="text-center mb-3">
{settings.name}{" "}
<span className="text-slate-500 text-xl"> settings</span>
</h3>
<div></div>
</div>
<hr />
</>
);
}

View File

@@ -0,0 +1,17 @@
"use client";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { getDateOnlyMarkdownString } from "@/models/local/utils/timeUtils";
import React from "react";
import { settingsBox } from "./sharedSettings";
export default function StartAndEndDate() {
const [settings] = useLocalCourseSettingsQuery();
const startDate = new Date(settings.startDate);
const endDate = new Date(settings.endDate);
return (
<div className={settingsBox}>
<div>Semester Start: {getDateOnlyMarkdownString(startDate)}</div>
<div>Semester End: {getDateOnlyMarkdownString(endDate)}</div>
</div>
);
}

View File

@@ -0,0 +1,77 @@
"use client";
import SelectInput from "@/components/form/SelectInput";
import {
useLocalCourseSettingsQuery,
useUpdateLocalCourseSettingsMutation,
} from "@/hooks/localCourse/localCoursesHooks";
import {
AssignmentSubmissionType,
AssignmentSubmissionTypeList,
} from "@/models/local/assignment/assignmentSubmissionType";
import React, { useEffect, useState } from "react";
import { settingsBox } from "./sharedSettings";
export default function SubmissionDefaults() {
const [settings] = useLocalCourseSettingsQuery();
const [defaultSubmissionTypes, setDefaultSubmissionTypes] = useState<
AssignmentSubmissionType[]
>(settings.defaultAssignmentSubmissionTypes);
const updateSettings = useUpdateLocalCourseSettingsMutation();
useEffect(() => {
if (
JSON.stringify(settings.defaultAssignmentSubmissionTypes) !==
JSON.stringify(defaultSubmissionTypes)
) {
updateSettings.mutate({
settings: {
...settings,
defaultAssignmentSubmissionTypes: defaultSubmissionTypes,
},
});
}
}, [defaultSubmissionTypes, settings, updateSettings]);
return (
<div className={settingsBox}>
<div className="text-center">Default Assignment Submission Type</div>
{defaultSubmissionTypes.map((type, index) => (
<div key={index} className="flex flex-row gap-3">
<SelectInput
value={type}
setValue={(newType) => {
if (newType)
setDefaultSubmissionTypes((oldTypes) =>
oldTypes.map((t, i) => (i === index ? newType : t))
);
}}
label={""}
options={AssignmentSubmissionTypeList}
getOptionName={(t) => t}
/>
</div>
))}
<div className="flex gap-3 mt-3">
<button
className="btn-danger"
onClick={() => {
setDefaultSubmissionTypes((old) => old.slice(0, -1));
}}
>
Remove Default Type
</button>
<button
onClick={() =>
setDefaultSubmissionTypes((old) => [
...old,
AssignmentSubmissionType.NONE,
])
}
>
Add Default Type
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import AllSettings from "./AllSettings";
export default function page() {
return (
<div className="flex justify-center h-full overflow-auto pt-5 ">
<div className=" w-fit ">
<AllSettings />
<br />
<br />
<br />
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export const settingsBox = "border w-full p-3 m-3 rounded-md border-slate-500"