feat: Implement initial e-learning platform frontend including landing page, course discovery, dashboard, and foundational UI components with i18n.

This commit is contained in:
supalerk-ar66 2026-02-26 15:20:44 +07:00
parent 5b9cf72046
commit 3a9da1007b
17 changed files with 1631 additions and 1524 deletions

View file

@ -1,44 +1,46 @@
<script setup lang="ts">
/**
* @file index.vue
* @description Dashboard Home Page matching FutureSkill design
* @description หนาหลกแดชบอร (Dashboard Home)
*/
// 1. MetaData
definePageMeta({
layout: "default",
middleware: "auth",
});
useHead({
title: "Dashboard - FutureSkill Clone",
title: "Dashboard - e-Learning Platform",
});
// 2. Composables
const { currentUser } = useAuth();
const { fetchCourses, fetchEnrolledCourses, getLocalizedText } = useCourse();
const { fetchCategories } = useCategory();
const { t } = useI18n();
// 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
// 4. (Data Initialization)
onMounted(async () => {
isLoading.value = true;
try {
const [catRes, enrollRes, courseRes] = await Promise.all([
const [catRes, enrollRes, courseRes, allCoursesRes] = await Promise.all([
fetchCategories(),
fetchEnrolledCourses({ limit: 10 }), // Fetch more enrolled courses for library section
fetchEnrolledCourses({ limit: 10 }), //
fetchCourses({
limit: 3,
random: true,
forceRefresh: true,
is_recommended: true,
}), // Fetch 3 Recommended Courses
}), //
fetchCourses({ limit: 1000 }) //
]);
if (catRes.success) {
@ -48,36 +50,44 @@ onMounted(async () => {
const catMap = new Map();
categories.value.forEach((c: any) => catMap.set(c.id, c.name));
// Map Enrolled Courses
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) {
// Sort by last_accessed_at descending (Newest first)
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) => ({
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),
),
// For CourseCard compatibility in library section
category: catMap.get(item.course.category_id),
lessons: item.course.total_lessons || 0,
image: item.course.thumbnail_url,
enrolled: true,
}));
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
};
});
// Update libraryCourses with only 2 courses
libraryCourses.value = enrolledCourses.value.slice(0, 2);
}
// Map Recommended Courses
// (Mapping Recommended Courses)
if (courseRes.success && courseRes.data) {
recommendedCourses.value = courseRes.data.map((c: any) => ({
id: c.id,
@ -98,326 +108,215 @@ onMounted(async () => {
}
});
// Helper for "Continue Learning" Hero Card
// 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 pb-20 transition-colors duration-300">
<div class="container mx-auto px-6 md:px-12 space-y-16 mt-10">
<!-- 1. Dashboard Hero Banner (Refined) -->
<section
class="relative overflow-hidden bg-gradient-to-br from-white to-slate-50 dark:from-slate-900 dark:to-slate-950 rounded-[2rem] py-10 md:py-14 px-8 md:px-12 shadow-sm border border-slate-100 dark:border-slate-800 flex flex-col items-center text-center transition-colors duration-300"
>
<!-- Subtle Decorative Elements -->
<div
class="absolute top-[-20%] left-[-10%] w-[300px] h-[300px] bg-blue-500/5 dark:bg-blue-500/10 rounded-full blur-3xl -z-10"
/>
<div
class="absolute bottom-[-20%] right-[-10%] w-[300px] h-[300px] bg-indigo-500/5 dark:bg-indigo-500/10 rounded-full blur-3xl -z-10"
/>
<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">
<!-- Left Column (Main Content) -->
<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="max-w-2xl space-y-6 relative z-10">
<h1
class="text-3xl md:text-4xl lg:text-5xl font-bold text-slate-900 dark:text-white leading-[1.5] tracking-tight"
>
{{ $t("dashboard.heroTitle") }}
<span class="inline-block text-blue-600 dark:text-blue-400 mt-1 md:mt-2">{{
$t("dashboard.heroSubtitle")
}}</span>
</h1>
<p
class="text-slate-500 dark:text-slate-400 font-medium text-base md:text-lg max-w-xl mx-auto leading-relaxed"
>
{{ $t("dashboard.heroDesc") }}
</p>
<div class="flex flex-wrap justify-center gap-4 pt-4">
<q-btn
unelevated
rounded
color="primary"
:label="$t('dashboard.goToMyCourses')"
class="px-8 h-[48px] font-bold no-caps shadow-lg shadow-blue-500/10 hover:-translate-y-0.5 transition-all text-sm"
to="/dashboard/my-courses"
/>
<q-btn
outline
rounded
color="primary"
:label="$t('dashboard.searchNewCourses')"
class="px-8 h-[48px] font-bold no-caps hover:bg-white dark:hover:bg-slate-800 transition-all border-1 text-sm dark:text-white dark:border-slate-600"
style="border-width: 1.5px"
to="/browse/discovery"
/>
</div>
</div>
</section>
<!-- 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] dark:text-white transition-colors">
{{ $t("dashboard.continueLearningTitle") }}
</h2>
<NuxtLink
to="/dashboard/my-courses"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium text-sm flex items-center gap-1 transition-colors"
>
{{ $t("dashboard.myCourses") }}
<q-icon name="arrow_forward" size="16px" />
</NuxtLink>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<!-- Hero Card (Left) -->
<div
v-if="heroCourse"
class="relative group cursor-pointer rounded-2xl overflow-hidden bg-white dark:bg-[#1e293b] shadow-sm border border-gray-100 dark:border-slate-700 hover:shadow-md transition-all h-[260px] md:h-[320px]"
@click="
navigateTo(`/classroom/learning?course_id=${heroCourse.id}`)
"
>
<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"
>
<h3
class="text-white text-2xl font-bold mb-4 line-clamp-2 leading-snug shadow-black/50 drop-shadow-sm"
>
{{ getLocalizedText(heroCourse.title) }}
</h3>
<!-- Progress -->
<div class="w-full">
<div class="flex justify-end text-gray-300 text-xs mb-2">
<span>{{ heroCourse.progress }}%</span>
</div>
<div
class="h-1.5 w-full bg-white/20 rounded-full overflow-hidden"
>
<div
class="h-full rounded-full transition-all duration-500"
:class="
heroCourse.progress === 100
? 'bg-emerald-500'
: 'bg-blue-500'
"
:style="{ width: `${heroCourse.progress}%` }"
></div>
</div>
<div class="mt-4 flex justify-end">
<span
class="font-bold text-sm hover:underline transition-colors"
:class="
heroCourse.progress === 100
? 'text-emerald-400'
: 'text-white'
"
>
{{
heroCourse.progress === 100
? $t("dashboard.studyAgain")
: $t("dashboard.continue")
}}
</span>
</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>
<!-- การเขยนโปรแกรม -->
<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>
<!-- Side List (Right) -->
<div class="flex flex-col gap-4">
<div
v-for="course in sideCourses"
:key="course.id"
class="flex-1 bg-white dark:!bg-slate-900/40 rounded-2xl p-4 border border-slate-100 dark:border-white/5 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"
<!-- 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>
<!-- Right Column (Sidebar/Profile Content) -->
<div class="xl:col-span-1 space-y-6">
<!-- Profile Widget -->
<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>
<div
class="flex-grow min-w-0 flex flex-col justify-between h-full py-1"
>
<h4 class="text-gray-800 dark:text-slate-200 font-bold text-sm line-clamp-2 mb-2 transition-colors">
{{ getLocalizedText(course.title) }}
</h4>
<div class="mt-auto">
<div
class="h-1.5 w-full bg-gray-100 dark:bg-slate-700 rounded-full overflow-hidden mb-2"
>
<div
class="h-full rounded-full transition-all duration-500"
:class="
course.progress === 100
? 'bg-emerald-500'
: 'bg-blue-600'
"
:style="{ width: `${course.progress}%` }"
></div>
</div>
<div class="flex justify-end items-center text-xs">
<span
class="font-bold cursor-pointer hover:underline transition-colors"
:class="
course.progress === 100
? 'text-emerald-600 dark:text-emerald-400'
: 'text-blue-600 dark:text-blue-400'
"
@click="
navigateTo(`/classroom/learning?course_id=${course.id}`)
"
>
{{
course.progress === 100
? $t("dashboard.studyAgain")
: $t("dashboard.continue")
}}
</span>
</div>
</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">{{ String(enrolledCourses.length || 0).padStart(2, '0') }}</span>
<span class="text-slate-400 text-[10px] font-bold tracking-wider">{{ $t('myCourses.filterProgress') }}</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">{{ String(enrolledCourses.filter(c => c.progress >= 100).length || 0).padStart(2, '0') }}</span>
<span class="text-slate-400 text-[10px] font-bold tracking-wider z-10">{{ $t('myCourses.filterCompleted') }}</span>
</div>
</div>
</div>
<!-- Empty State Placeholder if less than 2 side courses -->
<div
v-if="sideCourses.length < 2"
class="flex-1 bg-slate-50 dark:!bg-slate-900/30 rounded-2xl border border-dashed border-slate-200 dark:border-slate-800 flex items-center justify-center text-slate-400 dark:text-slate-600 text-sm transition-colors"
>
{{ $t("dashboard.startNewCourse") }}
</div>
<!-- Recommended Courses Widget -->
<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>
</div>
</div>
</section>
<!-- 3. Knowledge Library -->
<section>
<div class="mb-6">
<h2 class="text-xl md:text-2xl font-bold text-[#2D2D2D] dark:text-white mb-1 transition-colors">
{{ $t("dashboard.knowledgeLibrary") }}
</h2>
<p class="text-gray-500 dark:text-slate-400 text-sm transition-colors">
{{ $t("dashboard.libraryDesc") }}
</p>
</div>
<!-- Content when courses exist -->
<div
v-if="libraryCourses.length > 0"
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6"
>
<!-- Course Cards -->
<CourseCard
v-for="course in libraryCourses"
:key="course.id"
v-bind="course"
:image="course.thumbnail_url"
hide-progress
hide-actions
class="h-full md:col-span-1"
/>
<div
class="bg-white dark:!bg-slate-900/40 rounded-3xl border border-slate-100 dark:border-white/5 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 dark:text-slate-300 font-medium mb-6 mt-4 transition-colors">
{{ $t("dashboard.chooseLibrary") }}
</p>
<q-btn
flat
rounded
no-caps
class="text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/30 px-6 py-2 font-bold group-hover:scale-105 transition-transform"
to="/dashboard/my-courses"
>
{{ $t("dashboard.viewAll") }}
<q-icon name="arrow_forward" size="18px" class="ml-2" />
</q-btn>
</div>
</div>
<div
v-else
class="bg-white dark:!bg-slate-900/40 rounded-3xl border border-dashed border-slate-200 dark:border-slate-800 p-12 flex flex-col items-center justify-center text-center min-h-[300px] transition-colors"
>
<div class="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-full mb-6 transition-colors">
<q-icon name="school" size="48px" class="text-blue-200 dark:text-blue-400" />
</div>
<h3 class="text-xl font-bold text-gray-800 dark:text-white mb-2 transition-colors">
{{ $t("dashboard.emptyLibraryTitle") }}
</h3>
<p class="text-gray-500 dark:text-slate-400 mb-8 max-w-md transition-colors">
{{ $t("dashboard.emptyLibraryDesc") }}
</p>
<q-btn
unelevated
rounded
no-caps
class="bg-blue-600 text-white px-8 py-3 font-bold hover:bg-blue-700 shadow-lg shadow-blue-500/20 transition-all hover:scale-105"
to="/browse/discovery"
>
{{ $t("dashboard.viewAllCourses") }}
</q-btn>
</div>
</section>
<!-- 5. Recommended Courses -->
<section class="pb-20">
<div class="mb-6">
<h2 class="text-xl md:text-2xl font-bold text-[#2D2D2D] dark:text-white text-left transition-colors">
{{ $t("dashboard.recommendedCourses") }}
</h2>
</div>
<!-- Recommended Grid (3 columns) -->
<div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 animate-fade-in"
>
<CourseCard
v-for="course in recommendedCourses"
:key="course.id"
v-bind="course"
/>
</div>
<!-- Loading State -->
<div
v-if="recommendedCourses.length === 0 && !isLoading"
class="flex justify-center py-10 opacity-50"
>
<div class="text-gray-400 dark:text-slate-500">{{ $t("dashboard.noRecommended") }}</div>
</div>
</section>
<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>
/* 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);
}
}
:deep(.q-btn) {
text-transform: none; /* Prevent uppercase in Q-Btns */
.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>