feat: Implement instructor course structure management, enabling drag-and-drop reordering of chapters and lessons, and adding support for video and quiz lesson types.
This commit is contained in:
parent
be7348c74d
commit
310a5e7dd7
4 changed files with 1085 additions and 3 deletions
|
|
@ -417,9 +417,179 @@ export const instructorService = {
|
|||
}
|
||||
|
||||
await authRequest(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/reorder`,
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder-lessons`,
|
||||
{ method: 'PUT', body: { lesson_id: lessonId, sort_order: sortOrder } }
|
||||
);
|
||||
},
|
||||
|
||||
// Question CRUD
|
||||
async createQuestion(courseId: number, chapterId: number, lessonId: number, data: CreateQuestionRequest): Promise<QuizQuestionResponse> {
|
||||
const config = useRuntimeConfig();
|
||||
const useMockData = config.public.useMockData as boolean;
|
||||
|
||||
if (useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return {
|
||||
id: Date.now(),
|
||||
quiz_id: 1,
|
||||
question: data.question,
|
||||
explanation: data.explanation || null,
|
||||
question_type: data.question_type,
|
||||
score: data.score,
|
||||
sort_order: data.sort_order || 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
choices: data.choices.map((c, i) => ({
|
||||
id: Date.now() + i,
|
||||
question_id: Date.now(),
|
||||
text: c.text,
|
||||
is_correct: c.is_correct,
|
||||
sort_order: c.sort_order || i + 1
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
const response = await authRequest<{ code: number; data: QuizQuestionResponse }>(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions`,
|
||||
{ method: 'POST', body: data }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise<QuizQuestionResponse> {
|
||||
const config = useRuntimeConfig();
|
||||
const useMockData = config.public.useMockData as boolean;
|
||||
|
||||
if (useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return {
|
||||
id: questionId,
|
||||
quiz_id: 1,
|
||||
question: data.question,
|
||||
explanation: data.explanation || null,
|
||||
question_type: data.question_type,
|
||||
score: data.score,
|
||||
sort_order: data.sort_order || 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
choices: data.choices.map((c, i) => ({
|
||||
id: Date.now() + i,
|
||||
question_id: questionId,
|
||||
text: c.text,
|
||||
is_correct: c.is_correct,
|
||||
sort_order: c.sort_order || i + 1
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
const response = await authRequest<{ code: number; data: QuizQuestionResponse }>(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
||||
{ method: 'PUT', body: data }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise<void> {
|
||||
const config = useRuntimeConfig();
|
||||
const useMockData = config.public.useMockData as boolean;
|
||||
|
||||
if (useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return;
|
||||
}
|
||||
|
||||
await authRequest(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
},
|
||||
|
||||
// Video Upload
|
||||
async uploadLessonVideo(courseId: number, chapterId: number, lessonId: number, video: File, attachments?: File[]): Promise<LessonResponse> {
|
||||
const config = useRuntimeConfig();
|
||||
const useMockData = config.public.useMockData as boolean;
|
||||
|
||||
if (useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('video', video);
|
||||
if (attachments) {
|
||||
attachments.forEach(file => {
|
||||
formData.append('attachments', file);
|
||||
});
|
||||
}
|
||||
|
||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
||||
{ method: 'POST', body: formData }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateLessonVideo(courseId: number, chapterId: number, lessonId: number, video?: File, attachments?: File[]): Promise<LessonResponse> {
|
||||
const config = useRuntimeConfig();
|
||||
const useMockData = config.public.useMockData as boolean;
|
||||
|
||||
if (useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
if (video) {
|
||||
formData.append('video', video);
|
||||
}
|
||||
if (attachments) {
|
||||
attachments.forEach(file => {
|
||||
formData.append('attachments', file);
|
||||
});
|
||||
}
|
||||
|
||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
||||
{ method: 'PUT', body: formData }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Attachments
|
||||
async addAttachments(courseId: number, chapterId: number, lessonId: number, files: File[]): Promise<LessonResponse> {
|
||||
const config = useRuntimeConfig();
|
||||
const useMockData = config.public.useMockData as boolean;
|
||||
|
||||
if (useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
files.forEach(file => {
|
||||
formData.append('attachments', file);
|
||||
});
|
||||
|
||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments`,
|
||||
{ method: 'POST', body: formData }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteAttachment(courseId: number, chapterId: number, lessonId: number, attachmentId: number): Promise<void> {
|
||||
const config = useRuntimeConfig();
|
||||
const useMockData = config.public.useMockData as boolean;
|
||||
|
||||
if (useMockData) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return;
|
||||
}
|
||||
|
||||
await authRequest(
|
||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments/${attachmentId}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -472,9 +642,32 @@ export interface LessonResponse {
|
|||
is_published: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
video_url: string | null;
|
||||
attachments: AttachmentResponse[];
|
||||
quiz: QuizResponse | null;
|
||||
}
|
||||
|
||||
export interface QuizChoiceResponse {
|
||||
id: number;
|
||||
question_id: number;
|
||||
text: { en: string; th: string };
|
||||
is_correct: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
export interface QuizQuestionResponse {
|
||||
id: number;
|
||||
quiz_id: number;
|
||||
question: { en: string; th: string };
|
||||
explanation: { en: string; th: string } | null;
|
||||
question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE';
|
||||
score: number;
|
||||
sort_order: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
choices: QuizChoiceResponse[];
|
||||
}
|
||||
|
||||
export interface QuizResponse {
|
||||
id: number;
|
||||
lesson_id: number;
|
||||
|
|
@ -485,6 +678,9 @@ export interface QuizResponse {
|
|||
shuffle_questions: boolean;
|
||||
shuffle_choices: boolean;
|
||||
show_answers_after_completion: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
questions?: QuizQuestionResponse[];
|
||||
}
|
||||
|
||||
export interface CreateChapterRequest {
|
||||
|
|
@ -494,6 +690,20 @@ export interface CreateChapterRequest {
|
|||
is_published?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateChoiceRequest {
|
||||
text: { th: string; en: string };
|
||||
is_correct: boolean;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
export interface CreateQuestionRequest {
|
||||
question: { th: string; en: string };
|
||||
explanation?: { th: string; en: string } | null;
|
||||
question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE';
|
||||
sort_order?: number;
|
||||
choices: CreateChoiceRequest[];
|
||||
}
|
||||
|
||||
export interface CreateLessonRequest {
|
||||
title: { th: string; en: string };
|
||||
content?: { th: string; en: string } | null;
|
||||
|
|
@ -550,6 +760,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = {
|
|||
is_published: true,
|
||||
created_at: '2024-01-15T00:00:00Z',
|
||||
updated_at: '2024-01-15T00:00:00Z',
|
||||
video_url: null,
|
||||
attachments: [],
|
||||
quiz: null
|
||||
},
|
||||
{
|
||||
|
|
@ -566,6 +778,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = {
|
|||
is_published: true,
|
||||
created_at: '2024-01-15T00:00:00Z',
|
||||
updated_at: '2024-01-15T00:00:00Z',
|
||||
video_url: null,
|
||||
attachments: [],
|
||||
quiz: null
|
||||
},
|
||||
{
|
||||
|
|
@ -582,6 +796,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = {
|
|||
is_published: true,
|
||||
created_at: '2024-01-15T00:00:00Z',
|
||||
updated_at: '2024-01-15T00:00:00Z',
|
||||
video_url: null,
|
||||
attachments: [],
|
||||
quiz: {
|
||||
id: 1,
|
||||
lesson_id: 3,
|
||||
|
|
@ -620,6 +836,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = {
|
|||
is_published: true,
|
||||
created_at: '2024-01-15T00:00:00Z',
|
||||
updated_at: '2024-01-15T00:00:00Z',
|
||||
video_url: null,
|
||||
attachments: [],
|
||||
quiz: null
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue