feat: Implement e-learning classroom with video playback, progress tracking, and quiz functionality, alongside new course and category composables and Thai localization.

This commit is contained in:
supalerk-ar66 2026-01-29 13:17:58 +07:00
parent 9232b6a21d
commit 4c575dc734
5 changed files with 570 additions and 169 deletions

View file

@ -73,6 +73,24 @@ export const useCategory = () => {
}
} catch (err: any) {
console.error('Fetch categories failed:', err)
// Retry logic for 429 Too Many Requests
if (err.statusCode === 429 || err.status === 429) {
await new Promise(resolve => setTimeout(resolve, 1500)); // Wait 1.5s
try {
const retryRes = await $fetch<CategoryResponse>(`${API_BASE_URL}/categories`, {
method: 'GET',
headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}
})
const cats = retryRes.data?.categories || []
categoriesState.value = cats
isLoaded.value = true
return { success: true, data: cats, total: retryRes.data?.total || 0 }
} catch (retryErr) {
console.error('Retry fetch categories failed:', retryErr)
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error fetching categories'

View file

@ -148,6 +148,24 @@ export const useCourse = () => {
}
} catch (err: any) {
console.error('Fetch courses failed:', err)
// Retry logic for 429 Too Many Requests
if (err.statusCode === 429 || err.status === 429) {
await new Promise(resolve => setTimeout(resolve, 1500)); // Wait 1.5s
try {
const retryData = await $fetch<CourseResponse>(`${API_BASE_URL}/courses`, {
method: 'GET',
headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}
})
const courses = retryData.data || []
coursesState.value = courses
isCoursesLoaded.value = true
return { success: true, data: courses, total: retryData.total || 0 }
} catch (retryErr) {
console.error('Retry fetch courses failed:', retryErr)
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error fetching courses'
@ -215,21 +233,23 @@ export const useCourse = () => {
} catch (err: any) {
console.error('Enroll course failed:', err)
// เช็ค Error 409 Conflict (กรณีลงทะเบียนไปแล้ว)
// เช็ค Error 409 Conflict หรือ 400 Bad Request (กรณีลงทะเบียนไปแล้ว)
// API ใหม่ส่ง 400 พร้อม error.code = "VALIDATION_ERROR" และ message "Already enrolled..."
const status = err.statusCode || err.status || err.response?.status
const errorData = err.data?.error || err.data
if (status === 409) {
if (status === 409 || (status === 400 && errorData?.message?.includes('Already enrolled'))) {
return {
success: false,
error: 'ท่านได้ลงทะเบียนไปแล้ว',
code: 409
code: 409 // Treat as conflict logic internally
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error enrolling in course',
code: err.data?.code
error: errorData?.message || err.message || 'Error enrolling in course',
code: errorData?.code || err.data?.code
}
}
}
@ -259,6 +279,26 @@ export const useCourse = () => {
}
} catch (err: any) {
console.error('Fetch enrolled courses failed:', err)
// Retry for 429
if (err.statusCode === 429 || err.status === 429) {
await new Promise(resolve => setTimeout(resolve, 1500));
try {
const queryParams = new URLSearchParams()
if (params.page) queryParams.append('page', params.page.toString())
if (params.limit) queryParams.append('limit', params.limit.toString())
if (params.status && params.status !== 'ALL') queryParams.append('status', params.status)
const retryData = await $fetch<EnrolledCourseResponse>(`${API_BASE_URL}/students/courses?${queryParams.toString()}`, {
method: 'GET',
headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}
})
return { success: true, data: retryData.data || [], total: retryData.total || 0, page: retryData.page, limit: retryData.limit }
} catch (retryErr) {
console.error('Retry fetch enrolled courses failed:', retryErr)
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error fetching enrolled courses'
@ -283,12 +323,27 @@ export const useCourse = () => {
}
} catch (err: any) {
console.error('Fetch course learning info failed:', err)
// Retry for 429
if (err.statusCode === 429 || err.status === 429) {
await new Promise(resolve => setTimeout(resolve, 1500));
try {
const retryData = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/learn`, {
method: 'GET',
headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}
})
return { success: true, data: retryData.data }
} catch (retryErr) {
console.error('Retry fetch course learning info failed:', retryErr)
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error fetching course learning info',
code: err.data?.code
}
}
}
}
// ฟังก์ชันดึงเนื้อหาบทเรียน (Video, Content)
@ -347,17 +402,25 @@ export const useCourse = () => {
// ฟังก์ชันบันทึกเวลาที่ดูวิดีโอ (Video Progress)
// Endpoint: POST /students/lessons/:id/progress
const saveVideoProgress = async (lessonId: number, progressSeconds: number, durationSeconds: number) => {
const saveVideoProgress = async (lessonId: number, progressSeconds: number, durationSeconds: number, keepalive: boolean = false) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
method: 'POST',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
// User Request: "Backend รับ header แบบ Authorization: <token> (ยังไม่ใช้ Bearer)"
// Checking existing usage, most use Bearer. I will stick to existing Bearer for consistency unless it fails,
// BUT user explicitly said "not using Bearer" in this specific prompt.
// To be safe and minimal, I will keep using `Bearer ${token}` if that's what the codebase uses globally,
// UNLESS I see strong evidence otherwise.
// The user's curl example in Step 209 DOES NOT have "Bearer".
// So I will remove it for these 2 functions as requested.
Authorization: `${token.value}`
} : {},
body: {
video_progress_seconds: progressSeconds,
video_duration_seconds: durationSeconds
}
},
keepalive: keepalive
})
return {