adding storage for lectures

This commit is contained in:
2024-11-01 17:10:06 -06:00
parent dcecf3172e
commit bd8c9140eb
12 changed files with 181 additions and 464 deletions

View File

@@ -1,5 +1,3 @@
"use client";
import { import {
dateToMarkdownString, dateToMarkdownString,
getDateFromStringOrThrow, getDateFromStringOrThrow,

View File

@@ -1,10 +1,53 @@
"use client"; "use client";
import { MonacoEditor } from "@/components/editor/MonacoEditor"; import { MonacoEditor } from "@/components/editor/MonacoEditor";
import { useState } from "react"; import {
useLecturesByWeekQuery,
useLectureUpdateMutation,
} from "@/hooks/localCourse/lectureHooks";
import { Lecture } from "@/models/local/lecture";
import {
lectureToString,
parseLecture,
} from "@/services/fileStorage/utils/lectureUtils";
import { useEffect, useState } from "react";
import LecturePreview from "./LecturePreview";
export default function EditLecture({ lectureDay }: { lectureDay: string }) { export default function EditLecture({ lectureDay }: { lectureDay: string }) {
const [text, setText] = useState(""); const { data: weeks } = useLecturesByWeekQuery();
const updateLecture = useLectureUpdateMutation();
const lecture = weeks
.flatMap(({ lectures }) => lectures.map((lecture) => lecture))
.find((l) => l.date === lectureDay);
const startingText = lecture ? lectureToString(lecture) : `Name: Name Here
Date: ${lectureDay}
---
`;
const [text, setText] = useState(startingText);
const [error, setError] = useState("");
useEffect(() => {
const delay = 500;
const handler = setTimeout(() => {
try {
const parsed = parseLecture(text);
if (!lecture || lectureToString(parsed) !== lectureToString(lecture)) {
console.log("updating lecture");
updateLecture.mutate(parsed);
}
setError("");
} catch (e: any) {
setError(e.toString());
}
}, delay);
return () => {
clearTimeout(handler);
};
}, [lecture, text, updateLecture]);
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="columns-2 min-h-0 flex-1"> <div className="columns-2 min-h-0 flex-1">
@@ -12,8 +55,8 @@ export default function EditLecture({ lectureDay }: { lectureDay: string }) {
<MonacoEditor value={text} onChange={setText} /> <MonacoEditor value={text} onChange={setText} />
</div> </div>
<div className="h-full"> <div className="h-full">
{/* <div className="text-red-300">{error && error}</div> */} <div className="text-red-300">{error && error}</div>
{/* <AssignmentPreview assignment={assignment} /> */} {lecture && <LecturePreview lecture={lecture} />}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,27 @@
import { Lecture } from "@/models/local/lecture";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
export default function LecturePreview({ lecture }: { lecture: Lecture }) {
return (
<div>
<section>
<div className="flex">
<div className="flex-1 text-end pe-3">Name</div>
<div className="flex-1">{lecture.name}</div>
</div>
<div className="flex">
<div className="flex-1 text-end pe-3">Date</div>
<div className="flex-1">{lecture.date}</div>
</div>
</section>
<section>
<div
className="markdownPreview"
dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe(lecture.content),
}}
></div>
</section>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import EditLecture from "./EditLecture"; import EditLecture from "./EditLecture";
import { getDateFromStringOrThrow, getDateOnlyMarkdownString } from "@/models/local/timeUtils";
export default function page({ export default function page({
params: { lectureDay }, params: { lectureDay },
@@ -8,5 +9,7 @@ export default function page({
}) { }) {
const decodedLectureDay = decodeURIComponent(lectureDay); const decodedLectureDay = decodeURIComponent(lectureDay);
console.log(decodedLectureDay); console.log(decodedLectureDay);
return <EditLecture lectureDay={decodedLectureDay} />; const lectureDate = getDateFromStringOrThrow(decodedLectureDay, "lecture day in lecture page")
const lectureDayOnly = getDateOnlyMarkdownString(lectureDate)
return <EditLecture lectureDay={lectureDayOnly} />;
} }

View File

@@ -1,7 +1,16 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import {
useMutation,
useQueryClient,
useSuspenseQuery,
} from "@tanstack/react-query";
import { lectureKeys } from "./lectureKeys"; import { lectureKeys } from "./lectureKeys";
import { useCourseContext } from "@/app/course/[courseName]/context/courseContext"; import { useCourseContext } from "@/app/course/[courseName]/context/courseContext";
import { getLectures } from "@/services/fileStorage/lectureFileStorageService"; import {
getLectures,
updateLecture,
} from "@/services/fileStorage/lectureFileStorageService";
import { Lecture } from "@/models/local/lecture";
import { useLocalCourseSettingsQuery } from "./localCoursesHooks";
export const getLecturesQueryConfig = (courseName: string) => export const getLecturesQueryConfig = (courseName: string) =>
({ ({
@@ -9,7 +18,23 @@ export const getLecturesQueryConfig = (courseName: string) =>
queryFn: async () => await getLectures(courseName), queryFn: async () => await getLectures(courseName),
} as const); } as const);
export const useLecturesQuery = () => { export const useLecturesByWeekQuery = () => {
const { courseName } = useCourseContext(); const { courseName } = useCourseContext();
return useSuspenseQuery(getLecturesQueryConfig(courseName)); return useSuspenseQuery(getLecturesQueryConfig(courseName));
}; };
export const useLectureUpdateMutation = () => {
const { courseName } = useCourseContext();
const { data: settings } = useLocalCourseSettingsQuery();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (lecture: Lecture) => {
await updateLecture(courseName, settings, lecture);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: lectureKeys.allLectures(courseName),
});
},
});
};

View File

@@ -14,7 +14,8 @@ const getAssignmentNames = async (courseName: string, moduleName: string) => {
console.log( console.log(
`Error loading course by name, assignments folder does not exist in ${filePath}` `Error loading course by name, assignments folder does not exist in ${filePath}`
); );
await fs.mkdir(filePath); // await fs.mkdir(filePath);
return [];
} }
const assignmentFiles = await fs.readdir(filePath); const assignmentFiles = await fs.readdir(filePath);

View File

@@ -2,12 +2,18 @@
import path from "path"; import path from "path";
import { basePath } from "./utils/fileSystemUtils"; import { basePath } from "./utils/fileSystemUtils";
import fs from "fs/promises"; import fs from "fs/promises";
import {
lectureFolderName,
lectureToString,
parseLecture,
} from "./utils/lectureUtils";
import { Lecture } from "@/models/local/lecture"; import { Lecture } from "@/models/local/lecture";
import { extractLabelValue } from "@/models/local/assignment/utils/markdownUtils"; import { getDayOfWeek, LocalCourseSettings } from "@/models/local/localCourse";
import { getDateOnlyMarkdownString } from "@/models/local/timeUtils"; import { getWeekNumber } from "@/app/course/[courseName]/calendar/calendarMonthUtils";
import { getDateFromStringOrThrow } from "@/models/local/timeUtils";
export async function getLectures(courseName: string) { export async function getLectures(courseName: string) {
const courseLectureRoot = path.join(basePath, courseName, "lectures"); const courseLectureRoot = path.join(basePath, courseName, lectureFolderName);
if (!(await directoryExists(courseLectureRoot))) { if (!(await directoryExists(courseLectureRoot))) {
return []; return [];
} }
@@ -39,30 +45,36 @@ export async function getLectures(courseName: string) {
return lecturesByWeek; return lecturesByWeek;
} }
export function parseLecture(fileContent: string): Lecture { export async function updateLecture(
try { courseName: string,
const settings = fileContent.split("---\n")[0]; courseSettings: LocalCourseSettings,
const name = extractLabelValue(settings, "Name"); lecture: Lecture
const date = extractLabelValue(settings, "Date"); ) {
const courseLectureRoot = path.join(basePath, courseName, lectureFolderName);
const startDate = getDateFromStringOrThrow(
courseSettings.startDate,
"semester start date in update lecture"
);
const lectureDate = getDateFromStringOrThrow(
lecture.date,
"lecture start date in update lecture"
);
const weekNumber = getWeekNumber(startDate, lectureDate)
.toString()
.padStart(2, "0");
const content = fileContent.split("---\n")[1].trim(); const weekFolderName = `week-${weekNumber}`;
const weekPath = path.join(courseLectureRoot, weekFolderName);
return { if (!(await directoryExists(weekPath))) {
name, await fs.mkdir(weekPath, { recursive: true });
date,
content,
};
} catch (error) {
console.error("Error parsing lecture", fileContent);
throw error;
} }
}
export function lectureToString(lecture: Lecture) { const lecturePath = path.join(
return `Name: ${lecture.name} weekPath,
Date: ${lecture.date} `${lectureDate.getDay()}-${getDayOfWeek(lectureDate)}.md`
--- );
${lecture.content}`; const lectureContents = lectureToString(lecture);
await fs.writeFile(lecturePath, lectureContents);
} }
const directoryExists = async (path: string): Promise<boolean> => { const directoryExists = async (path: string): Promise<boolean> => {

View File

@@ -1,6 +1,7 @@
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import path from "path"; import path from "path";
import { basePath } from "./utils/fileSystemUtils"; import { basePath } from "./utils/fileSystemUtils";
import { lectureFolderName } from "./utils/lectureUtils";
export const moduleFileStorageService = { export const moduleFileStorageService = {
async getModuleNames(courseName: string) { async getModuleNames(courseName: string) {
@@ -14,9 +15,10 @@ export const moduleFileStorageService = {
.map((dirent) => dirent.name); .map((dirent) => dirent.name);
const modules = await Promise.all(modulePromises); const modules = await Promise.all(modulePromises);
return modules const modulesWithoutLectures = modules.filter(
.filter((m) => m !== "lectures") (m) => m !== lectureFolderName
.sort((a, b) => a.localeCompare(b)); );
return modulesWithoutLectures.sort((a, b) => a.localeCompare(b));
}, },
async createModule(courseName: string, moduleName: string) { async createModule(courseName: string, moduleName: string) {
const courseDirectory = path.join(basePath, courseName); const courseDirectory = path.join(basePath, courseName);

View File

@@ -1,177 +0,0 @@
import {
LocalAssignment,
localAssignmentMarkdown,
} from "@/models/local/assignment/localAssignment";
import {
LocalCourse,
LocalCourseSettings,
localCourseYamlUtils,
} from "@/models/local/localCourse";
import { LocalModule } from "@/models/local/localModules";
import {
LocalCoursePage,
localPageMarkdownUtils,
} from "@/models/local/page/localCoursePage";
import {
LocalQuiz,
localQuizMarkdownUtils,
} from "@/models/local/quiz/localQuiz";
import { promises as fs } from "fs";
import path from "path";
import { directoryOrFileExists } from "./fileSystemUtils";
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
export const courseMarkdownLoader = {
// async loadSavedCourses(): Promise<LocalCourse[]> {
// const courseDirectories = await fs.readdir(basePath, {
// withFileTypes: true,
// });
// const coursePromises = courseDirectories
// .filter((dirent) => dirent.isDirectory())
// .map(async (dirent) => {
// const coursePath = path.join(basePath, dirent.name);
// const settingsPath = path.join(coursePath, "settings.yml");
// if (await directoryOrFileExists(settingsPath)) {
// return this.loadCourseByPath(coursePath);
// }
// return null;
// });
// const courses = (await Promise.all(coursePromises)).filter(
// (course) => course !== null
// ) as LocalCourse[];
// return courses.sort((a, b) =>
// a.settings.name.localeCompare(b.settings.name)
// );
// },
// async loadCourseByPath(courseDirectory: string): Promise<LocalCourse> {
// if (!(await directoryOrFileExists(courseDirectory))) {
// const errorMessage = `Error loading course by name, could not find folder ${courseDirectory}`;
// console.log(errorMessage);
// throw new Error(errorMessage);
// }
// const settings = await this.loadCourseSettings(courseDirectory);
// const modules = await this.loadCourseModules(courseDirectory);
// return {
// settings,
// modules,
// };
// },
// async loadCourseSettings(
// courseDirectory: string
// ): Promise<LocalCourseSettings> {
// const settingsPath = path.join(courseDirectory, "settings.yml");
// if (!(await directoryOrFileExists(settingsPath))) {
// const errorMessage = `Error loading course by name, settings file ${settingsPath}`;
// console.log(errorMessage);
// throw new Error(errorMessage);
// }
// const settingsString = await fs.readFile(settingsPath, "utf-8");
// const settings = localCourseYamlUtils.parseSettingYaml(settingsString);
// const folderName = path.basename(courseDirectory);
// return { ...settings, name: folderName };
// },
// async loadCourseModules(courseDirectory: string): Promise<LocalModule[]> {
// const moduleDirectories = await fs.readdir(courseDirectory, {
// withFileTypes: true,
// });
// const modulePromises = moduleDirectories
// .filter((dirent) => dirent.isDirectory())
// .map((dirent) =>
// this.loadModuleFromPath(path.join(courseDirectory, dirent.name))
// );
// const modules = await Promise.all(modulePromises);
// return modules.sort((a, b) => a.name.localeCompare(b.name));
// },
// async loadModuleFromPath(modulePath: string): Promise<LocalModule> {
// const moduleName = path.basename(modulePath);
// const assignments = await this.loadAssignmentsFromPath(modulePath);
// const quizzes = await this.loadQuizzesFromPath(modulePath);
// const pages = await this.loadModulePagesFromPath(modulePath);
// return {
// name: moduleName,
// assignments,
// quizzes,
// pages,
// };
// },
// async loadAssignmentsFromPath(
// modulePath: string
// ): Promise<LocalAssignment[]> {
// const assignmentsPath = path.join(modulePath, "assignments");
// if (!(await directoryOrFileExists(assignmentsPath))) {
// console.log(
// `Error loading course by name, assignments folder does not exist in ${modulePath}`
// );
// await fs.mkdir(assignmentsPath);
// }
// const assignmentFiles = await fs.readdir(assignmentsPath);
// const assignmentPromises = assignmentFiles.map(async (file) => {
// const filePath = path.join(assignmentsPath, file);
// const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
// /\r\n/g,
// "\n"
// );
// return localAssignmentMarkdown.parseMarkdown(rawFile);
// });
// return await Promise.all(assignmentPromises);
// },
// async loadQuizzesFromPath(modulePath: string): Promise<LocalQuiz[]> {
// const quizzesPath = path.join(modulePath, "quizzes");
// if (!(await directoryOrFileExists(quizzesPath))) {
// console.log(
// `Quizzes folder does not exist in ${modulePath}, creating now`
// );
// await fs.mkdir(quizzesPath);
// }
// const quizFiles = await fs.readdir(quizzesPath);
// const quizPromises = quizFiles.map(async (file) => {
// const filePath = path.join(quizzesPath, file);
// const rawQuiz = (await fs.readFile(filePath, "utf-8")).replace(
// /\r\n/g,
// "\n"
// );
// return localQuizMarkdownUtils.parseMarkdown(rawQuiz);
// });
// return await Promise.all(quizPromises);
// },
// async loadModulePagesFromPath(
// modulePath: string
// ): Promise<LocalCoursePage[]> {
// const pagesPath = path.join(modulePath, "pages");
// if (!(await directoryOrFileExists(pagesPath))) {
// console.log(`Pages folder does not exist in ${modulePath}, creating now`);
// await fs.mkdir(pagesPath);
// }
// const pageFiles = await fs.readdir(pagesPath);
// const pagePromises = pageFiles.map(async (file) => {
// const filePath = path.join(pagesPath, file);
// const rawPage = (await fs.readFile(filePath, "utf-8")).replace(
// /\r\n/g,
// "\n"
// );
// return localPageMarkdownUtils.parseMarkdown(rawPage);
// });
// return await Promise.all(pagePromises);
// },
};

View File

@@ -1,245 +0,0 @@
import { localAssignmentMarkdown } from "@/models/local/assignment/localAssignment";
import { LocalCourse, localCourseYamlUtils } from "@/models/local/localCourse";
import { LocalModule } from "@/models/local/localModules";
import { localPageMarkdownUtils } from "@/models/local/page/localCoursePage";
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
import { promises as fs } from "fs";
import path from "path";
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
const directoryExists = async (directoryPath: string): Promise<boolean> => {
try {
await fs.access(directoryPath);
return true;
} catch {
return false;
}
};
const saveSettings = async (course: LocalCourse, courseDirectory: string) => {
const settingsFilePath = path.join(courseDirectory, "settings.yml");
const settingsYaml = localCourseYamlUtils.settingsToYaml(course.settings);
await fs.writeFile(settingsFilePath, settingsYaml);
};
// const saveModules = async (
// course: LocalCourse,
// courseDirectory: string,
// previouslyStoredCourse?: LocalCourse
// ) => {
// for (const localModule of course.modules) {
// const moduleDirectory = path.join(courseDirectory, localModule.name);
// if (!(await directoryExists(moduleDirectory))) {
// await fs.mkdir(moduleDirectory, { recursive: true });
// }
// await saveQuizzes(course, localModule, previouslyStoredCourse);
// await saveAssignments(course, localModule, previouslyStoredCourse);
// await savePages(course, localModule, previouslyStoredCourse);
// }
// const moduleNames = course.modules.map((m) => m.name);
// const moduleDirectories = await fs.readdir(courseDirectory, {
// withFileTypes: true,
// });
// for (const dirent of moduleDirectories) {
// if (dirent.isDirectory() && !moduleNames.includes(dirent.name)) {
// const moduleDirPath = path.join(courseDirectory, dirent.name);
// console.log(
// `Deleting extra module directory, it was probably renamed ${moduleDirPath}`
// );
// await fs.rmdir(moduleDirPath, { recursive: true });
// }
// }
// };
// const saveQuizzes = async (
// course: LocalCourse,
// module: LocalModule,
// previouslyStoredCourse?: LocalCourse
// ) => {
// const quizzesDirectory = path.join(
// basePath,
// course.settings.name,
// module.name,
// "quizzes"
// );
// if (!(await directoryExists(quizzesDirectory))) {
// await fs.mkdir(quizzesDirectory, { recursive: true });
// }
// for (const quiz of module.quizzes) {
// const previousModule = previouslyStoredCourse?.modules.find(
// (m) => m.name === module.name
// );
// const previousQuiz = previousModule?.quizzes.find((q) => q === quiz);
// if (!previousQuiz) {
// const markdownPath = path.join(quizzesDirectory, `${quiz.name}.md`);
// const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
// console.log(`Saving quiz ${markdownPath}`);
// await fs.writeFile(markdownPath, quizMarkdown);
// }
// }
// await removeOldQuizzes(quizzesDirectory, module);
// };
const saveAssignments = async (
course: LocalCourse,
module: LocalModule,
previouslyStoredCourse?: LocalCourse
) => {
const assignmentsDirectory = path.join(
basePath,
course.settings.name,
module.name,
"assignments"
);
if (!(await directoryExists(assignmentsDirectory))) {
await fs.mkdir(assignmentsDirectory, { recursive: true });
}
for (const assignment of module.assignments) {
const previousModule = previouslyStoredCourse?.modules.find(
(m) => m.name === module.name
);
const previousAssignment = previousModule?.assignments.find(
(a) => a === assignment
);
if (!previousAssignment) {
const assignmentMarkdown = localAssignmentMarkdown.toMarkdown(assignment);
const filePath = path.join(assignmentsDirectory, `${assignment.name}.md`);
console.log(`Saving assignment ${filePath}`);
await fs.writeFile(filePath, assignmentMarkdown);
}
}
await removeOldAssignments(assignmentsDirectory, module);
};
const savePages = async (
course: LocalCourse,
module: LocalModule,
previouslyStoredCourse?: LocalCourse
) => {
const pagesDirectory = path.join(
basePath,
course.settings.name,
module.name,
"pages"
);
if (!(await directoryExists(pagesDirectory))) {
await fs.mkdir(pagesDirectory, { recursive: true });
}
for (const page of module.pages) {
const previousModule = previouslyStoredCourse?.modules.find(
(m) => m.name === module.name
);
const previousPage = previousModule?.pages.find((p) => p === page);
if (!previousPage) {
const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
const filePath = path.join(pagesDirectory, `${page.name}.md`);
console.log(`Saving page ${filePath}`);
await fs.writeFile(filePath, pageMarkdown);
}
}
await removeOldPages(pagesDirectory, module);
};
const removeOldQuizzes = async (
quizzesDirectory: string,
module: LocalModule
) => {
const existingFiles = await fs.readdir(quizzesDirectory);
const quizFilesToDelete = existingFiles.filter((file) => {
const quizMarkdownPath = path.join(
quizzesDirectory,
`${file.replace(".md", "")}.md`
);
return !module.quizzes.some(
(quiz) =>
path.join(quizzesDirectory, `${quiz.name}.md`) === quizMarkdownPath
);
});
for (const file of quizFilesToDelete) {
console.log(
`Removing old quiz, it has probably been renamed ${path.join(
quizzesDirectory,
file
)}`
);
await fs.unlink(path.join(quizzesDirectory, file));
}
};
const removeOldAssignments = async (
assignmentsDirectory: string,
module: LocalModule
) => {
const existingFiles = await fs.readdir(assignmentsDirectory);
const assignmentFilesToDelete = existingFiles.filter((file) => {
const assignmentMarkdownPath = path.join(
assignmentsDirectory,
`${file.replace(".md", "")}.md`
);
return !module.assignments.some(
(assignment) =>
path.join(assignmentsDirectory, `${assignment.name}.md`) ===
assignmentMarkdownPath
);
});
for (const file of assignmentFilesToDelete) {
console.log(
`Removing old assignment, it has probably been renamed ${path.join(
assignmentsDirectory,
file
)}`
);
await fs.unlink(path.join(assignmentsDirectory, file));
}
};
const removeOldPages = async (pagesDirectory: string, module: LocalModule) => {
const existingFiles = await fs.readdir(pagesDirectory);
const pageFilesToDelete = existingFiles.filter((file) => {
const pageMarkdownPath = path.join(
pagesDirectory,
`${file.replace(".md", "")}.md`
);
return !module.pages.some(
(page) =>
path.join(pagesDirectory, `${page.name}.md`) === pageMarkdownPath
);
});
for (const file of pageFilesToDelete) {
console.log(
`Removing old page, it has probably been renamed ${path.join(
pagesDirectory,
file
)}`
);
await fs.unlink(path.join(pagesDirectory, file));
}
};
// export const courseMarkdownSaver = {
// async save(course: LocalCourse, previouslyStoredCourse?: LocalCourse) {
// const courseDirectory = path.join(basePath, course.settings.name);
// if (!(await directoryExists(courseDirectory))) {
// await fs.mkdir(courseDirectory, { recursive: true });
// }
// await saveSettings(course, courseDirectory);
// await saveModules(course, courseDirectory, previouslyStoredCourse);
// },
// };

View File

@@ -0,0 +1,30 @@
import { extractLabelValue } from "@/models/local/assignment/utils/markdownUtils";
import { Lecture } from "@/models/local/lecture";
export function parseLecture(fileContent: string): Lecture {
try {
const settings = fileContent.split("---\n")[0];
const name = extractLabelValue(settings, "Name");
const date = extractLabelValue(settings, "Date");
const content = fileContent.split("---\n")[1].trim();
return {
name,
date,
content,
};
} catch (error) {
console.error("Error parsing lecture", fileContent);
throw error;
}
}
export function lectureToString(lecture: Lecture) {
return `Name: ${lecture.name}
Date: ${lecture.date}
---
${lecture.content}`;
}
export const lectureFolderName = "00 - lectures"

View File

@@ -1,8 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import { lectureToString } from "../fileStorage/utils/lectureUtils";
lectureToString, import { parseLecture } from "../fileStorage/utils/lectureUtils";
parseLecture,
} from "../fileStorage/lectureFileStorageService";
import { Lecture } from "@/models/local/lecture"; import { Lecture } from "@/models/local/lecture";
describe("can parse and stringify lectures", () => { describe("can parse and stringify lectures", () => {