feat: Add foundational UI components, pages, and theme management for the e-learning application.

This commit is contained in:
supalerk-ar66 2026-01-27 14:02:07 +07:00
parent baf389643b
commit 5fe454df95
12 changed files with 359 additions and 338 deletions

View file

@ -283,15 +283,15 @@ onMounted(() => {
</p>
<!-- Course Syllabus -->
<div class="bg-white rounded-3xl p-8 shadow-sm border border-slate-100">
<h3 class="text-xl font-bold mb-6 text-slate-900 dark:text-black flex items-center gap-2">
<div class="bg-white dark:bg-slate-800 rounded-3xl p-8 shadow-sm border border-slate-100 dark:border-slate-700">
<h3 class="text-xl font-bold mb-6 text-slate-900 dark:text-white flex items-center gap-2">
<span class="w-1 h-6 bg-blue-500 rounded-full"></span>
{{ $t('course.courseContent') }}
</h3>
<div class="space-y-4">
<div v-for="(chapter, index) in selectedCourse.chapters" :key="chapter.id" class="border border-slate-200 rounded-xl overflow-hidden">
<div class="bg-slate-50 p-4 flex justify-between items-center cursor-pointer">
<div class="font-bold text-slate-800">
<div v-for="(chapter, index) in selectedCourse.chapters" :key="chapter.id" class="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
<div class="bg-slate-50 dark:bg-slate-700/50 p-4 flex justify-between items-center cursor-pointer">
<div class="font-bold text-slate-800 dark:text-slate-100">
<span class="opacity-50 mr-2">Chapter {{ Number(index) + 1 }}</span>
{{ getLocalizedText(chapter.title) }}
</div>
@ -300,13 +300,13 @@ onMounted(() => {
</span>
</div>
<!-- Lessons -->
<div class="divide-y divide-slate-100 bg-white">
<div v-for="(lesson, lIndex) in chapter.lessons" :key="lesson.id" class="p-4 flex justify-between items-center hover:bg-slate-50 transition-colors">
<div class="divide-y divide-slate-100 dark:divide-slate-700 bg-white dark:bg-slate-800">
<div v-for="(lesson, lIndex) in chapter.lessons" :key="lesson.id" class="p-4 flex justify-between items-center hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
<div class="flex items-center gap-3">
<div class="w-6 h-6 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-[10px] font-bold text-slate-500">
<div class="w-6 h-6 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-[10px] font-bold text-slate-500 dark:text-slate-400">
{{ Number(lIndex) + 1 }}
</div>
<span class="text-slate-700 dark:text-black font-medium text-sm">{{ getLocalizedText(lesson.title) }}</span>
<span class="text-slate-700 dark:text-slate-200 font-medium text-sm">{{ getLocalizedText(lesson.title) }}</span>
</div>
<span class="text-xs text-slate-400">{{ lesson.duration_minutes || 0 }}:00</span>
</div>

View file

@ -1,170 +0,0 @@
<script setup lang="ts">
/**
* @file opencovery.vue
* @description Public Course Detail Page.
* Displays detailed information about a specific course without requiring authentication.
*/
definePageMeta({
layout: 'landing',
auth: false // Explicitly public
})
useHead({
title: 'รายละเอียดคอร์ส - E-Learning System'
})
// Mock Data (Static for now, ideally would fetch based on ID)
const course = {
title: 'เบื้องต้นการออกแบบ UX/UI',
price: 'ฟรี',
originalPrice: '',
description: 'เนื้อหาครอบคลุมทุกอย่างตั้งแต่การวิจัยผู้ใช้ (User Research) ไปจนถึงการทำต้นแบบความละเอียดสูง (High-fidelity Prototyping) เหมาะสำหรับผู้เริ่มต้นที่ต้องการเข้าสู่สายงานออกแบบผลิตภัณฑ์',
duration: '4.5 ชั่วโมง',
lessons: 12,
certificate: true,
objectives: [
'วิธีการวิจัยผู้ใช้ (User Research)',
'การวาดโครงร่างและทำต้นแบบ',
'ระบบการออกแบบ (Design Systems)',
'การทดสอบการใช้งาน (Usability Testing)'
],
syllabus: [
{
title: '01. บทนำ',
lessonCount: 3,
lessons: [
{ title: '1.1 การออกแบบ UX คืออะไร?', duration: '10:00' },
{ title: '1.2 กระบวนการคิดเชิงออกแบบ (Design Thinking)', duration: '15:30' },
{ title: '1.3 เครื่องมือที่ต้องใช้', duration: '05:00' }
]
},
{
title: '02. การวิจัยผู้ใช้',
lessonCount: 4,
lessons: [
{ title: '2.1 การสัมภาษณ์ผู้ใช้', duration: '12:00' },
{ title: '2.2 การสร้าง Persona', duration: '18:00' }
]
}
]
}
</script>
<template>
<div class="relative min-h-screen text-slate-200 bg-slate-900 transition-colors pt-32 pb-20">
<!-- Background Effects -->
<div class="fixed inset-0 overflow-hidden pointer-events-none -z-10">
<div class="absolute top-[-20%] right-[-10%] w-[60%] h-[60%] rounded-full bg-blue-600/10 blur-[140px] animate-pulse-slow"/>
<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 class="container mx-auto max-w-6xl px-6">
<!-- Back Button -->
<NuxtLink to="/browse" class="inline-flex items-center gap-2 text-slate-400 hover:text-white mb-8 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
กลบหนารายการคอร
</NuxtLink>
<div class="grid grid-cols-1lg lg:grid-cols-12 gap-10">
<!-- Main Content (Left Column) -->
<div class="lg:col-span-8">
<!-- Hero Video Placeholder -->
<div class="relative aspect-video bg-slate-800 rounded-3xl overflow-hidden border border-white/5 shadow-2xl mb-8 group cursor-pointer">
<div class="absolute inset-0 bg-gradient-to-br from-slate-800 to-slate-900 flex items-center justify-center">
<div class="w-20 h-20 rounded-full bg-blue-600 flex items-center justify-center shadow-[0_0_30px_rgba(37,99,235,0.5)] group-hover:scale-110 transition-transform duration-300">
<div class="ml-1 w-0 h-0 border-t-[12px] border-t-transparent border-l-[20px] border-l-white border-b-[12px] border-b-transparent"/>
</div>
</div>
</div>
<h1 class="text-4xl font-black text-white mb-6 leading-tight">{{ course.title }}</h1>
<p class="text-slate-400 text-lg leading-relaxed mb-10">
{{ course.description }}
</p>
<!-- Learning Objectives -->
<div class="p-8 rounded-3xl bg-white/5 border border-white/5 mb-10">
<h3 class="font-bold text-xl text-white mb-6">งทณจะไดเรยนร</h3>
<ul class="grid grid-cols-1 md:grid-cols-2 gap-4">
<li v-for="(obj, idx) in course.objectives" :key="idx" class="flex gap-3 text-slate-300">
<span class="text-emerald-400 font-bold"></span> {{ obj }}
</li>
</ul>
</div>
<!-- Course Syllabus -->
<div class="p-8 rounded-3xl bg-white/5 border border-white/5">
<h3 class="font-bold text-xl text-white mb-6">เนอหาในคอร</h3>
<div class="space-y-4">
<div v-for="(chapter, idx) in course.syllabus" :key="idx">
<div class="flex justify-between items-center p-4 bg-white/5 rounded-2xl mb-2 border border-white/5">
<span class="font-bold text-white">{{ chapter.title }}</span>
<span class="text-sm text-slate-500">{{ chapter.lessonCount }} บทเรยน</span>
</div>
<div class="pl-4 space-y-1">
<div v-for="(lesson, lIdx) in chapter.lessons" :key="lIdx" class="flex justify-between py-3 px-4 border-b border-white/5 last:border-0 text-slate-400 hover:bg-white/5 rounded-xl transition-colors">
<span class="text-sm">
<span class="mr-2 opacity-50">🎥</span> {{ lesson.title }}
</span>
<span class="text-sm opacity-60">{{ lesson.duration }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar (Right Column): Sticky CTA -->
<div class="lg:col-span-4">
<div class="sticky top-28 p-8 rounded-3xl bg-slate-800/80 backdrop-blur-xl border border-white/10 shadow-2xl">
<div class="mb-8 text-center lg:text-left">
<span class="text-lg text-slate-500 line-through mr-4">{{ course.originalPrice }}</span>
<span class="text-5xl font-black text-white tracking-tight">{{ course.price }}</span>
</div>
<NuxtLink to="/auth/register" class="flex items-center justify-center w-full py-4 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-lg shadow-lg shadow-blue-600/30 transition-all hover:-translate-y-1 mb-6">
ลงทะเบยนเรยนทนท
</NuxtLink>
<div class="space-y-4 text-sm text-slate-400">
<div class="flex justify-between py-3 border-b border-white/5">
<span>ระยะเวลาเรยน</span>
<span class="font-bold text-white">{{ course.duration }}</span>
</div>
<div class="flex justify-between py-3 border-b border-white/5">
<span>จำนวนบทเรยน</span>
<span class="font-bold text-white">{{ course.lessons }} บท</span>
</div>
<div class="flex justify-between py-3 border-b border-white/5">
<span>ใบประกาศนยบตร</span>
<span class="font-bold text-white">{{ course.certificate ? 'มี' : 'ไม่มี' }}</span>
</div>
<div class="flex justify-between py-3 border-b border-white/5">
<span>ระด</span>
<span class="font-bold text-white">เหมาะสำหรบทกคน</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
@keyframes pulse-slow {
0%, 100% { opacity: 0.1; transform: scale(1); }
50% { opacity: 0.15; transform: scale(1.15); }
}
.animate-pulse-slow {
animation: pulse-slow 10s linear infinite;
}
</style>

View file

@ -255,7 +255,7 @@ onBeforeUnmount(() => {
<span>{{ $t('classroom.backToDashboard') }}</span>
</NuxtLink>
<q-toolbar-title class="text-base font-bold text-center lg:text-left truncate">
<q-toolbar-title class="text-base font-bold text-center lg:text-left truncate text-slate-900 dark:text-white">
{{ courseData ? getLocalizedText(courseData.course.title) : $t('classroom.loadingTitle') }}
</q-toolbar-title>
@ -279,7 +279,7 @@ onBeforeUnmount(() => {
<div v-if="courseData" class="h-full scroll">
<q-list class="pb-10">
<template v-for="chapter in courseData.chapters" :key="chapter.id">
<q-item-label header class="bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-bold sticky top-0 z-10 border-b dark:border-white/5 text-sm py-4">
<q-item-label header class="bg-slate-100 dark:bg-slate-800 text-[var(--text-main)] font-bold sticky top-0 z-10 border-b dark:border-white/5 text-sm py-4">
{{ getLocalizedText(chapter.title) }}
</q-item-label>
@ -299,7 +299,7 @@ onBeforeUnmount(() => {
</q-item-section>
<q-item-section>
<q-item-label class="text-sm md:text-base line-clamp-2">
<q-item-label class="text-sm md:text-base line-clamp-2 text-[var(--text-main)]">
{{ getLocalizedText(lesson.title) }}
</q-item-label>
</q-item-section>
@ -357,7 +357,9 @@ onBeforeUnmount(() => {
<!-- Lesson Info -->
<div v-if="currentLesson" class="bg-[var(--bg-surface)] p-6 rounded-2xl shadow-sm border border-[var(--border-color)] mt-6">
<!-- ใชจากตวแปรกลาง: จะแยกโหมดใหตโนม (สวาง=ดำ / =ขาว) -->
<h1 class="text-2xl md:text-3xl font-bold mb-3 text-[var(--text-main)]">{{ getLocalizedText(currentLesson.title) }}</h1>
<p class="text-[var(--text-secondary)] text-base md:text-lg" v-if="currentLesson.description">{{ getLocalizedText(currentLesson.description) }}</p>
<!-- Lesson Content Area -->

View file

@ -1,142 +0,0 @@
<script setup lang="ts">
definePageMeta({
layout: 'default'
})
useHead({
title: 'Loading Demo - e-Learning'
})
const isLoading = ref(false)
const showFullPageLoading = ref(false)
const simulateLoading = () => {
isLoading.value = true
setTimeout(() => isLoading.value = false, 2000)
}
const simulateFullPageLoading = () => {
showFullPageLoading.value = true
setTimeout(() => showFullPageLoading.value = false, 2000)
}
</script>
<template>
<div>
<LoadingSpinner v-if="showFullPageLoading" full-page text="กำลังโหลดหน้า..." />
<h1 style="font-size: 28px; font-weight: 700; margin-bottom: 8px;">UI Loading & Validation Demo</h1>
<p class="text-muted mb-8">หนานแสดง component สำหร Loading states และ Form Validation</p>
<div class="grid-12">
<!-- Loading Spinners -->
<div class="col-span-6">
<div class="card mb-6">
<h2 class="font-bold mb-4">Loading Spinners</h2>
<div class="flex items-center gap-8 mb-6">
<div class="text-center">
<LoadingSpinner size="sm" />
<p class="text-xs text-muted mt-2">Small</p>
</div>
<div class="text-center">
<LoadingSpinner size="md" />
<p class="text-xs text-muted mt-2">Medium</p>
</div>
<div class="text-center">
<LoadingSpinner size="lg" />
<p class="text-xs text-muted mt-2">Large</p>
</div>
</div>
<div class="flex gap-4">
<button class="btn btn-primary" :disabled="isLoading" @click="simulateLoading">
<LoadingSpinner v-if="isLoading" size="sm" />
<span v-else>Submit Button</span>
</button>
<button class="btn btn-secondary" @click="simulateFullPageLoading">
Show Full Page Loading
</button>
</div>
</div>
</div>
<!-- Skeleton Loaders -->
<div class="col-span-6">
<div class="card mb-6">
<h2 class="font-bold mb-4">Skeleton Loaders</h2>
<div class="mb-4">
<p class="text-sm text-muted mb-2">Text Skeleton:</p>
<LoadingSkeleton type="text" :count="3" />
</div>
<div class="mb-4">
<p class="text-sm text-muted mb-2">Avatar + Button:</p>
<div class="flex items-center gap-4">
<LoadingSkeleton type="avatar" />
<LoadingSkeleton type="button" width="100px" />
</div>
</div>
</div>
</div>
<!-- Card Skeleton -->
<div class="col-span-12">
<div class="card mb-6">
<h2 class="font-bold mb-4">Card Skeleton</h2>
<div class="course-grid">
<LoadingSkeleton type="card" />
<LoadingSkeleton type="card" />
<LoadingSkeleton type="card" />
</div>
</div>
</div>
<!-- Form Validation Demo -->
<div class="col-span-12">
<div class="card">
<h2 class="font-bold mb-4">Form Validation Demo</h2>
<p class="text-muted mb-6">ลองกด Submit โดยไมกรอกขอมลเพอด validation error messages</p>
<div class="grid-12">
<div class="col-span-6">
<FormInput
model-value=""
label="อีเมล"
type="email"
placeholder="student@example.com"
error="รูปแบบอีเมลไม่ถูกต้อง"
required
/>
</div>
<div class="col-span-6">
<FormInput
model-value=""
label="รหัสผ่าน"
type="password"
placeholder="••••••••"
error="รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร"
required
/>
</div>
<div class="col-span-6">
<FormInput
model-value="สมชาย"
label="ชื่อ-นามสกุล"
placeholder="สมชาย ใจดี"
required
/>
<p class="text-xs text-success"> กรอกถกตอง (ไม error)</p>
</div>
<div class="col-span-6">
<FormInput
model-value=""
label="เบอร์โทรศัพท์ (ไม่บังคับ)"
placeholder="081-234-5678"
/>
<p class="text-xs text-muted">กรอกหรอไมกรอกกได</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>