more refactoring by feature

This commit is contained in:
2025-07-23 09:46:35 -06:00
parent d5a40e52d9
commit 3e371247d6
92 changed files with 159 additions and 158 deletions

View File

@@ -0,0 +1,4 @@
export interface IModuleItem {
name: string;
dueAt: string;
}

View File

@@ -0,0 +1,173 @@
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { useTRPC } from "@/services/serverFunctions/trpcClient";
import { CalendarItemsInterface } from "@/app/course/[courseName]/context/calendarItemsContext";
import {
getDateOnlyMarkdownString,
getDateFromStringOrThrow,
} from "@/models/local/utils/timeUtils";
import {
useSuspenseQuery,
useMutation,
useQueryClient,
useSuspenseQueries,
} from "@tanstack/react-query";
export const useModuleNamesQuery = () => {
const { courseName } = useCourseContext();
const trpc = useTRPC();
return useSuspenseQuery(
trpc.module.getModuleNames.queryOptions({ courseName })
);
};
export const useCreateModuleMutation = () => {
const { courseName } = useCourseContext();
const trpc = useTRPC();
const queryClient = useQueryClient();
return useMutation(
trpc.module.createModule.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: trpc.module.getModuleNames.queryKey({ courseName }),
});
},
})
);
};
export const useCourseQuizzesByModuleByDateQuery = () => {
const { courseName } = useCourseContext();
const { data: moduleNames } = useModuleNamesQuery();
const trpc = useTRPC();
const quizzesResults = useSuspenseQueries({
queries: moduleNames.map((moduleName: string) =>
trpc.quiz.getAllQuizzes.queryOptions({ courseName, moduleName })
),
});
const quizzes = quizzesResults.map((result) => result.data ?? []);
const quizzesAndModules = moduleNames.flatMap(
(moduleName: string, index: number) => {
return quizzes[index].map((quiz) => ({ moduleName, quiz }));
}
);
const quizzesByModuleByDate = quizzesAndModules.reduce(
(previous, { quiz, moduleName }) => {
const dueDay = getDateOnlyMarkdownString(
getDateFromStringOrThrow(quiz.dueAt, "due at for quiz in items context")
);
const previousModules = previous[dueDay] ?? {};
const previousModule = previousModules[moduleName] ?? {
quizzes: [],
};
const updatedModule = {
...previousModule,
quizzes: [...previousModule.quizzes, quiz],
};
return {
...previous,
[dueDay]: {
...previousModules,
[moduleName]: updatedModule,
},
};
},
{} as CalendarItemsInterface
);
return quizzesByModuleByDate;
};
export const useCoursePagesByModuleByDateQuery = () => {
const { courseName } = useCourseContext();
const { data: moduleNames } = useModuleNamesQuery();
const trpc = useTRPC();
const pagesResults = useSuspenseQueries({
queries: moduleNames.map((moduleName: string) =>
trpc.page.getAllPages.queryOptions({ courseName, moduleName })
),
});
const pages = pagesResults.map((result) => result.data ?? []);
const pagesAndModules = moduleNames.flatMap(
(moduleName: string, index: number) => {
return pages[index].map((page) => ({ moduleName, page }));
}
);
const pagesByModuleByDate = pagesAndModules.reduce(
(
previous,
{ page, moduleName }
) => {
const dueDay = getDateOnlyMarkdownString(
getDateFromStringOrThrow(page.dueAt, "due at for page in items context")
);
const previousModules = previous[dueDay] ?? {};
const previousModule = previousModules[moduleName] ?? {
pages: [],
};
const updatedModule = {
...previousModule,
pages: [...previousModule.pages, page],
};
return {
...previous,
[dueDay]: {
...previousModules,
[moduleName]: updatedModule,
},
};
},
{} as CalendarItemsInterface
);
return pagesByModuleByDate;
};
export const useCourseAssignmentsByModuleByDateQuery = () => {
const { courseName } = useCourseContext();
const { data: moduleNames } = useModuleNamesQuery();
const trpc = useTRPC();
const assignmentsResults = useSuspenseQueries({
queries: moduleNames.map((moduleName: string) =>
trpc.assignment.getAllAssignments.queryOptions({ courseName, moduleName })
),
});
const assignments = assignmentsResults.map(
(result) => result.data
);
const assignmentsAndModules = moduleNames.flatMap(
(moduleName: string, index: number) => {
return assignments[index].map((assignment) => ({
moduleName,
assignment,
}));
}
);
const assignmentsByModuleByDate = assignmentsAndModules.reduce(
(
previous,
{ assignment, moduleName }
) => {
const dueDay = getDateOnlyMarkdownString(
getDateFromStringOrThrow(
assignment.dueAt,
"due at for assignment in items context"
)
);
const previousModules = previous[dueDay] ?? {};
const previousModule = previousModules[moduleName] ?? {
assignments: [],
};
const updatedModule = {
...previousModule,
assignments: [...previousModule.assignments, assignment],
};
return {
...previous,
[dueDay]: {
...previousModules,
[moduleName]: updatedModule,
},
};
},
{} as CalendarItemsInterface
);
return assignmentsByModuleByDate;
};

