feat: Implement initial e-learning platform frontend structure including dashboard, course management, authentication, and common UI components.

This commit is contained in:
supalerk-ar66 2026-02-27 10:05:33 +07:00
parent aceeb80d9a
commit ad11c6b7c5
44 changed files with 720 additions and 578 deletions

View file

@ -1,22 +1,27 @@
/**
* @composable useMediaPrefs
* @description (Mute) /
* <video>
*/
export const useMediaPrefs = () => {
// 1. Global State
// ใช้ useState เพื่อแชร์ค่าเดียวกันทั่วทั้ง App (เช่น เปลี่ยนหน้าแล้วเสียงยังเท่าเดิม)
// 1. สถานะส่วนกลาง (Global State)
// ใช้ useState เพื่อแชร์ค่าเดียวกันทั่วหน้าเว็บ (เช่น เปลี่ยนหน้าแล้วระดับเสียงยังคงที่)
const volume = useState<number>('media_prefs_volume', () => 1)
const muted = useState<boolean>('media_prefs_muted', () => false)
const { user } = useAuth()
// 2. Storage Key Helper (User Specific)
// 2. ฟังก์ชันช่วยสร้าง Key สำหรับ Storage (เก็บแยกตาม User)
const getStorageKey = () => {
const userId = user.value?.id || 'guest'
return `media:prefs:v1:${userId}`
}
// 3. Save Logic (Throttled)
// 3. ระบบบันทึกการตั้งค่าลงเบราว์เซอร์ (Throttled เพื่อไม่ให้บันทึกถี่เกินไป)
let saveTimeout: ReturnType<typeof setTimeout> | null = null
const save = () => {
if (import.meta.server) return
if (import.meta.server) return // เลี่ยงไม่ได้ต้องทำงานบนฝั่ง Client เท่านั้น
if (saveTimeout) clearTimeout(saveTimeout)
saveTimeout = setTimeout(() => {
@ -29,12 +34,12 @@ export const useMediaPrefs = () => {
}
localStorage.setItem(key, JSON.stringify(data))
} catch (e) {
console.error('Failed to save media prefs', e)
console.error('ไม่สามารถบันทึกการตั้งค่าสื่อได้', e)
}
}, 500) // Throttle 500ms
}, 500) // หน่วงเวลา 500ms
}
// 4. Load Logic
// 4. ระบบโหลดการตั้งค่าเก่าขึ้นมา (Load Logic)
const load = () => {
if (import.meta.server) return
@ -51,20 +56,20 @@ export const useMediaPrefs = () => {
}
}
} catch (e) {
console.error('Failed to load media prefs', e)
console.error('ไม่สามารถโหลดการตั้งค่าสื่อได้', e)
}
}
// 5. Setters (With Logic)
// 5. ฟังก์ชันสำหรับอัปเดตและสั่งบันทึกการตั้งค่า (Setters)
const setVolume = (val: number) => {
const clamped = Math.max(0, Math.min(1, val))
volume.value = clamped
// Auto unmute if volume increased from 0
// ยกเลิกปิดเสียงอัตโนมัติ ถ้าระดับเสียงเพิ่มขึ้นจาก 0
if (clamped > 0 && muted.value) {
muted.value = false
}
// Auto mute if volume set to 0
// ปิดเสียงอัตโนมัติ ถ้าระดับเสียงกลายเป็น 0
if (clamped === 0 && !muted.value) {
muted.value = true
}
@ -75,7 +80,7 @@ export const useMediaPrefs = () => {
const setMuted = (val: boolean) => {
muted.value = val
// Logic: Unmuting should restore volume if it was 0
// หากผู้ใช้กดยกเลิกการปิดเสียงขณะที่ระดับเสียงเคยเป็น 0 ควรตั้งค่าเริ่มต้นให้เป็น 1
if (!val && volume.value === 0) {
volume.value = 1
}
@ -83,15 +88,15 @@ export const useMediaPrefs = () => {
save()
}
// 6. Apply & Bind to Element (The Magic)
// 6. ฟังก์ชันจับคู่ใช้กับการเล่นสื่อ (ตย. <video ref="videoEl"> -> applyTo(videoEl.value))
const applyTo = (el: HTMLMediaElement | null | undefined) => {
if (!el) return () => {}
// Initial Apply
// ใส่ค่าตั้งต้นให้กับออบเจ็กต์สื่อ
el.volume = volume.value
el.muted = muted.value
// A. Watch State -> Update Element
// A. สังเกตการเปลี่ยนแปลงจาก State -> เพื่อส่งไปอัปเดต Element สื่อ
const stopVolWatch = watch(volume, (v) => {
if (Math.abs(el.volume - v) > 0.01) el.volume = v
})
@ -99,9 +104,9 @@ export const useMediaPrefs = () => {
if (el.muted !== m) el.muted = m
})
// B. Listen Element -> Update State (e.g. Native Controls)
// B. สังเกตการเปลี่ยนแปลงจาก Element (เช่น ผู้ใช้กดปุ่มเร่งเสียงในวิดีโอตรงๆ) -> เพื่อเอาค่ามาอัปเดต State
const onVolumeChange = () => {
// Update state only if diff allows (prevent loop)
// อัปเดตเฉพาะเมื่อมีความแตกต่างเพื่อหลีกเลี่ยง Loop อนันต์
if (Math.abs(el.volume - volume.value) > 0.01) {
volume.value = el.volume
save()
@ -113,7 +118,7 @@ export const useMediaPrefs = () => {
}
el.addEventListener('volumechange', onVolumeChange)
// Cleanup function
// ฟังก์ชันล้างค่าเพื่อเลิกติดตาม (Cleanup แบบส่งกลับ (Return))
return () => {
stopVolWatch()
stopMutedWatch()
@ -121,11 +126,11 @@ export const useMediaPrefs = () => {
}
}
// 7. Lifecycle & Sync
// 7. จังหวะวงจรชีวิตตอนโหลดเสร็จและระบบ Sync
if (import.meta.client) {
onMounted(() => {
load()
// Cross-tab sync
// ระบบ Sync กับแท็บหรือหน้าต่างเดียวกันหากถูกเปิดไว้
window.addEventListener('storage', (e) => {
if (e.key === getStorageKey()) {
load()