feat: implement core e-learning pages, course composable, and i18n localization.
This commit is contained in:
parent
f736eb7f38
commit
e94410d0e7
9 changed files with 235 additions and 138 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue