feat: Add course discovery page, course card component, and dashboard index page.
This commit is contained in:
parent
62e9c9c566
commit
6d31cc2e45
3 changed files with 149 additions and 334 deletions
|
|
@ -130,19 +130,19 @@ onMounted(() => {
|
|||
<template>
|
||||
<div class="page-container">
|
||||
|
||||
|
||||
<!-- CATALOG VIEW: Browse courses -->
|
||||
<div v-if="!showDetail">
|
||||
|
||||
|
||||
<!-- Top Header Area -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
||||
<!-- Title -->
|
||||
<h1 class="text-3xl font-black text-slate-900 dark:text-white">
|
||||
<h1 class="text-3xl font-black text-slate-900 dark:text-white flex items-center gap-3">
|
||||
<span class="w-1.5 h-8 bg-blue-600 rounded-full shadow-sm shadow-blue-500/50"></span>
|
||||
{{ $t('discovery.title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Right Side: Search & Sort -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-3 w-full md:w-auto">
|
||||
<!-- Search Input -->
|
||||
<q-input
|
||||
v-model="searchQuery"
|
||||
|
|
@ -150,22 +150,21 @@ onMounted(() => {
|
|||
outlined
|
||||
rounded
|
||||
:placeholder="$t('discovery.searchPlaceholder')"
|
||||
class="disc-search w-full md:w-64"
|
||||
class="w-full md:w-72 bg-white dark:bg-slate-800 search-input"
|
||||
bg-color="transparent"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" class="text-slate-400" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Layout: Sidebar + Grid -->
|
||||
<div class="flex flex-col lg:flex-row gap-8 items-start">
|
||||
<div class="flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- LEFT SIDEBAR: Category Filter -->
|
||||
<div class="w-full lg:w-64 flex-shrink-0 lg:sticky lg:top-24">
|
||||
<div class="w-full lg:w-64 flex-shrink-0 lg:sticky lg:top-24 z-10">
|
||||
<CategorySidebar
|
||||
:categories="categories"
|
||||
v-model="selectedCategoryIds"
|
||||
|
|
@ -174,48 +173,45 @@ onMounted(() => {
|
|||
|
||||
<!-- RIGHT CONTENT: Course Grid -->
|
||||
<div class="flex-1 w-full">
|
||||
<div v-if="filteredCourses.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<CourseCard
|
||||
v-for="course in filteredCourses"
|
||||
:key="course.id"
|
||||
:title="course.title"
|
||||
:price="course.price"
|
||||
:description="course.description"
|
||||
:rating="course.rating"
|
||||
:lessons="course.lessons"
|
||||
:image="course.thumbnail_url"
|
||||
show-view-details
|
||||
@view-details="selectCourse(course.id)"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="filteredCourses.length > 0">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<CourseCard
|
||||
v-for="course in filteredCourses"
|
||||
:key="course.id"
|
||||
v-bind="{ ...course, image: course.thumbnail_url }"
|
||||
show-view-details
|
||||
@view-details="selectCourse(course.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div v-if="totalPages > 1" class="flex justify-center mt-12 pb-10">
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
:max="totalPages"
|
||||
:max-pages="6"
|
||||
boundary-numbers
|
||||
direction-links
|
||||
color="primary"
|
||||
flat
|
||||
active-design="unelevated"
|
||||
active-color="primary"
|
||||
@update:model-value="loadCourses"
|
||||
/>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<div v-if="totalPages > 1" class="flex justify-center mt-12 pb-10">
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
:max="totalPages"
|
||||
:max-pages="6"
|
||||
boundary-numbers
|
||||
direction-links
|
||||
color="primary"
|
||||
flat
|
||||
active-design="unelevated"
|
||||
active-color="primary"
|
||||
@update:model-value="loadCourses"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center py-20 bg-slate-50 dark:bg-slate-800/50 rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-700"
|
||||
>
|
||||
|
||||
<q-icon name="search_off" size="64px" class="text-slate-300 dark:text-slate-600 mb-4" />
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">{{ $t('discovery.emptyTitle') }}</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-center max-w-md">
|
||||
{{ $t('discovery.emptyDesc') }}
|
||||
</p>
|
||||
<button class="mt-6 font-bold text-blue-600 hover:underline" @click="searchQuery = ''; selectedCategoryIds = []">
|
||||
<button class="mt-6 font-bold text-blue-600 hover:text-blue-700 dark:hover:text-blue-400 transition-colors" @click="searchQuery = ''; selectedCategoryIds = []">
|
||||
{{ $t('discovery.showAll') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -231,7 +227,7 @@ onMounted(() => {
|
|||
@click="showDetail = false"
|
||||
class="inline-flex items-center gap-2 text-slate-600 dark:text-white hover:text-blue-600 dark:hover:text-blue-300 mb-6 transition-all font-black text-lg md:text-xl group"
|
||||
>
|
||||
<q-icon name="arrow_back" size="28px" class="transition-transform group-hover:-translate-x-2" />
|
||||
<q-icon name="arrow_back" size="24px" class="transition-transform group-hover:-translate-x-1" />
|
||||
{{ $t('discovery.backToCatalog') }}
|
||||
</button>
|
||||
|
||||
|
|
@ -251,55 +247,11 @@ onMounted(() => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* =========================
|
||||
Discovery: Quasar Field Fix
|
||||
- จำกัดผลกระทบเฉพาะช่องค้นหา/เรียงตาม
|
||||
- ตัด focus ring (กรอบขาว 4 เหลี่ยม)
|
||||
========================= */
|
||||
|
||||
/* ให้หน้าตา input เป็น pill ขาวเหมือนรูป (ทั้ง Light/Dark) */
|
||||
.disc-search :deep(.q-field__control),
|
||||
.disc-sort :deep(.q-field__control) {
|
||||
background: #ffffff !important;
|
||||
border-radius: 9999px !important;
|
||||
/* Standard overrides for Quasar inputs to match Tailwind theme */
|
||||
.search-input :deep(.q-field__control) {
|
||||
border-radius: 9999px; /* Full rounded pill */
|
||||
}
|
||||
|
||||
/* สีตัวอักษร/placeholder/icon ให้คมชัด */
|
||||
.disc-search :deep(.q-field__native),
|
||||
.disc-search :deep(.q-field__input),
|
||||
.disc-sort :deep(.q-field__native),
|
||||
.disc-sort :deep(.q-field__input) {
|
||||
color: #0f172a !important;
|
||||
-webkit-text-fill-color: #0f172a !important;
|
||||
}
|
||||
|
||||
.disc-search :deep(.q-placeholder),
|
||||
.disc-sort :deep(.q-placeholder) {
|
||||
color: #64748b !important; /* slate-500 */
|
||||
}
|
||||
|
||||
.disc-search :deep(.q-icon),
|
||||
.disc-sort :deep(.q-icon),
|
||||
.disc-sort :deep(.q-select__dropdown-icon) {
|
||||
color: #64748b !important;
|
||||
}
|
||||
|
||||
/* ✅ ตัด “Focus outline / Focus ring” ที่เป็นกรอบสว่าง */
|
||||
.disc-search :deep(.q-field__control:after),
|
||||
.disc-sort :deep(.q-field__control:after) {
|
||||
opacity: 0 !important; /* ตัวนี้มักเป็นเส้น/แสงตอน focus */
|
||||
}
|
||||
|
||||
/* คุมเส้นขอบปกติให้บาง ๆ ไม่เด้งเป็นกรอบขาว */
|
||||
.disc-search :deep(.q-field__control:before),
|
||||
.disc-sort :deep(.q-field__control:before) {
|
||||
border-color: rgba(203, 213, 225, 0.9) !important; /* slate-300 */
|
||||
}
|
||||
|
||||
/* กัน browser outline ซ้อน */
|
||||
.disc-search :deep(input:focus),
|
||||
.disc-sort :deep(input:focus) {
|
||||
outline: none !important;
|
||||
.search-input :deep(.q-field__shadow) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue