135 lines
3.5 KiB
TypeScript
Executable File
135 lines
3.5 KiB
TypeScript
Executable File
import type { Course, CourseLevel, Lesson } from "@/types/course";
|
|
|
|
const STORAGE_KEY = "acve.teacher-courses.v1";
|
|
const TEACHER_UPDATED_EVENT = "acve:teacher-courses-updated";
|
|
|
|
export type TeacherCourseInput = {
|
|
title: string;
|
|
level: CourseLevel;
|
|
summary: string;
|
|
instructor: string;
|
|
weeks: number;
|
|
};
|
|
|
|
export type TeacherCoursePatch = Partial<Omit<Course, "slug" | "lessons">> & {
|
|
summary?: string;
|
|
title?: string;
|
|
level?: CourseLevel;
|
|
instructor?: string;
|
|
weeks?: number;
|
|
};
|
|
|
|
const isBrowser = () => typeof window !== "undefined";
|
|
|
|
const readStore = (): Course[] => {
|
|
if (!isBrowser()) return [];
|
|
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return [];
|
|
|
|
try {
|
|
return JSON.parse(raw) as Course[];
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const writeStore = (courses: Course[]) => {
|
|
if (!isBrowser()) return;
|
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(courses));
|
|
window.dispatchEvent(new Event(TEACHER_UPDATED_EVENT));
|
|
};
|
|
|
|
const slugify = (value: string) =>
|
|
value
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
.replace(/\s+/g, "-")
|
|
.replace(/-+/g, "-");
|
|
|
|
const lessonId = (index: number) => `lesson-${index}`;
|
|
|
|
const normalizeCourse = (course: Course): Course => ({
|
|
...course,
|
|
lessonsCount: course.lessons.length,
|
|
});
|
|
|
|
export const teacherCoursesUpdatedEventName = TEACHER_UPDATED_EVENT;
|
|
|
|
export const getTeacherCourses = (): Course[] => readStore().map(normalizeCourse);
|
|
|
|
export const getTeacherCourseBySlug = (slug: string): Course | undefined =>
|
|
getTeacherCourses().find((course) => course.slug === slug);
|
|
|
|
export const ensureUniqueTeacherCourseSlug = (title: string, reservedSlugs: string[]) => {
|
|
const baseSlug = slugify(title) || "untitled-course";
|
|
let slug = baseSlug;
|
|
let index = 2;
|
|
|
|
const pool = new Set(reservedSlugs.map((item) => item.toLowerCase()));
|
|
while (pool.has(slug.toLowerCase())) {
|
|
slug = `${baseSlug}-${index}`;
|
|
index += 1;
|
|
}
|
|
|
|
return slug;
|
|
};
|
|
|
|
export const createTeacherCourse = (input: TeacherCourseInput, reservedSlugs: string[]) => {
|
|
const courses = getTeacherCourses();
|
|
const slug = ensureUniqueTeacherCourseSlug(input.title, reservedSlugs);
|
|
|
|
const starterLesson: Lesson = {
|
|
id: lessonId(1),
|
|
title: "Course introduction",
|
|
type: "video",
|
|
minutes: 8,
|
|
isPreview: true,
|
|
videoUrl: "https://example.com/video-placeholder",
|
|
};
|
|
|
|
const next: Course = {
|
|
slug,
|
|
title: input.title,
|
|
level: input.level,
|
|
status: "Draft",
|
|
summary: input.summary,
|
|
rating: 5,
|
|
weeks: Math.max(1, input.weeks),
|
|
lessonsCount: 1,
|
|
students: 0,
|
|
instructor: input.instructor,
|
|
lessons: [starterLesson],
|
|
};
|
|
|
|
writeStore([...courses, next]);
|
|
return next;
|
|
};
|
|
|
|
export const updateTeacherCourse = (slug: string, patch: TeacherCoursePatch) => {
|
|
const courses = getTeacherCourses();
|
|
const nextCourses = courses.map((course) =>
|
|
course.slug === slug ? normalizeCourse({ ...course, ...patch }) : course,
|
|
);
|
|
writeStore(nextCourses);
|
|
return nextCourses.find((course) => course.slug === slug);
|
|
};
|
|
|
|
export const addLessonToTeacherCourse = (slug: string, lesson: Omit<Lesson, "id">) => {
|
|
const courses = getTeacherCourses();
|
|
const nextCourses = courses.map((course) => {
|
|
if (course.slug !== slug) return course;
|
|
const nextLesson: Lesson = {
|
|
...lesson,
|
|
id: lessonId(course.lessons.length + 1),
|
|
};
|
|
return normalizeCourse({
|
|
...course,
|
|
lessons: [...course.lessons, nextLesson],
|
|
});
|
|
});
|
|
|
|
writeStore(nextCourses);
|
|
return nextCourses.find((course) => course.slug === slug);
|
|
};
|