839 lines
24 KiB
TypeScript
839 lines
24 KiB
TypeScript
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
|
|
import type {
|
|
CourseSectionWithLessons,
|
|
CreateCourseSectionRequest,
|
|
CreateLessonComment,
|
|
CreateLessonCommentRequest,
|
|
CreateLessonRequest,
|
|
DeleteCourseSectionRequest,
|
|
DeleteLessonCommentRequest,
|
|
DeleteLessonRequest,
|
|
GetLessonCommentsRequest,
|
|
GetLessonHistoryRequest,
|
|
GetLessonLanguageRequest,
|
|
GetLessonRequest,
|
|
Lesson,
|
|
LessonComments,
|
|
PromoteUngroupedSectionRequest,
|
|
ReorderCourseSectionsRequest,
|
|
ReorderLessonsRequest,
|
|
UpdateCourseSectionRequest,
|
|
UpdateLessonCommentRequest,
|
|
UpdateLessonCompletionRequest,
|
|
UpdateLessonRequest
|
|
} from '../utils/types';
|
|
import type {
|
|
TCourseSectionCreate,
|
|
TCourseSectionPromoteUngrouped,
|
|
TCourseSectionReorder,
|
|
TCourseSectionUpdate
|
|
} from '@cio/utils/validation/course/section';
|
|
import type { TLessonCreate, TLessonReorder, TLessonUpdate } from '@cio/utils/validation/lesson';
|
|
import {
|
|
ZCourseSectionCreate,
|
|
ZCourseSectionPromoteUngrouped,
|
|
ZCourseSectionReorder,
|
|
ZCourseSectionUpdate
|
|
} from '@cio/utils/validation/course/section';
|
|
import {
|
|
ZLessonCommentCreate,
|
|
ZLessonCommentUpdate,
|
|
ZLessonCreate,
|
|
ZLessonReorder,
|
|
ZLessonUpdate
|
|
} from '@cio/utils/validation/lesson';
|
|
|
|
import type { TLocale } from '@cio/db/types';
|
|
import { get } from 'svelte/store';
|
|
import { mapZodErrorsToTranslations } from '$lib/utils/validation';
|
|
import { mediaApi } from '$features/media/api';
|
|
import { profile } from '$lib/utils/store/user';
|
|
import { snackbar } from '$features/ui/snackbar/store';
|
|
|
|
/**
|
|
* API class for lesson operations
|
|
*/
|
|
export class LessonApi extends BaseApiWithErrors {
|
|
lesson = $state<Lesson | null>(null);
|
|
sections = $state<CourseSectionWithLessons[]>([]);
|
|
commentsByLessonId = $state<
|
|
Record<
|
|
string,
|
|
{
|
|
items: LessonComments;
|
|
totalCount: number;
|
|
hasMore: boolean;
|
|
isLoading: boolean;
|
|
cursor: string | null;
|
|
}
|
|
>
|
|
>({});
|
|
currentLocale = $state<TLocale>('en');
|
|
isSaving = $state(false);
|
|
isDirty = $state(false);
|
|
isCommenting = $state(false);
|
|
isUpdatingComment = $state(false);
|
|
|
|
translations = $state<Record<string, Record<TLocale, string>>>({});
|
|
note = $state('');
|
|
|
|
/**
|
|
* Gets a lesson by ID
|
|
*/
|
|
async get(courseId: string, lessonId: string) {
|
|
return this.execute<GetLessonRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].$get({
|
|
param: { courseId, lessonId }
|
|
}),
|
|
logContext: 'fetching lesson',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
this.lesson = response.data;
|
|
|
|
if (this.lesson.lessonLanguages) {
|
|
this.setTranslations();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a new lesson
|
|
*/
|
|
async create(courseId: string, fields: TLessonCreate) {
|
|
const result = ZLessonCreate.safeParse({ ...fields, courseId });
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
await this.execute<CreateLessonRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson.$post({
|
|
param: { courseId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'creating lesson',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
const createdLesson = response.data as Lesson;
|
|
this.lesson = {
|
|
...createdLesson,
|
|
lessonLanguages: createdLesson.lessonLanguages ?? []
|
|
};
|
|
snackbar.success('snackbar.lessons.lesson_created');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.lesson_create_failed');
|
|
return;
|
|
}
|
|
if ('error' in result && 'field' in result && result.field) {
|
|
this.errors[result.field] = result.error;
|
|
snackbar.error(result.error);
|
|
} else if ('error' in result) {
|
|
this.errors.general = result.error;
|
|
snackbar.error(result.error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates a lesson
|
|
*/
|
|
async update(courseId: string, lessonId: string, fields: TLessonUpdate): Promise<boolean> {
|
|
const result = ZLessonUpdate.safeParse(fields);
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return false;
|
|
}
|
|
|
|
this.isSaving = true;
|
|
|
|
const response = await this.execute<UpdateLessonRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].$put({
|
|
param: { courseId, lessonId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'updating lesson',
|
|
onError: (result) => {
|
|
console.log('onError', result);
|
|
snackbar.error('snackbar.lessons.lesson_update_failed');
|
|
}
|
|
});
|
|
|
|
this.isSaving = false;
|
|
return Boolean(response);
|
|
}
|
|
|
|
/**
|
|
* Deletes a lesson
|
|
*/
|
|
async delete(courseId: string, lessonId: string) {
|
|
await this.execute<DeleteLessonRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].$delete({
|
|
param: { courseId, lessonId }
|
|
}),
|
|
logContext: 'deleting lesson',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
snackbar.success('snackbar.lessons.lesson_deleted');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.lesson_delete_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Course Section methods
|
|
|
|
/**
|
|
* Creates a course section
|
|
*/
|
|
async createSection(courseId: string, fields: TCourseSectionCreate) {
|
|
const result = ZCourseSectionCreate.safeParse({ ...fields, courseId });
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
await this.execute<CreateCourseSectionRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].section.$post({
|
|
param: { courseId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'creating course section',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
snackbar.success('snackbar.lessons.section_created');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.section_create_failed');
|
|
return;
|
|
}
|
|
if ('error' in result && 'field' in result && result.field) {
|
|
this.errors[result.field] = result.error;
|
|
snackbar.error(result.error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a real section from synthetic ungrouped content and moves all ungrouped items.
|
|
*/
|
|
async promoteUngroupedSection(courseId: string, fields: TCourseSectionPromoteUngrouped) {
|
|
const result = ZCourseSectionPromoteUngrouped.safeParse(fields);
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
await this.execute<PromoteUngroupedSectionRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].section['promote-ungrouped'].$post({
|
|
param: { courseId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'promoting ungrouped section',
|
|
onSuccess: () => {
|
|
snackbar.success('snackbar.lessons.section_updated');
|
|
this.success = true;
|
|
this.errors = {};
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.section_update_failed');
|
|
return;
|
|
}
|
|
if ('error' in result && 'field' in result && result.field) {
|
|
this.errors[result.field] = result.error;
|
|
snackbar.error(result.error);
|
|
return;
|
|
}
|
|
if ('error' in result) {
|
|
this.errors.general = result.error;
|
|
snackbar.error(result.error);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates a course section
|
|
*/
|
|
async updateSection(courseId: string, sectionId: string, fields: TCourseSectionUpdate) {
|
|
const result = ZCourseSectionUpdate.safeParse(fields);
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
await this.execute<UpdateCourseSectionRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].section[':sectionId'].$put({
|
|
param: { courseId, sectionId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'updating course section',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
snackbar.success('snackbar.lessons.section_updated');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.section_update_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Deletes a course section
|
|
*/
|
|
async deleteSection(courseId: string, sectionId: string) {
|
|
await this.execute<DeleteCourseSectionRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].section[':sectionId'].$delete({
|
|
param: { courseId, sectionId }
|
|
}),
|
|
logContext: 'deleting course section'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reorders course sections
|
|
*/
|
|
async reorderSections(courseId: string, sections: TCourseSectionReorder['sections']) {
|
|
const result = ZCourseSectionReorder.safeParse({ sections });
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
await this.execute<ReorderCourseSectionsRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].section.reorder.$post({
|
|
param: { courseId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'reordering course sections',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
snackbar.success('snackbar.lessons.sections_reordered');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.sections_reorder_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reorders lessons
|
|
*/
|
|
async reorderLessons(courseId: string, lessons: TLessonReorder['lessons']) {
|
|
const result = ZLessonReorder.safeParse({ lessons });
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
await this.execute<ReorderLessonsRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson.reorder.$post({
|
|
param: { courseId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'reordering lessons',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
snackbar.success('snackbar.lessons.lessons_reordered');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.lessons_reorder_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Lesson Comment methods
|
|
|
|
/**
|
|
* Initializes comments state for a lesson
|
|
*/
|
|
initComments(lessonId: string, totalCount: number) {
|
|
if (!this.commentsByLessonId[lessonId]) {
|
|
this.commentsByLessonId[lessonId] = {
|
|
items: [],
|
|
totalCount,
|
|
hasMore: totalCount > 0,
|
|
isLoading: false,
|
|
cursor: null
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets comments for a lesson
|
|
*/
|
|
async getComments(courseId: string, lessonId: string, limit = 10) {
|
|
if (!this.commentsByLessonId[lessonId]) {
|
|
this.initComments(lessonId, 0);
|
|
}
|
|
this.commentsByLessonId[lessonId].isLoading = true;
|
|
|
|
await this.execute<GetLessonCommentsRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].comment.$get({
|
|
param: { courseId, lessonId },
|
|
query: { limit: String(limit) }
|
|
}),
|
|
logContext: 'fetching lesson comments',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
this.commentsByLessonId[lessonId] = {
|
|
items: response.data.items,
|
|
totalCount: response.data.totalCount,
|
|
hasMore: response.data.hasMore,
|
|
isLoading: false,
|
|
cursor: response.data.nextCursor
|
|
};
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
this.commentsByLessonId[lessonId].isLoading = false;
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.comment_fetch_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load more comments (uses cursor)
|
|
*/
|
|
async loadMoreComments(courseId: string, lessonId: string, limit = 10) {
|
|
const commentState = this.commentsByLessonId[lessonId];
|
|
if (!commentState || !commentState.hasMore || commentState.isLoading || !commentState.cursor) {
|
|
return;
|
|
}
|
|
|
|
commentState.isLoading = true;
|
|
|
|
await this.execute<GetLessonCommentsRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].comment.$get({
|
|
param: { courseId, lessonId },
|
|
query: { cursor: commentState.cursor!, limit: String(limit) }
|
|
}),
|
|
logContext: 'loading more lesson comments',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
this.commentsByLessonId[lessonId] = {
|
|
items: [...commentState.items, ...response.data.items],
|
|
totalCount: response.data.totalCount,
|
|
hasMore: response.data.hasMore,
|
|
isLoading: false,
|
|
cursor: response.data.nextCursor
|
|
};
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
commentState.isLoading = false;
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.load_more_comments_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a lesson comment
|
|
*/
|
|
async createComment(courseId: string, lessonId: string, comment: string) {
|
|
if (!courseId || !lessonId) {
|
|
snackbar.error('snackbar.lessons.comment_add_failed');
|
|
return;
|
|
}
|
|
|
|
const result = ZLessonCommentCreate.safeParse({ lessonId, comment });
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
this.isCommenting = true;
|
|
try {
|
|
await this.execute<CreateLessonCommentRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].comment.$post({
|
|
param: { courseId, lessonId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'creating lesson comment',
|
|
onSuccess: (response) => {
|
|
if (response.data && this.commentsByLessonId[lessonId]) {
|
|
this.commentsByLessonId[lessonId].items = [
|
|
this.getCommentFromResponse(response.data),
|
|
...this.commentsByLessonId[lessonId].items
|
|
];
|
|
this.commentsByLessonId[lessonId].totalCount++;
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.comment_add_failed');
|
|
}
|
|
}
|
|
});
|
|
} finally {
|
|
this.isCommenting = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a lesson comment
|
|
*/
|
|
async updateComment(courseId: string, lessonId: string, commentId: string, comment: string) {
|
|
const result = ZLessonCommentUpdate.safeParse({ comment });
|
|
if (!result.success) {
|
|
this.errors = mapZodErrorsToTranslations(result.error, 'lesson');
|
|
return;
|
|
}
|
|
|
|
this.success = false;
|
|
this.isUpdatingComment = true;
|
|
|
|
try {
|
|
await this.execute<UpdateLessonCommentRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson.comment[':commentId'].$put({
|
|
param: { courseId, commentId },
|
|
json: result.data
|
|
}),
|
|
logContext: 'updating lesson comment',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
// Update local state
|
|
if (this.commentsByLessonId[lessonId]) {
|
|
this.commentsByLessonId[lessonId].items = this.commentsByLessonId[lessonId].items.map((c) =>
|
|
c.id === Number(commentId) ? this.getCommentFromResponse(response.data) : c
|
|
);
|
|
}
|
|
snackbar.success('snackbar.lessons.comment_updated');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.comment_update_failed');
|
|
}
|
|
}
|
|
});
|
|
} finally {
|
|
this.isUpdatingComment = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes a lesson comment
|
|
*/
|
|
async deleteComment(courseId: string, lessonId: string, commentId: string) {
|
|
await this.execute<DeleteLessonCommentRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson.comment[':commentId'].$delete({
|
|
param: { courseId, commentId }
|
|
}),
|
|
logContext: 'deleting lesson comment',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
// Update local state
|
|
if (this.commentsByLessonId[lessonId]) {
|
|
this.commentsByLessonId[lessonId].items = this.commentsByLessonId[lessonId].items.filter(
|
|
(c) => c.id !== Number(commentId)
|
|
);
|
|
this.commentsByLessonId[lessonId].totalCount--;
|
|
}
|
|
snackbar.success('snackbar.lessons.comment_deleted');
|
|
this.success = true;
|
|
this.errors = {};
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.comment_delete_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates lesson completion
|
|
*/
|
|
async updateCompletion(courseId: string, lessonId: string, isComplete: boolean) {
|
|
return this.execute<UpdateLessonCompletionRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].completion.$put({
|
|
param: { courseId, lessonId },
|
|
json: { isComplete }
|
|
}),
|
|
logContext: 'updating lesson completion',
|
|
onSuccess: (response) => {
|
|
if (response.data) {
|
|
// Update the current lesson if it's the one being updated
|
|
if (this.lesson?.id === lessonId) {
|
|
this.lesson = { ...this.lesson, isComplete };
|
|
}
|
|
}
|
|
},
|
|
onError: (result) => {
|
|
if (typeof result === 'string') {
|
|
snackbar.error('snackbar.lessons.completion_update_failed');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets translations for a lesson
|
|
* Translations are stored separately from the lesson data
|
|
*/
|
|
setTranslations() {
|
|
if (!this.lesson) return;
|
|
|
|
const translations: Record<TLocale, string> = this.lesson?.lessonLanguages?.reduce(
|
|
(acc, cur) => {
|
|
if (cur.locale) {
|
|
acc[cur.locale] = cur.content || '';
|
|
}
|
|
return acc;
|
|
},
|
|
{} as Record<TLocale, string>
|
|
);
|
|
|
|
this.translations = {
|
|
...this.translations,
|
|
[this.lesson.id]: translations
|
|
};
|
|
}
|
|
|
|
updateTranslation(lessonId: string, locale: TLocale, content: string) {
|
|
this.translations = {
|
|
...this.translations,
|
|
[lessonId]: {
|
|
...this.translations[lessonId],
|
|
[locale]: content
|
|
}
|
|
};
|
|
}
|
|
|
|
getCommentFromResponse(response: CreateLessonComment): LessonComments[number] {
|
|
const userProfile = get(profile);
|
|
|
|
return {
|
|
id: response.id,
|
|
createdAt: response.createdAt,
|
|
updatedAt: response.updatedAt,
|
|
lessonId: response.lessonId,
|
|
groupmemberId: response.groupmemberId,
|
|
comment: response.comment,
|
|
groupmember: {
|
|
id: response.groupmemberId!
|
|
},
|
|
profile: {
|
|
fullname: userProfile.fullname,
|
|
avatarUrl: userProfile.avatarUrl
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deletes a video from lesson materials
|
|
*/
|
|
async deleteLessonVideo(videoIndex: number) {
|
|
if (!this.lesson) return;
|
|
|
|
const videos = Array.isArray(this.lesson.videos) ? [...this.lesson.videos] : [];
|
|
const removedVideo = videos[videoIndex] as { assetId?: string } | undefined;
|
|
|
|
if (removedVideo?.assetId && this.lesson.id) {
|
|
await mediaApi.detachAsset(removedVideo.assetId, {
|
|
targetType: 'lesson',
|
|
targetId: this.lesson.id,
|
|
slotType: 'lesson_video',
|
|
position: videoIndex
|
|
});
|
|
}
|
|
|
|
this.lesson = {
|
|
...this.lesson,
|
|
videos: videos.filter((_v, i) => i !== videoIndex)
|
|
};
|
|
this.isDirty = true;
|
|
}
|
|
|
|
/**
|
|
* Deletes a document from lesson materials
|
|
*/
|
|
async deleteLessonDocument(documentIndex: number) {
|
|
if (!this.lesson) return;
|
|
|
|
const documents = Array.isArray(this.lesson.documents) ? [...this.lesson.documents] : [];
|
|
const removedDocument = documents[documentIndex] as { assetId?: string } | undefined;
|
|
|
|
if (removedDocument?.assetId && this.lesson.id) {
|
|
await mediaApi.detachAsset(removedDocument.assetId, {
|
|
targetType: 'lesson',
|
|
targetId: this.lesson.id,
|
|
slotType: 'lesson_document',
|
|
position: documentIndex
|
|
});
|
|
}
|
|
|
|
this.lesson = {
|
|
...this.lesson,
|
|
documents: documents.filter((_d, i) => i !== documentIndex)
|
|
};
|
|
this.isDirty = true;
|
|
}
|
|
|
|
/**
|
|
* Generic method to update lesson state
|
|
* @param field - The field name to update (type-safe)
|
|
* @param value - The new value for the field (or array of items to append)
|
|
* @param options - Optional configuration for array operations
|
|
*/
|
|
updateLessonState<K extends keyof Lesson>(
|
|
field: K,
|
|
value: Lesson[K] | (Lesson[K] extends Array<infer T> ? T | T[] : never),
|
|
options?: { append?: boolean }
|
|
) {
|
|
if (!this.lesson) return;
|
|
|
|
// Handle array fields with append option
|
|
if (options?.append && Array.isArray(this.lesson[field])) {
|
|
const existingArray = this.lesson[field];
|
|
const newItems = Array.isArray(value) ? value : [value];
|
|
|
|
this.lesson = {
|
|
...this.lesson,
|
|
[field]: [...existingArray, ...newItems] as Lesson[K]
|
|
};
|
|
} else {
|
|
// Direct field replacement
|
|
this.lesson = {
|
|
...this.lesson,
|
|
[field]: value as Lesson[K]
|
|
};
|
|
}
|
|
this.isDirty = true;
|
|
}
|
|
|
|
/**
|
|
* Gets lesson version history for a lesson and locale
|
|
* @param courseId Course ID
|
|
* @param lessonId Lesson ID
|
|
* @param locale Locale
|
|
* @param endRange End range for pagination (0-indexed, inclusive)
|
|
* @returns Lesson history data or null on error
|
|
*/
|
|
async getHistory(courseId: string, lessonId: string, locale: TLocale, endRange: number) {
|
|
return this.execute<GetLessonHistoryRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId']['lesson'][':lessonId']['history'].$get({
|
|
param: { courseId, lessonId },
|
|
query: { locale, endRange: endRange.toString() }
|
|
}),
|
|
logContext: 'fetching lesson history'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates or updates a lesson language translation (upsert)
|
|
* @param courseId Course ID
|
|
* @param lessonId Lesson ID
|
|
* @param locale Locale
|
|
* @param content Translation content
|
|
*/
|
|
async upsertLanguage(courseId: string, lessonId: string, locale: TLocale, content: string) {
|
|
return this.execute<GetLessonLanguageRequest>({
|
|
requestFn: () =>
|
|
classroomio.course[':courseId'].lesson[':lessonId'].language.$post({
|
|
param: { courseId, lessonId },
|
|
json: { locale, content }
|
|
}),
|
|
logContext: 'upserting lesson language',
|
|
onSuccess: () => {
|
|
this.success = true;
|
|
this.errors = {};
|
|
},
|
|
onError: () => {
|
|
snackbar.error('snackbar.materials.update_translations');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Saves the current lesson state (materials and translations)
|
|
* @param courseId Course ID
|
|
* @param lessonId Lesson ID
|
|
*/
|
|
async save(courseId: string, lessonId: string) {
|
|
if (!this.lesson) return false;
|
|
|
|
// Prevent autosave loops: we only set this back to `true` when the user edits again.
|
|
this.isDirty = false;
|
|
|
|
await Promise.all([
|
|
this.update(courseId, lessonId, {
|
|
slideUrl: this.lesson.slideUrl || undefined,
|
|
videos: this.lesson.videos || [],
|
|
documents: this.lesson.documents || []
|
|
}),
|
|
this.upsertLanguage(
|
|
courseId,
|
|
lessonId,
|
|
this.currentLocale,
|
|
this.translations[lessonId]?.[this.currentLocale] || ''
|
|
)
|
|
]);
|
|
}
|
|
}
|
|
|
|
export const lessonApi = new LessonApi();
|