View File

@@ -0,0 +1,75 @@
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalAssignment } from "../assignments/models/localAssignment";
import { IModuleItem } from "./IModuleItem";
import { getDateFromString } from "../../../models/local/utils/timeUtils";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
export interface LocalModule {
name: string;
assignments: LocalAssignment[];
quizzes: LocalQuiz[];
pages: LocalCoursePage[];
}
export const LocalModuleUtils = {
getSortedModuleItems(module: LocalModule): IModuleItem[] {
return [...module.assignments, ...module.quizzes, ...module.pages].sort(
(a, b) =>
(getDateFromString(a.dueAt)?.getTime() ?? 0) -
(getDateFromString(b.dueAt)?.getTime() ?? 0)
);
},
equals(module1: LocalModule, module2: LocalModule): boolean {
return (
module1.name.toLowerCase() === module2.name.toLowerCase() &&
LocalModuleUtils.compareCollections(
module1.assignments.sort((a, b) => a.name.localeCompare(b.name)),
module2.assignments.sort((a, b) => a.name.localeCompare(b.name))
) &&
LocalModuleUtils.compareCollections(
module1.quizzes.sort((a, b) => a.name.localeCompare(b.name)),
module2.quizzes.sort((a, b) => a.name.localeCompare(b.name))
) &&
LocalModuleUtils.compareCollections(
module1.pages.sort((a, b) => a.name.localeCompare(b.name)),
module2.pages.sort((a, b) => a.name.localeCompare(b.name))
)
);
},
compareCollections<T>(first: T[], second: T[]): boolean {
if (first.length !== second.length) return false;
for (let i = 0; i < first.length; i++) {
if (JSON.stringify(first[i]) !== JSON.stringify(second[i])) return false;
}
return true;
},
getHashCode(module: LocalModule): number {
const hash = new Map<string, number>();
hash.set(module.name.toLowerCase(), 1);
LocalModuleUtils.addRangeToHash(
hash,
module.assignments.sort((a, b) => a.name.localeCompare(b.name))
);
LocalModuleUtils.addRangeToHash(
hash,
module.quizzes.sort((a, b) => a.name.localeCompare(b.name))
);
LocalModuleUtils.addRangeToHash(
hash,
module.pages.sort((a, b) => a.name.localeCompare(b.name))
);
return Array.from(hash.values()).reduce((acc, val) => acc + val, 0);
},
addRangeToHash<T>(hash: Map<string, number>, items: T[]): void {
for (const item of items) {
hash.set(JSON.stringify(item), 1);
}
},
};

View File

@@ -0,0 +1,27 @@
import { promises as fs } from "fs";
import { lectureFolderName } from "../../../services/fileStorage/utils/lectureUtils";
import { getCoursePathByName } from "../../../services/fileStorage/globalSettingsFileStorageService";
export const moduleFileStorageService = {
async getModuleNames(courseName: string) {
const courseDirectory = await getCoursePathByName(courseName);
const moduleDirectories = await fs.readdir(courseDirectory, {
withFileTypes: true,
});
const modulePromises = moduleDirectories
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
const modules = await Promise.all(modulePromises);
const modulesWithoutLectures = modules.filter(
(m) => m !== lectureFolderName
);
return modulesWithoutLectures.sort((a, b) => a.localeCompare(b));
},
async createModule(courseName: string, moduleName: string) {
const courseDirectory = await getCoursePathByName(courseName);
await fs.mkdir(courseDirectory + "/" + moduleName, { recursive: true });
},
};

View File

@@ -0,0 +1,26 @@
import { z } from "zod";
import { fileStorageService } from "@/services/fileStorage/fileStorageService";
import { router } from "@/services/serverFunctions/trpcSetup";
import publicProcedure from "@/services/serverFunctions/procedures/public";
export const moduleRouter = router({
getModuleNames: publicProcedure
.input(
z.object({
courseName: z.string(),
})
)
.query(async ({ input: { courseName } }) => {
return await fileStorageService.modules.getModuleNames(courseName);
}),
createModule: publicProcedure
.input(
z.object({
courseName: z.string(),
moduleName: z.string(),
})
)
.mutation(async ({ input: { courseName, moduleName } }) => {
await fileStorageService.modules.createModule(courseName, moduleName);
}),
});