elearning/Frontend-Learner/pages/dashboard/index.vue

322 lines
19 KiB
Vue

<script setup lang="ts">
/**
* @file index.vue
* @description หน้าหลักแดชบอร์ด (Dashboard Home)
*/
// 1. นำเข้าระบบและกำหนด MetaData
definePageMeta({
layout: "default",
middleware: "auth",
});
useHead({
title: "Dashboard - e-Learning Platform",
});
// 2. เรียกใช้งาน Composables
const { currentUser } = useAuth();
const { fetchCourses, fetchEnrolledCourses, getLocalizedText } = useCourse();
const { fetchCategories } = useCategory();
const { t } = useI18n();
// 3. กำหนดสถานะ (State)
const enrolledCourses = ref<any[]>([]);
const recommendedCourses = ref<any[]>([]);
const libraryCourses = ref<any[]>([]);
const categories = ref<any[]>([]);
const isLoading = ref(true);
// 4. การจัดการโหลดข้อมูล (Data Initialization)
onMounted(async () => {
isLoading.value = true;
try {
const [catRes, enrollRes, courseRes, allCoursesRes] = await Promise.all([
fetchCategories(),
fetchEnrolledCourses({ limit: 10 }), // ดึงข้อมูลคอร์สที่ลงทะเบียนไว้
fetchCourses({
limit: 3,
random: true,
forceRefresh: true,
is_recommended: true,
}), // ดึงข้อมูลคอร์สแนะนำ
fetchCourses({ limit: 1000 }) // สำหรับแมปหมวดหมู่
]);
if (catRes.success) {
categories.value = catRes.data || [];
}
const catMap = new Map();
categories.value.forEach((c: any) => catMap.set(c.id, c.name));
const catIdMap = new Map();
if (allCoursesRes && allCoursesRes.success && allCoursesRes.data) {
allCoursesRes.data.forEach((c: any) => catIdMap.set(c.id, c.category_id));
}
// จัดการข้อมูลคอร์สที่ลงทะเบียน (Mapping Enrolled Courses)
if (enrollRes.success && enrollRes.data) {
const sortedEnrollments = [...enrollRes.data].sort((a, b) => {
const dateA = new Date(a.last_accessed_at || a.enrolled_at).getTime();
const dateB = new Date(b.last_accessed_at || b.enrolled_at).getTime();
return dateB - dateA;
});
enrolledCourses.value = sortedEnrollments.map((item: any) => {
const mappedCategoryId = catIdMap.get(item.course.id) || item.course.category_id;
return {
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 || 0,
completed_lessons: Math.floor(
(item.progress_percentage / 100) * (item.course.total_lessons || 0),
),
category: catMap.get(mappedCategoryId),
lessons: item.course.total_lessons || 0,
image: item.course.thumbnail_url,
enrolled: true,
instructor: item.course.creator || item.course.instructor,
last_accessed: item.last_accessed_at || item.enrolled_at
};
});
libraryCourses.value = enrolledCourses.value.slice(0, 2);
}
// จัดการข้อมูลคอร์สแนะนำ (Mapping Recommended Courses)
if (courseRes.success && courseRes.data) {
recommendedCourses.value = courseRes.data.map((c: any) => ({
id: c.id,
title: c.title,
category: catMap.get(c.category_id),
description: c.description,
lessons: c.total_lessons || 0,
image: c.thumbnail_url || "",
rating: c.rating,
price: c.price,
is_free: c.is_free,
}));
}
} catch (err) {
console.error("Failed to load dashboard data", err);
} finally {
isLoading.value = false;
}
});
// 5. ตัวแปร Computed และฟังก์ชันเสริม
const heroCourse = computed(() => enrolledCourses.value[0] || null);
const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
const navigateToCategory = (catName: string) => {
const cat = categories.value.find(c => getLocalizedText(c.name) === catName);
if (cat) {
navigateTo(`/browse/discovery?category_id=${cat.id}`);
} else {
navigateTo(`/browse/discovery`);
}
}
</script>
<template>
<div class="bg-[#F8F9FA] dark:bg-[#020617] min-h-screen font-inter p-4 md:p-8 transition-colors duration-300">
<div class="max-w-[1400px] mx-auto grid grid-cols-1 xl:grid-cols-3 gap-8">
<!-- คอลมนาย (เนอหาหล) -->
<div class="xl:col-span-2 space-y-6">
<!-- ายตอนร (Welcome Banner) -->
<div class="bg-[#3B6BE8] rounded-[2rem] p-6 md:p-10 relative overflow-hidden text-white shadow-[0_8px_30px_rgb(59,107,232,0.2)]">
<!-- ลวดลายพนหลงและดาวตกแต -->
<div class="absolute inset-0 bg-grid-pattern opacity-10 md:opacity-20 pointer-events-none"></div>
<div class="absolute right-5 md:right-10 top-1/2 -translate-y-1/2 w-20 h-20 md:w-28 md:h-28 border border-white/20 rounded-[1.5rem] md:rounded-[2rem] flex items-center justify-center rotate-12 bg-white/5 backdrop-blur-sm opacity-30 md:opacity-100">
<q-icon name="auto_awesome" size="32px" md-size="48px" class="text-white" />
</div>
<div class="relative z-10 max-w-lg">
<h1 class="text-2xl md:text-4xl font-bold mb-2 md:mb-3 tracking-tight">{{ $t('dashboard.welcomeTitle') }} {{ currentUser?.firstName || 'User' }} !</h1>
<p class="text-blue-100/90 text-[13px] md:text-[15px] leading-relaxed mb-6 md:mb-8 font-medium">
{{ $t('dashboard.welcomeSubtitle') }}
</p>
<button @click="navigateTo('/browse/discovery')" class="bg-white text-[#3B6BE8] font-bold px-5 py-2.5 md:px-6 md:py-3 rounded-full text-xs md:text-sm flex items-center gap-2 hover:bg-slate-50 hover:scale-105 shadow-md transition-all">
{{ $t('dashboard.moreCourses') }} <q-icon name="chevron_right" size="16px" />
</button>
</div>
</div>
<!-- การดหมวดหมวน (3 Stats Cards) -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- การออกแบบ -->
<div @click="navigateToCategory('การออกแบบ')" class="bg-white dark:!bg-slate-900 rounded-[1.5rem] p-5 flex items-center gap-4 shadow-sm border border-slate-100 dark:border-slate-800 transition-all hover:scale-105 hover:shadow-md cursor-pointer">
<div class="w-12 h-12 rounded-2xl bg-[#E9EFFD] dark:bg-blue-900/30 text-[#3B6BE8] flex items-center justify-center shrink-0">
<q-icon name="palette" size="24px" />
</div>
<div>
<h3 class="font-bold text-slate-800 dark:text-slate-200 text-sm">{{ $t('discovery.design') }}</h3>
</div>
</div>
<!-- การเขยนโปรแกรม -->
<div @click="navigateToCategory('การเขียนโปรแกรม')" class="bg-white dark:!bg-slate-900 rounded-[1.5rem] p-5 flex items-center gap-4 shadow-sm border border-slate-100 dark:border-slate-800 transition-all hover:scale-105 hover:shadow-md cursor-pointer">
<div class="w-12 h-12 rounded-2xl bg-[#FFF3EB] dark:bg-orange-900/30 text-[#FF8A4C] flex items-center justify-center shrink-0">
<q-icon name="code" size="24px" />
</div>
<div>
<h3 class="font-bold text-slate-800 dark:text-slate-200 text-sm">{{ $t('discovery.programming') }}</h3>
</div>
</div>
<!-- รก -->
<div @click="navigateToCategory('ธุรกิจ')" class="bg-white dark:!bg-slate-900 rounded-[1.5rem] p-5 flex items-center gap-4 shadow-sm border border-slate-100 dark:border-slate-800 transition-all hover:scale-105 hover:shadow-md cursor-pointer">
<div class="w-12 h-12 rounded-2xl bg-[#EBFAF6] dark:bg-emerald-900/30 text-[#10B981] flex items-center justify-center shrink-0">
<q-icon name="work_outline" size="24px" />
</div>
<div>
<h3 class="font-bold text-slate-800 dark:text-slate-200 text-sm">{{ $t('discovery.business') }}</h3>
</div>
</div>
</div>
<!-- วนเรยนตอจากครงกอน (Continue Learning) -->
<div class="bg-white dark:!bg-slate-900 rounded-[2rem] p-6 md:p-8 shadow-sm border border-slate-100 dark:border-slate-800 transition-colors">
<div class="flex items-center justify-between mb-6">
<h2 class="text-[1.35rem] font-bold text-slate-900 dark:text-white tracking-tight">{{ $t('dashboard.continueLearningTitle') }}</h2>
<NuxtLink to="/dashboard/my-courses" class="text-[#3B6BE8] font-bold text-sm flex items-center gap-1 hover:underline">
{{ $t('dashboard.viewAll') }}
</NuxtLink>
</div>
<div v-if="heroCourse" class="bg-[#F8F9FA] dark:bg-slate-800/50 rounded-3xl p-4 md:p-6 flex flex-col md:flex-row gap-6 md:gap-8 items-center border border-slate-100 dark:border-slate-800">
<div class="w-full md:w-[35%] aspect-[4/3] rounded-[1.5rem] overflow-hidden flex-shrink-0 bg-slate-200">
<img :src="heroCourse.thumbnail_url" class="w-full h-full object-cover" />
</div>
<div class="flex-1 w-full flex flex-col">
<div class="flex items-center justify-between mb-3">
<!-- ายบอกหมวดหม (Category Badge) -->
<div v-if="heroCourse.category">
<span class="bg-[#E9EFFD] dark:bg-blue-900/40 text-[#3B6BE8] dark:text-blue-400 px-3 py-1 rounded-full text-[11px] font-bold tracking-wide">{{ getLocalizedText(heroCourse.category) }}</span>
</div>
<div v-else></div>
<span class="text-slate-400 dark:text-slate-400 text-xs flex items-center gap-1.5 font-medium" v-if="heroCourse.last_accessed">
<q-icon name="schedule" size="14px" /> {{ $t('common.latest') }} {{ new Date(heroCourse.last_accessed).toLocaleDateString('th-TH', { day: 'numeric', month: 'short' }) }}
</span>
</div>
<h3 class="text-2xl font-bold text-slate-900 dark:text-white mb-2 leading-snug line-clamp-2">
{{ getLocalizedText(heroCourse.title) || 'Advanced UI/UX Design มาสเตอร์คลาส' }}
</h3>
<!-- ไมแสดงเลขบทเรยนตามทไดบคำขอมา (Removed Lesson Title/Number as per request) -->
<div class="mb-6 mt-4">
<div class="flex justify-between text-[13px] font-bold mb-2">
<span class="text-[#3B6BE8] dark:text-blue-400">{{ $t('course.progress') }}: {{ heroCourse.progress || 0 }}%</span>
</div>
<div class="h-2.5 w-full bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
<div class="h-full bg-[#3B6BE8] rounded-full transition-all duration-500" :style="{ width: `${heroCourse.progress || 0}%` }"></div>
</div>
</div>
<div class="flex items-center justify-between mt-auto">
<div class="flex items-center gap-3" v-if="heroCourse.instructor">
<div class="w-10 h-10 rounded-full bg-orange-100 overflow-hidden shrink-0 border border-slate-200 dark:border-slate-700">
<img :src="heroCourse.instructor.profile_image_url || heroCourse.instructor.photoURL || `https://api.dicebear.com/7.x/avataaars/svg?seed=${heroCourse.instructor.username || 'Inst'}`" class="w-full h-full object-cover" />
</div>
<div>
<p class="font-bold text-slate-900 dark:text-white text-[13px] leading-tight mb-0.5">
{{ heroCourse.instructor.firstName || heroCourse.instructor.first_name ? `${heroCourse.instructor.firstName || heroCourse.instructor.first_name} ${heroCourse.instructor.lastName || heroCourse.instructor.last_name || ''}` : heroCourse.instructor.username || 'ผู้สอน' }}
</p>
<p class="text-slate-500 dark:text-slate-400 text-[11px] font-medium line-clamp-1">{{ heroCourse.instructor.bio || heroCourse.instructor.role?.name?.th || 'Instructor' }}</p>
</div>
</div>
<div v-else class="flex items-center gap-3"></div>
<button @click="navigateTo(`/classroom/learning?course_id=${heroCourse.id}`)" class="bg-[#3B6BE8] hover:bg-blue-700 text-white px-5 py-2.5 rounded-full font-bold text-sm flex items-center gap-2 shadow-lg shadow-blue-500/20 transition-all hover:scale-105 shrink-0">
<q-icon name="play_circle" size="18px" /> {{ $t('course.continueLearning') }}
</button>
</div>
</div>
</div>
<div v-else class="bg-[#F8F9FA] dark:bg-slate-800/50 rounded-3xl p-10 flex items-center justify-center text-slate-400 border border-dashed border-slate-200 dark:border-slate-700">
ไมคอรสเรยนปจจ
</div>
</div>
</div>
<!-- คอลมนขวา (แถบขอมลโปรไฟลและคอรสแนะนำ) -->
<div class="xl:col-span-1 space-y-6">
<!-- ดเจตโปรไฟลใช -->
<div class="bg-white dark:!bg-slate-900 rounded-[2rem] p-8 shadow-sm border border-slate-100 dark:border-slate-800 text-center flex flex-col items-center relative overflow-hidden transition-colors">
<!-- นหลงตกแต (decorative bg) -->
<div class="absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-[#F8FAFC] to-white dark:from-slate-800 dark:to-slate-900"></div>
<div class="relative z-10 w-24 h-24 rounded-full bg-white dark:bg-slate-800 mb-4 shadow-md flex items-center justify-center">
<UserAvatar
:photo-u-r-l="currentUser?.photoURL"
:first-name="currentUser?.firstName || 'ผู้ใช้งาน'"
:last-name="currentUser?.lastName"
size="88"
class="rounded-full object-cover"
/>
</div>
<h2 class="text-xl font-bold text-slate-900 dark:text-white relative z-10 tracking-tight">
{{ currentUser?.firstName ? `${currentUser.firstName} ${currentUser.lastName || ''}` : 'ผู้ใช้งาน' }}
</h2>
<p class="text-slate-500 dark:text-slate-400 text-xs mt-1 relative z-10 font-medium tracking-wide">{{ $t('common.student') }}</p>
<div class="flex w-full gap-3 mt-7 relative z-10 px-2">
<div class="flex-1 bg-[#F8FAFC] dark:bg-slate-800 rounded-2xl p-3.5 flex flex-col items-center justify-center transition-colors shadow-sm">
<span class="text-[1.35rem] font-black text-[#3B6BE8] dark:text-blue-400 mb-1 leading-none">{{ enrolledCourses.length || 0 }}</span>
<span class="text-slate-400 text-[10px] font-bold tracking-wider">{{ $t('sidebar.onlineCourses') }}</span>
</div>
<div class="flex-1 bg-[#F8FAFC] dark:bg-slate-800 rounded-2xl p-3.5 flex flex-col items-center justify-center transition-colors shadow-sm">
<span class="text-[1.35rem] font-black text-[#10B981] dark:text-emerald-400 mb-1 leading-none z-10">{{ enrolledCourses.filter(c => c.progress >= 100).length || 0 }}</span>
<span class="text-slate-400 text-[10px] font-bold tracking-wider z-10">{{ $t('myCourses.filterCompleted') }}</span>
</div>
</div>
</div>
<!-- ดเจตคอรสแนะนำ -->
<div v-if="recommendedCourses.length > 0" class="bg-white dark:!bg-slate-900 rounded-[2rem] p-6 shadow-sm border border-slate-100 dark:border-slate-800 transition-colors">
<h2 class="text-[1.1rem] font-bold text-slate-900 dark:text-white mb-5 tracking-tight flex items-center justify-between">
{{ $t('dashboard.recommendedCourses') }}
</h2>
<div class="flex flex-col gap-5">
<div v-for="course in recommendedCourses.slice(0, 3)" :key="course.id" class="flex gap-4 group cursor-pointer transition-all" @click="navigateTo(`/browse/discovery?course_id=${course.id}`)">
<!-- รูปหน้าปก (Thumbnail) -->
<div class="w-24 h-[68px] rounded-xl overflow-hidden bg-slate-100 shrink-0 relative shadow-sm">
<img :src="course.image" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
</div>
<!-- อมลคอร (Info) -->
<div class="flex-1 flex flex-col justify-center min-w-0">
<h3 class="font-bold text-[13px] text-slate-900 dark:text-white leading-snug line-clamp-2 mb-1.5 group-hover:text-[#3B6BE8] transition-colors pr-1">{{ getLocalizedText(course.title) }}</h3>
<div class="flex items-center justify-between mt-auto">
<span class="text-slate-500 dark:text-slate-400 text-[11px] font-medium bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded text-ellipsis overflow-hidden whitespace-nowrap max-w-[80px]">{{ getLocalizedText(course.category) || 'อื่นๆ' }}</span>
<span v-if="course.is_free" class="text-[#10B981] font-bold text-[12px]">ฟรี</span>
<span v-else class="text-[#3B6BE8] font-bold text-[12px]">฿{{ Number(course.price).toLocaleString() }}</span>
</div>
</div>
</div>
</div>
<button @click="navigateTo('/browse/discovery')" class="w-full mt-6 py-2.5 rounded-xl text-[13px] font-bold text-[#3B6BE8] dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors">
{{ $t('dashboard.viewAllCourses') }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.bg-grid-pattern {
background-image:
linear-gradient(to right, rgba(255,255,255,0.08) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255,255,255,0.08) 1px, transparent 1px);
background-size: 24px 24px;
}
</style>