feat: Implement initial core features including course browsing, authentication, user dashboard, and internationalization.
This commit is contained in:
parent
031ca5c984
commit
797e3db644
19 changed files with 401 additions and 399 deletions
|
|
@ -1,20 +1,27 @@
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
// ดึงฟังก์ชันจัดการ Authentication
|
/**
|
||||||
const { fetchUserProfile, isAuthenticated } = useAuth()
|
* @file app.vue
|
||||||
|
* @description Root application component.
|
||||||
|
* Handles initialization of authentication and theme settings.
|
||||||
|
*/
|
||||||
|
|
||||||
// เมื่อ App เริ่มทำงาน (Mounted)
|
// Initialize composables
|
||||||
|
const { fetchUserProfile, isAuthenticated } = useAuth()
|
||||||
|
const { isDark, set: setTheme } = useThemeMode()
|
||||||
|
|
||||||
|
// App initialization logic
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 1. หากผู้ใช้ Login ค้างไว้ (มี Token) ให้ดึงข้อมูล Profile ล่าสุดทันที
|
// 1. Fetch user profile if tokens exist
|
||||||
if (isAuthenticated.value) {
|
if (isAuthenticated.value) {
|
||||||
fetchUserProfile()
|
fetchUserProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ตรวจสอบและคืนค่า Theme (Dark/Light) จาก LocalStorage
|
// 2. Initialize theme from persistent storage or system preference
|
||||||
const savedTheme = localStorage.getItem('theme')
|
const savedTheme = localStorage.getItem('theme')
|
||||||
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (savedTheme) {
|
||||||
document.documentElement.classList.add('dark')
|
setTheme(savedTheme === 'dark')
|
||||||
} else {
|
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
document.documentElement.classList.remove('dark')
|
setTheme(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ const displayCategory = computed(() => getLocalizedText(props.category))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="group relative flex flex-col bg-white dark:!bg-[#0f172a] rounded-3xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-sm hover:shadow-xl dark:shadow-none hover:-translate-y-1 transition-all duration-300 h-full">
|
<div class="group relative flex flex-col bg-white dark:!bg-slate-900 rounded-3xl overflow-hidden border border-slate-200 dark:border-white/5 shadow-sm hover:shadow-xl dark:shadow-none hover:-translate-y-1 transition-all duration-300 h-full">
|
||||||
|
|
||||||
<!-- Thumbnail Section -->
|
<!-- Thumbnail Section -->
|
||||||
<div class="relative w-full aspect-video overflow-hidden">
|
<div class="relative w-full aspect-video overflow-hidden">
|
||||||
|
|
@ -125,7 +125,7 @@ const displayCategory = computed(() => getLocalizedText(props.category))
|
||||||
v-if="showViewDetails && !completed && !progress"
|
v-if="showViewDetails && !completed && !progress"
|
||||||
flat
|
flat
|
||||||
rounded
|
rounded
|
||||||
class="w-full font-bold !text-blue-600 !bg-blue-50 hover:!bg-blue-100 dark:!bg-blue-900/40 dark:!text-blue-300 dark:hover:!bg-blue-900/60"
|
class="w-full font-bold !text-blue-600 !bg-blue-50 hover:!bg-blue-100 dark:!bg-blue-500/10 dark:!text-blue-400 dark:hover:!bg-blue-500/20"
|
||||||
:label="$t('menu.viewDetails')"
|
:label="$t('menu.viewDetails')"
|
||||||
:to="`/course/${id}`"
|
:to="`/course/${id}`"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ const showConfirmPassword = ref(false);
|
||||||
<template>
|
<template>
|
||||||
<div :class="[!flat ? 'card-premium p-6 md:p-8' : '']" class="h-fit">
|
<div :class="[!flat ? 'card-premium p-6 md:p-8' : '']" class="h-fit">
|
||||||
<div v-if="!flat" class="flex items-center gap-3 mb-8">
|
<div v-if="!flat" class="flex items-center gap-3 mb-8">
|
||||||
<div class="w-10 h-10 rounded-xl bg-amber-50 dark:bg-amber-900/30 flex items-center justify-center">
|
<div class="w-10 h-10 rounded-xl bg-blue-50 dark:bg-blue-900/30 flex items-center justify-center">
|
||||||
<q-icon name="lock" class="text-amber-600 dark:text-amber-400 text-xl" />
|
<q-icon name="lock" class="text-blue-600 dark:text-blue-400 text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-black text-slate-900 dark:text-white">
|
<h2 class="text-xl font-black text-slate-900 dark:text-white">
|
||||||
{{ $t('profile.security') }}
|
{{ $t('profile.security') }}
|
||||||
|
|
@ -118,8 +118,8 @@ const showConfirmPassword = ref(false);
|
||||||
type="submit"
|
type="submit"
|
||||||
unelevated
|
unelevated
|
||||||
rounded
|
rounded
|
||||||
class="w-full py-3 font-bold text-base shadow-lg shadow-amber-500/20"
|
class="w-full py-3 font-bold text-base shadow-lg shadow-blue-500/20"
|
||||||
style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white;"
|
style="background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white;"
|
||||||
:label="$t('profile.changePasswordBtn')"
|
:label="$t('profile.changePasswordBtn')"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,4 @@
|
||||||
|
import type { User, LoginResponse, RegisterPayload } from '@/types/auth'
|
||||||
// Interface สำหรับข้อมูลผู้ใช้งาน (User)
|
|
||||||
interface User {
|
|
||||||
id: number
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
email_verified_at?: string | null
|
|
||||||
created_at?: string
|
|
||||||
updated_at?: string
|
|
||||||
role: {
|
|
||||||
code: string // เช่น 'STUDENT', 'INSTRUCTOR', 'ADMIN'
|
|
||||||
name: { th: string; en: string }
|
|
||||||
}
|
|
||||||
profile?: {
|
|
||||||
prefix: { th: string; en: string }
|
|
||||||
first_name: string
|
|
||||||
last_name: string
|
|
||||||
phone: string | null
|
|
||||||
avatar_url: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Interface สำหรับข้อมูลตอบกลับตอน Login
|
|
||||||
interface loginResponse {
|
|
||||||
token: string
|
|
||||||
refreshToken: string
|
|
||||||
user: User
|
|
||||||
profile: User['profile']
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface สำหรับข้อมูลที่ใช้ลงทะเบียน
|
|
||||||
interface RegisterPayload {
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
first_name: string
|
|
||||||
last_name: string
|
|
||||||
prefix: { th: string; en: string }
|
|
||||||
phone: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Composable: useAuth
|
// Composable: useAuth
|
||||||
|
|
|
||||||
|
|
@ -1,151 +1,14 @@
|
||||||
// Interface สำหรับข้อมูลคอร์สเรียน (Public Course Data)
|
import type {
|
||||||
export interface Course {
|
Course,
|
||||||
id: number
|
CourseResponse,
|
||||||
title: string | { th: string; en: string } // รองรับ 2 ภาษา
|
SingleCourseResponse,
|
||||||
slug: string
|
EnrolledCourse,
|
||||||
description: string | { th: string; en: string }
|
EnrolledCourseResponse,
|
||||||
thumbnail_url: string
|
QuizAnswerSubmission,
|
||||||
price: string
|
QuizSubmitRequest,
|
||||||
is_free: boolean
|
QuizResult,
|
||||||
original_price?: string
|
Certificate
|
||||||
have_certificate: boolean
|
} from '@/types/course'
|
||||||
status: string // DRAFT, PUBLISHED
|
|
||||||
category_id: number
|
|
||||||
created_at?: string
|
|
||||||
updated_at?: string
|
|
||||||
created_by?: number
|
|
||||||
updated_by?: number
|
|
||||||
approved_at?: string
|
|
||||||
approved_by?: number
|
|
||||||
rejection_reason?: string
|
|
||||||
enrolled?: boolean
|
|
||||||
total_lessons?: number
|
|
||||||
|
|
||||||
|
|
||||||
rating?: string
|
|
||||||
lessons?: number | string
|
|
||||||
levelType?: 'neutral' | 'warning' | 'success' // ใช้สำหรับการแสดงผล Badge ระดับความยาก (Frontend Logic)
|
|
||||||
|
|
||||||
// โครงสร้างบทเรียน (Chapters & Lessons)
|
|
||||||
chapters?: {
|
|
||||||
id: number
|
|
||||||
title: string | { th: string; en: string }
|
|
||||||
lessons: {
|
|
||||||
id: number
|
|
||||||
title: string | { th: string; en: string }
|
|
||||||
duration_minutes: number
|
|
||||||
video_url?: string
|
|
||||||
}[]
|
|
||||||
}[]
|
|
||||||
|
|
||||||
// ข้อมูลผู้สอนและเจ้าของคอร์ส
|
|
||||||
creator?: {
|
|
||||||
id: number
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
profile: {
|
|
||||||
first_name: string
|
|
||||||
last_name: string
|
|
||||||
avatar_url: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
instructors?: {
|
|
||||||
user_id: number
|
|
||||||
is_primary: boolean
|
|
||||||
user: {
|
|
||||||
id: number
|
|
||||||
username: string
|
|
||||||
email: string
|
|
||||||
profile: {
|
|
||||||
first_name: string
|
|
||||||
last_name: string
|
|
||||||
avatar_url: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CourseResponse {
|
|
||||||
code: number
|
|
||||||
message: string
|
|
||||||
data: Course[]
|
|
||||||
total: number
|
|
||||||
page?: number
|
|
||||||
limit?: number
|
|
||||||
totalPages?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SingleCourseResponse {
|
|
||||||
code: number
|
|
||||||
message: string
|
|
||||||
data: Course
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface สำหรับคอร์สที่ผู้ใช้ลงทะเบียนเรียนแล้ว (My Course)
|
|
||||||
export interface EnrolledCourse {
|
|
||||||
id: number
|
|
||||||
course_id: number
|
|
||||||
course: Course
|
|
||||||
status: 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED'
|
|
||||||
progress_percentage: number
|
|
||||||
enrolled_at: string
|
|
||||||
started_at?: string
|
|
||||||
completed_at?: string
|
|
||||||
last_accessed_at?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnrolledCourseResponse {
|
|
||||||
code: number
|
|
||||||
message: string
|
|
||||||
data: EnrolledCourse[]
|
|
||||||
total: number
|
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface สำหรับการส่งคำตอบแบบทดสอบ (Quiz Submission)
|
|
||||||
export interface QuizAnswerSubmission {
|
|
||||||
question_id: number
|
|
||||||
choice_id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QuizSubmitRequest {
|
|
||||||
answers: QuizAnswerSubmission[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface สำหรับผลลัพธ์การสอบ (Quiz Result)
|
|
||||||
export interface QuizResult {
|
|
||||||
answers_review: {
|
|
||||||
score: number
|
|
||||||
is_correct: boolean
|
|
||||||
correct_choice_id: number
|
|
||||||
selected_choice_id: number
|
|
||||||
question_id: number
|
|
||||||
}[]
|
|
||||||
completed_at: string
|
|
||||||
started_at: string
|
|
||||||
attempt_number: number
|
|
||||||
passing_score: number
|
|
||||||
is_passed: boolean
|
|
||||||
correct_answers: number
|
|
||||||
total_questions: number
|
|
||||||
total_score: number
|
|
||||||
score: number
|
|
||||||
quiz_id: number
|
|
||||||
attempt_id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface สำหรับ Certificate
|
|
||||||
export interface Certificate {
|
|
||||||
certificate_id: number
|
|
||||||
course_id: number
|
|
||||||
course_title: {
|
|
||||||
en: string
|
|
||||||
th: string
|
|
||||||
}
|
|
||||||
issued_at: string
|
|
||||||
download_url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Composable: useCourse
|
// Composable: useCourse
|
||||||
|
|
|
||||||
55
Frontend-Learner/constants/landing.ts
Normal file
55
Frontend-Learner/constants/landing.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* @file landing.ts
|
||||||
|
* @description Static data for the landing page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const CATEGORY_CARDS = [
|
||||||
|
{
|
||||||
|
title: 'โปรแกรมมิ่ง',
|
||||||
|
desc: 'เชี่ยวชาญการเขียนโค้ดและพัฒนาซอฟต์แวร์',
|
||||||
|
icon: 'code',
|
||||||
|
slug: 'programming',
|
||||||
|
iconColor: 'text-blue-600',
|
||||||
|
iconBg: 'bg-blue-600/5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'การออกแบบ',
|
||||||
|
desc: 'ทักษะ UI/UX และการออกแบบระดับมือโปร',
|
||||||
|
icon: 'palette',
|
||||||
|
slug: 'design',
|
||||||
|
iconColor: 'text-indigo-600',
|
||||||
|
iconBg: 'bg-indigo-600/5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ธุรกิจ',
|
||||||
|
desc: 'ทักษะการจัดการและความเป็นผู้นำสากล',
|
||||||
|
icon: 'business_center',
|
||||||
|
slug: 'business',
|
||||||
|
iconColor: 'text-blue-700',
|
||||||
|
iconBg: 'bg-blue-700/5'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const WHY_CHOOSE_US = [
|
||||||
|
{
|
||||||
|
title: 'ผู้สอนเชี่ยวชาญ',
|
||||||
|
desc: 'เรียนรู้จากผู้นำในอุตสาหกรรมที่มีประสบการณ์การทำงานหลายปีในบริษัทเทคโนโลยีชั้นนำระดับโลก',
|
||||||
|
icon: 'groups',
|
||||||
|
iconBg: 'bg-blue-600/10',
|
||||||
|
iconColor: 'text-blue-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'การเรียนรู้ที่ยืดหยุ่น',
|
||||||
|
desc: 'เรียนตามจังหวะของคุณเอง ได้ทุกที่ทุกเวลา เข้าถึงเนื้อหาคอร์สที่สมัครเรียนได้ตลอดชีพ',
|
||||||
|
icon: 'schedule',
|
||||||
|
iconBg: 'bg-indigo-600/10',
|
||||||
|
iconColor: 'text-indigo-600'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ประกาศนียบัตรเมื่อเรียนจบ',
|
||||||
|
desc: 'รับวุฒิบัตรที่เป็นที่ยอมรับเพื่อเสริมพอร์ตโฟลิโอระดับมืออาชีพของคุณและแชร์ลง LinkedIn ได้โดยตรง',
|
||||||
|
icon: 'verified',
|
||||||
|
iconBg: 'bg-blue-600/10',
|
||||||
|
iconColor: 'text-blue-600'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
"overview": "Home",
|
"overview": "Home",
|
||||||
"myCourses": "My Courses",
|
"myCourses": "My Courses",
|
||||||
"browseCourses": "Browse Courses",
|
"browseCourses": "Browse Courses",
|
||||||
"onlineCourses": "Online Courses",
|
"onlineCourses": "All Courses",
|
||||||
"recommendedCourses": "Recommended Courses",
|
"recommendedCourses": "Recommended Courses",
|
||||||
"announcements": "Announcements",
|
"announcements": "Announcements",
|
||||||
"profile": "My Profile"
|
"profile": "My Profile"
|
||||||
|
|
@ -107,9 +107,14 @@
|
||||||
"backToCatalog": "Back to Catalog",
|
"backToCatalog": "Back to Catalog",
|
||||||
"selectable": "Selected",
|
"selectable": "Selected",
|
||||||
"foundTotal": "Found Total",
|
"foundTotal": "Found Total",
|
||||||
"items": "items"
|
"items": "items",
|
||||||
|
"subtitle": "Choose to learn new skills from our curated quality courses",
|
||||||
|
"searchBtn": "Search"
|
||||||
},
|
},
|
||||||
"myCourses": {
|
"myCourses": {
|
||||||
|
"title": "My Courses",
|
||||||
|
"subtitle": "Track your progress and continue learning from where you left off",
|
||||||
|
"searchPlaceholder": "Search my courses...",
|
||||||
"filterAll": "All",
|
"filterAll": "All",
|
||||||
"filterProgress": "In Progress",
|
"filterProgress": "In Progress",
|
||||||
"filterCompleted": "Completed",
|
"filterCompleted": "Completed",
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
"overview": "หน้าหลัก",
|
"overview": "หน้าหลัก",
|
||||||
"myCourses": "คอร์สของฉัน",
|
"myCourses": "คอร์สของฉัน",
|
||||||
"browseCourses": "ค้นหาคอร์ส",
|
"browseCourses": "ค้นหาคอร์ส",
|
||||||
"onlineCourses": "คอร์สออนไลน์",
|
"onlineCourses": "คอร์สเรียนทั้งหมด",
|
||||||
"recommendedCourses": "คอร์สเรียนแนะนำ",
|
"recommendedCourses": "คอร์สเรียนแนะนำ",
|
||||||
"announcements": "ข่าวประกาศ",
|
"announcements": "ข่าวประกาศ",
|
||||||
"profile": "บัญชีผู้ใช้"
|
"profile": "บัญชีผู้ใช้"
|
||||||
|
|
@ -108,9 +108,13 @@
|
||||||
"selectable": "รายการที่เลือก",
|
"selectable": "รายการที่เลือก",
|
||||||
"foundTotal": "พบทั้งหมด",
|
"foundTotal": "พบทั้งหมด",
|
||||||
"items": "รายการ",
|
"items": "รายการ",
|
||||||
"subtitle": "เลือกเรียนรู้ทักษะใหม่ๆ จากหลักสูตรคุณภาพที่คัดสรรมาเพื่อคุณ"
|
"subtitle": "เลือกเรียนรู้ทักษะใหม่ๆ จากหลักสูตรคุณภาพที่คัดสรรมาเพื่อคุณ",
|
||||||
|
"searchBtn": "ค้นหา"
|
||||||
},
|
},
|
||||||
"myCourses": {
|
"myCourses": {
|
||||||
|
"title": "คอร์สของฉัน",
|
||||||
|
"subtitle": "ติดตามความคืบหน้าและเรียนรู้ต่อจากจุดที่ค้างไว้",
|
||||||
|
"searchPlaceholder": "ค้นหาชื่อคอร์สของฉัน...",
|
||||||
"filterAll": "ทั้งหมด",
|
"filterAll": "ทั้งหมด",
|
||||||
"filterProgress": "กำลังเรียน",
|
"filterProgress": "กำลังเรียน",
|
||||||
"filterCompleted": "เรียนจบแล้ว",
|
"filterCompleted": "เรียนจบแล้ว",
|
||||||
|
|
@ -187,7 +191,7 @@
|
||||||
"logout": "ออกจากระบบ"
|
"logout": "ออกจากระบบ"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"allCourses": "คอร์สทั้งหมด",
|
"allCourses": "คอร์สเรียนทั้งหมด",
|
||||||
"discovery": "ค้นพบ",
|
"discovery": "ค้นพบ",
|
||||||
"goToDashboard": "เข้าสู่หน้าจัดการเรียน"
|
"goToDashboard": "เข้าสู่หน้าจัดการเรียน"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,12 @@ const shouldHideSidebar = computed(() => {
|
||||||
v-model="rightDrawerOpen"
|
v-model="rightDrawerOpen"
|
||||||
side="right"
|
side="right"
|
||||||
overlay
|
overlay
|
||||||
bordered
|
|
||||||
class="bg-white dark:!bg-[#0f172a]"
|
class="bg-white dark:!bg-[#0f172a]"
|
||||||
:width="300"
|
:width="300"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col h-full bg-white dark:bg-[#0f172a]">
|
<div class="flex flex-col h-full bg-white dark:!bg-[#0f172a] text-slate-900 dark:!text-slate-100">
|
||||||
<!-- 1. Account Section -->
|
<!-- 1. Account Section -->
|
||||||
<div class="p-6 bg-slate-50/50 dark:bg-slate-800/30 border-b border-slate-100 dark:border-slate-800">
|
<div class="p-6 bg-slate-50/50 dark:!bg-slate-800/20">
|
||||||
<div class="flex items-center justify-between mb-8">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-black">E</div>
|
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-black">E</div>
|
||||||
|
|
@ -78,39 +77,37 @@ const shouldHideSidebar = computed(() => {
|
||||||
<img :src="currentUser?.photoURL || 'https://cdn.quasar.dev/img/avatar.png'" />
|
<img :src="currentUser?.photoURL || 'https://cdn.quasar.dev/img/avatar.png'" />
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<p class="font-bold text-slate-900 dark:text-white mb-0 truncate text-lg">
|
<p class="font-bold text-slate-900 dark:!text-white mb-0 truncate text-lg">
|
||||||
{{ currentUser?.firstName || 'Guest' }} {{ currentUser?.lastName || '' }}
|
{{ currentUser?.firstName || 'Guest' }} {{ currentUser?.lastName || '' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 truncate">{{ currentUser?.email || 'e-learning@platform.com' }}</p>
|
<p class="text-xs text-slate-500 dark:!text-slate-400 truncate">{{ currentUser?.email || 'e-learning@platform.com' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. Integrated Content Hub -->
|
<!-- 2. Integrated Content Hub -->
|
||||||
<div class="flex-grow overflow-y-auto pt-4">
|
<div class="flex-grow overflow-y-auto pt-4">
|
||||||
<q-list padding class="text-slate-600 dark:text-slate-300">
|
<q-list padding class="text-slate-600 dark:!text-slate-300">
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<q-item-label header class="text-[11px] font-black tracking-[0.2em] text-slate-400 uppercase px-6 pb-2">เมนูหลัก</q-item-label>
|
<q-item-label header class="text-[11px] font-black tracking-[0.2em] text-slate-400 uppercase px-6 pb-2">เมนูหลัก</q-item-label>
|
||||||
|
|
||||||
<q-item to="/dashboard" clickable v-ripple class="px-6 py-4" active-class="bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400 font-bold" @click="rightDrawerOpen = false">
|
<q-item to="/dashboard" clickable v-ripple class="px-6 py-4" active-class="bg-blue-50 text-blue-600 dark:!bg-blue-900/30 dark:!text-blue-400 font-bold" @click="rightDrawerOpen = false">
|
||||||
<q-item-section avatar><q-icon name="dashboard" size="24px" /></q-item-section>
|
<q-item-section avatar><q-icon name="dashboard" size="24px" /></q-item-section>
|
||||||
<q-item-section><span class="text-[15px] font-bold">{{ $t("sidebar.overview") }}</span></q-item-section>
|
<q-item-section><span class="text-[15px] font-bold">{{ $t("sidebar.overview") }}</span></q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item to="/browse/discovery" clickable v-ripple class="px-6 py-4" active-class="bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400 font-bold" @click="rightDrawerOpen = false">
|
<q-item to="/browse/discovery" clickable v-ripple class="px-6 py-4" active-class="bg-blue-50 text-blue-600 dark:!bg-blue-900/30 dark:!text-blue-400 font-bold" @click="rightDrawerOpen = false">
|
||||||
<q-item-section avatar><q-icon name="explore" size="24px" /></q-item-section>
|
<q-item-section avatar><q-icon name="explore" size="24px" /></q-item-section>
|
||||||
<q-item-section><span class="text-[15px] font-bold">{{ $t("landing.allCourses") }}</span></q-item-section>
|
<q-item-section><span class="text-[15px] font-bold">{{ $t("landing.allCourses") }}</span></q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item to="/dashboard/my-courses" clickable v-ripple class="px-6 py-4" active-class="bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400 font-bold" @click="rightDrawerOpen = false">
|
<q-item to="/dashboard/my-courses" clickable v-ripple class="px-6 py-4" active-class="bg-blue-50 text-blue-600 dark:!bg-blue-900/30 dark:!text-blue-400 font-bold" @click="rightDrawerOpen = false">
|
||||||
<q-item-section avatar><q-icon name="school" size="24px" /></q-item-section>
|
<q-item-section avatar><q-icon name="school" size="24px" /></q-item-section>
|
||||||
<q-item-section><span class="text-[15px] font-bold">{{ $t("sidebar.myCourses") || 'คอร์สเรียนของฉัน' }}</span></q-item-section>
|
<q-item-section><span class="text-[15px] font-bold">{{ $t("sidebar.myCourses") || 'คอร์สเรียนของฉัน' }}</span></q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-separator class="my-4 mx-6 opacity-50" />
|
|
||||||
|
|
||||||
<!-- Tools & Settings -->
|
<!-- Tools & Settings -->
|
||||||
<q-item-label header class="text-[11px] font-black tracking-[0.2em] text-slate-400 uppercase px-6 pb-2">เครื่องมือและการตั้งค่า</q-item-label>
|
<q-item-label header class="text-[11px] font-black tracking-[0.2em] text-slate-400 uppercase px-6 pb-2 mt-4">เครื่องมือและการตั้งค่า</q-item-label>
|
||||||
|
|
||||||
<!-- Language Selection -->
|
<!-- Language Selection -->
|
||||||
<q-item class="px-6 py-2">
|
<q-item class="px-6 py-2">
|
||||||
|
|
@ -140,16 +137,16 @@ const shouldHideSidebar = computed(() => {
|
||||||
|
|
||||||
<q-item clickable v-ripple @click="navigateTo('/dashboard/profile'); rightDrawerOpen = false" class="px-6 py-4">
|
<q-item clickable v-ripple @click="navigateTo('/dashboard/profile'); rightDrawerOpen = false" class="px-6 py-4">
|
||||||
<q-item-section avatar><q-icon name="person_outline" size="24px" /></q-item-section>
|
<q-item-section avatar><q-icon name="person_outline" size="24px" /></q-item-section>
|
||||||
<q-item-section><span class="font-bold text-[15px]">จัดการโปรไฟล์</span></q-item-section>
|
<q-item-section><span class="font-bold text-[15px] dark:!text-slate-300">จัดการโปรไฟล์</span></q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. Bottom Actions -->
|
<!-- 3. Bottom Actions -->
|
||||||
<div class="p-6 mt-auto border-t border-slate-100 dark:border-slate-800">
|
<div class="p-6 mt-auto border-t border-slate-100 dark:!border-white/10">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
class="full-width rounded-xl bg-red-50 text-red-600 dark:bg-red-900/20 dark:text-red-400 font-bold py-3 no-caps transition-all active:scale-95"
|
class="full-width rounded-xl bg-red-50 text-red-600 dark:!bg-red-900/20 dark:!text-red-400 font-bold py-3 no-caps transition-all active:scale-95"
|
||||||
@click="logout"
|
@click="logout"
|
||||||
>
|
>
|
||||||
<q-icon name="logout" size="20px" class="mr-2" />
|
<q-icon name="logout" size="20px" class="mr-2" />
|
||||||
|
|
|
||||||
|
|
@ -237,17 +237,24 @@ onMounted(() => {
|
||||||
<div v-else class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
<div v-else class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Test Credentials Box -->
|
||||||
|
<div class="mt-4 p-5 bg-blue-50/50 border border-blue-100 rounded-2xl flex flex-col items-center gap-2 animate-fade-in">
|
||||||
|
<div class="text-[11px] font-black uppercase tracking-[0.2em] text-blue-600 mb-1">บัญชีสำหรับทดสอบ (Test Account)</div>
|
||||||
|
<div class="flex flex-col items-center gap-1">
|
||||||
|
<div class="text-base font-black text-slate-900 select-all cursor-copy hover:text-blue-600 transition-colors">
|
||||||
|
studentedtest@example.com
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[11px] font-black uppercase tracking-wider text-slate-600">Password:</span>
|
||||||
|
<span class="text-base font-black select-all cursor-copy hover:text-blue-600 transition-colors text-slate-900">admin123</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="my-8 flex items-center gap-4">
|
|
||||||
<div class="h-px bg-slate-200 flex-1"></div>
|
|
||||||
<span class="text-slate-400 text-xs font-medium uppercase tracking-wider">หรือ</span>
|
|
||||||
<div class="h-px bg-slate-200 flex-1"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Register Link -->
|
<!-- Register Link -->
|
||||||
<div class="text-center">
|
<div class="text-center mt-8">
|
||||||
<p class="text-slate-600 text-sm">
|
<p class="text-slate-600 text-sm">
|
||||||
ยังไม่มีบัญชีสมาชิก?
|
ยังไม่มีบัญชีสมาชิก?
|
||||||
<NuxtLink to="/auth/register" class="font-bold text-blue-600 hover:text-blue-700 transition-colors ml-1">
|
<NuxtLink to="/auth/register" class="font-bold text-blue-600 hover:text-blue-700 transition-colors ml-1">
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,6 @@ onMounted(() => {
|
||||||
<!-- New Enhanced Search Section (Image 1 Style) -->
|
<!-- New Enhanced Search Section (Image 1 Style) -->
|
||||||
<div class="bg-blue-50/50 dark:bg-blue-900/10 rounded-[2.5rem] p-8 md:p-10 mb-8 border border-blue-100/50 dark:border-blue-500/10 transition-colors duration-300">
|
<div class="bg-blue-50/50 dark:bg-blue-900/10 rounded-[2.5rem] p-8 md:p-10 mb-8 border border-blue-100/50 dark:border-blue-500/10 transition-colors duration-300">
|
||||||
<div class="flex items-center gap-4 mb-2">
|
<div class="flex items-center gap-4 mb-2">
|
||||||
<span class="w-2 h-8 bg-blue-600 rounded-full shadow-lg shadow-blue-500/30"></span>
|
|
||||||
<h1 class="text-2xl md:text-3xl font-black text-slate-900 dark:text-white">
|
<h1 class="text-2xl md:text-3xl font-black text-slate-900 dark:text-white">
|
||||||
{{ $t("discovery.title") }}
|
{{ $t("discovery.title") }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -178,7 +177,7 @@ onMounted(() => {
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('discovery.searchPlaceholder') || 'ค้นหาคอร์สที่น่าสนใจที่นี่...'"
|
:placeholder="$t('discovery.searchPlaceholder') || 'ค้นหาคอร์สที่น่าสนใจที่นี่...'"
|
||||||
class="w-full pl-14 pr-6 py-3.5 bg-white dark:bg-slate-800 border-2 border-transparent rounded-2xl text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:border-blue-500/20 focus:ring-4 focus:ring-blue-500/5 transition-all text-base font-medium shadow-sm"
|
class="w-full pl-14 pr-6 py-3.5 bg-white dark:!bg-slate-900/80 border-2 border-transparent dark:border-white/5 rounded-2xl text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:border-blue-500/20 focus:ring-4 focus:ring-blue-500/5 transition-all text-base font-medium shadow-sm"
|
||||||
@keyup.enter="loadCourses(1)"
|
@keyup.enter="loadCourses(1)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -193,13 +192,13 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<q-icon name="search" size="20px" />
|
<q-icon name="search" size="20px" />
|
||||||
<span class="text-base">ค้นหา</span>
|
<span class="text-base">{{ $t("discovery.searchBtn") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-4 px-2">
|
<div class="flex items-center justify-between mb-8 px-2">
|
||||||
<div class="text-slate-500 dark:text-slate-400 text-sm font-bold uppercase tracking-wider">
|
<div class="text-slate-500 dark:text-slate-400 text-sm font-bold uppercase tracking-wider">
|
||||||
{{ $t("discovery.foundTotal") }} <span class="text-blue-600">{{ filteredCourses.length }}</span> {{ $t("discovery.items") }}
|
{{ $t("discovery.foundTotal") }} <span class="text-blue-600">{{ filteredCourses.length }}</span> {{ $t("discovery.items") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -207,7 +206,7 @@ onMounted(() => {
|
||||||
|
|
||||||
<!-- Unified Filter Section: Categories -->
|
<!-- Unified Filter Section: Categories -->
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-slate-900/50 p-2 rounded-2xl border border-slate-100 dark:border-white/5 inline-flex flex-wrap items-center gap-1.5 shadow-sm"
|
class="bg-white dark:!bg-slate-900/50 p-2 rounded-2xl border border-slate-100 dark:border-white/5 inline-flex flex-wrap items-center gap-1.5 shadow-sm mb-12"
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
|
|
@ -217,7 +216,7 @@ onMounted(() => {
|
||||||
:class="
|
:class="
|
||||||
selectedCategoryIds.length === 0
|
selectedCategoryIds.length === 0
|
||||||
? 'bg-blue-600 text-white shadow-md shadow-blue-600/20'
|
? 'bg-blue-600 text-white shadow-md shadow-blue-600/20'
|
||||||
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'
|
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:!hover:bg-slate-800/50'
|
||||||
"
|
"
|
||||||
@click="selectedCategoryIds = []"
|
@click="selectedCategoryIds = []"
|
||||||
:label="$t('discovery.showAll')"
|
:label="$t('discovery.showAll')"
|
||||||
|
|
@ -232,7 +231,7 @@ onMounted(() => {
|
||||||
:class="
|
:class="
|
||||||
selectedCategoryIds.includes(cat.id)
|
selectedCategoryIds.includes(cat.id)
|
||||||
? 'bg-blue-600 text-white shadow-md shadow-blue-600/20'
|
? 'bg-blue-600 text-white shadow-md shadow-blue-600/20'
|
||||||
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'
|
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:!hover:bg-slate-800/50'
|
||||||
"
|
"
|
||||||
@click="toggleCategory(cat.id)"
|
@click="toggleCategory(cat.id)"
|
||||||
:label="getLocalizedText(cat.name)"
|
:label="getLocalizedText(cat.name)"
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
|
||||||
<div
|
<div
|
||||||
v-for="course in sideCourses"
|
v-for="course in sideCourses"
|
||||||
:key="course.id"
|
:key="course.id"
|
||||||
class="flex-1 bg-white dark:bg-[#1e293b] rounded-2xl p-4 border border-gray-100 dark:border-slate-700 shadow-sm hover:shadow-md transition-all flex gap-4 items-center"
|
class="flex-1 bg-white dark:!bg-slate-900/40 rounded-2xl p-4 border border-slate-100 dark:border-white/5 shadow-sm hover:shadow-md transition-all flex gap-4 items-center"
|
||||||
>
|
>
|
||||||
<div class="w-32 h-20 rounded-xl overflow-hidden flex-shrink-0">
|
<div class="w-32 h-20 rounded-xl overflow-hidden flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
|
|
@ -291,7 +291,7 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
|
||||||
<!-- Empty State Placeholder if less than 2 side courses -->
|
<!-- Empty State Placeholder if less than 2 side courses -->
|
||||||
<div
|
<div
|
||||||
v-if="sideCourses.length < 2"
|
v-if="sideCourses.length < 2"
|
||||||
class="flex-1 bg-gray-50 dark:bg-[#1e293b]/50 rounded-2xl border border-dashed border-gray-200 dark:border-slate-700 flex items-center justify-center text-gray-400 dark:text-slate-500 text-sm transition-colors"
|
class="flex-1 bg-slate-50 dark:!bg-slate-900/30 rounded-2xl border border-dashed border-slate-200 dark:border-slate-800 flex items-center justify-center text-slate-400 dark:text-slate-600 text-sm transition-colors"
|
||||||
>
|
>
|
||||||
{{ $t("dashboard.startNewCourse") }}
|
{{ $t("dashboard.startNewCourse") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -326,9 +326,8 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
|
||||||
class="h-full md:col-span-1"
|
class="h-full md:col-span-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- CTA Card (Large) -->
|
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-[#1e293b] rounded-3xl border border-gray-100 dark:border-slate-700 shadow-sm p-8 flex flex-col items-center justify-center text-center h-full min-h-[300px] hover:shadow-md transition-all group"
|
class="bg-white dark:!bg-slate-900/40 rounded-3xl border border-slate-100 dark:border-white/5 shadow-sm p-8 flex flex-col items-center justify-center text-center h-full min-h-[300px] hover:shadow-md transition-all group"
|
||||||
>
|
>
|
||||||
<p class="text-gray-600 dark:text-slate-300 font-medium mb-6 mt-4 transition-colors">
|
<p class="text-gray-600 dark:text-slate-300 font-medium mb-6 mt-4 transition-colors">
|
||||||
{{ $t("dashboard.chooseLibrary") }}
|
{{ $t("dashboard.chooseLibrary") }}
|
||||||
|
|
@ -346,10 +345,9 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State when no courses -->
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="bg-white dark:bg-[#1e293b] rounded-3xl border border-dashed border-gray-200 dark:border-slate-700 p-12 flex flex-col items-center justify-center text-center min-h-[300px] transition-colors"
|
class="bg-white dark:!bg-slate-900/40 rounded-3xl border border-dashed border-slate-200 dark:border-slate-800 p-12 flex flex-col items-center justify-center text-center min-h-[300px] transition-colors"
|
||||||
>
|
>
|
||||||
<div class="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-full mb-6 transition-colors">
|
<div class="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-full mb-6 transition-colors">
|
||||||
<q-icon name="school" size="48px" class="text-blue-200 dark:text-blue-400" />
|
<q-icon name="school" size="48px" class="text-blue-200 dark:text-blue-400" />
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,10 @@ definePageMeta({
|
||||||
middleware: 'auth'
|
middleware: 'auth'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'คอร์สของฉัน - e-Learning'
|
title: `${t('sidebar.myCourses')} - e-Learning`
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
@ -28,7 +30,6 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { locale } = useI18n()
|
|
||||||
|
|
||||||
// Helper to get localized text
|
// Helper to get localized text
|
||||||
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
|
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
|
||||||
|
|
@ -153,8 +154,8 @@ const validCourseId = computed(() => {
|
||||||
<!-- Page Header & Filters (Unified Layout) -->
|
<!-- Page Header & Filters (Unified Layout) -->
|
||||||
<!-- New Enhanced Search Section (Image 2 Style) -->
|
<!-- New Enhanced Search Section (Image 2 Style) -->
|
||||||
<div class="bg-blue-50/50 dark:bg-blue-900/10 rounded-[2.5rem] p-8 md:p-10 mb-6 border border-blue-100/50 dark:border-blue-500/10">
|
<div class="bg-blue-50/50 dark:bg-blue-900/10 rounded-[2.5rem] p-8 md:p-10 mb-6 border border-blue-100/50 dark:border-blue-500/10">
|
||||||
<h2 class="text-2xl md:text-3xl font-black text-slate-900 dark:text-white mb-2">คอร์สของฉัน</h2>
|
<h2 class="text-2xl md:text-3xl font-black text-slate-900 dark:text-white mb-2">{{ $t('myCourses.title') }}</h2>
|
||||||
<p class="text-slate-500 dark:text-slate-400 font-medium mb-8">ติดตามความคืบหน้าและเรียนรู้ต่อจากจุดที่ค้างไว้</p>
|
<p class="text-slate-500 dark:text-slate-400 font-medium mb-8">{{ $t('myCourses.subtitle') }}</p>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
<!-- Search Input -->
|
<!-- Search Input -->
|
||||||
|
|
@ -165,8 +166,8 @@ const validCourseId = computed(() => {
|
||||||
<input
|
<input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="ค้นหาชื่อคอร์สของฉัน..."
|
:placeholder="$t('myCourses.searchPlaceholder')"
|
||||||
class="w-full pl-14 pr-6 py-3.5 bg-white dark:bg-slate-800 border-2 border-transparent rounded-2xl text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:border-blue-500/20 focus:ring-4 focus:ring-blue-500/5 transition-all text-base font-medium shadow-sm"
|
class="w-full pl-14 pr-6 py-3.5 bg-white dark:!bg-slate-900/80 border-2 border-transparent dark:border-white/5 rounded-2xl text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:border-blue-500/20 focus:ring-4 focus:ring-blue-500/5 transition-all text-base font-medium shadow-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -179,15 +180,15 @@ const validCourseId = computed(() => {
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<q-icon name="search" size="20px" />
|
<q-icon name="search" size="20px" />
|
||||||
<span class="text-base">ค้นหา</span>
|
<span class="text-base">{{ $t("discovery.searchBtn") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-12">
|
||||||
<!-- Filter Tabs (Horizontal Bar) -->
|
<!-- Filter Tabs (Horizontal Bar) -->
|
||||||
<div class="bg-white dark:bg-slate-900/50 p-1.5 rounded-2xl border border-slate-100 dark:border-white/5 inline-flex items-center gap-1 shadow-sm">
|
<div class="bg-white dark:!bg-slate-900/50 p-1.5 rounded-2xl border border-slate-100 dark:border-white/5 inline-flex items-center gap-1 shadow-sm">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-for="filter in ['all', 'progress', 'completed']"
|
v-for="filter in ['all', 'progress', 'completed']"
|
||||||
:key="filter"
|
:key="filter"
|
||||||
|
|
@ -196,7 +197,7 @@ const validCourseId = computed(() => {
|
||||||
rounded
|
rounded
|
||||||
dense
|
dense
|
||||||
class="px-5 py-2 font-bold transition-all text-[11px] uppercase tracking-wider"
|
class="px-5 py-2 font-bold transition-all text-[11px] uppercase tracking-wider"
|
||||||
:class="activeFilter === filter ? 'bg-blue-600 text-white shadow-md shadow-blue-600/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'"
|
:class="activeFilter === filter ? 'bg-blue-600 text-white shadow-md shadow-blue-600/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:!hover:bg-slate-800/50'"
|
||||||
:label="$t(`myCourses.filter${filter.charAt(0).toUpperCase() + filter.slice(1)}`)"
|
:label="$t(`myCourses.filter${filter.charAt(0).toUpperCase() + filter.slice(1)}`)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -236,7 +237,7 @@ const validCourseId = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-if="!isLoading && filteredEnrolledCourses.length === 0" class="flex flex-col items-center justify-center py-20 bg-slate-50 dark:bg-slate-800/50 rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-700 mt-4">
|
<div v-if="!isLoading && enrolledCourses.length === 0" class="flex flex-col items-center justify-center py-20 bg-white dark:!bg-slate-900/40 rounded-3xl border border-dashed border-slate-200 dark:border-white/5 mt-4">
|
||||||
<q-icon v-if="searchQuery" name="search_off" size="64px" class="text-slate-300 dark:text-slate-600 mb-4" />
|
<q-icon v-if="searchQuery" name="search_off" size="64px" class="text-slate-300 dark:text-slate-600 mb-4" />
|
||||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">
|
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">
|
||||||
{{ searchQuery ? $t('discovery.emptyTitle') : $t('myCourses.emptyTitle') }}
|
{{ searchQuery ? $t('discovery.emptyTitle') : $t('myCourses.emptyTitle') }}
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,6 @@ onMounted(async () => {
|
||||||
@click="toggleEdit(false)"
|
@click="toggleEdit(false)"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<span class="w-1.5 h-10 md:h-12 bg-blue-600 rounded-full shadow-lg shadow-blue-500/50 mt-1 flex-shrink-0"></span>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl md:text-4xl font-black text-slate-900 dark:text-white leading-tight">
|
<h1 class="text-3xl md:text-4xl font-black text-slate-900 dark:text-white leading-tight">
|
||||||
{{ (isHydrated && isEditing) ? $t('profile.editProfile') : $t('profile.myProfile') }}
|
{{ (isHydrated && isEditing) ? $t('profile.editProfile') : $t('profile.myProfile') }}
|
||||||
|
|
@ -250,7 +249,7 @@ onMounted(async () => {
|
||||||
<div v-else class="max-w-4xl mx-auto pb-20">
|
<div v-else class="max-w-4xl mx-auto pb-20">
|
||||||
|
|
||||||
<!-- VIEW MODE: Premium Card with Banner -->
|
<!-- VIEW MODE: Premium Card with Banner -->
|
||||||
<div v-if="!isEditing" class="bg-white dark:bg-[#1e293b] border border-slate-200 dark:border-slate-700/50 rounded-3xl shadow-xl dark:shadow-none overflow-hidden fade-in min-h-[500px] flex flex-col transition-colors duration-300">
|
<div v-if="!isEditing" class="bg-white dark:!bg-slate-900/50 border border-slate-200 dark:border-white/5 rounded-3xl shadow-xl dark:shadow-none overflow-hidden fade-in min-h-[500px] flex flex-col transition-colors duration-300">
|
||||||
|
|
||||||
<!-- Identity Header (Banner & Avatar) -->
|
<!-- Identity Header (Banner & Avatar) -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|
@ -269,7 +268,7 @@ onMounted(async () => {
|
||||||
:first-name="userData.firstName"
|
:first-name="userData.firstName"
|
||||||
:last-name="userData.lastName"
|
:last-name="userData.lastName"
|
||||||
size="140"
|
size="140"
|
||||||
class="border-[6px] border-white dark:border-[#1e293b] shadow-2xl rounded-[2.5rem] bg-white dark:bg-slate-800 transition-colors duration-300"
|
class="border-[6px] border-white dark:border-slate-900 shadow-2xl rounded-[2.5rem] bg-white dark:bg-slate-800 transition-colors duration-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -278,11 +277,11 @@ onMounted(async () => {
|
||||||
{{ userData.firstName }} {{ userData.lastName }}
|
{{ userData.firstName }} {{ userData.lastName }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-wrap items-center justify-center md:justify-start gap-4">
|
<div class="flex flex-wrap items-center justify-center md:justify-start gap-4">
|
||||||
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-800/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-slate-700">
|
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-900/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-white/5">
|
||||||
<q-icon name="alternate_email" size="xs" class="text-blue-500" />
|
<q-icon name="alternate_email" size="xs" class="text-blue-500" />
|
||||||
<span class="text-sm">{{ userData.email }}</span>
|
<span class="text-sm">{{ userData.email }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-800/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-slate-700">
|
<div class="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-bold bg-slate-50 dark:bg-slate-900/50 px-3 py-1.5 rounded-xl border border-slate-100 dark:border-white/5">
|
||||||
<q-icon name="verified_user" size="xs" :class="userData.emailVerifiedAt ? 'text-green-500' : 'text-amber-500'" />
|
<q-icon name="verified_user" size="xs" :class="userData.emailVerifiedAt ? 'text-green-500' : 'text-amber-500'" />
|
||||||
<span class="text-sm">{{ userData.emailVerifiedAt ? $t('profile.emailVerified') : $t('profile.verifyEmail') }}</span>
|
<span class="text-sm">{{ userData.emailVerifiedAt ? $t('profile.emailVerified') : $t('profile.verifyEmail') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -325,7 +324,7 @@ onMounted(async () => {
|
||||||
<div v-else class="fade-in">
|
<div v-else class="fade-in">
|
||||||
<!-- Tab Selector -->
|
<!-- Tab Selector -->
|
||||||
<div class="flex justify-center mb-8">
|
<div class="flex justify-center mb-8">
|
||||||
<div class="bg-white dark:bg-[#1e293b] p-1.5 rounded-2xl flex items-center gap-1 border border-slate-200 dark:border-slate-700 shadow-sm">
|
<div class="bg-white dark:!bg-slate-900/50 p-1.5 rounded-2xl flex items-center gap-1 border border-slate-200 dark:border-white/5 shadow-sm">
|
||||||
<button
|
<button
|
||||||
@click="activeTab = 'general'"
|
@click="activeTab = 'general'"
|
||||||
class="px-6 md:px-8 py-3 rounded-xl font-black text-xs uppercase tracking-widest transition-all flex items-center gap-2"
|
class="px-6 md:px-8 py-3 rounded-xl font-black text-xs uppercase tracking-widest transition-all flex items-center gap-2"
|
||||||
|
|
@ -336,7 +335,7 @@ onMounted(async () => {
|
||||||
<button
|
<button
|
||||||
@click="activeTab = 'security'"
|
@click="activeTab = 'security'"
|
||||||
class="px-6 md:px-8 py-3 rounded-xl font-black text-xs uppercase tracking-widest transition-all flex items-center gap-2"
|
class="px-6 md:px-8 py-3 rounded-xl font-black text-xs uppercase tracking-widest transition-all flex items-center gap-2"
|
||||||
:class="activeTab === 'security' ? 'bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 shadow-sm scale-100' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 scale-95 opacity-70'"
|
:class="activeTab === 'security' ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 shadow-sm scale-100' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 scale-95 opacity-70'"
|
||||||
>
|
>
|
||||||
<q-icon name="lock_open" size="18px" /> {{ $t('profile.security') }}
|
<q-icon name="lock_open" size="18px" /> {{ $t('profile.security') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -345,7 +344,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
<!-- Edit Content -->
|
<!-- Edit Content -->
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-3xl mx-auto">
|
||||||
<div v-if="activeTab === 'general'" class="bg-white dark:bg-[#1e293b] border border-slate-200 dark:border-slate-700/50 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
|
<div v-if="activeTab === 'general'" class="bg-white dark:!bg-slate-900/50 border border-slate-200 dark:border-white/5 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
|
||||||
<ProfileEditForm
|
<ProfileEditForm
|
||||||
v-model="userData"
|
v-model="userData"
|
||||||
:loading="isProfileSaving"
|
:loading="isProfileSaving"
|
||||||
|
|
@ -355,7 +354,7 @@ onMounted(async () => {
|
||||||
@verify="handleSendVerifyEmail"
|
@verify="handleSendVerifyEmail"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="bg-white dark:bg-[#1e293b] border border-slate-200 dark:border-slate-700/50 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
|
<div v-else class="bg-white dark:!bg-slate-900/50 border border-slate-200 dark:border-white/5 rounded-3xl shadow-xl dark:shadow-none p-6 md:p-10">
|
||||||
<PasswordChangeForm
|
<PasswordChangeForm
|
||||||
v-model="passwordForm"
|
v-model="passwordForm"
|
||||||
:loading="isPasswordSaving"
|
:loading="isPasswordSaving"
|
||||||
|
|
|
||||||
|
|
@ -13,37 +13,14 @@ useHead({
|
||||||
title: 'E-Learning System - ระบบการเรียนการสอนออนไลน์'
|
title: 'E-Learning System - ระบบการเรียนการสอนออนไลน์'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
import { CATEGORY_CARDS, WHY_CHOOSE_US } from '@/constants/landing'
|
||||||
|
|
||||||
const { fetchCategories } = useCategory()
|
const { fetchCategories } = useCategory()
|
||||||
const { fetchCourses, getLocalizedText } = useCourse()
|
const { fetchCourses, getLocalizedText } = useCourse()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
const stepOneCards = [
|
const categoryCards = CATEGORY_CARDS
|
||||||
{ title: 'AI Foundations', desc: 'เข้าใจพื้นฐาน AI ใช้งานจริงได้ทุกสายงาน', bgClass: 'bg-slate-900', textClass: 'text-white', arrowClass: 'text-white/60 border-white/20 group-hover:text-slate-900', categorySlug: 'programming' },
|
const whyChooseUs = WHY_CHOOSE_US
|
||||||
{ title: 'Data Analyst', desc: 'เรียนจนทำ Dashboard วิเคราะห์ Data ได้เลย', bgClass: 'bg-amber-500', textClass: 'text-slate-900', arrowClass: 'text-slate-900/40 border-slate-900/10 group-hover:text-amber-500', categorySlug: 'business' },
|
|
||||||
{ title: 'Front-End Web Developer', desc: 'เขียนเว็บสวย ใช้งานได้จริงตั้งแต่หน้าแรก', bgClass: 'bg-orange-500', textClass: 'text-white', arrowClass: 'text-white/60 border-white/20 group-hover:text-orange-500', categorySlug: 'programming' },
|
|
||||||
{ title: 'UX/UI Designer', desc: 'ต่อยอดทำ Portfolio ไม่มีประสบการณ์ก็เรียนได้', bgClass: 'bg-pink-600', textClass: 'text-white', arrowClass: 'text-white/60 border-white/20 group-hover:text-pink-600', categorySlug: 'design' },
|
|
||||||
{ title: 'Product Manager', desc: 'เก็บทุกทักษะ ปั้น Product วางแผนแบบมือโปร', bgClass: 'bg-teal-500', textClass: 'text-white', arrowClass: 'text-white/60 border-white/20 group-hover:text-teal-500', categorySlug: 'business' },
|
|
||||||
{ title: 'Back-End Developer', desc: 'เข้าใจโครงสร้างระบบและฐานข้อมูลหลังบ้าน', bgClass: 'bg-blue-600', textClass: 'text-white', arrowClass: 'text-white/60 border-white/20 group-hover:text-blue-600', categorySlug: 'programming' },
|
|
||||||
{ title: 'Supply Chain & Logistics', desc: 'ใช้ Data วางแผนโลจิสติกส์ได้อย่างมีประสิทธิภาพ', bgClass: 'bg-slate-700', textClass: 'text-white', arrowClass: 'text-white/60 border-white/20 group-hover:text-slate-700', categorySlug: 'business' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const learningStyles = [
|
|
||||||
{
|
|
||||||
title: 'คอร์สออนไลน์', icon: 'desktop_windows', type: 'ONLINE',
|
|
||||||
subtitle: 'เรียนได้ทุกที่ ทุกเวลา', desc: 'คัดสรรเนื้อหาคุณภาพจากผู้เชี่ยวชาญ\nพร้อมให้คุณเริ่มต้นเรียนรู้ได้ทันที',
|
|
||||||
time: 'เข้าถึงได้ตลอดชีพ',
|
|
||||||
features: ['เนื้อหาครบทุกประเด็นสำคัญ', 'โจทย์ตัวอย่างและแบบฝึกหัด', 'เรียนซ้ำได้ไม่จำกัด', 'ใบเซอร์ทิฟิเคตหลังเรียนจบ'],
|
|
||||||
iconBg: 'bg-blue-50', iconColor: 'text-blue-600', titleClass: 'text-blue-700',
|
|
||||||
btnClass: 'bg-indigo-900 text-white hover:bg-indigo-800'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const promoCategories = [
|
|
||||||
{ title: 'Data', desc: 'เรียนรู้และฝึกฝนกระบวนการคิดสร้างมูลค่าให้ธุรกิจด้วยข้อมูล', icon: 'analytics' },
|
|
||||||
{ title: 'Design', desc: 'ออกแบบ Digital Product เพื่อให้ผู้ใช้งานได้รับประสบการณ์ที่ดีที่สุด', icon: 'palette' },
|
|
||||||
{ title: 'Tech', desc: 'พร้อมเป็นที่ต้องการของตลาดแรงงานด้วยทักษะการเขียนโปรแกรม', icon: 'code' },
|
|
||||||
{ title: 'Business', desc: 'พลิกโฉมธุรกิจในยุคดิจิทัลด้วยการเข้าถึงลูกค้าในช่องทางและเวลาที่เหมาะสม', icon: 'trending_up' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const categories = ref<any[]>([])
|
const categories = ref<any[]>([])
|
||||||
const topCourses = ref<any[]>([])
|
const topCourses = ref<any[]>([])
|
||||||
|
|
@ -173,128 +150,73 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="pt-16 pb-12 md:pt-24 md:pb-20 bg-white">
|
<!-- Why Choose Us Section -->
|
||||||
|
<section class="pt-20 pb-12 bg-white relative">
|
||||||
<div class="container mx-auto px-6 lg:px-12">
|
<div class="container mx-auto px-6 lg:px-12">
|
||||||
<div class="text-center mb-16 slide-up">
|
<div class="text-center mb-16 slide-up">
|
||||||
<h2 class="text-3xl md:text-5xl font-bold text-slate-900 mb-6 px-4">
|
<h2 class="text-3xl md:text-5xl font-black text-slate-900 mb-6">
|
||||||
เพราะ “ก้าวแรก” ของการพัฒนาตัวเอง ท้าทายเสมอ
|
ทำไมต้องเลือกแพลตฟอร์มของเรา?
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-slate-500 text-lg md:text-xl font-medium max-w-3xl mx-auto leading-relaxed">
|
<p class="text-slate-500 text-lg md:text-xl font-medium max-w-3xl mx-auto leading-relaxed">
|
||||||
เราจึงตั้งใจออกแบบบทเรียนให้ <span class="text-blue-600 font-bold">‘เข้าใจง่าย’</span> และ <span class="text-blue-600 font-bold">‘นำไปใช้ได้จริง’</span> เพื่อให้ทุกก้าวของคุณ มั่นคงและไปถึงเป้าหมายได้สำเร็จ
|
เรามีเครื่องมือและความเชี่ยวชาญที่จะช่วยให้คุณประสบความสำเร็จในการเปลี่ยนสายอาชีพและการสร้างทักษะระดับมืออาชีพ
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid Container (Bento Layout) -->
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
<div class="relative">
|
<div v-for="(item, i) in whyChooseUs" :key="i"
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
class="slide-up p-10 rounded-[2.5rem] bg-slate-50/50 border border-slate-100 hover:bg-white hover:shadow-2xl hover:shadow-blue-600/5 transition-all duration-500 group"
|
||||||
<div v-for="(card, i) in stepOneCards" :key="i"
|
:style="`animation-delay: ${i * 0.1}s`"
|
||||||
class="group cursor-pointer rounded-3xl p-6 flex flex-col justify-between transition-all hover:-translate-y-1 shadow-lg hover:shadow-2xl overflow-hidden relative"
|
>
|
||||||
:class="[
|
<div class="w-16 h-16 rounded-3xl flex items-center justify-center mb-8 transition-transform group-hover:scale-110 duration-500"
|
||||||
card.bgClass,
|
:class="item.iconBg"
|
||||||
i === 0 ? 'lg:row-span-2 min-h-[380px]' : 'min-h-[220px]'
|
|
||||||
]"
|
|
||||||
@click="goBrowse(card.categorySlug)"
|
|
||||||
>
|
>
|
||||||
<!-- Background Accent -->
|
<q-icon :name="item.icon" size="32px" :class="item.iconColor" />
|
||||||
<div class="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-1/2 translate-x-1/2" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span class="text-[10px] font-bold uppercase tracking-[0.15em] opacity-80 mb-3 block" :class="card.textClass === 'text-white' ? 'text-white/80' : 'text-slate-900/60'">ก้าวแรกของ</span>
|
|
||||||
<h3 class="text-2xl font-bold leading-tight tracking-tight mb-2" :class="card.textClass">{{ card.title }}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4 relative z-10">
|
|
||||||
<p class="text-sm font-medium leading-relaxed opacity-90" :class="card.textClass">{{ card.desc }}</p>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<div class="w-10 h-10 rounded-full border border-white/20 flex items-center justify-center transition-all bg-white/10 group-hover:bg-white/20 group-hover:scale-105 backdrop-blur-sm"
|
|
||||||
:class="[i === 0 ? 'w-12 h-12' : '']">
|
|
||||||
<q-icon name="arrow_forward" :size="i === 0 ? '24px' : '20px'" :class="card.textClass" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-900 mb-4 group-hover:text-blue-600 transition-colors">
|
||||||
|
{{ item.title }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-slate-500 text-lg leading-relaxed font-medium">
|
||||||
|
{{ item.desc }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section 2: "Value Proposition" - Why Online Learning here? -->
|
<section class="pt-16 pb-12 md:pt-24 md:pb-20 bg-white">
|
||||||
<section class="pt-12 pb-12 md:pt-20 md:pb-20 bg-white relative overflow-hidden">
|
|
||||||
<!-- Decorative background blur -->
|
|
||||||
<div class="absolute top-1/2 left-0 -translate-y-1/2 w-96 h-96 bg-blue-100/50 rounded-full blur-[100px] -z-10" />
|
|
||||||
|
|
||||||
<div class="container mx-auto px-6 lg:px-12">
|
<div class="container mx-auto px-6 lg:px-12">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-20 items-center">
|
<div class="mb-12 slide-up">
|
||||||
<!-- Left side: Visual representation -->
|
<h2 class="text-3xl md:text-4xl font-black text-slate-900 px-4">
|
||||||
<div class="relative slide-up">
|
เลือกเรียนตามเรื่องที่คุณสนใจ
|
||||||
<div class="relative z-10 bg-gradient-to-br from-blue-600 to-indigo-700 rounded-[4rem] p-12 md:p-20 shadow-3xl overflow-hidden group">
|
</h2>
|
||||||
<!-- Animated background shapes -->
|
</div>
|
||||||
<div class="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2 group-hover:scale-110 transition-transform duration-1000" />
|
|
||||||
<div class="absolute bottom-0 left-0 w-48 h-48 bg-amber-400/20 rounded-full translate-y-1/2 -translate-x-1/2" />
|
|
||||||
|
|
||||||
<div class="relative z-20 flex flex-col items-center text-center text-white">
|
|
||||||
<div class="w-24 h-24 md:w-32 md:h-32 bg-white/20 backdrop-blur-md rounded-[2.5rem] flex items-center justify-center mb-8 shadow-inner">
|
|
||||||
<q-icon name="laptop_mac" size="64px" class="text-white" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-3xl md:text-5xl font-black leading-[1.2] md:leading-[1.15] tracking-tight mb-0 pt-1 overflow-visible">
|
|
||||||
คอร์สออนไลน์<br class="hidden md:block" />
|
|
||||||
ที่ออกแบบมาสำหรับคุณ
|
|
||||||
</h3>
|
|
||||||
<p class="mt-5 text-blue-100/90 text-base md:text-lg font-medium leading-relaxed max-w-md">
|
|
||||||
เรียนรู้ทักษะใหม่จากผู้เชี่ยวชาญตัวจริง พร้อมเนื้อหาที่เข้มข้นและใช้งานได้จริง
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Floating Stats pill -->
|
|
||||||
<div class="absolute -bottom-10 -right-6 md:right-10 z-30 bg-white p-6 rounded-3xl shadow-2xl animate-float">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 rounded-2xl bg-amber-50 flex items-center justify-center text-amber-600">
|
|
||||||
<q-icon name="query_builder" size="28px" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-xs font-bold text-slate-400 uppercase tracking-tighter">Access Status</div>
|
|
||||||
<div class="text-xl font-black text-slate-900">เข้าถึงได้ตลอดชีพ</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right side: Content & Benefits -->
|
<!-- Horizontal Cards (New Layout - Image 2) -->
|
||||||
<div class="slide-up" style="animation-delay: 0.2s;">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
|
||||||
<div class="mb-12">
|
<div v-for="(card, i) in categoryCards" :key="i"
|
||||||
<span class="inline-flex items-center px-5 py-2 rounded-full bg-blue-50 text-blue-600 text-xs md:text-sm font-extrabold uppercase tracking-widest mb-5 border border-blue-100">
|
class="group cursor-pointer bg-white rounded-[2rem] p-6 border border-slate-100/80 shadow-sm hover:shadow-2xl hover:shadow-blue-600/5 hover:-translate-y-1 transition-all duration-500 relative flex items-center gap-5"
|
||||||
Premium Learning Experience
|
@click="goBrowse(card.slug)"
|
||||||
</span>
|
>
|
||||||
<h2 class="text-4xl md:text-6xl font-bold text-slate-900 leading-[1.2] md:leading-[1.2] tracking-tight mb-0 pt-1 overflow-visible">
|
<!-- Icon Box -->
|
||||||
ก้าวข้ามทุกขีดจำกัด<br />
|
<div class="flex-shrink-0 w-16 h-16 rounded-[1.5rem] flex items-center justify-center bg-blue-50/50 group-hover:scale-110 transition-transform duration-500"
|
||||||
ด้วยการเรียนรู้ที่ <span class="text-blue-600">“อิสระ”</span>
|
>
|
||||||
</h2>
|
<q-icon :name="card.icon" size="28px" class="text-blue-600" />
|
||||||
<p class="mt-6 text-slate-500 text-lg md:text-xl font-medium leading-relaxed max-w-2xl">
|
</div>
|
||||||
เราคัดสรรและคราฟต์ทุกคอร์สเรียนเพื่อให้มั่นใจว่าคุณจะได้รับประสบการณ์การเรียนรู้ที่ดีที่สุด ไม่ว่าจะอยู่ที่ไหนหรือเวลาใดก็ตาม
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-grow pr-2">
|
||||||
|
<h3 class="text-lg md:text-xl font-black text-slate-900 mb-1 group-hover:text-blue-600 transition-colors leading-tight">
|
||||||
|
{{ card.title }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-slate-500 text-xs md:text-sm font-medium leading-relaxed opacity-70">
|
||||||
|
{{ card.desc }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
<!-- Arrow -->
|
||||||
<div v-for="(feature, f) in learningStyles[0].features" :key="f"
|
<div class="flex-shrink-0 text-slate-300 group-hover:text-blue-600 transition-colors transform group-hover:translate-x-1 duration-300">
|
||||||
class="flex items-center gap-4 p-5 rounded-3xl bg-slate-50 border border-slate-100 group hover:border-blue-200 hover:bg-white hover:shadow-xl transition-all duration-300"
|
<q-icon name="chevron_right" size="24px" />
|
||||||
>
|
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-white flex items-center justify-center text-green-500 shadow-sm group-hover:bg-green-500 group-hover:text-white transition-colors">
|
|
||||||
<q-icon name="check" size="20px" />
|
|
||||||
</div>
|
|
||||||
<span class="text-base font-black text-slate-700 leading-snug">{{ feature }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
rounded
|
|
||||||
color="primary"
|
|
||||||
label="เริ่มต้นบทเรียนแรกของคุณ"
|
|
||||||
class="px-12 h-20 font-black text-xl md:text-2xl transition-all shadow-xl hover:shadow-2xl hover:-translate-y-1"
|
|
||||||
no-caps
|
|
||||||
:to="user ? '/browse' : '/auth/login'"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -302,6 +224,7 @@ onMounted(() => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Section 4: "คอร์สออนไลน์" -->
|
<!-- Section 4: "คอร์สออนไลน์" -->
|
||||||
<section class="pt-12 pb-24 md:pt-20 md:pb-40 bg-slate-50/50">
|
<section class="pt-12 pb-24 md:pt-20 md:pb-40 bg-slate-50/50">
|
||||||
<div class="container mx-auto px-6 lg:px-12">
|
<div class="container mx-auto px-6 lg:px-12">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'default'
|
layout: 'auth'
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
@ -68,8 +68,8 @@ const navigateToHome = () => {
|
||||||
|
|
||||||
<!-- Success State -->
|
<!-- Success State -->
|
||||||
<div v-else-if="isSuccess" class="flex flex-col items-center animate-bounce-in">
|
<div v-else-if="isSuccess" class="flex flex-col items-center animate-bounce-in">
|
||||||
<div class="w-24 h-24 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-6">
|
<div class="w-24 h-24 rounded-full bg-green-500 flex items-center justify-center mb-10 shadow-lg shadow-green-500/20">
|
||||||
<q-icon name="check_circle" class="text-6xl text-green-500" />
|
<q-icon name="check" class="text-5xl text-white font-black" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-3xl font-black text-slate-900 dark:text-white mb-2">
|
<h2 class="text-3xl font-black text-slate-900 dark:text-white mb-2">
|
||||||
|
|
|
||||||
41
Frontend-Learner/types/auth.ts
Normal file
41
Frontend-Learner/types/auth.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @file auth.ts
|
||||||
|
* @description Type definitions for authentication and user profiles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
email_verified_at?: string | null
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
role: {
|
||||||
|
code: string // เช่น 'STUDENT', 'INSTRUCTOR', 'ADMIN'
|
||||||
|
name: { th: string; en: string }
|
||||||
|
}
|
||||||
|
profile?: {
|
||||||
|
prefix: { th: string; en: string }
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
phone: string | null
|
||||||
|
avatar_url: string | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string
|
||||||
|
refreshToken: string
|
||||||
|
user: User
|
||||||
|
profile: User['profile']
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterPayload {
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
prefix: { th: string; en: string }
|
||||||
|
phone: string
|
||||||
|
}
|
||||||
142
Frontend-Learner/types/course.ts
Normal file
142
Frontend-Learner/types/course.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
/**
|
||||||
|
* @file course.ts
|
||||||
|
* @description Type definitions for courses, enrollments, quizzes, and certificates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Course {
|
||||||
|
id: number
|
||||||
|
title: string | { th: string; en: string }
|
||||||
|
slug: string
|
||||||
|
description: string | { th: string; en: string }
|
||||||
|
thumbnail_url: string
|
||||||
|
price: string
|
||||||
|
is_free: boolean
|
||||||
|
original_price?: string
|
||||||
|
have_certificate: boolean
|
||||||
|
status: string
|
||||||
|
category_id: number
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
created_by?: number
|
||||||
|
updated_by?: number
|
||||||
|
approved_at?: string
|
||||||
|
approved_by?: number
|
||||||
|
rejection_reason?: string
|
||||||
|
enrolled?: boolean
|
||||||
|
total_lessons?: number
|
||||||
|
rating?: string
|
||||||
|
lessons?: number | string
|
||||||
|
levelType?: 'neutral' | 'warning' | 'success'
|
||||||
|
chapters?: {
|
||||||
|
id: number
|
||||||
|
title: string | { th: string; en: string }
|
||||||
|
lessons: {
|
||||||
|
id: number
|
||||||
|
title: string | { th: string; en: string }
|
||||||
|
duration_minutes: number
|
||||||
|
video_url?: string
|
||||||
|
}[]
|
||||||
|
}[]
|
||||||
|
creator?: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
profile: {
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
avatar_url: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instructors?: {
|
||||||
|
user_id: number
|
||||||
|
is_primary: boolean
|
||||||
|
user: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
profile: {
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
avatar_url: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CourseResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: Course[]
|
||||||
|
total: number
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
totalPages?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SingleCourseResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: Course
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnrolledCourse {
|
||||||
|
id: number
|
||||||
|
course_id: number
|
||||||
|
course: Course
|
||||||
|
status: 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED'
|
||||||
|
progress_percentage: number
|
||||||
|
enrolled_at: string
|
||||||
|
started_at?: string
|
||||||
|
completed_at?: string
|
||||||
|
last_accessed_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnrolledCourseResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: EnrolledCourse[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuizAnswerSubmission {
|
||||||
|
question_id: number
|
||||||
|
choice_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuizSubmitRequest {
|
||||||
|
answers: QuizAnswerSubmission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuizResult {
|
||||||
|
answers_review: {
|
||||||
|
score: number
|
||||||
|
is_correct: boolean
|
||||||
|
correct_choice_id: number
|
||||||
|
selected_choice_id: number
|
||||||
|
question_id: number
|
||||||
|
}[]
|
||||||
|
completed_at: string
|
||||||
|
started_at: string
|
||||||
|
attempt_number: number
|
||||||
|
passing_score: number
|
||||||
|
is_passed: boolean
|
||||||
|
correct_answers: number
|
||||||
|
total_questions: number
|
||||||
|
total_score: number
|
||||||
|
score: number
|
||||||
|
quiz_id: number
|
||||||
|
attempt_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Certificate {
|
||||||
|
certificate_id: number
|
||||||
|
course_id: number
|
||||||
|
course_title: {
|
||||||
|
en: string
|
||||||
|
th: string
|
||||||
|
}
|
||||||
|
issued_at: string
|
||||||
|
download_url: string
|
||||||
|
}
|
||||||
2
Frontend-Learner/types/index.ts
Normal file
2
Frontend-Learner/types/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './auth'
|
||||||
|
export * from './course'
|
||||||
Loading…
Add table
Add a link
Reference in a new issue