diff --git a/Frontend-Learner/app.vue b/Frontend-Learner/app.vue
index 424854b6..8a22761b 100644
--- a/Frontend-Learner/app.vue
+++ b/Frontend-Learner/app.vue
@@ -1,6 +1,9 @@
+
+
+
+
diff --git a/Frontend-Learner/composables/useAuth.ts b/Frontend-Learner/composables/useAuth.ts
index de73dc2b..634b6538 100644
--- a/Frontend-Learner/composables/useAuth.ts
+++ b/Frontend-Learner/composables/useAuth.ts
@@ -1,4 +1,5 @@
+// Interface สำหรับข้อมูลผู้ใช้งาน (User)
interface User {
id: number
username: string
@@ -6,7 +7,7 @@ interface User {
created_at?: string
updated_at?: string
role: {
- code: string
+ code: string // เช่น 'STUDENT', 'INSTRUCTOR', 'ADMIN'
name: { th: string; en: string }
}
profile?: {
@@ -18,6 +19,7 @@ interface User {
}
}
+// Interface สำหรับข้อมูลตอบกลับตอน Login
interface loginResponse {
token: string
refreshToken: string
@@ -25,6 +27,7 @@ interface loginResponse {
profile: User['profile']
}
+// Interface สำหรับข้อมูลที่ใช้ลงทะเบียน
interface RegisterPayload {
username: string
email: string
@@ -35,32 +38,44 @@ interface RegisterPayload {
phone: string
}
+// ==========================================
+// Composable: useAuth
+// หน้าที่: จัดการระบบ Authentication และ Authorization
+// - จัดการ Login/Logout/Register
+// - จัดการ Token (Access Token & Refresh Token)
+// - เก็บ State ของผู้ใช้ปัจจุบัน (User State)
+// ==========================================
export const useAuth = () => {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBase as string
+
+ // Cookie สำหรับเก็บ Access Token (หมดอายุ 1 วัน)
const token = useCookie('auth_token', {
maxAge: 60 * 60 * 24, // 1 day
sameSite: 'lax',
- secure: false // Set to true in production with HTTPS
+ secure: false // ควรเป็น true ใน production (HTTPS)
})
+ // Cookie สำหรับเก็บข้อมูล User (หมดอายุ 7 วัน)
const user = useCookie('auth_user_data', {
maxAge: 60 * 60 * 24 * 7, // 1 week
sameSite: 'lax',
secure: false
})
+ // Computed property เช็คว่า Login อยู่หรือไม่
const isAuthenticated = computed(() => !!token.value)
- // Refresh Token Logic
+ // Cookie สำหรับเก็บ Refresh Token (หมดอายุ 7 วัน)
const refreshToken = useCookie('auth_refresh_token', {
- maxAge: 60 * 60 * 24 * 7, // 7 days (matching API likely)
+ maxAge: 60 * 60 * 24 * 7, // 7 days (ตรงกับ API)
sameSite: 'lax',
secure: false
})
+ // ฟังก์ชันเข้าสู่ระบบ (Login)
const login = async (credentials: { email: string; password: string }) => {
try {
const data = await $fetch(`${API_BASE_URL}/auth/login`, {
@@ -69,15 +84,15 @@ export const useAuth = () => {
})
if (data) {
- // Validation: Only allow STUDENT role to login
+ // Validation: อนุญาตเฉพาะ Role 'STUDENT' เท่านั้น
if (data.user.role.code !== 'STUDENT') {
return { success: false, error: 'Email ไม่ถูกต้อง' }
}
token.value = data.token
- refreshToken.value = data.refreshToken // Save refresh token
+ refreshToken.value = data.refreshToken // บันทึก Refresh Token
- // The API returns the profile nested inside the user object
+ // API ส่งข้อมูล profile มาใน user object
user.value = data.user
return { success: true }
@@ -92,6 +107,7 @@ export const useAuth = () => {
}
}
+ // ฟังก์ชันลงทะเบียน (Register)
const register = async (payload: RegisterPayload) => {
try {
const data = await $fetch(`${API_BASE_URL}/auth/register-learner`, {
@@ -110,6 +126,7 @@ export const useAuth = () => {
}
}
+ // ฟังก์ชันดึงข้อมูลโปรไฟล์ผู้ใช้ล่าสุด
const fetchUserProfile = async () => {
if (!token.value) return
@@ -124,11 +141,12 @@ export const useAuth = () => {
user.value = data
}
} catch (error: any) {
+ // กรณี Token หมดอายุ (401)
if (error.statusCode === 401) {
- // Try to refresh token
+ // พยายามขอ Token ใหม่ (Refresh Token)
const refreshed = await refreshAccessToken()
if (refreshed) {
- // Retry fetch with new token
+ // ถ้าได้ Token ใหม่ ให้ลองดึงข้อมูลอีกครั้ง
try {
const retryData = await $fetch(`${API_BASE_URL}/user/me`, {
headers: {
@@ -144,6 +162,7 @@ export const useAuth = () => {
console.error('Failed to fetch user profile after refresh:', retryErr)
}
} else {
+ // ถ้า Refresh ไม่ผ่าน ให้ Logout
logout()
}
} else {
@@ -152,6 +171,7 @@ export const useAuth = () => {
}
}
+ // ฟังก์ชันอัปเดตข้อมูลโปรไฟล์
const updateUserProfile = async (payload: {
first_name: string
last_name: string
@@ -169,7 +189,7 @@ export const useAuth = () => {
body: payload
})
- // If successful, refresh the local user data
+ // หากสำเร็จ ให้ดึงข้อมูลโปรไฟล์ล่าสุดมาอัปเดตใน State
await fetchUserProfile()
return { success: true }
@@ -223,6 +243,7 @@ export const useAuth = () => {
}
}
+ // ฟังก์ชันขอ Access Token ใหม่ด้วย Refresh Token
const refreshAccessToken = async () => {
if (!refreshToken.value) return false
@@ -238,16 +259,17 @@ export const useAuth = () => {
return true
}
} catch (err) {
- // Refresh failed, force logout
+ // Refresh failed (เช่น Refresh Token หมดอายุ) ให้ Force Logout
logout()
return false
}
return false
}
+ // ฟังก์ชันออกจากระบบ (Logout)
const logout = () => {
token.value = null
- refreshToken.value = null // Clear refresh token
+ refreshToken.value = null // ลบ Refresh Token
user.value = null
const router = useRouter()
router.push('/auth/login')
@@ -260,6 +282,7 @@ export const useAuth = () => {
currentUser: computed(() => {
if (!user.value) return null
+ // Helper ในการดึงข้อมูล user ที่อาจซ้อนกันอยู่หลายชั้น
const prefix = user.value.profile?.prefix?.th || ''
const firstName = user.value.profile?.first_name || user.value.username
const lastName = user.value.profile?.last_name || ''
diff --git a/Frontend-Learner/composables/useCategory.ts b/Frontend-Learner/composables/useCategory.ts
index 324cc8bb..09073b5c 100644
--- a/Frontend-Learner/composables/useCategory.ts
+++ b/Frontend-Learner/composables/useCategory.ts
@@ -1,4 +1,4 @@
-
+// Interface สำหรับข้อมูลหมวดหมู่ (Category)
export interface Category {
id: number
name: {
@@ -24,17 +24,20 @@ export interface CategoryData {
categories: Category[]
}
-export interface CategoryResponse {
+interface CategoryResponse {
code: number
message: string
data: CategoryData
}
+// Composable สำหรับจัดการข้อมูลหมวดหมู่
export const useCategory = () => {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBase as string
const { token } = useAuth()
+ // ฟังก์ชันดึงข้อมูลหมวดหมู่ทั้งหมด
+ // Endpoint: GET /categories
const fetchCategories = async () => {
try {
const response = await $fetch(`${API_BASE_URL}/categories`, {
diff --git a/Frontend-Learner/composables/useCourse.ts b/Frontend-Learner/composables/useCourse.ts
index 6b48f6ef..a670d31e 100644
--- a/Frontend-Learner/composables/useCourse.ts
+++ b/Frontend-Learner/composables/useCourse.ts
@@ -1,13 +1,14 @@
+// Interface สำหรับข้อมูลคอร์สเรียน (Public Course Data)
export interface Course {
id: number
- title: string | { th: string; en: string }
+ title: string | { th: string; en: string } // รองรับ 2 ภาษา
slug: string
description: string | { th: string; en: string }
thumbnail_url: string
price: string
is_free: boolean
have_certificate: boolean
- status: string
+ status: string // DRAFT, PUBLISHED
category_id: number
created_at?: string
updated_at?: string
@@ -20,8 +21,9 @@ export interface Course {
rating?: string
lessons?: number | string
- levelType?: 'neutral' | 'warning' | 'success'
+ levelType?: 'neutral' | 'warning' | 'success' // ใช้สำหรับการแสดงผล Badge ระดับความยาก (Frontend Logic)
+ // โครงสร้างบทเรียน (Chapters & Lessons)
chapters?: {
id: number
title: string | { th: string; en: string }
@@ -41,6 +43,7 @@ interface CourseResponse {
total: number
}
+// Interface สำหรับคอร์สที่ผู้ใช้ลงทะเบียนเรียนแล้ว (My Course)
export interface EnrolledCourse {
id: number
course_id: number
@@ -75,11 +78,13 @@ export const useCourse = () => {
const { token } = useAuth()
// ฟังก์ชันดึงรายชื่อคอร์สทั้งหมด (Catalog)
+ // ใช้สำหรับหน้า Discover/Browse
// Endpoint: GET /courses
const fetchCourses = async () => {
try {
const data = await $fetch(`${API_BASE_URL}/courses`, {
method: 'GET',
+ // ส่ง Token ไปด้วยถ้ามี (เผื่อ Logic ในอนาคตที่ต้องเช็คสิทธิ์)
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
@@ -110,15 +115,14 @@ export const useCourse = () => {
} : {}
})
- // API might return an array (list) or single object
+ // Logic จัดการข้อมูลที่ได้รับ (API อาจส่งกลับมาเป็น Array หรือ Object)
let courseData: any = null
if (Array.isArray(data.data)) {
- // Try to find the matching course ID in the array
+ // ถ้าเป็น Array ให้หาอันที่ ID ตรงกัน
courseData = data.data.find((c: any) => c.id == id)
- // Fallback: If not found, and array has length 1, it might be the one (if ID mismatch isn't the issue)
- // But generally, we should expect a match. If not match, maybe the API returned a generic list.
+ // Fallback: ถ้าหาไม่เจอ แต่มีข้อมูลตัวเดียว อาจจะเป็นตัวนั้น
if (!courseData && data.data.length === 1) {
courseData = data.data[0]
}
@@ -141,7 +145,7 @@ export const useCourse = () => {
}
}
- // ฟังก์ชันลงทะเบียนเรียน
+ // ฟังก์ชันลงทะเบียนเรียน (Enroll)
// Endpoint: POST /students/courses/:id/enroll
const enrollCourse = async (courseId: number) => {
try {
@@ -160,8 +164,7 @@ export const useCourse = () => {
} catch (err: any) {
console.error('Enroll course failed:', err)
- // Check for 409 Conflict (Already Enrolled)
- // ofetch/h3 error properties might vary, check common ones
+ // เช็ค Error 409 Conflict (กรณีลงทะเบียนไปแล้ว)
const status = err.statusCode || err.status || err.response?.status
if (status === 409) {
@@ -180,6 +183,8 @@ export const useCourse = () => {
}
}
+ // ฟังก์ชันดึงคอร์สที่ฉันลงทะเบียนเรียน (My Courses)
+ // รองรับ Pagination และการกรอง Status (ENROLLED, IN_PROGRESS, COMPLETED)
const fetchEnrolledCourses = async (params: { page?: number; limit?: number; status?: string } = {}) => {
try {
const queryParams = new URLSearchParams()
@@ -262,6 +267,9 @@ export const useCourse = () => {
}
}
+ // ฟังก์ชันเช็คสิทธิ์การเข้าถึงบทเรียน (Access Control)
+ // ต้อง Enrolled ก่อนถึงจะเข้าได้ และต้องผ่านเงื่อนไข Prerequisites (ถ้ามี)
+ // Endpoint: GET /students/courses/:cid/lessons/:lid/access-check
const checkLessonAccess = async (courseId: number, lessonId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/access-check`, {
@@ -315,6 +323,8 @@ export const useCourse = () => {
}
}
+ // ฟังก์ชันดึง Video Progress ปัจจุบันของบทเรียน
+ // Endpoint: GET /students/lessons/:id/progress
const fetchVideoProgress = async (lessonId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
diff --git a/Frontend-Learner/layouts/default.vue b/Frontend-Learner/layouts/default.vue
index 1dbfae11..40db9c1d 100644
--- a/Frontend-Learner/layouts/default.vue
+++ b/Frontend-Learner/layouts/default.vue
@@ -1,23 +1,23 @@
-
+
diff --git a/Frontend-Learner/middleware/auth.ts b/Frontend-Learner/middleware/auth.ts
index 28c90551..1c338920 100644
--- a/Frontend-Learner/middleware/auth.ts
+++ b/Frontend-Learner/middleware/auth.ts
@@ -1,7 +1,9 @@
+// Middleware สำหรับตรวจสอบสิทธิ์การเข้าถึงหน้าเว็บ (Authentication Guard)
export default defineNuxtRouteMiddleware((to) => {
const { isAuthenticated, user } = useAuth()
- // Pages that are accessible only when NOT logged in (Auth pages)
+ // รายชื่อหน้าสำหรับ Guest (ห้าม User ที่ Login แล้วเข้า)
+ // เช่น หน้า Login, Register
const authPages = [
'/auth/login',
'/auth/register',
@@ -9,13 +11,11 @@ export default defineNuxtRouteMiddleware((to) => {
'/auth/reset-password'
]
- // Pages that are accessible as public landing
- // Note: /courses and /discovery (now in browse/) might be public depending on logic,
- // but let's assume browse pages are public or handled separately.
- // For now, we list the root.
+ // รายชื่อหน้าที่เข้าถึงได้โดยไม่ต้อง Login (Public Pages)
const publicPages = ['/', '/courses', '/browse', '/browse/discovery']
- // 1. If user is authenticated and tries to access login/register (Keep landing page accessible)
+ // กรณีที่ 1: ผู้ใช้ Login แล้ว แต่พยายามเข้าหน้า Login/Register
+ // ระบบจะดีดกลับไปหน้า Dashboard ตาม Role ของผู้ใช้
if (isAuthenticated.value && authPages.includes(to.path)) {
const role = user.value?.role?.code
if (role === 'ADMIN') return navigateTo('/admin', { replace: true })
@@ -23,8 +23,8 @@ export default defineNuxtRouteMiddleware((to) => {
return navigateTo('/dashboard', { replace: true })
}
- // 2. If user is NOT authenticated and tries to access a page that has this middleware applied
- // and is NOT one of the public or auth pages.
+ // กรณีที่ 2: ผู้ใช้ยังไม่ Login แต่พยายามเข้าหน้าที่ต้อง Login (Protected Pages)
+ // (หน้าอื่น ๆ ที่ไม่ได้อยู่ใน publicPages และ authPages)
if (!isAuthenticated.value && !authPages.includes(to.path) && !publicPages.includes(to.path)) {
return navigateTo('/auth/login', { replace: true })
}
diff --git a/Frontend-Learner/nuxt.config.ts b/Frontend-Learner/nuxt.config.ts
index 5bc02e87..ecd2929a 100644
--- a/Frontend-Learner/nuxt.config.ts
+++ b/Frontend-Learner/nuxt.config.ts
@@ -1,13 +1,18 @@
// Nuxt 3 + Quasar + Tailwind + TypeScript
// Configuration for E-Learning Platform
+// ไฟล์ตั้งค่าหลักของ Nuxt.js ใช้สำหรับกำหนด Modules, Plugins, CSS และ Environment Variables
export default defineNuxtConfig({
+ // Modules ที่ใช้ในโปรเจกต์
+ // - nuxt-quasar-ui: สำหรับ UI Component Library (Quasar)
+ // - @nuxtjs/tailwindcss: สำหรับ Utility-first CSS Framework
+ // - @nuxtjs/i18n: สำหรับระบบหลายภาษา (Internationalization)
modules: ["nuxt-quasar-ui", "@nuxtjs/tailwindcss", "@nuxtjs/i18n"],
- // i18n Configuration
+ // การตั้งค่า i18n (ระบบภาษา)
i18n: {
- strategy: 'no_prefix',
- defaultLocale: 'th',
- langDir: 'locales',
+ strategy: 'no_prefix', // ไม่ใส่ prefix URL สำหรับภาษา default
+ defaultLocale: 'th', // ภาษาเริ่มต้นเป็นภาษาไทย
+ langDir: 'locales', // โฟลเดอร์เก็บไฟล์แปลภาษา
locales: [
{ code: 'th', name: 'ไทย', iso: 'th-TH', file: 'th.json' },
{ code: 'en', name: 'English', iso: 'en-US', file: 'en.json' }
@@ -18,14 +23,19 @@ export default defineNuxtConfig({
redirectOn: 'root'
}
},
+
+ // ไฟล์ CSS หลักของโปรเจกต์
css: ["~/assets/css/main.css"],
+
typescript: {
strict: true,
},
+
+ // การตั้งค่า Quasar Framework
quasar: {
- plugins: ["Notify"],
+ plugins: ["Notify"], // เปิดใช้ Plugin Notify
config: {
- brand: {
+ brand: { // กำหนดชุดสีหลัก (Theme Colors)
primary: "#4b82f7",
secondary: "#2f5ed7",
accent: "#44d4a8",
@@ -33,12 +43,16 @@ export default defineNuxtConfig({
},
},
},
+
+ // กำหนดให้ Nuxt สแกน Components ในโฟลเดอร์ ~/components โดยอัตโนมัติ
components: [
{
path: "~/components",
- pathPrefix: false,
+ pathPrefix: false, // เรียกใช้ Component ได้โดยไม่ต้องมี prefix ชื่อโฟลเดอร์
},
],
+
+ // การตั้งค่า HTML Head (Meta tags, Google Fonts)
app: {
head: {
htmlAttrs: {
@@ -51,11 +65,14 @@ export default defineNuxtConfig({
link: [
{
rel: "stylesheet",
+ // โหลด Font: Inter, Prompt, Sarabun
href: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Prompt:wght@300;400;500;600;700;800;900&family=Sarabun:wght@300;400;500;600;700;800&display=swap",
},
],
},
},
+
+ // Environment Variables ที่ใช้ในโปรเจกต์ (เข้าถึงได้ทั้ง Server และ Client)
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:4000/api'
diff --git a/Frontend-Learner/pages/index.vue b/Frontend-Learner/pages/index.vue
index 27afcfac..0ee2ee36 100644
--- a/Frontend-Learner/pages/index.vue
+++ b/Frontend-Learner/pages/index.vue
@@ -1,7 +1,12 @@