feat: Scaffold new Nuxt.js application with initial pages, layouts, composables, and middleware.
This commit is contained in:
parent
ab3124628c
commit
9bfb852ad0
8 changed files with 113 additions and 48 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// ดึงฟังก์ชันจัดการ Authentication
|
||||||
const { fetchUserProfile, isAuthenticated } = useAuth()
|
const { fetchUserProfile, isAuthenticated } = useAuth()
|
||||||
|
|
||||||
|
// เมื่อ App เริ่มทำงาน (Mounted)
|
||||||
|
// หากผู้ใช้ Login ค้างไว้ (มี Token) ให้ดึงข้อมูล Profile ล่าสุดทันที
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isAuthenticated.value) {
|
if (isAuthenticated.value) {
|
||||||
fetchUserProfile()
|
fetchUserProfile()
|
||||||
|
|
@ -9,8 +12,12 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- แสดง Loader ระหว่างเปลี่ยนหน้า หรือโหลดข้อมูล -->
|
||||||
<GlobalLoader />
|
<GlobalLoader />
|
||||||
|
|
||||||
|
<!-- NuxtLayout: แสดง Layout ที่กำหนดในแต่ละเพจ (default: layouts/default.vue) -->
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<!-- NuxtPage: แสดงเนื้อหาของเพจปัจจุบัน (ตาม URL routng) -->
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
// Interface สำหรับข้อมูลผู้ใช้งาน (User)
|
||||||
interface User {
|
interface User {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
|
|
@ -6,7 +7,7 @@ interface User {
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
role: {
|
role: {
|
||||||
code: string
|
code: string // เช่น 'STUDENT', 'INSTRUCTOR', 'ADMIN'
|
||||||
name: { th: string; en: string }
|
name: { th: string; en: string }
|
||||||
}
|
}
|
||||||
profile?: {
|
profile?: {
|
||||||
|
|
@ -18,6 +19,7 @@ interface User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface สำหรับข้อมูลตอบกลับตอน Login
|
||||||
interface loginResponse {
|
interface loginResponse {
|
||||||
token: string
|
token: string
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
|
|
@ -25,6 +27,7 @@ interface loginResponse {
|
||||||
profile: User['profile']
|
profile: User['profile']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface สำหรับข้อมูลที่ใช้ลงทะเบียน
|
||||||
interface RegisterPayload {
|
interface RegisterPayload {
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
|
|
@ -35,32 +38,44 @@ interface RegisterPayload {
|
||||||
phone: string
|
phone: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Composable: useAuth
|
||||||
|
// หน้าที่: จัดการระบบ Authentication และ Authorization
|
||||||
|
// - จัดการ Login/Logout/Register
|
||||||
|
// - จัดการ Token (Access Token & Refresh Token)
|
||||||
|
// - เก็บ State ของผู้ใช้ปัจจุบัน (User State)
|
||||||
|
// ==========================================
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBase as string
|
const API_BASE_URL = config.public.apiBase as string
|
||||||
|
|
||||||
|
// Cookie สำหรับเก็บ Access Token (หมดอายุ 1 วัน)
|
||||||
const token = useCookie('auth_token', {
|
const token = useCookie('auth_token', {
|
||||||
maxAge: 60 * 60 * 24, // 1 day
|
maxAge: 60 * 60 * 24, // 1 day
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
secure: false // Set to true in production with HTTPS
|
secure: false // ควรเป็น true ใน production (HTTPS)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cookie สำหรับเก็บข้อมูล User (หมดอายุ 7 วัน)
|
||||||
const user = useCookie<User | null>('auth_user_data', {
|
const user = useCookie<User | null>('auth_user_data', {
|
||||||
maxAge: 60 * 60 * 24 * 7, // 1 week
|
maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
secure: false
|
secure: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Computed property เช็คว่า Login อยู่หรือไม่
|
||||||
const isAuthenticated = computed(() => !!token.value)
|
const isAuthenticated = computed(() => !!token.value)
|
||||||
|
|
||||||
// Refresh Token Logic
|
// Cookie สำหรับเก็บ Refresh Token (หมดอายุ 7 วัน)
|
||||||
const refreshToken = useCookie('auth_refresh_token', {
|
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',
|
sameSite: 'lax',
|
||||||
secure: false
|
secure: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ฟังก์ชันเข้าสู่ระบบ (Login)
|
||||||
const login = async (credentials: { email: string; password: string }) => {
|
const login = async (credentials: { email: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch<loginResponse>(`${API_BASE_URL}/auth/login`, {
|
const data = await $fetch<loginResponse>(`${API_BASE_URL}/auth/login`, {
|
||||||
|
|
@ -69,15 +84,15 @@ export const useAuth = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// Validation: Only allow STUDENT role to login
|
// Validation: อนุญาตเฉพาะ Role 'STUDENT' เท่านั้น
|
||||||
if (data.user.role.code !== 'STUDENT') {
|
if (data.user.role.code !== 'STUDENT') {
|
||||||
return { success: false, error: 'Email ไม่ถูกต้อง' }
|
return { success: false, error: 'Email ไม่ถูกต้อง' }
|
||||||
}
|
}
|
||||||
|
|
||||||
token.value = data.token
|
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
|
user.value = data.user
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
|
|
@ -92,6 +107,7 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ฟังก์ชันลงทะเบียน (Register)
|
||||||
const register = async (payload: RegisterPayload) => {
|
const register = async (payload: RegisterPayload) => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch(`${API_BASE_URL}/auth/register-learner`, {
|
const data = await $fetch(`${API_BASE_URL}/auth/register-learner`, {
|
||||||
|
|
@ -110,6 +126,7 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ฟังก์ชันดึงข้อมูลโปรไฟล์ผู้ใช้ล่าสุด
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
if (!token.value) return
|
if (!token.value) return
|
||||||
|
|
||||||
|
|
@ -124,11 +141,12 @@ export const useAuth = () => {
|
||||||
user.value = data
|
user.value = data
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
// กรณี Token หมดอายุ (401)
|
||||||
if (error.statusCode === 401) {
|
if (error.statusCode === 401) {
|
||||||
// Try to refresh token
|
// พยายามขอ Token ใหม่ (Refresh Token)
|
||||||
const refreshed = await refreshAccessToken()
|
const refreshed = await refreshAccessToken()
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
// Retry fetch with new token
|
// ถ้าได้ Token ใหม่ ให้ลองดึงข้อมูลอีกครั้ง
|
||||||
try {
|
try {
|
||||||
const retryData = await $fetch<User>(`${API_BASE_URL}/user/me`, {
|
const retryData = await $fetch<User>(`${API_BASE_URL}/user/me`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -144,6 +162,7 @@ export const useAuth = () => {
|
||||||
console.error('Failed to fetch user profile after refresh:', retryErr)
|
console.error('Failed to fetch user profile after refresh:', retryErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// ถ้า Refresh ไม่ผ่าน ให้ Logout
|
||||||
logout()
|
logout()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -152,6 +171,7 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ฟังก์ชันอัปเดตข้อมูลโปรไฟล์
|
||||||
const updateUserProfile = async (payload: {
|
const updateUserProfile = async (payload: {
|
||||||
first_name: string
|
first_name: string
|
||||||
last_name: string
|
last_name: string
|
||||||
|
|
@ -169,7 +189,7 @@ export const useAuth = () => {
|
||||||
body: payload
|
body: payload
|
||||||
})
|
})
|
||||||
|
|
||||||
// If successful, refresh the local user data
|
// หากสำเร็จ ให้ดึงข้อมูลโปรไฟล์ล่าสุดมาอัปเดตใน State
|
||||||
await fetchUserProfile()
|
await fetchUserProfile()
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
|
|
@ -223,6 +243,7 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ฟังก์ชันขอ Access Token ใหม่ด้วย Refresh Token
|
||||||
const refreshAccessToken = async () => {
|
const refreshAccessToken = async () => {
|
||||||
if (!refreshToken.value) return false
|
if (!refreshToken.value) return false
|
||||||
|
|
||||||
|
|
@ -238,16 +259,17 @@ export const useAuth = () => {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Refresh failed, force logout
|
// Refresh failed (เช่น Refresh Token หมดอายุ) ให้ Force Logout
|
||||||
logout()
|
logout()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ฟังก์ชันออกจากระบบ (Logout)
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
token.value = null
|
token.value = null
|
||||||
refreshToken.value = null // Clear refresh token
|
refreshToken.value = null // ลบ Refresh Token
|
||||||
user.value = null
|
user.value = null
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
router.push('/auth/login')
|
router.push('/auth/login')
|
||||||
|
|
@ -260,6 +282,7 @@ export const useAuth = () => {
|
||||||
currentUser: computed(() => {
|
currentUser: computed(() => {
|
||||||
if (!user.value) return null
|
if (!user.value) return null
|
||||||
|
|
||||||
|
// Helper ในการดึงข้อมูล user ที่อาจซ้อนกันอยู่หลายชั้น
|
||||||
const prefix = user.value.profile?.prefix?.th || ''
|
const prefix = user.value.profile?.prefix?.th || ''
|
||||||
const firstName = user.value.profile?.first_name || user.value.username
|
const firstName = user.value.profile?.first_name || user.value.username
|
||||||
const lastName = user.value.profile?.last_name || ''
|
const lastName = user.value.profile?.last_name || ''
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
|
// Interface สำหรับข้อมูลหมวดหมู่ (Category)
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: number
|
id: number
|
||||||
name: {
|
name: {
|
||||||
|
|
@ -24,17 +24,20 @@ export interface CategoryData {
|
||||||
categories: Category[]
|
categories: Category[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryResponse {
|
interface CategoryResponse {
|
||||||
code: number
|
code: number
|
||||||
message: string
|
message: string
|
||||||
data: CategoryData
|
data: CategoryData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Composable สำหรับจัดการข้อมูลหมวดหมู่
|
||||||
export const useCategory = () => {
|
export const useCategory = () => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBase as string
|
const API_BASE_URL = config.public.apiBase as string
|
||||||
const { token } = useAuth()
|
const { token } = useAuth()
|
||||||
|
|
||||||
|
// ฟังก์ชันดึงข้อมูลหมวดหมู่ทั้งหมด
|
||||||
|
// Endpoint: GET /categories
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await $fetch<CategoryResponse>(`${API_BASE_URL}/categories`, {
|
const response = await $fetch<CategoryResponse>(`${API_BASE_URL}/categories`, {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
|
// Interface สำหรับข้อมูลคอร์สเรียน (Public Course Data)
|
||||||
export interface Course {
|
export interface Course {
|
||||||
id: number
|
id: number
|
||||||
title: string | { th: string; en: string }
|
title: string | { th: string; en: string } // รองรับ 2 ภาษา
|
||||||
slug: string
|
slug: string
|
||||||
description: string | { th: string; en: string }
|
description: string | { th: string; en: string }
|
||||||
thumbnail_url: string
|
thumbnail_url: string
|
||||||
price: string
|
price: string
|
||||||
is_free: boolean
|
is_free: boolean
|
||||||
have_certificate: boolean
|
have_certificate: boolean
|
||||||
status: string
|
status: string // DRAFT, PUBLISHED
|
||||||
category_id: number
|
category_id: number
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
|
|
@ -20,8 +21,9 @@ export interface Course {
|
||||||
|
|
||||||
rating?: string
|
rating?: string
|
||||||
lessons?: number | string
|
lessons?: number | string
|
||||||
levelType?: 'neutral' | 'warning' | 'success'
|
levelType?: 'neutral' | 'warning' | 'success' // ใช้สำหรับการแสดงผล Badge ระดับความยาก (Frontend Logic)
|
||||||
|
|
||||||
|
// โครงสร้างบทเรียน (Chapters & Lessons)
|
||||||
chapters?: {
|
chapters?: {
|
||||||
id: number
|
id: number
|
||||||
title: string | { th: string; en: string }
|
title: string | { th: string; en: string }
|
||||||
|
|
@ -41,6 +43,7 @@ interface CourseResponse {
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface สำหรับคอร์สที่ผู้ใช้ลงทะเบียนเรียนแล้ว (My Course)
|
||||||
export interface EnrolledCourse {
|
export interface EnrolledCourse {
|
||||||
id: number
|
id: number
|
||||||
course_id: number
|
course_id: number
|
||||||
|
|
@ -75,11 +78,13 @@ export const useCourse = () => {
|
||||||
const { token } = useAuth()
|
const { token } = useAuth()
|
||||||
|
|
||||||
// ฟังก์ชันดึงรายชื่อคอร์สทั้งหมด (Catalog)
|
// ฟังก์ชันดึงรายชื่อคอร์สทั้งหมด (Catalog)
|
||||||
|
// ใช้สำหรับหน้า Discover/Browse
|
||||||
// Endpoint: GET /courses
|
// Endpoint: GET /courses
|
||||||
const fetchCourses = async () => {
|
const fetchCourses = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses`, {
|
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
// ส่ง Token ไปด้วยถ้ามี (เผื่อ Logic ในอนาคตที่ต้องเช็คสิทธิ์)
|
||||||
headers: token.value ? {
|
headers: token.value ? {
|
||||||
Authorization: `Bearer ${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
|
let courseData: any = null
|
||||||
|
|
||||||
if (Array.isArray(data.data)) {
|
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)
|
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)
|
// Fallback: ถ้าหาไม่เจอ แต่มีข้อมูลตัวเดียว อาจจะเป็นตัวนั้น
|
||||||
// But generally, we should expect a match. If not match, maybe the API returned a generic list.
|
|
||||||
if (!courseData && data.data.length === 1) {
|
if (!courseData && data.data.length === 1) {
|
||||||
courseData = data.data[0]
|
courseData = data.data[0]
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +145,7 @@ export const useCourse = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ฟังก์ชันลงทะเบียนเรียน
|
// ฟังก์ชันลงทะเบียนเรียน (Enroll)
|
||||||
// Endpoint: POST /students/courses/:id/enroll
|
// Endpoint: POST /students/courses/:id/enroll
|
||||||
const enrollCourse = async (courseId: number) => {
|
const enrollCourse = async (courseId: number) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -160,8 +164,7 @@ export const useCourse = () => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Enroll course failed:', err)
|
console.error('Enroll course failed:', err)
|
||||||
|
|
||||||
// Check for 409 Conflict (Already Enrolled)
|
// เช็ค Error 409 Conflict (กรณีลงทะเบียนไปแล้ว)
|
||||||
// ofetch/h3 error properties might vary, check common ones
|
|
||||||
const status = err.statusCode || err.status || err.response?.status
|
const status = err.statusCode || err.status || err.response?.status
|
||||||
|
|
||||||
if (status === 409) {
|
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 } = {}) => {
|
const fetchEnrolledCourses = async (params: { page?: number; limit?: number; status?: string } = {}) => {
|
||||||
try {
|
try {
|
||||||
const queryParams = new URLSearchParams()
|
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) => {
|
const checkLessonAccess = async (courseId: number, lessonId: number) => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/access-check`, {
|
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) => {
|
const fetchVideoProgress = async (lessonId: number) => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
|
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* @file default.vue
|
* @file default.vue
|
||||||
* @description Default application layout for authenticated users.
|
* @description Layout หลักสำหรับหน้าเว็บของผู้ใช้ (Authenticated Users)
|
||||||
* Includes the AppHeader and MobileNav.
|
* ประกอบด้วย Header (Navbar) และ Mobile Navigation
|
||||||
*/
|
*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- App Shell: Main container with global background and text color -->
|
<!-- App Shell: คอนเทนเนอร์หลักของแอปพลิเคชัน -->
|
||||||
<div class="app-shell min-h-screen transition-colors duration-200">
|
<div class="app-shell min-h-screen transition-colors duration-200">
|
||||||
<!-- Header -->
|
<!-- Header: แถบเมนูด้านบน -->
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area: ส่วนแสดงเนื้อหาหลัก -->
|
||||||
<main class="app-main">
|
<main class="app-main">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Mobile Bottom Navigation (Visible only on small screens) -->
|
<!-- Mobile Bottom Navigation: แถบเมนูด้านล่าง (แสดงเฉพาะมือถือ) -->
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
// Middleware สำหรับตรวจสอบสิทธิ์การเข้าถึงหน้าเว็บ (Authentication Guard)
|
||||||
export default defineNuxtRouteMiddleware((to) => {
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
const { isAuthenticated, user } = useAuth()
|
const { isAuthenticated, user } = useAuth()
|
||||||
|
|
||||||
// Pages that are accessible only when NOT logged in (Auth pages)
|
// รายชื่อหน้าสำหรับ Guest (ห้าม User ที่ Login แล้วเข้า)
|
||||||
|
// เช่น หน้า Login, Register
|
||||||
const authPages = [
|
const authPages = [
|
||||||
'/auth/login',
|
'/auth/login',
|
||||||
'/auth/register',
|
'/auth/register',
|
||||||
|
|
@ -9,13 +11,11 @@ export default defineNuxtRouteMiddleware((to) => {
|
||||||
'/auth/reset-password'
|
'/auth/reset-password'
|
||||||
]
|
]
|
||||||
|
|
||||||
// Pages that are accessible as public landing
|
// รายชื่อหน้าที่เข้าถึงได้โดยไม่ต้อง Login (Public Pages)
|
||||||
// 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.
|
|
||||||
const publicPages = ['/', '/courses', '/browse', '/browse/discovery']
|
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)) {
|
if (isAuthenticated.value && authPages.includes(to.path)) {
|
||||||
const role = user.value?.role?.code
|
const role = user.value?.role?.code
|
||||||
if (role === 'ADMIN') return navigateTo('/admin', { replace: true })
|
if (role === 'ADMIN') return navigateTo('/admin', { replace: true })
|
||||||
|
|
@ -23,8 +23,8 @@ export default defineNuxtRouteMiddleware((to) => {
|
||||||
return navigateTo('/dashboard', { replace: true })
|
return navigateTo('/dashboard', { replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. If user is NOT authenticated and tries to access a page that has this middleware applied
|
// กรณีที่ 2: ผู้ใช้ยังไม่ Login แต่พยายามเข้าหน้าที่ต้อง Login (Protected Pages)
|
||||||
// and is NOT one of the public or auth pages.
|
// (หน้าอื่น ๆ ที่ไม่ได้อยู่ใน publicPages และ authPages)
|
||||||
if (!isAuthenticated.value && !authPages.includes(to.path) && !publicPages.includes(to.path)) {
|
if (!isAuthenticated.value && !authPages.includes(to.path) && !publicPages.includes(to.path)) {
|
||||||
return navigateTo('/auth/login', { replace: true })
|
return navigateTo('/auth/login', { replace: true })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
// Nuxt 3 + Quasar + Tailwind + TypeScript
|
// Nuxt 3 + Quasar + Tailwind + TypeScript
|
||||||
// Configuration for E-Learning Platform
|
// Configuration for E-Learning Platform
|
||||||
|
// ไฟล์ตั้งค่าหลักของ Nuxt.js ใช้สำหรับกำหนด Modules, Plugins, CSS และ Environment Variables
|
||||||
export default defineNuxtConfig({
|
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"],
|
modules: ["nuxt-quasar-ui", "@nuxtjs/tailwindcss", "@nuxtjs/i18n"],
|
||||||
|
|
||||||
// i18n Configuration
|
// การตั้งค่า i18n (ระบบภาษา)
|
||||||
i18n: {
|
i18n: {
|
||||||
strategy: 'no_prefix',
|
strategy: 'no_prefix', // ไม่ใส่ prefix URL สำหรับภาษา default
|
||||||
defaultLocale: 'th',
|
defaultLocale: 'th', // ภาษาเริ่มต้นเป็นภาษาไทย
|
||||||
langDir: 'locales',
|
langDir: 'locales', // โฟลเดอร์เก็บไฟล์แปลภาษา
|
||||||
locales: [
|
locales: [
|
||||||
{ code: 'th', name: 'ไทย', iso: 'th-TH', file: 'th.json' },
|
{ code: 'th', name: 'ไทย', iso: 'th-TH', file: 'th.json' },
|
||||||
{ code: 'en', name: 'English', iso: 'en-US', file: 'en.json' }
|
{ code: 'en', name: 'English', iso: 'en-US', file: 'en.json' }
|
||||||
|
|
@ -18,14 +23,19 @@ export default defineNuxtConfig({
|
||||||
redirectOn: 'root'
|
redirectOn: 'root'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ไฟล์ CSS หลักของโปรเจกต์
|
||||||
css: ["~/assets/css/main.css"],
|
css: ["~/assets/css/main.css"],
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
strict: true,
|
strict: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// การตั้งค่า Quasar Framework
|
||||||
quasar: {
|
quasar: {
|
||||||
plugins: ["Notify"],
|
plugins: ["Notify"], // เปิดใช้ Plugin Notify
|
||||||
config: {
|
config: {
|
||||||
brand: {
|
brand: { // กำหนดชุดสีหลัก (Theme Colors)
|
||||||
primary: "#4b82f7",
|
primary: "#4b82f7",
|
||||||
secondary: "#2f5ed7",
|
secondary: "#2f5ed7",
|
||||||
accent: "#44d4a8",
|
accent: "#44d4a8",
|
||||||
|
|
@ -33,12 +43,16 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// กำหนดให้ Nuxt สแกน Components ในโฟลเดอร์ ~/components โดยอัตโนมัติ
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
path: "~/components",
|
path: "~/components",
|
||||||
pathPrefix: false,
|
pathPrefix: false, // เรียกใช้ Component ได้โดยไม่ต้องมี prefix ชื่อโฟลเดอร์
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// การตั้งค่า HTML Head (Meta tags, Google Fonts)
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
|
|
@ -51,11 +65,14 @@ export default defineNuxtConfig({
|
||||||
link: [
|
link: [
|
||||||
{
|
{
|
||||||
rel: "stylesheet",
|
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",
|
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: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:4000/api'
|
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:4000/api'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* @file index.vue
|
||||||
|
* @description หน้า Landing Page (หน้าแรกของเว็บสำหรับ Guest)
|
||||||
|
* ใช้ Layout: 'landing' (ไม่มี Sidebar / Navbar แบบ Dashboard)
|
||||||
|
*/
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'landing',
|
layout: 'landing',
|
||||||
middleware: 'auth'
|
middleware: 'auth' // ใช้ auth middleware เพื่อเช็ค: ถ้า Login แล้วให้ดีดไป Dashboard
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
|
|
@ -18,7 +23,7 @@ useHead({
|
||||||
<div class="absolute bottom-[-20%] left-[-10%] w-[60%] h-[60%] rounded-full bg-indigo-600/10 blur-[140px] animate-pulse-slow" style="animation-delay: 3s;"/>
|
<div class="absolute bottom-[-20%] left-[-10%] w-[60%] h-[60%] rounded-full bg-indigo-600/10 blur-[140px] animate-pulse-slow" style="animation-delay: 3s;"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section: ส่วนหัวของหน้าเว็บ แสดงข้อความต้อนรับและปุ่ม CTA -->
|
||||||
<section class="hero-section min-h-[95vh] flex items-center relative overflow-hidden pt-32 pb-20">
|
<section class="hero-section min-h-[95vh] flex items-center relative overflow-hidden pt-32 pb-20">
|
||||||
<div class="container relative z-10 w-full">
|
<div class="container relative z-10 w-full">
|
||||||
<div class="grid-hero items-center">
|
<div class="grid-hero items-center">
|
||||||
|
|
@ -135,7 +140,7 @@ useHead({
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Platform Info Section -->
|
<!-- Platform Info Section: ส่วนแสดงจุดเด่นของแพลตฟอร์ม (Features) -->
|
||||||
<section class="info-section py-40 bg-slate-900 relative transition-colors">
|
<section class="info-section py-40 bg-slate-900 relative transition-colors">
|
||||||
<!-- Background detail -->
|
<!-- Background detail -->
|
||||||
<div class="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"/>
|
<div class="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"/>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue