feat: Implement core application UI with new headers, navigation, and initial pages.
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 42s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 3s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 42s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 3s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s
This commit is contained in:
parent
3fa236cff5
commit
1b9119e606
10 changed files with 345 additions and 190 deletions
|
|
@ -19,16 +19,15 @@ const searchText = ref('')
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-toolbar class="bg-transparent text-slate-800 dark:text-white h-16 px-4">
|
||||
<!-- Menu Toggle (Always Visible) -->
|
||||
<q-toolbar class="bg-white text-slate-900 h-16 px-4 md:px-8">
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="menu"
|
||||
@click="emit('toggleSidebar')"
|
||||
class="mr-2 text-slate-900 dark:text-white bg-slate-100 dark:bg-slate-700/50 hover:bg-slate-200 dark:hover:bg-slate-600"
|
||||
aria-label="Menu"
|
||||
class="lg:hidden mr-2 text-gray-500"
|
||||
@click="$emit('toggleSidebar')"
|
||||
/>
|
||||
|
||||
<!-- Branding -->
|
||||
|
|
@ -37,49 +36,53 @@ const searchText = ref('')
|
|||
E
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-black text-lg leading-none tracking-tight text-slate-900 dark:text-white group-hover:text-blue-600 transition-colors">E-Learning</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-[0.2em] leading-none mt-1 text-slate-500 dark:text-slate-400">Platform</span>
|
||||
<span class="font-black text-lg leading-none tracking-tight text-slate-900 group-hover:text-blue-600 transition-colors">E-Learning</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-[0.2em] leading-none mt-1 text-slate-500">Platform</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<nav class="hidden md:flex items-center gap-6 ml-8 text-sm font-bold">
|
||||
<NuxtLink to="/browse" class="text-slate-600 hover:text-blue-600 transition-colors">
|
||||
{{ $t('sidebar.onlineCourses') }}
|
||||
<nav class="hidden lg:flex items-center gap-6 text-sm font-medium text-gray-600">
|
||||
<div class="cursor-pointer hover:text-purple-600 flex items-center gap-1 transition-colors">
|
||||
คอร์สออนไลน์ <q-icon name="keyboard_arrow_down" />
|
||||
<q-menu>
|
||||
<q-list dense style="min-width: 150px">
|
||||
<q-item clickable v-close-popup to="/browse">
|
||||
<q-item-section>ทั้งหมด</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 flex items-center gap-1 transition-colors">
|
||||
หลักสูตร Onsite <q-icon name="keyboard_arrow_down" />
|
||||
</div>
|
||||
<NuxtLink to="/browse/recommended" class="hover:text-purple-600 transition-colors">
|
||||
คอร์สแนะนำ
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/browse/discovery" class="text-slate-600 hover:text-blue-600 transition-colors">
|
||||
{{ $t('sidebar.recommendedCourses') }}
|
||||
</NuxtLink>
|
||||
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">บทความ</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">สมาชิกรายปี</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">สำหรับองค์กร</div>
|
||||
</nav>
|
||||
|
||||
<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 -->
|
||||
<div class="flex items-center gap-2 sm:gap-4 text-gray-500">
|
||||
<!-- Search Icon -->
|
||||
<q-btn flat round dense icon="search" class="hover:text-purple-600 transition-colors" />
|
||||
|
||||
<!-- Cart Icon -->
|
||||
<q-btn flat round dense icon="shopping_cart" class="hover:text-purple-600 transition-colors" />
|
||||
|
||||
<!-- Notifications -->
|
||||
<q-btn flat round dense icon="notifications_none" class="hover:text-purple-600 transition-colors">
|
||||
<q-badge color="red" floating rounded dense v-if="false">2</q-badge>
|
||||
</q-btn>
|
||||
|
||||
<!-- Language -->
|
||||
<LanguageSwitcher />
|
||||
|
||||
<!-- User Profile Dropdown -->
|
||||
<!-- User Profile -->
|
||||
<UserMenu />
|
||||
</div>
|
||||
</q-toolbar>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,16 @@
|
|||
const isScrolled = ref(false)
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
||||
onMounted(() => {
|
||||
// Add scroll listener to toggle 'isScrolled' class
|
||||
window.addEventListener('scroll', () => {
|
||||
const handleScroll = () => {
|
||||
isScrolled.value = window.scrollY > 20
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ export const useNavItems = () => {
|
|||
to: '/browse',
|
||||
labelKey: 'sidebar.onlineCourses',
|
||||
icon: 'video_library',
|
||||
showOn: ['sidebar', 'mobile', 'userMenu']
|
||||
showOn: ['mobile']
|
||||
},
|
||||
{
|
||||
to: '/browse/discovery',
|
||||
labelKey: 'sidebar.recommendedCourses',
|
||||
icon: 'auto_awesome',
|
||||
showOn: ['sidebar', 'mobile', 'userMenu']
|
||||
showOn: ['mobile']
|
||||
},
|
||||
{
|
||||
to: '/browse/discovery',
|
||||
|
|
@ -47,13 +47,13 @@ export const useNavItems = () => {
|
|||
to: '/dashboard/announcements',
|
||||
labelKey: 'sidebar.announcements',
|
||||
icon: 'campaign',
|
||||
showOn: ['sidebar', 'mobile', 'userMenu']
|
||||
showOn: ['mobile']
|
||||
},
|
||||
{
|
||||
to: '/dashboard/profile',
|
||||
labelKey: 'sidebar.profile',
|
||||
icon: 'person',
|
||||
showOn: ['sidebar'] // Already in settings for userMenu
|
||||
showOn: [] // Was ['sidebar']
|
||||
},
|
||||
{
|
||||
to: '/dashboard/profile',
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ const toggleLeftDrawer = () => {
|
|||
<q-layout view="hHh LpR lFf" class="bg-slate-50 dark:!bg-[#020617] text-slate-900 dark:!text-slate-50">
|
||||
<!-- Header -->
|
||||
<q-header
|
||||
bordered
|
||||
class="bg-white/80 dark:!bg-[#0f172a]/80 backdrop-blur-md text-slate-900 dark:!text-white border-b border-slate-200 dark:border-slate-800"
|
||||
class="bg-white/80 dark:!bg-[#0f172a]/80 backdrop-blur-md text-slate-900 dark:!text-white"
|
||||
>
|
||||
<AppHeader @toggleSidebar="toggleLeftDrawer" />
|
||||
</q-header>
|
||||
|
|
@ -28,9 +27,8 @@ const toggleLeftDrawer = () => {
|
|||
<q-drawer
|
||||
v-model="leftDrawerOpen"
|
||||
show-if-above
|
||||
bordered
|
||||
:width="280"
|
||||
class="bg-white dark:!bg-[#0f172a] border-r border-slate-200 dark:border-slate-800"
|
||||
class="bg-white dark:!bg-[#0f172a]"
|
||||
>
|
||||
<AppSidebar />
|
||||
</q-drawer>
|
||||
|
|
@ -45,8 +43,7 @@ const toggleLeftDrawer = () => {
|
|||
<!-- Mobile Bottom Nav -->
|
||||
<q-footer
|
||||
v-if="$q.screen.lt.md"
|
||||
bordered
|
||||
class="!bg-white dark:!bg-[#1e293b] text-primary border-t border-slate-200 dark:border-slate-700"
|
||||
class="!bg-white dark:!bg-[#1e293b] text-primary"
|
||||
>
|
||||
<MobileNav />
|
||||
</q-footer>
|
||||
|
|
|
|||
|
|
@ -35,11 +35,7 @@ const selectedCategory = ref((route.query.category as string) || 'all')
|
|||
|
||||
const selectCategory = (slug: string) => {
|
||||
if (slug === 'all') {
|
||||
// Remove query param for 'all' or keep it? Protocol says /browse?category=all is fine.
|
||||
// But usually clean URL is better. Let's keep it simple as requested: "Click programming => /browse?category=programming".
|
||||
// "all" might just remove the param or be ?category=all. The prompt didn't specify 'all' behavior for URL, but fallback is fine.
|
||||
// If I use 'all', the watcher sets 'all'.
|
||||
router.push({ query: { category: 'all' } })
|
||||
router.push({ query: { ...route.query, category: 'all' } })
|
||||
} else {
|
||||
router.push({ query: { ...route.query, category: slug } })
|
||||
}
|
||||
|
|
@ -55,7 +51,7 @@ const categoryLabels: Record<string, string> = {
|
|||
all: "ทั้งหมด",
|
||||
programming: "การเขียนโปรแกรม",
|
||||
design: "การออกแบบ",
|
||||
business: "เกี่ยวกับธุรกิจ"
|
||||
business: "ธุรกิจ"
|
||||
}
|
||||
|
||||
const getCategoryLabel = (category: any) => {
|
||||
|
|
@ -68,8 +64,6 @@ const getCategoryLabel = (category: any) => {
|
|||
// Fetch categories on mount
|
||||
await useAsyncData('categories-list', () => fetchCategories())
|
||||
|
||||
// Fetch courses from API (reactive to selectedCategory)
|
||||
// Fetch courses from API (reactive to selectedCategory)
|
||||
// Fetch courses from API (reactive to selectedCategory)
|
||||
const { data: coursesResponse, error, refresh } = await useAsyncData(
|
||||
'browse-courses-list',
|
||||
|
|
@ -307,15 +301,16 @@ const filteredCourses = computed(() => {
|
|||
CTA SECTION
|
||||
Call to action to register
|
||||
========================================== -->
|
||||
<section class="py-20 relative overflow-hidden">
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent pointer-events-none"/>
|
||||
<section class="py-24 relative overflow-hidden">
|
||||
<!-- Background Decoration -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-blue-50/80 pointer-events-none -z-10"/>
|
||||
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-blue-400/10 blur-[120px] rounded-full -z-10 pointer-events-none"/>
|
||||
|
||||
<div class="container mx-auto max-w-4xl text-center relative z-10 px-6">
|
||||
<h2 class="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">
|
||||
<h2 class="text-4xl md:text-5xl font-black text-slate-900 mb-6 tracking-tight">
|
||||
พร้อมจะเริ่มต้นแล้วหรือยัง?
|
||||
</h2>
|
||||
<p class="text-slate-400 text-xl mb-12 max-w-2xl mx-auto leading-relaxed">
|
||||
<p class="text-slate-500 text-lg md:text-xl mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
ลงทะเบียนฟรีวันนี้เพื่อเข้าถึงบทเรียนพื้นฐาน และติดตามความคืบหน้าการเรียนของคุณได้ทันที ไม่มีค่าใช้จ่ายแอบแฝง
|
||||
</p>
|
||||
<NuxtLink
|
||||
|
|
|
|||
|
|
@ -270,15 +270,16 @@ const filteredCourses = computed(() => {
|
|||
<!-- ==========================================
|
||||
CTA SECTION
|
||||
========================================== -->
|
||||
<section class="py-20 relative overflow-hidden">
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent pointer-events-none"/>
|
||||
<section class="py-24 relative overflow-hidden">
|
||||
<!-- Background Decoration -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-blue-50/80 pointer-events-none -z-10"/>
|
||||
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-blue-400/10 blur-[120px] rounded-full -z-10 pointer-events-none"/>
|
||||
|
||||
<div class="container mx-auto max-w-4xl text-center relative z-10 px-6">
|
||||
<h2 class="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">
|
||||
<h2 class="text-4xl md:text-5xl font-black text-slate-900 mb-6 tracking-tight">
|
||||
ปลดล็อกศักยภาพของคุณวันนี้
|
||||
</h2>
|
||||
<p class="text-slate-400 text-xl mb-12 max-w-2xl mx-auto leading-relaxed">
|
||||
<p class="text-slate-500 text-lg md:text-xl mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
เริ่มเรียนรู้กับคอร์สที่เราแนะนำ และเติบโตไปพร้อมกับผู้เรียนนับหมื่นคน
|
||||
</p>
|
||||
<NuxtLink
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
/**
|
||||
* @file home.vue
|
||||
* @description หน้าแดชบอร์ดหลัก (Dashboard)
|
||||
* แสดงข้อความต้อนรับ และคอร์สแนะนำสำหรับผู้เรียน
|
||||
* @file index.vue
|
||||
* @description Dashboard Home Page matching FutureSkill design
|
||||
*/
|
||||
|
||||
definePageMeta({
|
||||
|
|
@ -11,84 +10,206 @@ definePageMeta({
|
|||
})
|
||||
|
||||
useHead({
|
||||
title: 'Dashboard - e-Learning'
|
||||
title: 'Dashboard - FutureSkill Clone'
|
||||
})
|
||||
|
||||
const { currentUser } = useAuth()
|
||||
const { fetchCourses, getLocalizedText } = useCourse() // Import useCourse
|
||||
const { fetchCategories } = useCategory() // Import useCategory
|
||||
|
||||
const { fetchCourses, fetchEnrolledCourses, getLocalizedText } = useCourse()
|
||||
const { fetchCategories } = useCategory()
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
|
||||
// Recommended Courses State
|
||||
// เก็บข้อมูลคอร์สแนะนำ (สุ่มมา 3 คอร์ส)
|
||||
// State
|
||||
const enrolledCourses = ref<any[]>([])
|
||||
const recommendedCourses = ref<any[]>([])
|
||||
const libraryCourses = ref<any[]>([])
|
||||
const categories = ref<any[]>([])
|
||||
|
||||
const isLoading = ref(true)
|
||||
|
||||
// Initial Data Fetch
|
||||
onMounted(async () => {
|
||||
// 1. Fetch Categories for mapping
|
||||
const catRes = await fetchCategories()
|
||||
const catMap = new Map()
|
||||
isLoading.value = true
|
||||
try {
|
||||
const [catRes, enrollRes, courseRes] = await Promise.all([
|
||||
fetchCategories(),
|
||||
fetchEnrolledCourses({ limit: 3, status: 'IN_PROGRESS' }), // Fetch recent enrolled
|
||||
fetchCourses({ limit: 3, random: true, forceRefresh: true, is_recommended: true }) // Fetch 3 Recommended Courses
|
||||
])
|
||||
|
||||
if (catRes.success) {
|
||||
catRes.data?.forEach((c: any) => catMap.set(c.id, c.name))
|
||||
categories.value = catRes.data || []
|
||||
}
|
||||
|
||||
// 2. Fetch 3 Random Courses from Server
|
||||
// ดึงคอร์สแบบสุ่มจาก Server โดยตรง (ผ่าน API ใหม่ที่เพิ่ม parameter random และ limit)
|
||||
const res = await fetchCourses({ random: true, limit: 3, forceRefresh: true, is_recommended: true })
|
||||
const catMap = new Map()
|
||||
categories.value.forEach((c: any) => catMap.set(c.id, c.name))
|
||||
|
||||
if (res.success && res.data?.length) {
|
||||
recommendedCourses.value = res.data.map((c: any) => ({
|
||||
|
||||
// Map Enrolled Courses
|
||||
if (enrollRes.success && enrollRes.data) {
|
||||
enrolledCourses.value = enrollRes.data.map((item: any) => ({
|
||||
id: item.course_id,
|
||||
title: item.course.title,
|
||||
thumbnail_url: item.course.thumbnail_url,
|
||||
progress: item.progress_percentage || 0,
|
||||
total_lessons: item.course.total_lessons || 10,
|
||||
completed_lessons: Math.floor((item.progress_percentage / 100) * (item.course.total_lessons || 10))
|
||||
}))
|
||||
}
|
||||
|
||||
// Map Recommended/Library Courses
|
||||
if (courseRes.success && courseRes.data) {
|
||||
// Use fetched courses for recommended section
|
||||
recommendedCourses.value = courseRes.data.map((c: any) => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
category: catMap.get(c.category_id),
|
||||
lessons: c.lessons,
|
||||
description: c.description,
|
||||
lessons: c.total_lessons || 0,
|
||||
image: c.thumbnail_url || '',
|
||||
badge: '',
|
||||
badgeType: ''
|
||||
rating: c.rating,
|
||||
price: c.price,
|
||||
is_free: c.is_free
|
||||
}))
|
||||
|
||||
// Just for demo, use same data for library if needed or fetch separately
|
||||
libraryCourses.value = courseRes.data.slice(0, 2)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load dashboard data', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Helper for "Continue Learning" Hero Card
|
||||
const heroCourse = computed(() => enrolledCourses.value[0] || null)
|
||||
const sideCourses = computed(() => enrolledCourses.value.slice(1, 3))
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container !pt-4">
|
||||
<!-- Welcome Header Section (Minimalist) -->
|
||||
<div class="flex items-center gap-6 mb-10 py-2 animate-fade-in">
|
||||
<!-- Avatar with premium shadow -->
|
||||
<div class="relative group cursor-pointer" @click="$router.push('/dashboard/profile')">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-full blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||
<q-avatar size="84px" class="relative shadow-2xl ring-4 ring-white dark:ring-slate-900 transition-all duration-500 overflow-hidden">
|
||||
<img
|
||||
:src="currentUser?.photoURL || 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y'"
|
||||
class="object-cover"
|
||||
/>
|
||||
</q-avatar>
|
||||
</div>
|
||||
<div class="bg-[#F8F9FA] min-h-screen font-inter pb-20">
|
||||
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-2xl md:text-4xl font-black text-slate-900 dark:text-white tracking-tight leading-tight mb-1">
|
||||
{{ $t('dashboard.welcomeTitle') }}, {{ currentUser?.firstName }}!
|
||||
<!-- 1. Greeting Section -->
|
||||
<section class="bg-white pt-8 pb-10 px-4 md:px-12">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-[#2D2D2D] mb-4 flex items-center gap-3">
|
||||
สวัสดี {{ currentUser?.firstName || 'User' }} <span class="text-3xl">😊</span>
|
||||
</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">{{ $t('dashboard.welcomeSubtitle') }}</span>
|
||||
<p class="text-gray-500 text-base md:text-lg font-light max-w-3xl">
|
||||
เข้าถึง +490 หลักสูตรออนไลน์สำหรับสมาชิกรายปี เพื่อบรรลุเป้าหมายด้านอาชีพและพัฒนาการเรียนรู้สำหรับคุณ ✨
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 md:px-12 space-y-16 mt-10">
|
||||
|
||||
<!-- 2. Continue Learning Section -->
|
||||
<section v-if="enrolledCourses.length > 0">
|
||||
<div class="flex justify-between items-end mb-6">
|
||||
<h2 class="text-xl md:text-2xl font-bold text-[#2D2D2D]">เรียนต่อกับคอร์สของคุณ</h2>
|
||||
<NuxtLink to="/dashboard/my-courses" class="text-purple-600 hover:text-purple-700 font-medium text-sm flex items-center gap-1">
|
||||
การเรียนของฉัน <q-icon name="arrow_forward" size="16px" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Hero Card (Left) -->
|
||||
<div v-if="heroCourse" class="relative group cursor-pointer rounded-2xl overflow-hidden bg-white shadow-sm border border-gray-100 hover:shadow-md transition-all h-[320px]">
|
||||
<img :src="heroCourse.thumbnail_url" class="w-full h-full object-cover brightness-75 group-hover:brightness-90 transition-all duration-500" />
|
||||
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent p-8 flex flex-col justify-end">
|
||||
<div class="bg-purple-600 text-white text-xs font-bold px-3 py-1 rounded w-fit mb-3">COURSE</div>
|
||||
<h3 class="text-white text-2xl font-bold mb-4 line-clamp-2 leading-snug">{{ getLocalizedText(heroCourse.title) }}</h3>
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between text-gray-300 text-xs mb-2">
|
||||
<span>{{ heroCourse.completed_lessons }}/{{ heroCourse.total_lessons }} บทเรียน</span>
|
||||
<span>{{ heroCourse.progress }}%</span>
|
||||
</div>
|
||||
<div class="h-1.5 w-full bg-white/20 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-purple-500 rounded-full" :style="{ width: `${heroCourse.progress}%` }"></div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<span class="text-white font-bold text-sm hover:underline">เรียนต่อ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div>
|
||||
<!-- Section: Recommended Courses -->
|
||||
<!-- Side List (Right) -->
|
||||
<div class="flex flex-col gap-4 h-[320px]">
|
||||
<div v-for="course in sideCourses" :key="course.id" class="flex-1 bg-white rounded-2xl p-4 border border-gray-100 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">
|
||||
<img :src="course.thumbnail_url" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="flex-grow min-w-0 flex flex-col justify-between h-full py-1">
|
||||
<h4 class="text-gray-800 font-bold text-sm line-clamp-2 mb-2">{{ getLocalizedText(course.title) }}</h4>
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="h-1.5 w-full bg-gray-100 rounded-full overflow-hidden mb-2">
|
||||
<div class="h-full bg-purple-600 rounded-full" :style="{ width: `${course.progress}%` }"></div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-gray-500">{{ course.completed_lessons }}/{{ course.total_lessons }} บทเรียน</span>
|
||||
<span class="text-purple-600 font-bold cursor-pointer hover:underline" @click="navigateTo(`/classroom/learning?course_id=${course.id}`)">เรียนต่อ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Empty State Placeholder if less than 2 side courses -->
|
||||
<div v-if="sideCourses.length < 2" class="flex-1 bg-gray-50 rounded-2xl border border-dashed border-gray-200 flex items-center justify-center text-gray-400 text-sm">
|
||||
เริ่มเรียนคอร์สใหม่ๆ เพื่อเติมเต็มส่วนนี้
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Knowledge Library -->
|
||||
<section>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-black flex items-center gap-3 tracking-tight text-slate-900 dark:text-white">
|
||||
<span class="w-1 h-6 bg-emerald-500 rounded-full shadow-sm shadow-emerald-500/50"/>
|
||||
{{ $t('menu.recommendedCourses') }}
|
||||
</h2>
|
||||
<h2 class="text-xl md:text-2xl font-bold text-[#2D2D2D] mb-1">คลังความรู้</h2>
|
||||
<p class="text-gray-500 text-sm">คุณสามารถเลือกเรียนคอร์สเรียนที่คุณเป็นเจ้าของ</p>
|
||||
</div>
|
||||
|
||||
<!-- Recommended Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pb-20">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Course Cards -->
|
||||
<CourseCard
|
||||
v-for="course in libraryCourses"
|
||||
:key="course.id"
|
||||
v-bind="course"
|
||||
:image="course.thumbnail_url"
|
||||
class="h-full md:col-span-1"
|
||||
/>
|
||||
|
||||
<!-- CTA Card (Large) -->
|
||||
<div class="bg-white rounded-3xl border border-gray-100 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 font-medium mb-6 mt-4">เลือกเรียนคอร์สในคลังความรู้ของคุณ</p>
|
||||
<q-btn
|
||||
flat
|
||||
rounded
|
||||
no-caps
|
||||
class="text-purple-600 hover:bg-purple-50 px-6 py-2 font-bold group-hover:scale-105 transition-transform"
|
||||
to="/dashboard/my-courses"
|
||||
>
|
||||
ดูทั้งหมด <q-icon name="arrow_forward" size="18px" class="ml-2" />
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<!-- 5. Recommended Courses -->
|
||||
<section class="pb-20">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl md:text-2xl font-bold text-[#2D2D2D] text-left">คอร์สแนะนำ</h2>
|
||||
</div>
|
||||
|
||||
<!-- Recommended Grid (3 columns) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-fade-in">
|
||||
<CourseCard
|
||||
v-for="course in recommendedCourses"
|
||||
:key="course.id"
|
||||
|
|
@ -96,21 +217,27 @@ onMounted(async () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading State for Recommended -->
|
||||
<div v-if="recommendedCourses.length === 0" class="flex justify-center py-10 opacity-50">
|
||||
<div class="animate-pulse">Loading recommendations...</div>
|
||||
<!-- Loading State -->
|
||||
<div v-if="recommendedCourses.length === 0 && !isLoading" class="flex justify-center py-10 opacity-50">
|
||||
<div class="text-gray-400">ไม่พบข้อมูลคอร์สแนะนำ</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
/* Scoped specific styles */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
:deep(.q-btn) {
|
||||
text-transform: none; /* Prevent uppercase in Q-Btns */
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,35 @@ useHead({
|
|||
|
||||
const { fetchCategories } = useCategory()
|
||||
const { fetchCourses, getLocalizedText } = useCourse()
|
||||
const { user } = useAuth()
|
||||
|
||||
const stepOneCards = [
|
||||
{ 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' },
|
||||
{ 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 topCourses = ref<any[]>([])
|
||||
|
|
@ -289,7 +318,7 @@ onMounted(() => {
|
|||
:class="selectedCategory === 'all' ? 'bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-600/30' : 'bg-white border-slate-100 text-slate-500 hover:border-slate-300'"
|
||||
@click="selectedCategory = 'all'"
|
||||
>
|
||||
All
|
||||
ทั้งหมด
|
||||
</button>
|
||||
<button
|
||||
v-for="category in categories"
|
||||
|
|
@ -360,44 +389,7 @@ onMounted(() => {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// Supplementary static data for the sections
|
||||
export default {
|
||||
setup() {
|
||||
const { user } = useAuth()
|
||||
return { user }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stepOneCards: [
|
||||
{ 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' },
|
||||
{ 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' }
|
||||
],
|
||||
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'
|
||||
}
|
||||
],
|
||||
promoCategories: [
|
||||
{ title: 'Data', desc: 'เรียนรู้และฝึกฝนกระบวนการคิดสร้างมูลค่าให้ธุรกิจด้วยข้อมูล', icon: 'analytics' },
|
||||
{ title: 'Design', desc: 'ออกแบบ Digital Product เพื่อให้ผู้ใช้งานได้รับประสบการณ์ที่ดีที่สุด', icon: 'palette' },
|
||||
{ title: 'Tech', desc: 'พร้อมเป็นที่ต้องการของตลาดแรงงานด้วยทักษะการเขียนโปรแกรม', icon: 'code' },
|
||||
{ title: 'Business', desc: 'พลิกโฉมธุรกิจในยุคดิจิทัลด้วยการเข้าถึงลูกค้าในช่องทางและเวลาที่เหมาะสม', icon: 'trending_up' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.landing-page {
|
||||
|
|
|
|||
35
Frontend-Learner/คู่มืออธิบาย/ED.md
Normal file
35
Frontend-Learner/คู่มืออธิบาย/ED.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
วันที่บันทึกปฏิบัติงาน \*
|
||||
18/02/2026
|
||||
|
||||
องค์ความรู้ที่ได้รับ \*
|
||||
|
||||
- การใช้งาน Nuxt 3 Routing (useRoute, useRouter) ในการจัดการ Query Parameters
|
||||
- การเชื่อมโยง State กับ URL เพื่อสร้าง Deep Linking ให้แชร์ลิงก์หมวดหมู่ได้
|
||||
- การใช้งาน Vue 3 Composition API (Script Setup) แทน Options API เพื่อความเป็นระเบียบและลดความซับซ้อน
|
||||
- การตกแต่ง UI ด้วย Tailwind CSS ขั้นสูง (Gradients, Glow Effects, Backdrop Filters) เพื่อให้ได้ธีม Premium/Clean
|
||||
- การจัดการ Event Listener ใน Vue Component (onMounted, onUnmounted) เพื่อป้องกัน Memory Leak
|
||||
|
||||
รายละเอียด \*
|
||||
|
||||
- พัฒนาระบบ Filter หมวดหมู่คอร์สเรียน เชื่อมโยงหน้าแรก (Home) กับหน้าค้นหา (Browse)
|
||||
- ปรับแก้โครงสร้าง Code ในหน้า `index.vue` ให้ใช้ Script Setup ทั้งหมดเพื่อลดความซับซ้อนและแก้ปัญหาการเรียกใช้ตัวแปร
|
||||
- ปรับดีไซน์ส่วน Call to Action (CTA) ในหน้า `browse/index.vue` และ `browse/recommended.vue` ให้ดูสว่างและทันสมัยขึ้นโดยใช้แสงและเงา (Blue Glow)
|
||||
|
||||
ปัญหาและอุปสรรค \*
|
||||
|
||||
- พบปัญหาการทำงานร่วมกันระหว่าง Options API และ Script Setup ในไฟล์ `index.vue` ทำให้ฟังก์ชันบางตัวเรียกใช้ไม่ได้
|
||||
- ระบบ Filter เดิมไม่ทำงานเมื่อกดย้อนกลับ (Back Button) เนื่องจากไม่ได้ Watch การเปลี่ยนแปลงของ URL Query
|
||||
- การดึงข้อมูลคอร์สเริ่มต้นได้ไม่ครบเนื่องจาก API มีการกำหนด Limit ไว้
|
||||
|
||||
รายละเอียด \*
|
||||
|
||||
- แก้ไขโดยการยุบรวม Code ทั้งหมดให้เป็นรูปแบบ Script Setup มาตรฐานเดียว
|
||||
- เพิ่ม `watch(() => route.query.category)` ในหน้า Browse เพื่อให้อัปเดตข้อมูลทุกครั้งที่ URL เปลี่ยน
|
||||
- เพิ่มพารามิเตอร์ `limit: 1000` ในการเรียก API เพื่อดึงข้อมูลคอร์สทั้งหมดมาแสดง
|
||||
|
||||
หลักฐานการปฏิบัติงาน (เฉพาะไฟล์ JPG, JPEG, PNG, PDF.)
|
||||
|
||||
- แก้ไขไฟล์ `pages/index.vue` (เพิ่ม goBrowse, ย้าย Logic)
|
||||
- แก้ไขไฟล์ `pages/browse/index.vue` (เพิ่ม Query Watcher, ปรับ UI)
|
||||
- แก้ไขไฟล์ `pages/browse/recommended.vue` (ปรับ UI ส่วน CTA)
|
||||
- แก้ไขไฟล์ `components/layout/LandingHeader.vue` (แก้ Memory Leak)
|
||||
Loading…
Add table
Add a link
Reference in a new issue