feat: implement course discovery page with category filtering sidebar and course detail view.
This commit is contained in:
parent
088bbf4b1b
commit
efb50a1ddb
3 changed files with 80 additions and 201 deletions
|
|
@ -34,14 +34,13 @@ const totalPages = ref(1);
|
|||
const itemsPerPage = 12;
|
||||
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const { currentUser } = useAuth();
|
||||
const { fetchCategories } = useCategory();
|
||||
const { fetchCourses, fetchCourseById, enrollCourse, getLocalizedText } = useCourse();
|
||||
|
||||
|
||||
// 2. Computed Properties
|
||||
const sortOption = ref(computed(() => t('discovery.sortRecent')));
|
||||
const sortOption = ref(t('discovery.sortRecent'));
|
||||
const sortOptions = computed(() => [t('discovery.sortRecent')]);
|
||||
|
||||
const filteredCourses = computed(() => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ definePageMeta({
|
|||
const route = useRoute()
|
||||
// ดึง courseId จาก URL params (แปลงเป็น integer)
|
||||
const courseId = computed(() => parseInt(route.params.id as string))
|
||||
const { currentUser } = useAuth()
|
||||
const { fetchCourseById, enrollCourse, getLocalizedText } = useCourse()
|
||||
|
||||
// ใช้ useAsyncData ดึงข้อมูลคอร์ส Server-side rendering (SSR)
|
||||
|
|
@ -65,85 +66,13 @@ useHead({
|
|||
<div class="page-container">
|
||||
|
||||
|
||||
<div v-if="course" class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
|
||||
<div class="lg:col-span-8">
|
||||
<div class="relative aspect-video bg-slate-900 rounded-2xl overflow-hidden shadow-lg mb-8 group cursor-pointer border border-slate-200 dark:border-slate-700">
|
||||
<div v-if="course.thumbnail_url" class="absolute inset-0">
|
||||
<img :src="course.thumbnail_url" class="w-full h-full object-cover opacity-90 group-hover:opacity-75 transition-opacity" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl md:text-4xl font-black text-slate-900 dark:text-white mb-6 leading-tight">{{ getLocalizedText(course.title) }}</h1>
|
||||
|
||||
<div class="prose prose-slate dark:prose-invert max-w-none mb-10 text-slate-600 dark:text-slate-300">
|
||||
<p class="text-lg leading-relaxed">{{ getLocalizedText(course.description) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-sm border border-slate-100">
|
||||
<h3 class="font-bold text-xl text-slate-900 dark:text-black mb-6 flex items-center gap-2">
|
||||
<span class="w-1 h-6 bg-blue-500 rounded-full"></span>
|
||||
{{ $t('course.courseContent') }}
|
||||
</h3>
|
||||
|
||||
<div v-if="course.chapters && course.chapters.length > 0">
|
||||
<div v-for="(chapter, cIdx) in course.chapters" :key="cIdx" class="mb-6 last:mb-0">
|
||||
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/50 rounded-xl mb-2 border border-slate-100 dark:border-slate-600">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">{{ getLocalizedText(chapter.title) }}</span>
|
||||
<span class="text-xs font-semibold px-2 py-1 bg-white dark:bg-slate-600 rounded text-slate-500 dark:text-slate-300 border border-slate-200 dark:border-slate-500">
|
||||
{{ chapter.lessons ? chapter.lessons.length : 0 }} {{ $t('course.lessons') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pl-4 pr-2 space-y-1">
|
||||
<div v-for="(lesson, lIdx) in chapter.lessons" :key="lIdx" class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors group">
|
||||
<div class="flex items-center gap-3 overflow-hidden">
|
||||
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-slate-100 dark:bg-slate-700 text-slate-400 dark:text-slate-500 text-[10px] flex items-center justify-center font-bold">
|
||||
{{ Number(lIdx) + 1 }}
|
||||
</span>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300 truncate group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{{ getLocalizedText(lesson.title) }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-400 flex-shrink-0 font-mono">{{ lesson.duration_minutes || 0 }}:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-8 text-slate-400 dark:text-black font-medium">
|
||||
<p>{{ $t('course.noContent') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-4">
|
||||
<div class="sticky top-24 bg-white rounded-3xl p-6 md:p-8 shadow-lg border border-slate-100">
|
||||
<div class="mb-8">
|
||||
<span class="text-sm text-slate-400 line-through mr-3" v-if="course.original_price">{{ course.original_price }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-4xl font-black text-blue-600 dark:text-blue-400 tracking-tight">{{ course.is_free ? 'ฟรี' : course.price }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleEnroll"
|
||||
:disabled="isEnrolling"
|
||||
class="w-full py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white rounded-xl font-bold text-lg shadow-lg shadow-blue-600/30 transition-all hover:translate-y-[-2px] mb-6 flex items-center justify-center disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
<span v-if="isEnrolling" class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></span>
|
||||
{{ isEnrolling ? 'กำลังลงทะเบียน...' : $t('course.enrollNow') }}
|
||||
</button>
|
||||
|
||||
<div class="space-y-4 text-sm text-slate-600 dark:text-black pt-6 border-t border-slate-100">
|
||||
<div class="flex justify-between py-2">
|
||||
<span>{{ $t('course.certificate') }}</span>
|
||||
<span class="font-bold text-slate-900 dark:text-black">{{ course.have_certificate ? $t('course.available') : '-' }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="course" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<CourseDetailView
|
||||
:course="course"
|
||||
:user="currentUser"
|
||||
@back="navigateTo('/browse/discovery')"
|
||||
@enroll="handleEnroll"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading / Error State -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue