feat: Add foundational UI components, pages, and theme management for the e-learning application.
This commit is contained in:
parent
baf389643b
commit
5fe454df95
12 changed files with 359 additions and 338 deletions
|
|
@ -9,8 +9,8 @@
|
|||
/* Colors - Light Mode Default */
|
||||
--bg-body: #f8fafc;
|
||||
--bg-surface: #ffffff;
|
||||
--text-main: #0f172a; /* Dark Text */
|
||||
--text-secondary: #64748b; /* Secondary Text */
|
||||
--text-main: #000000; /* ดำสนิทสำหรับหัวข้อ */
|
||||
--text-secondary: #111827; /* เทาเข้มมากสำหรับคำอธิบาย */
|
||||
--primary: #3b82f6; /* Primary Blue */
|
||||
|
||||
/* Semantic mappings */
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const displayDescription = computed(() => getLocalizedText(props.description))
|
|||
<!-- Progress Bar -->
|
||||
<div v-if="progress !== undefined && !completed" class="mb-8 p-3 rounded-2xl" style="background-color: rgba(148, 163, 184, 0.1);">
|
||||
<div class="flex justify-between items-center text-[10px] font-black uppercase tracking-widest mb-1.5">
|
||||
<span style="color: var(--text-secondary);">{{ $t('course.progress') }}</span>
|
||||
<span class="text-slate-950 dark:text-slate-300">{{ $t('course.progress') }}</span>
|
||||
<span class="text-blue-600">{{ progress }}%</span>
|
||||
</div>
|
||||
<div class="w-full h-1.5 bg-slate-300 dark:bg-slate-700 rounded-full overflow-hidden">
|
||||
|
|
|
|||
81
Frontend-Learner/components/layout/AppHeader.vue
Normal file
81
Frontend-Learner/components/layout/AppHeader.vue
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file AppHeader.vue
|
||||
* @description The main header for the authenticated application dashboard.
|
||||
* Uses Quasar QToolbar.
|
||||
*/
|
||||
|
||||
defineProps<{
|
||||
/** Controls visibility of the search bar */
|
||||
showSearch?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** Emitted when the hamburger menu is clicked */
|
||||
toggleSidebar: []
|
||||
}>()
|
||||
|
||||
const searchText = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-toolbar class="bg-transparent text-slate-800 dark:text-white h-16 px-4">
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="menu"
|
||||
@click="emit('toggleSidebar')"
|
||||
class="md:hidden mr-2 text-slate-700 dark:text-white"
|
||||
aria-label="Menu"
|
||||
/>
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="flex items-center gap-2 cursor-pointer" @click="navigateTo('/dashboard')">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold">
|
||||
E
|
||||
</div>
|
||||
<span class="font-bold text-xl text-blue-600 dark:text-blue-400 hidden xs:block">e-Learning</span>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<!-- Center Search (Optional) -->
|
||||
<div v-if="showSearch !== false" class="hidden md:block w-1/3 max-w-md mx-4">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
rounded
|
||||
v-model="searchText"
|
||||
:placeholder="$t('menu.searchCourses')"
|
||||
class="bg-slate-50 dark:bg-slate-700/50 search-input"
|
||||
bg-color="transparent"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" class="text-slate-400" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<!-- Right Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Language Switcher -->
|
||||
<LanguageSwitcher />
|
||||
|
||||
<!-- User Profile Dropdown -->
|
||||
<UserMenu />
|
||||
</div>
|
||||
</q-toolbar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-input :deep(.q-field__control) {
|
||||
border-radius: 9999px; /* Full rounded */
|
||||
}
|
||||
.search-input :deep(.q-field__control:before) {
|
||||
border-color: #e2e8f0; /* slate-200 */
|
||||
}
|
||||
</style>
|
||||
90
Frontend-Learner/components/layout/AppSidebar.vue
Normal file
90
Frontend-Learner/components/layout/AppSidebar.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file AppSidebar.vue
|
||||
* @description Sidebar navigation for the authenticated dashboard.
|
||||
* Uses Quasar QList for structure.
|
||||
*/
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const navItems = computed(() => [
|
||||
{
|
||||
to: "/dashboard",
|
||||
label: t('sidebar.overview'),
|
||||
icon: "dashboard", // Using Material Icons names where possible or SVG paths
|
||||
isSvg: false
|
||||
},
|
||||
{
|
||||
to: "/dashboard/my-courses",
|
||||
label: t('sidebar.myCourses'),
|
||||
icon: "school",
|
||||
isSvg: false
|
||||
},
|
||||
{
|
||||
to: "/browse/discovery",
|
||||
label: t('sidebar.browseCourses'),
|
||||
icon: "explore",
|
||||
isSvg: false
|
||||
},
|
||||
{
|
||||
to: "/dashboard/announcements",
|
||||
label: t('sidebar.announcements'),
|
||||
icon: "campaign",
|
||||
isSvg: false
|
||||
},
|
||||
{
|
||||
to: "/dashboard/profile",
|
||||
label: t('sidebar.profile'),
|
||||
icon: "person",
|
||||
isSvg: false
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-transparent">
|
||||
<!-- Branding Area (Optional if not in Header) -->
|
||||
|
||||
<q-list padding class="text-slate-600 dark:text-slate-300 flex-grow">
|
||||
|
||||
|
||||
<q-item
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
clickable
|
||||
v-ripple
|
||||
class="rounded-r-full mr-2 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-white/5"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="item.icon" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ item.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<!-- Footer / Version -->
|
||||
<div class="mt-auto p-4 text-xs text-center opacity-50 pb-8">
|
||||
<p>e-Learning v0.1.0</p>
|
||||
<p>© 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Light Mode Active State */
|
||||
.q-item.q-router-link--active {
|
||||
background: rgb(239 246 255); /* blue-50 */
|
||||
color: rgb(29 78 216); /* blue-700 */
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Dark Mode Active State */
|
||||
.dark .q-item.q-router-link--active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgb(147 197 253); /* blue-300 */
|
||||
}
|
||||
</style>
|
||||
129
Frontend-Learner/components/layout/LandingHeader.vue
Normal file
129
Frontend-Learner/components/layout/LandingHeader.vue
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file LandingHeader.vue
|
||||
* @description The main header for the public landing pages.
|
||||
* Features a transparent background that becomes solid/glass upon scrolling.
|
||||
*/
|
||||
|
||||
// Track scrolling state to adjust header styling
|
||||
const isScrolled = ref(false)
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
||||
onMounted(() => {
|
||||
// Add scroll listener to toggle 'isScrolled' class
|
||||
window.addEventListener('scroll', () => {
|
||||
isScrolled.value = window.scrollY > 20
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!--
|
||||
Header Container
|
||||
- Transitions between transparent and glass effect based on scroll.
|
||||
-->
|
||||
<header
|
||||
class="landing-header transition-all duration-300"
|
||||
:class="[isScrolled ? 'h-16 glass-nav shadow-lg' : 'h-24 bg-transparent']"
|
||||
>
|
||||
<div class="container h-full flex items-center justify-between">
|
||||
<!--
|
||||
Left Section: Logo & Desktop Navigation
|
||||
-->
|
||||
<div class="flex items-center gap-12">
|
||||
<!-- Logo -->
|
||||
<NuxtLink to="/" class="flex items-center gap-3 group">
|
||||
<div class="logo-box bg-blue-600 text-white font-black rounded-xl w-10 h-10 flex items-center justify-center shadow-lg shadow-blue-600/30 group-hover:scale-110 transition-transform">
|
||||
E
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-black text-lg leading-none tracking-tight text-white group-hover:text-blue-400 transition-colors">E-Learning</span>
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em] leading-none mt-1">Platform</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Desktop Links -->
|
||||
<nav class="hidden md:block">
|
||||
<ul class="flex items-center gap-8 text-sm font-bold">
|
||||
<li>
|
||||
<NuxtLink to="/browse" class="text-slate-400 hover:text-white transition-colors relative group">
|
||||
{{ $t('landing.allCourses') }}
|
||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-blue-600 transition-all group-hover:w-full"/>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/browse/discovery" class="text-slate-400 hover:text-white transition-colors relative group">
|
||||
{{ $t('landing.discovery') }}
|
||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-blue-600 transition-all group-hover:w-full"/>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Right Section: Action Buttons (Login/Register or Dashboard)
|
||||
-->
|
||||
<div class="flex items-center gap-4">
|
||||
<template v-if="!isAuthenticated">
|
||||
<NuxtLink to="/auth/login" class="text-sm font-bold text-slate-300 hover:text-white px-4 py-2 transition-colors">{{ $t('auth.login') }}</NuxtLink>
|
||||
<NuxtLink to="/auth/register" class="btn-primary-premium shadow-lg shadow-blue-600/20">
|
||||
{{ $t('auth.getStarted') }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NuxtLink to="/dashboard" class="btn-primary-premium shadow-lg shadow-blue-600/20">
|
||||
{{ $t('landing.goToDashboard') }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Header content */
|
||||
.landing-header {
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Glassmorphism Effect for Scrolled Header */
|
||||
.glass-nav {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* Premium Primary Button Styling */
|
||||
.btn-primary-premium {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 800;
|
||||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-primary-premium:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px -4px rgba(37, 99, 235, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
Frontend-Learner/components/layout/MobileNav.vue
Normal file
34
Frontend-Learner/components/layout/MobileNav.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
const navItems = [
|
||||
{ to: '/dashboard', icon: 'dashboard', label: 'หน้าหลัก' },
|
||||
{ to: '/browse/discovery', icon: 'explore', label: 'รายการคอร์ส' },
|
||||
{ to: '/dashboard/my-courses', icon: 'school', label: 'คอร์สของฉัน' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-tabs
|
||||
indicator-color="primary"
|
||||
active-color="primary"
|
||||
class="bg-white text-slate-500 shadow-up-1"
|
||||
align="justify"
|
||||
dense
|
||||
>
|
||||
<q-route-tab
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
no-caps
|
||||
class="py-2"
|
||||
/>
|
||||
</q-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Optional shadow for better separation */
|
||||
.shadow-up-1 {
|
||||
box-shadow: 0 -1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -13,17 +13,7 @@ export const useThemeMode = () => {
|
|||
localStorage.setItem('theme', value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
const initTheme = () => {
|
||||
if (!process.client) return
|
||||
const saved = localStorage.getItem('theme') // 'dark' | 'light' | null
|
||||
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false
|
||||
|
||||
const value = saved ? saved === 'dark' : prefersDark
|
||||
isDark.value = value
|
||||
applyTheme(value)
|
||||
}
|
||||
|
||||
onMounted(initTheme)
|
||||
|
||||
watch(isDark, (v) => applyTheme(v))
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const { $q } = useQuasar()
|
||||
const nuxt = useNuxtApp()
|
||||
const $q = nuxt.vueApp.config.globalProperties.$q
|
||||
|
||||
|
||||
|
||||
// ฟังก์ชันสลับโหมดและ Sync กับ Quasar
|
||||
const updateTheme = (isDark: boolean) => {
|
||||
|
|
@ -14,8 +17,12 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||
|
||||
// ตอนเริ่มทำงานบน Client
|
||||
if (process.client) {
|
||||
const { set } = useThemeMode()
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const isDark = savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
|
||||
set(isDark)
|
||||
// Also explicitly force updateTheme initially since watcher might not fire if state matches default
|
||||
updateTheme(isDark)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue