mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 15:48:32 -06:00
more refactor
This commit is contained in:
28
src/features/local/utils/directoriesRouter.ts
Normal file
28
src/features/local/utils/directoriesRouter.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import z from "zod";
|
||||
import publicProcedure from "../../../services/serverFunctions/publicProcedure";
|
||||
import { router } from "../../../services/serverFunctions/trpcSetup";
|
||||
import { fileStorageService } from "@/features/local/utils/fileStorageService";
|
||||
|
||||
export const directoriesRouter = router({
|
||||
getEmptyDirectories: publicProcedure.query(async () => {
|
||||
return await fileStorageService.getEmptyDirectories();
|
||||
}),
|
||||
getDirectoryContents: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
relativePath: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { relativePath } }) => {
|
||||
return await fileStorageService.getDirectoryContents(relativePath);
|
||||
}),
|
||||
directoryIsCourse: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
folderPath: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { folderPath } }) => {
|
||||
return await fileStorageService.settings.folderIsCourse(folderPath);
|
||||
}),
|
||||
});
|
||||
86
src/features/local/utils/fileStorageService.ts
Normal file
86
src/features/local/utils/fileStorageService.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
basePath,
|
||||
directoryOrFileExists,
|
||||
} from "../../../services/fileStorage/utils/fileSystemUtils";
|
||||
import { quizFileStorageService } from "../quizzes/quizFileStorageService";
|
||||
import { pageFileStorageService } from "../pages/pageFileStorageService";
|
||||
import { moduleFileStorageService } from "../modules/moduleFileStorageService";
|
||||
import { settingsFileStorageService } from "../course/settingsFileStorageService";
|
||||
import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService";
|
||||
import { assignmentsFileStorageService } from "@/features/local/assignments/assignmentsFileStorageService";
|
||||
|
||||
export const fileStorageService = {
|
||||
settings: settingsFileStorageService,
|
||||
modules: moduleFileStorageService,
|
||||
assignments: assignmentsFileStorageService,
|
||||
quizzes: quizFileStorageService,
|
||||
pages: pageFileStorageService,
|
||||
|
||||
async getEmptyDirectories(): Promise<string[]> {
|
||||
if (!(await directoryOrFileExists(basePath))) {
|
||||
throw new Error(
|
||||
`Cannot get empty directories, ${basePath} does not exist`
|
||||
);
|
||||
}
|
||||
|
||||
const directories = await fs.readdir(basePath, { withFileTypes: true });
|
||||
const emptyDirectories = (
|
||||
await Promise.all(
|
||||
directories
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => path.join(dirent.name))
|
||||
.map(async (directory) => {
|
||||
return {
|
||||
directory,
|
||||
files: await fs.readdir(path.join(basePath, directory)),
|
||||
};
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter(({ files }) => files.length === 0)
|
||||
.map(({ directory }) => directory);
|
||||
|
||||
return emptyDirectories;
|
||||
},
|
||||
|
||||
async createCourseFolderForTesting(courseName: string) {
|
||||
const courseDirectory = await getCoursePathByName(courseName);
|
||||
|
||||
await fs.mkdir(courseDirectory, { recursive: true });
|
||||
},
|
||||
|
||||
async createModuleFolderForTesting(courseName: string, moduleName: string) {
|
||||
const courseDirectory = path.join(basePath, courseName, moduleName);
|
||||
|
||||
await fs.mkdir(courseDirectory, { recursive: true });
|
||||
},
|
||||
|
||||
async getDirectoryContents(
|
||||
relativePath: string
|
||||
): Promise<{ files: string[]; folders: string[] }> {
|
||||
const fullPath = path.join(basePath, relativePath);
|
||||
// Security: ensure fullPath is inside basePath
|
||||
const resolvedBase = path.resolve(basePath);
|
||||
const resolvedFull = path.resolve(fullPath);
|
||||
if (!resolvedFull.startsWith(resolvedBase)) {
|
||||
return { files: [], folders: [] };
|
||||
}
|
||||
if (!(await directoryOrFileExists(fullPath))) {
|
||||
throw new Error(`Directory ${fullPath} does not exist`);
|
||||
}
|
||||
|
||||
const contents = await fs.readdir(fullPath, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
const folders: string[] = [];
|
||||
for (const dirent of contents) {
|
||||
if (dirent.isDirectory()) {
|
||||
folders.push(dirent.name);
|
||||
} else if (dirent.isFile()) {
|
||||
files.push(dirent.name);
|
||||
}
|
||||
}
|
||||
return { files, folders };
|
||||
},
|
||||
};
|
||||
11
src/features/local/utils/lectureUtils.ts
Normal file
11
src/features/local/utils/lectureUtils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Lecture } from "../lectures/lectureModel";
|
||||
import { getDateOnlyMarkdownString } from "./timeUtils";
|
||||
|
||||
export function getLectureForDay(
|
||||
weeks: { weekName: string; lectures: Lecture[] }[],
|
||||
dayAsDate: Date
|
||||
) {
|
||||
return weeks
|
||||
.flatMap((w) => w.lectures)
|
||||
.find((l) => l.date == getDateOnlyMarkdownString(dayAsDate));
|
||||
}
|
||||
124
src/features/local/utils/semesterTransferUtils.ts
Normal file
124
src/features/local/utils/semesterTransferUtils.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||
import { LocalAssignment } from "../assignments/models/localAssignment";
|
||||
import { Lecture } from "../lectures/lectureModel";
|
||||
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
|
||||
import { getDateFromStringOrThrow, dateToMarkdownString } from "./timeUtils";
|
||||
|
||||
export const prepAssignmentForNewSemester = (
|
||||
assignment: LocalAssignment,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalAssignment => {
|
||||
const descriptionWithoutGithubClassroom = replaceClassroomUrl(
|
||||
assignment.description
|
||||
);
|
||||
return {
|
||||
...assignment,
|
||||
description: descriptionWithoutGithubClassroom,
|
||||
dueAt:
|
||||
newDateOffset(
|
||||
assignment.dueAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
) ?? assignment.dueAt,
|
||||
lockAt: newDateOffset(
|
||||
assignment.lockAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
),
|
||||
githubClassroomAssignmentLink: undefined,
|
||||
githubClassroomAssignmentShareLink: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const prepQuizForNewSemester = (
|
||||
quiz: LocalQuiz,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalQuiz => {
|
||||
const descriptionWithoutGithubClassroom = replaceClassroomUrl(
|
||||
quiz.description
|
||||
);
|
||||
return {
|
||||
...quiz,
|
||||
description: descriptionWithoutGithubClassroom,
|
||||
dueAt:
|
||||
newDateOffset(quiz.dueAt, oldSemesterStartDate, newSemesterStartDate) ??
|
||||
quiz.dueAt,
|
||||
lockAt: newDateOffset(
|
||||
quiz.lockAt,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const prepPageForNewSemester = (
|
||||
page: LocalCoursePage,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): LocalCoursePage => {
|
||||
const updatedText = replaceClassroomUrl(page.text);
|
||||
return {
|
||||
...page,
|
||||
text: updatedText,
|
||||
dueAt:
|
||||
newDateOffset(page.dueAt, oldSemesterStartDate, newSemesterStartDate) ??
|
||||
page.dueAt,
|
||||
};
|
||||
};
|
||||
export const prepLectureForNewSemester = (
|
||||
lecture: Lecture,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
): Lecture => {
|
||||
const updatedText = replaceClassroomUrl(lecture.content);
|
||||
const newDate = newDateOffset(
|
||||
lecture.date,
|
||||
oldSemesterStartDate,
|
||||
newSemesterStartDate
|
||||
);
|
||||
const newDateOnly = newDate?.split(" ")[0];
|
||||
return {
|
||||
...lecture,
|
||||
content: updatedText,
|
||||
date: newDateOnly ?? lecture.date,
|
||||
};
|
||||
};
|
||||
|
||||
const replaceClassroomUrl = (value: string) => {
|
||||
const classroomPattern =
|
||||
/https:\/\/classroom\.github\.com\/[a-zA-Z0-9\/._-]+/g;
|
||||
const withoutGithubClassroom = value.replace(
|
||||
classroomPattern,
|
||||
"insert_github_classroom_url"
|
||||
);
|
||||
return withoutGithubClassroom;
|
||||
};
|
||||
|
||||
const newDateOffset = (
|
||||
dateString: string | undefined,
|
||||
oldSemesterStartDate: string,
|
||||
newSemesterStartDate: string
|
||||
) => {
|
||||
if (!dateString) return dateString;
|
||||
const oldStart = getDateFromStringOrThrow(
|
||||
oldSemesterStartDate,
|
||||
"semester start date in new semester offset"
|
||||
);
|
||||
const newStart = getDateFromStringOrThrow(
|
||||
newSemesterStartDate,
|
||||
"new semester start date in new semester offset"
|
||||
);
|
||||
const date = getDateFromStringOrThrow(
|
||||
dateString,
|
||||
"date in new semester offset"
|
||||
);
|
||||
const offset = date.getTime() - oldStart.getTime();
|
||||
|
||||
const newUnixTime = offset + newStart.getTime();
|
||||
|
||||
const newDate = new Date(newUnixTime);
|
||||
|
||||
return dateToMarkdownString(newDate);
|
||||
};
|
||||
50
src/features/local/utils/settingsUtils.tsx
Normal file
50
src/features/local/utils/settingsUtils.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
getDateFromStringOrThrow,
|
||||
getDateOnlyMarkdownString,
|
||||
} from "./timeUtils";
|
||||
|
||||
export const parseHolidays = (
|
||||
inputText: string
|
||||
): {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[] => {
|
||||
let holidays: {
|
||||
name: string;
|
||||
days: 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 = [...holidays, { name: holidayName, days: [] }];
|
||||
} else if (currentHoliday && line.startsWith("-")) {
|
||||
const date = line.replace("-", "").trim();
|
||||
const dateObject = getDateFromStringOrThrow(date, "parsing holiday text");
|
||||
|
||||
const holiday = holidays.find((h) => h.name == currentHoliday);
|
||||
holiday?.days.push(getDateOnlyMarkdownString(dateObject));
|
||||
}
|
||||
});
|
||||
|
||||
return holidays;
|
||||
};
|
||||
|
||||
export const holidaysToString = (
|
||||
holidays: {
|
||||
name: string;
|
||||
days: string[];
|
||||
}[]
|
||||
) => {
|
||||
const entries = holidays.map((holiday) => {
|
||||
const title = holiday.name + ":\n";
|
||||
const days = holiday.days.map((d) => `- ${d}\n`);
|
||||
return title + days.join("");
|
||||
});
|
||||
|
||||
return entries.join("");
|
||||
};
|
||||
25
src/features/local/utils/storageDirectoryHooks.ts
Normal file
25
src/features/local/utils/storageDirectoryHooks.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useTRPC } from "@/services/serverFunctions/trpcClient";
|
||||
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
|
||||
export const directoryKeys = {
|
||||
emptyFolders: ["empty folders"] as const,
|
||||
};
|
||||
|
||||
export const useEmptyDirectoriesQuery = () => {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.directories.getEmptyDirectories.queryOptions());
|
||||
};
|
||||
|
||||
export const useDirectoryContentsQuery = (relativePath: string) => {
|
||||
const trpc = useTRPC();
|
||||
return useQuery(
|
||||
trpc.directories.getDirectoryContents.queryOptions({ relativePath })
|
||||
);
|
||||
};
|
||||
|
||||
export const useDirectoryIsCourseQuery = (folderPath: string) => {
|
||||
const trpc = useTRPC();
|
||||
return useQuery(
|
||||
trpc.directories.directoryIsCourse.queryOptions({ folderPath })
|
||||
);
|
||||
};
|
||||
138
src/features/local/utils/timeUtils.ts
Normal file
138
src/features/local/utils/timeUtils.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
||||
|
||||
const _getDateFromAMPM = (
|
||||
datePart: string,
|
||||
timePart: string,
|
||||
amPmPart: string
|
||||
): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
||||
|
||||
let adjustedHours = hours;
|
||||
if (amPmPart) {
|
||||
const upperMeridian = amPmPart.toUpperCase();
|
||||
if (upperMeridian === "PM" && hours < 12) {
|
||||
adjustedHours += 12;
|
||||
} else if (upperMeridian === "AM" && hours === 12) {
|
||||
adjustedHours = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(year, month - 1, day, adjustedHours, minutes, seconds);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromMilitary = (
|
||||
datePart: string,
|
||||
timePart: string
|
||||
): Date | undefined => {
|
||||
const [month, day, year] = datePart.split("/").map(Number);
|
||||
const [hours, minutes, seconds] = timePart.split(":").map(Number);
|
||||
|
||||
const date = new Date(year, month - 1, day, hours, minutes, seconds);
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
const _getDateFromISO = (value: string): Date | undefined => {
|
||||
const date = new Date(value);
|
||||
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 => {
|
||||
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"
|
||||
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 dateOnlyRegex = /^\d{1,2}\/\d{1,2}\/\d{4}$/; // "M/D/YYYY" or "MM/DD/YYYY"
|
||||
|
||||
if (isoDateRegex.test(value)) {
|
||||
return _getDateFromISO(value);
|
||||
} else if (ampmDateRegex.test(value)) {
|
||||
const [datePart, timePart, amPmPart] = value.split(/,?[\s\u202F]+/);
|
||||
return _getDateFromAMPM(datePart, timePart, amPmPart);
|
||||
} else if (militaryDateRegex.test(value)) {
|
||||
const [datePart, timePart] = value.split(" ");
|
||||
return _getDateFromMilitary(datePart, timePart);
|
||||
}
|
||||
if (dateOnlyRegex.test(value)) {
|
||||
return _getDateFromDateOnly(value);
|
||||
} else {
|
||||
if (value) console.log("invalid date format", value);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDateFromStringOrThrow = (
|
||||
value: string,
|
||||
labelForError: string
|
||||
): Date => {
|
||||
const d = getDateFromString(value);
|
||||
if (!d) throw Error(`Invalid date format for ${labelForError}, ${value}`);
|
||||
return d;
|
||||
};
|
||||
|
||||
export const verifyDateStringOrUndefined = (
|
||||
value: string
|
||||
): string | undefined => {
|
||||
const date = getDateFromString(value);
|
||||
return date ? dateToMarkdownString(date) : undefined;
|
||||
};
|
||||
|
||||
export const verifyDateOrThrow = (
|
||||
value: string,
|
||||
labelForError: string
|
||||
): string => {
|
||||
const myDate = getDateFromString(value);
|
||||
if (!myDate) throw new Error(`Invalid format for ${labelForError}: ${value}`);
|
||||
return dateToMarkdownString(myDate);
|
||||
};
|
||||
|
||||
export const dateToMarkdownString = (date: Date) => {
|
||||
const stringDay = String(date.getDate()).padStart(2, "0");
|
||||
const stringMonth = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-based
|
||||
const stringYear = date.getFullYear();
|
||||
const stringHours = String(date.getHours()).padStart(2, "0");
|
||||
const stringMinutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const stringSeconds = String(date.getSeconds()).padStart(2, "0");
|
||||
|
||||
return `${stringMonth}/${stringDay}/${stringYear} ${stringHours}:${stringMinutes}:${stringSeconds}`;
|
||||
};
|
||||
|
||||
export const getDateOnlyMarkdownString = (date: Date) => {
|
||||
return dateToMarkdownString(date).split(" ")[0];
|
||||
};
|
||||
|
||||
export function getTermName(startDate: string) {
|
||||
const [year, month, ..._rest] = startDate.split("-");
|
||||
if (month < "04") return "Spring " + year;
|
||||
if (month < "07") return "Summer " + year;
|
||||
return "Fall " + year;
|
||||
}
|
||||
|
||||
export function getDateKey(dateString: string) {
|
||||
return dateString.split("T")[0];
|
||||
}
|
||||
export function groupByStartDate(courses: LocalCourseSettings[]): {
|
||||
[key: string]: LocalCourseSettings[];
|
||||
} {
|
||||
return courses.reduce(
|
||||
(acc, course) => {
|
||||
const { startDate } = course;
|
||||
const key = getDateKey(startDate);
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(course);
|
||||
return acc;
|
||||
},
|
||||
{} as {
|
||||
[key: string]: LocalCourseSettings[];
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user