mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
holiday editing works
This commit is contained in:
@@ -1,15 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import TextInput from "@/components/form/TextInput";
|
import TextInput from "@/components/form/TextInput";
|
||||||
|
import { SuspenseAndErrorHandling } from "@/components/SuspenseAndErrorHandling";
|
||||||
import {
|
import {
|
||||||
useLocalCourseSettingsQuery,
|
useLocalCourseSettingsQuery,
|
||||||
useUpdateLocalCourseSettingsMutation,
|
useUpdateLocalCourseSettingsMutation,
|
||||||
} from "@/hooks/localCourse/localCoursesHooks";
|
} from "@/hooks/localCourse/localCoursesHooks";
|
||||||
import {
|
import {
|
||||||
dateToMarkdownString,
|
dateToMarkdownString,
|
||||||
|
getDateFromString,
|
||||||
getDateFromStringOrThrow,
|
getDateFromStringOrThrow,
|
||||||
|
getDateOnlyMarkdownString,
|
||||||
} from "@/models/local/timeUtils";
|
} from "@/models/local/timeUtils";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
holidaysToString,
|
||||||
|
parseHolidays,
|
||||||
|
} from "../../../../models/local/settingsUtils";
|
||||||
|
|
||||||
const exampleString = `springBreak:
|
const exampleString = `springBreak:
|
||||||
- 10/12/2024
|
- 10/12/2024
|
||||||
@@ -18,68 +25,126 @@ const exampleString = `springBreak:
|
|||||||
laborDay:
|
laborDay:
|
||||||
- 9/1/2024`;
|
- 9/1/2024`;
|
||||||
|
|
||||||
|
export const holidaysAreEqual = (
|
||||||
|
obj1: { [key: string]: string[] },
|
||||||
|
obj2: { [key: string]: string[] }
|
||||||
|
): boolean => {
|
||||||
|
const keys1 = Object.keys(obj1);
|
||||||
|
const keys2 = Object.keys(obj2);
|
||||||
|
|
||||||
|
if (keys1.length !== keys2.length) return false;
|
||||||
|
|
||||||
|
for (const key of keys1) {
|
||||||
|
if (!obj2.hasOwnProperty(key)) return false;
|
||||||
|
|
||||||
|
const arr1 = obj1[key];
|
||||||
|
const arr2 = obj2[key];
|
||||||
|
|
||||||
|
if (arr1.length !== arr2.length) return false;
|
||||||
|
|
||||||
|
const sortedArr1 = [...arr1].sort();
|
||||||
|
const sortedArr2 = [...arr2].sort();
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedArr1.length; i++) {
|
||||||
|
if (sortedArr1[i] !== sortedArr2[i]) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export default function HolidayConfig() {
|
export default function HolidayConfig() {
|
||||||
|
return (
|
||||||
|
<SuspenseAndErrorHandling>
|
||||||
|
<InnerHolidayConfig />
|
||||||
|
</SuspenseAndErrorHandling>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function InnerHolidayConfig() {
|
||||||
const { data: settings } = useLocalCourseSettingsQuery();
|
const { data: settings } = useLocalCourseSettingsQuery();
|
||||||
const updateSettings = useUpdateLocalCourseSettingsMutation();
|
const updateSettings = useUpdateLocalCourseSettingsMutation();
|
||||||
|
|
||||||
const [rawText, setRawText] = useState("");
|
const [rawText, setRawText] = useState(holidaysToString(settings.holidays));
|
||||||
|
|
||||||
const parsedText = parseHolidays(rawText);
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const parsed = parseHolidays(rawText);
|
||||||
|
|
||||||
|
if (!holidaysAreEqual(settings.holidays, parsed))
|
||||||
|
updateSettings.mutate({
|
||||||
|
...settings,
|
||||||
|
holidays: parsed,
|
||||||
|
});
|
||||||
|
} catch (error: any) {}
|
||||||
|
}, 500);
|
||||||
|
return () => clearTimeout(id);
|
||||||
|
}, [rawText, settings, updateSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-3 border w-fit p-3 m-3 rounded-md">
|
<div className=" border w-fit p-3 m-3 rounded-md">
|
||||||
<TextInput
|
<div className="flex flex-row gap-3">
|
||||||
value={rawText}
|
<TextInput
|
||||||
setValue={setRawText}
|
value={rawText}
|
||||||
label={"Holiday Days"}
|
setValue={setRawText}
|
||||||
isTextArea={true}
|
label={"Holiday Days"}
|
||||||
/>
|
isTextArea={true}
|
||||||
<div>
|
/>
|
||||||
Format your holidays like so:
|
<div>
|
||||||
<pre>
|
Format your holidays like so:
|
||||||
<code>{exampleString}</code>
|
<pre>
|
||||||
</pre>
|
<code>{exampleString}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{Object.keys(parsedText).map((k) => (
|
<SuspenseAndErrorHandling>
|
||||||
<div key={k}>
|
<ParsedHolidaysDisplay value={rawText} />
|
||||||
<div>{k}</div>
|
</SuspenseAndErrorHandling>
|
||||||
<div>
|
|
||||||
{parsedText[k].map((day) => {
|
|
||||||
const parsedDate = getDateFromStringOrThrow(
|
|
||||||
day,
|
|
||||||
"holiday preview display"
|
|
||||||
);
|
|
||||||
return <div key={day}>{dateToMarkdownString(parsedDate)}</div>;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseHolidays = (
|
function ParsedHolidaysDisplay({ value }: { value: string }) {
|
||||||
inputText: string
|
const [parsedHolidays, setParsedHolidays] = useState<{
|
||||||
): { [holidayName: string]: string[] } => {
|
[holidayName: string]: string[];
|
||||||
const holidays: { [holidayName: string]: string[] } = {};
|
}>({});
|
||||||
|
const [error, setError] = useState("");
|
||||||
const lines = inputText.split("\n").filter(line => line.trim() !== "");
|
|
||||||
let currentHoliday: string | null = null;
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
useEffect(() => {
|
||||||
if (line.includes(":")) {
|
try {
|
||||||
// It's a holiday name
|
const parsed = parseHolidays(value);
|
||||||
const holidayName = line.split(":")[0].trim();
|
setParsedHolidays(parsed);
|
||||||
currentHoliday = holidayName;
|
setError("");
|
||||||
holidays[currentHoliday] = [];
|
} catch (error: any) {
|
||||||
} else if (currentHoliday && line.startsWith("-")) {
|
setError(error + "");
|
||||||
// It's a date under the current holiday
|
}
|
||||||
const date = line.replace("-", "").trim();
|
}, [value]);
|
||||||
holidays[currentHoliday].push(date);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return holidays;
|
return (
|
||||||
};
|
<div>
|
||||||
|
<div className="text-rose-500">{error}</div>
|
||||||
|
{Object.keys(parsedHolidays).map((k) => (
|
||||||
|
<div key={k}>
|
||||||
|
<div>{k}</div>
|
||||||
|
<div>
|
||||||
|
{parsedHolidays[k].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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useCourseContext } from "../context/courseContext";
|
|
||||||
import StartAndEndDate from "./StartAndEndDate";
|
import StartAndEndDate from "./StartAndEndDate";
|
||||||
import SettingsHeader from "./SettingsHeader";
|
import SettingsHeader from "./SettingsHeader";
|
||||||
import DefaultDueTime from "./DefaultDueTime";
|
import DefaultDueTime from "./DefaultDueTime";
|
||||||
|
|||||||
40
nextjs/src/models/local/settingsUtils.tsx
Normal file
40
nextjs/src/models/local/settingsUtils.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
dateToMarkdownString,
|
||||||
|
getDateFromString,
|
||||||
|
getDateFromStringOrThrow,
|
||||||
|
getDateOnlyMarkdownString,
|
||||||
|
} from "./timeUtils";
|
||||||
|
|
||||||
|
export const parseHolidays = (
|
||||||
|
inputText: string
|
||||||
|
): { [holidayName: string]: string[] } => {
|
||||||
|
const holidays: { [holidayName: string]: string[] } = {};
|
||||||
|
|
||||||
|
const lines = inputText.split("\n").filter((line) => line.trim() !== "");
|
||||||
|
let currentHoliday: string | null = null;
|
||||||
|
|
||||||
|
lines.forEach((line) => {
|
||||||
|
if (line.includes(":")) {
|
||||||
|
const holidayName = line.split(":")[0].trim();
|
||||||
|
currentHoliday = holidayName;
|
||||||
|
holidays[currentHoliday] = [];
|
||||||
|
} else if (currentHoliday && line.startsWith("-")) {
|
||||||
|
const date = line.replace("-", "").trim();
|
||||||
|
const dateObject = getDateFromStringOrThrow(date, "parsing holiday text");
|
||||||
|
holidays[currentHoliday].push(getDateOnlyMarkdownString(dateObject));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return holidays;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const holidaysToString = (holidays: { [holidayName: string]: string[] })=> {
|
||||||
|
const entries = Object.keys(holidays).map(holiday => {
|
||||||
|
const title = holiday + ":\n"
|
||||||
|
const days = holidays[holiday].map(d => `- ${d}\n`)
|
||||||
|
return title + days.join("")
|
||||||
|
})
|
||||||
|
|
||||||
|
return entries.join("")
|
||||||
|
}
|
||||||
29
nextjs/src/models/local/tests/testHolidayParsing.test.ts
Normal file
29
nextjs/src/models/local/tests/testHolidayParsing.test.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { parseHolidays } from "../settingsUtils";
|
||||||
|
|
||||||
|
describe("can parse holiday string", () => {
|
||||||
|
it("can parse empty list", () => {
|
||||||
|
const testString = `
|
||||||
|
springBreak:
|
||||||
|
`;
|
||||||
|
const output = parseHolidays(testString);
|
||||||
|
expect(output).toEqual({ springBreak: [] });
|
||||||
|
});
|
||||||
|
it("can parse list with date", () => {
|
||||||
|
const testString = `
|
||||||
|
springBreak:
|
||||||
|
- 10/12/2024
|
||||||
|
`;
|
||||||
|
const output = parseHolidays(testString);
|
||||||
|
expect(output).toEqual({ springBreak: ["10/12/2024"] });
|
||||||
|
});
|
||||||
|
it("can parse list with two dates", () => {
|
||||||
|
const testString = `
|
||||||
|
springBreak:
|
||||||
|
- 10/12/2024
|
||||||
|
- 10/13/2024
|
||||||
|
`;
|
||||||
|
const output = parseHolidays(testString);
|
||||||
|
expect(output).toEqual({ springBreak: ["10/12/2024", "10/13/2024"] });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -61,4 +61,12 @@ describe("Can properly handle expected date formats", () => {
|
|||||||
expect(updatedString).toBe("08/29/2024 17:00:00")
|
expect(updatedString).toBe("08/29/2024 17:00:00")
|
||||||
|
|
||||||
})
|
})
|
||||||
|
it("can handle date without time", () => {
|
||||||
|
const dateString = "8/29/2024";
|
||||||
|
const dateObject = getDateFromString(dateString);
|
||||||
|
|
||||||
|
expect(dateObject).not.toBeUndefined()
|
||||||
|
const updatedString = dateToMarkdownString(dateObject!)
|
||||||
|
expect(updatedString).toBe("08/29/2024 00:00:00")
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,11 +36,18 @@ const _getDateFromISO = (value: string): Date | undefined => {
|
|||||||
return isNaN(date.getTime()) ? undefined : date;
|
return isNaN(date.getTime()) ? undefined : date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _getDateFromDateOnly = (datePart: string): Date | undefined => {
|
||||||
|
const [month, day, year] = datePart.split("/").map(Number);
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
return isNaN(date.getTime()) ? undefined : date;
|
||||||
|
};
|
||||||
|
|
||||||
export const getDateFromString = (value: string): Date | undefined => {
|
export const getDateFromString = (value: string): Date | undefined => {
|
||||||
const ampmDateRegex =
|
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" or "M/D/YYYY, h:mm:ss AM/PM"
|
/^\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" or "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 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+)|(Z))$/; //"2024-08-26T00:00:00.0000000"
|
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}((.\d+)|(Z))$/; //"2024-08-26T00:00:00.0000000"
|
||||||
|
const dateOnlyRegex = /^\d{1,2}\/\d{1,2}\/\d{4}$/; // "M/D/YYYY" or "MM/DD/YYYY"
|
||||||
|
|
||||||
if (isoDateRegex.test(value)) {
|
if (isoDateRegex.test(value)) {
|
||||||
return _getDateFromISO(value);
|
return _getDateFromISO(value);
|
||||||
@@ -50,6 +57,8 @@ export const getDateFromString = (value: string): Date | undefined => {
|
|||||||
} else if (militaryDateRegex.test(value)) {
|
} else if (militaryDateRegex.test(value)) {
|
||||||
const [datePart, timePart] = value.split(" ");
|
const [datePart, timePart] = value.split(" ");
|
||||||
return _getDateFromMilitary(datePart, timePart);
|
return _getDateFromMilitary(datePart, timePart);
|
||||||
|
} if (dateOnlyRegex.test(value)) {
|
||||||
|
return _getDateFromDateOnly(value);
|
||||||
} else {
|
} else {
|
||||||
if (value) console.log("invalid date format", value);
|
if (value) console.log("invalid date format", value);
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user