feat: implement core e-learning pages, course composable, and i18n localization.

This commit is contained in:
supalerk-ar66 2026-02-09 11:40:41 +07:00
parent f736eb7f38
commit e94410d0e7
9 changed files with 235 additions and 138 deletions

View file

@ -7,6 +7,7 @@ export interface Course {
thumbnail_url: string
price: string
is_free: boolean
original_price?: string
have_certificate: boolean
status: string // DRAFT, PUBLISHED
category_id: number
@ -41,6 +42,15 @@ interface CourseResponse {
message: string
data: Course[]
total: number
page?: number
limit?: number
totalPages?: number
}
interface SingleCourseResponse {
code: number
message: string
data: Course
}
// Interface สำหรับคอร์สที่ผู้ใช้ลงทะเบียนเรียนแล้ว (My Course)
@ -97,12 +107,6 @@ export interface QuizResult {
attempt_id: number
}
// ==========================================
// Composable: useCourse
// หน้าที่: จัดการ Logic ทุกอย่างเกี่ยวกับคอร์สเรียน
// - ดึงข้อมูลคอร์ส (Public & Protected)
// - ลงทะเบียนเรียน (Enroll)
// - ติดตามความคืบหน้าการเรียน (Progress tracking)
// Interface สำหรับ Certificate
export interface Certificate {
certificate_id: number
@ -116,21 +120,37 @@ export interface Certificate {
}
// ==========================================
// Composable: useCourse
// หน้าที่: จัดการ Logic ทุกอย่างเกี่ยวกับคอร์สเรียน
// - ดึงข้อมูลคอร์ส (Public & Protected)
// - ลงทะเบียนเรียน (Enroll)
// - ติดตามความคืบหน้าการเรียน (Progress tracking)
export const useCourse = () => {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBase as string
const { token } = useAuth()
// ใช้ useState เพื่อเก็บรายชื่อคอร์สทั้งหมดใน Memory
// ใช้ useState เพื่อเก็บรายชื่อคอร์สทั้งหมดใน Memory (สำหรับกรณีดึงทั้งหมด)
const coursesState = useState<Course[]>('courses_cache', () => [])
const isCoursesLoaded = useState<boolean>('courses_loaded', () => false)
// ฟังก์ชันดึงรายชื่อคอร์สทั้งหมด (Catalog)
// ใช้สำหรับหน้า Discover/Browse
// Endpoint: GET /courses
const fetchCourses = async (forceRefresh = false) => {
// ถ้าโหลดไปแล้ว และไม่ได้บังคับ Refresh ให้ใช้ข้อมูลจาก State
if (isCoursesLoaded.value && !forceRefresh && coursesState.value.length > 0) {
/**
* (Catalog)
* Pagination
* Endpoint: GET /courses
*/
const fetchCourses = async (params: {
category_id?: number;
page?: number;
limit?: number;
random?: boolean;
forceRefresh?: boolean
} = {}) => {
const { forceRefresh = false, ...apiParams } = params
// ใช้ Cache เฉพาะกรณีดึง "ทั้งหมด" แบบปกติ (ไม่มี params)
const isRequestingAll = Object.keys(apiParams).length === 0
if (isRequestingAll && isCoursesLoaded.value && !forceRefresh && coursesState.value.length > 0) {
return {
success: true,
data: coursesState.value,
@ -139,9 +159,18 @@ export const useCourse = () => {
}
try {
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses`, {
// สร้าง Query String
const queryParams = new URLSearchParams()
if (apiParams.category_id) queryParams.append('category_id', apiParams.category_id.toString())
if (apiParams.page) queryParams.append('page', apiParams.page.toString())
if (apiParams.limit) queryParams.append('limit', apiParams.limit.toString())
if (apiParams.random !== undefined) queryParams.append('random', apiParams.random.toString())
const queryString = queryParams.toString()
const url = `${API_BASE_URL}/courses${queryString ? `?${queryString}` : ''}`
const data = await $fetch<CourseResponse>(url, {
method: 'GET',
// ส่ง Token ไปด้วยถ้ามี
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
@ -149,33 +178,27 @@ export const useCourse = () => {
const courses = data.data || []
// เก็บลง State
coursesState.value = courses
isCoursesLoaded.value = true
// เก็บลง State เฉพาะกรณีดึง "ทั้งหมด"
if (isRequestingAll) {
coursesState.value = courses
isCoursesLoaded.value = true
}
return {
success: true,
data: courses,
total: data.total || 0
total: data.total || 0,
page: data.page,
limit: data.limit,
totalPages: data.totalPages
}
} catch (err: any) {
console.error('Fetch courses failed:', err)
// Retry logic for 429 Too Many Requests
// Retry logic logic for 429
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)
}
await new Promise(resolve => setTimeout(resolve, 1500))
return fetchCourses(params) // Recursive retry
}
return {
@ -185,37 +208,24 @@ export const useCourse = () => {
}
}
// ฟังก์ชันดึงรายละเอียดคอร์สตาม ID
// Endpoint: GET /courses/:id
/**
* ID
* Endpoint: GET /courses/:id
*/
const fetchCourseById = async (id: number) => {
try {
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses/${id}`, {
const data = await $fetch<SingleCourseResponse>(`${API_BASE_URL}/courses/${id}`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
// Logic จัดการข้อมูลที่ได้รับ (API อาจส่งกลับมาเป็น Array หรือ Object)
let courseData: any = null
if (Array.isArray(data.data)) {
// ถ้าเป็น Array ให้หาอันที่ ID ตรงกัน
courseData = data.data.find((c: any) => c.id == id)
// Fallback: ถ้าหาไม่เจอ แต่มีข้อมูลตัวเดียว อาจจะเป็นตัวนั้น
if (!courseData && data.data.length === 1) {
courseData = data.data[0]
}
} else {
courseData = data.data
}
if (!courseData) throw new Error('Course not found')
if (!data.data) throw new Error('Course not found')
return {
success: true,
data: courseData
data: data.data // ข้อมูลคอร์สตัวเดียว
}
} catch (err: any) {
console.error('Fetch course details failed:', err)
@ -634,7 +644,18 @@ export const useCourse = () => {
}
}
/**
* Helper: แปลงข้อมูล 2 locale
*/
const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => {
if (!text) return ''
if (typeof text === 'string') return text
// @ts-ignore
return text.th || text.en || ''
}
return {
getLocalizedText,
fetchCourses,
fetchCourseById,
enrollCourse,