From 9bfb852ad058246ee1e149634936b6763cfdc1ce Mon Sep 17 00:00:00 2001 From: supalerk-ar66 Date: Fri, 23 Jan 2026 09:54:35 +0700 Subject: [PATCH] feat: Scaffold new Nuxt.js application with initial pages, layouts, composables, and middleware. --- Frontend-Learner/app.vue | 7 +++ Frontend-Learner/composables/useAuth.ts | 47 +++++++++++++++------ Frontend-Learner/composables/useCategory.ts | 7 ++- Frontend-Learner/composables/useCourse.ts | 30 ++++++++----- Frontend-Learner/layouts/default.vue | 12 +++--- Frontend-Learner/middleware/auth.ts | 16 +++---- Frontend-Learner/nuxt.config.ts | 31 +++++++++++--- Frontend-Learner/pages/index.vue | 11 +++-- 8 files changed, 113 insertions(+), 48 deletions(-) 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 @@