feat: Implement user profile management, course browsing, and dashboard structure with new components and layouts.
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 45s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 4s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s

This commit is contained in:
supalerk-ar66 2026-02-19 17:37:28 +07:00
parent c118e5c3dc
commit 0f92f0d00c
10 changed files with 446 additions and 195 deletions

View file

@ -32,7 +32,7 @@ onMounted(async () => {
try {
const [catRes, enrollRes, courseRes] = await Promise.all([
fetchCategories(),
fetchEnrolledCourses({ limit: 3, status: 'IN_PROGRESS' }), // Fetch recent enrolled
fetchEnrolledCourses({ limit: 10 }), // Fetch more enrolled courses for library section
fetchCourses({ limit: 3, random: true, forceRefresh: true, is_recommended: true }) // Fetch 3 Recommended Courses
])
@ -46,19 +46,33 @@ onMounted(async () => {
// Map Enrolled Courses
if (enrollRes.success && enrollRes.data) {
enrolledCourses.value = enrollRes.data.map((item: any) => ({
// 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))
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
}))
// Update libraryCourses with only 2 courses
libraryCourses.value = enrolledCourses.value.slice(0, 2)
}
// Map Recommended/Library Courses
// Map Recommended 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,
@ -70,9 +84,6 @@ onMounted(async () => {
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) {
@ -93,7 +104,46 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3))
<div class="max-w-7xl mx-auto px-4 md:px-12 space-y-16 mt-10">
<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 rounded-[2rem] py-10 md:py-14 px-8 md:px-12 shadow-sm border border-slate-100 flex flex-col items-center text-center">
<!-- Subtle Decorative Elements -->
<div class="absolute top-[-20%] left-[-10%] w-[300px] h-[300px] bg-blue-500/5 rounded-full blur-3xl -z-10" />
<div class="absolute bottom-[-20%] right-[-10%] w-[300px] h-[300px] bg-indigo-500/5 rounded-full blur-3xl -z-10" />
<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 leading-[1.5] tracking-tight">
ปสกลของคณตอเนอง
<span class="inline-block text-blue-600 mt-1 md:mt-2">เพอเปาหมายทวางไว</span>
</h1>
<p class="text-slate-500 font-medium text-base md:text-lg max-w-xl mx-auto leading-relaxed">
นนณเรยนไปกนาทแล? มาสรางนยการเรยนรยอดเยยมกนเถอะ เรามคอรสแนะนำใหม มากมายรอคณอย
</p>
<div class="flex flex-wrap justify-center gap-4 pt-4">
<q-btn
unelevated
rounded
color="primary"
label="ไปที่คอร์สเรียนของฉัน"
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="ค้นหาคอร์สใหม่"
class="px-8 h-[48px] font-bold no-caps hover:bg-white transition-all border-1 text-sm"
style="border-width: 1.5px;"
to="/browse/discovery"
/>
</div>
</div>
</section>
<!-- 2. Continue Learning Section -->
<section v-if="enrolledCourses.length > 0">
@ -106,11 +156,12 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3))
<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]">
<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]"
@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">
<div class="bg-blue-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 -->
@ -119,10 +170,15 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3))
<span>{{ heroCourse.progress }}%</span>
</div>
<div class="h-1.5 w-full bg-white/20 rounded-full overflow-hidden">
<div class="h-full bg-blue-500 rounded-full" :style="{ width: `${heroCourse.progress}%` }"></div>
<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="text-white font-bold text-sm hover:underline">{{ heroCourse.progress === 100 ? 'เรียนอีกครั้ง' : 'เรียนต่อ' }}</span>
<span class="font-bold text-sm hover:underline transition-colors"
:class="heroCourse.progress === 100 ? 'text-emerald-400' : 'text-white'">
{{ heroCourse.progress === 100 ? 'เรียนอีกครั้ง' : 'เรียนต่อ' }}
</span>
</div>
</div>
</div>
@ -139,10 +195,16 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3))
<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-blue-600 rounded-full" :style="{ width: `${course.progress}%` }"></div>
<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="text-blue-600 font-bold cursor-pointer hover:underline" @click="navigateTo(`/classroom/learning?course_id=${course.id}`)">{{ course.progress === 100 ? 'เรียนอีกครั้ง' : 'เรียนต่อ' }}</span>
<span class="font-bold cursor-pointer hover:underline transition-colors"
:class="course.progress === 100 ? 'text-emerald-600' : 'text-blue-600'"
@click="navigateTo(`/classroom/learning?course_id=${course.id}`)">
{{ course.progress === 100 ? 'เรียนอีกครั้ง' : 'เรียนต่อ' }}
</span>
</div>
</div>
</div>
@ -170,6 +232,8 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3))
:key="course.id"
v-bind="course"
:image="course.thumbnail_url"
hide-progress
hide-actions
class="h-full md:col-span-1"
/>