128 lines
6.1 KiB
Vue
128 lines
6.1 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* @file home.vue
|
|
* @description หน้าแดชบอร์ดหลัก (Dashboard)
|
|
* แสดงข้อความต้อนรับ และคอร์สแนะนำสำหรับผู้เรียน
|
|
*/
|
|
|
|
definePageMeta({
|
|
layout: 'default',
|
|
middleware: 'auth'
|
|
})
|
|
|
|
useHead({
|
|
title: 'Dashboard - e-Learning'
|
|
})
|
|
|
|
const { currentUser } = useAuth()
|
|
const { fetchCourses } = useCourse() // Import useCourse
|
|
const { fetchCategories } = useCategory() // Import useCategory
|
|
|
|
// Helper to get localized text
|
|
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
|
|
if (!text) return ''
|
|
if (typeof text === 'string') return text
|
|
return text.th || text.en || ''
|
|
}
|
|
|
|
// Recommended Courses State
|
|
// เก็บข้อมูลคอร์สแนะนำ (สุ่มมา 3 คอร์ส)
|
|
const recommendedCourses = ref<any[]>([])
|
|
|
|
onMounted(async () => {
|
|
// 1. Fetch Categories for mapping
|
|
// ดึงหมวดหมู่เพื่อเอามาแสดงชื่อหมวดหมู่ในการ์ด
|
|
const catRes = await fetchCategories()
|
|
const catMap = new Map()
|
|
if (catRes.success) {
|
|
catRes.data?.forEach((c: any) => catMap.set(c.id, c.name))
|
|
}
|
|
|
|
// 2. Fetch All Courses and Randomize
|
|
// ดึงคอร์สทั้งหมดและสุ่มเลือกมา 3 อัน
|
|
const res = await fetchCourses()
|
|
if (res.success && res.data?.length) {
|
|
// Shuffle array (สุ่มลำดับ)
|
|
const shuffled = [...res.data].sort(() => 0.5 - Math.random())
|
|
|
|
// Pick first 3 (เลือกมา 3 อันแรก)
|
|
recommendedCourses.value = shuffled.slice(0, 3).map((c: any) => ({
|
|
id: c.id,
|
|
title: getLocalizedText(c.title),
|
|
category: getLocalizedText(catMap.get(c.category_id)) || 'General', // Map Category ID to Name
|
|
duration: c.lessons ? `${c.lessons} บทเรียน` : 'พร้อมเรียน', // Use lesson count or default
|
|
image: c.thumbnail_url || '',
|
|
badge: '', // No mock badge
|
|
badgeType: ''
|
|
}))
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page-container">
|
|
<!-- Welcome Header Section -->
|
|
<div class="welcome-section mb-10 overflow-hidden relative rounded-[2.5rem] p-10 md:p-14 text-white shadow-lg dark:shadow-2xl dark:shadow-blue-900/20 transition-all">
|
|
<div class="relative z-10 flex flex-col md:flex-row justify-between items-center gap-8">
|
|
<div>
|
|
<h1 class="text-4xl md:text-5xl font-black mb-3 slide-up tracking-tight text-white dark:text-white">{{ $t('dashboard.welcomeTitle') }}, {{ currentUser?.firstName }}!</h1>
|
|
<p class="text-lg slide-up font-medium text-blue-100" style="animation-delay: 0.1s;">{{ $t('dashboard.welcomeSubtitle') }}</p>
|
|
</div>
|
|
<div class="stats-mini flex gap-6 slide-up" style="animation-delay: 0.2s;"/>
|
|
</div>
|
|
<!-- Decorative Background elements -->
|
|
<div class="absolute inset-0 bg-gradient-to-br from-blue-500 via-blue-600 to-indigo-700 dark:from-blue-600 dark:via-blue-700 dark:to-indigo-900 -z-0"/>
|
|
<div class="absolute -top-20 -right-20 w-80 h-80 bg-white/10 blur-[100px] rounded-full"/>
|
|
<div class="absolute -bottom-20 -left-20 w-80 h-80 bg-blue-400/20 blur-[100px] rounded-full"/>
|
|
</div>
|
|
|
|
<!-- Main Content Area -->
|
|
<div class="lg:col-span-12">
|
|
<!-- Section: Recommended Courses -->
|
|
<div class="mb-8">
|
|
<h2 class="text-2xl font-black flex items-center gap-3 tracking-tight text-slate-900 dark:text-white">
|
|
<span class="w-1.5 h-8 bg-emerald-500 rounded-full shadow-[0_0_15px_rgba(16,185,129,0.5)]"/>
|
|
{{ $t('menu.recommendedCourses') }}
|
|
</h2>
|
|
</div>
|
|
|
|
<!-- Recommended Grid -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 pb-20">
|
|
<NuxtLink v-for="(course, idx) in recommendedCourses" :key="course.id" :to="`/course/${course.id}`" class="p-0 overflow-hidden group border border-slate-200 dark:border-white/5 rounded-3xl shadow-sm dark:shadow-xl transition-all hover:-translate-y-1 block" style="background-color: var(--bg-surface);">
|
|
<div class="h-48 overflow-hidden relative rounded-t-3xl">
|
|
<img v-if="course.image" :src="course.image" :alt="course.title" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" >
|
|
<div class="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-slate-900/20 dark:from-[#1e293b] to-transparent"/>
|
|
</div>
|
|
<div class="p-7" style="background-color: var(--bg-surface);">
|
|
<div class="text-[10px] font-black text-slate-700 dark:text-slate-500 uppercase tracking-[0.2em] mb-2">{{ course.category }}</div>
|
|
<h4 class="font-black text-xl mb-6 text-slate-900 dark:text-white group-hover:text-blue-700 dark:group-hover:text-blue-400 transition-colors line-clamp-2 h-14">{{ course.title }}</h4>
|
|
<div class="flex items-center justify-between pt-4 border-t border-slate-50 dark:border-white/5">
|
|
<span class="text-xs font-bold text-slate-700 dark:text-slate-400 flex items-center gap-2">
|
|
<q-icon name="schedule" size="14px" />
|
|
{{ course.duration }}
|
|
</span>
|
|
<span class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-md">{{ $t('menu.viewDetails') }}</span>
|
|
</div>
|
|
</div>
|
|
</NuxtLink>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@keyframes slide-up {
|
|
from { opacity: 0; transform: translateY(30px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.slide-up {
|
|
animation: slide-up 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
|
opacity: 0;
|
|
}
|
|
</style>
|