feat: Scaffold new Nuxt.js application with initial pages, layouts, composables, and middleware.

This commit is contained in:
supalerk-ar66 2026-01-23 09:54:35 +07:00
parent ab3124628c
commit 9bfb852ad0
8 changed files with 113 additions and 48 deletions

View file

@ -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>

View file

@ -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 || ''

View file

@ -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`, {

View file

@ -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`, {

View file

@ -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>

View file

@ -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 })
} }

View file

@ -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'

View file

@ -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"/>