feat: add initial frontend pages for course browsing, recommendations, and user dashboard.
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 38s
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:
supalerk-ar66 2026-02-23 17:44:02 +07:00
parent 0588ad7acd
commit 01d249c19a
14 changed files with 570 additions and 267 deletions

View file

@ -171,11 +171,11 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
</NuxtLink>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<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-[320px]"
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}`)
"
@ -233,7 +233,7 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
</div>
<!-- Side List (Right) -->
<div class="flex flex-col gap-4 h-[320px]">
<div class="flex flex-col gap-4">
<div
v-for="course in sideCourses"
:key="course.id"
@ -313,7 +313,7 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
<!-- Content when courses exist -->
<div
v-if="libraryCourses.length > 0"
class="grid grid-cols-1 md:grid-cols-3 gap-6"
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6"
>
<!-- Course Cards -->
<CourseCard
@ -382,7 +382,7 @@ const sideCourses = computed(() => enrolledCourses.value.slice(1, 3));
<!-- Recommended Grid (3 columns) -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-fade-in"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 animate-fade-in"
>
<CourseCard
v-for="course in recommendedCourses"

View file

@ -151,48 +151,54 @@ const validCourseId = computed(() => {
<!-- Page Header & Filters (Unified Layout) -->
<div class="mb-8">
<div class="flex items-start gap-4 mb-2">
<span class="w-1.5 h-10 md:h-12 bg-blue-600 rounded-full shadow-lg shadow-blue-500/50 mt-1 flex-shrink-0"></span>
<div>
<h1 class="text-3xl md:text-4xl font-black text-slate-900 dark:text-white leading-tight">
{{ $t('sidebar.myCourses') }}
</h1>
<!-- New Enhanced Search Section (Image 2 Style) -->
<div class="bg-blue-50/50 dark:bg-blue-900/10 rounded-[2.5rem] p-8 md:p-10 mb-6 border border-blue-100/50 dark:border-blue-500/10">
<h2 class="text-2xl md:text-3xl font-black text-slate-900 dark:text-white mb-2">คอรสของฉ</h2>
<p class="text-slate-500 dark:text-slate-400 font-medium mb-8">ดตามความคบหนาและเรยนรอจากจดทางไว</p>
<div class="flex flex-col md:flex-row gap-4">
<!-- Search Input -->
<div class="relative flex-1 group">
<div class="absolute left-5 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-blue-600 transition-colors">
<q-icon name="search" size="24px" />
</div>
</div>
<input
v-model="searchQuery"
type="text"
placeholder="ค้นหาชื่อคอร์สของฉัน..."
class="w-full pl-14 pr-6 py-3.5 bg-white dark:bg-slate-800 border-2 border-transparent rounded-2xl text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:border-blue-500/20 focus:ring-4 focus:ring-blue-500/5 transition-all text-base font-medium shadow-sm"
/>
</div>
<!-- Search Button -->
<q-btn
unelevated
color="primary"
class="px-8 h-[52px] rounded-2xl font-black shadow-lg shadow-blue-600/20 hover:scale-[1.02] transition-transform"
no-caps
>
<div class="flex items-center gap-2">
<q-icon name="search" size="20px" />
<span class="text-base">นหา</span>
</div>
</q-btn>
</div>
</div>
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mt-4">
<!-- Filter Tabs (Horizontal Bar) -->
<div class="bg-white dark:bg-slate-900/50 p-1.5 rounded-2xl border border-slate-100 dark:border-white/5 inline-flex items-center gap-1 shadow-sm">
<q-btn
v-for="filter in ['all', 'progress', 'completed']"
:key="filter"
@click="activeFilter = filter as any"
flat
rounded
dense
class="px-5 py-2 font-bold transition-all text-[11px] uppercase tracking-wider"
:class="activeFilter === filter ? 'bg-blue-600 text-white shadow-md shadow-blue-600/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'"
:label="$t(`myCourses.filter${filter.charAt(0).toUpperCase() + filter.slice(1)}`)"
/>
</div>
<!-- Search Input -->
<div class="w-full md:w-72">
<q-input
v-model="searchQuery"
dense
outlined
rounded
:placeholder="$t('discovery.searchPlaceholder')"
class="search-input shadow-sm"
bg-color="transparent"
>
<template v-slot:prepend>
<q-icon name="search" class="text-slate-400" />
</template>
</q-input>
</div>
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
<!-- Filter Tabs (Horizontal Bar) -->
<div class="bg-white dark:bg-slate-900/50 p-1.5 rounded-2xl border border-slate-100 dark:border-white/5 inline-flex items-center gap-1 shadow-sm">
<q-btn
v-for="filter in ['all', 'progress', 'completed']"
:key="filter"
@click="activeFilter = filter as any"
flat
rounded
dense
class="px-5 py-2 font-bold transition-all text-[11px] uppercase tracking-wider"
:class="activeFilter === filter ? 'bg-blue-600 text-white shadow-md shadow-blue-600/20' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'"
:label="$t(`myCourses.filter${filter.charAt(0).toUpperCase() + filter.slice(1)}`)"
/>
</div>
</div>