178 lines
8.5 KiB
Vue
178 lines
8.5 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* @file [id].vue
|
|
* @description Dynamic Course Detail Page.
|
|
* Displays detailed information about a specific course based on the ID.
|
|
*/
|
|
|
|
definePageMeta({
|
|
layout: 'default',
|
|
middleware: 'auth'
|
|
})
|
|
|
|
const route = useRoute()
|
|
// ดึง courseId จาก URL params (แปลงเป็น integer)
|
|
const courseId = computed(() => parseInt(route.params.id as string))
|
|
const { fetchCourseById, enrollCourse } = useCourse()
|
|
|
|
// ใช้ useAsyncData ดึงข้อมูลคอร์ส Server-side rendering (SSR)
|
|
// Key: 'course-{id}' เพื่อให้ cache แยกกันตาม ID
|
|
const { data: courseData, error, refresh } = await useAsyncData(`course-${courseId.value}`, () => fetchCourseById(courseId.value))
|
|
|
|
const course = computed(() => {
|
|
return courseData.value?.success ? courseData.value.data : null
|
|
})
|
|
|
|
const isEnrolling = ref(false)
|
|
|
|
// ฟังก์ชันสำหรับกดปุ่ม "ลงทะเบียนเรียน"
|
|
const handleEnroll = async () => {
|
|
if (!course.value) return
|
|
if (isEnrolling.value) return
|
|
isEnrolling.value = true
|
|
|
|
// เรียก API ลงทะเบียนเรียน
|
|
const res = await enrollCourse(course.value.id)
|
|
|
|
if (res.success) {
|
|
// ถ้าสำเร็จ ให้เปลี่ยนหน้าไปที่ "คอร์สของฉัน" พร้อม params enrolled=true
|
|
return navigateTo(`/dashboard/my-courses?enrolled=true&course_id=${course.value.id}`)
|
|
} else {
|
|
// กรณี error แสดง alert (อนาคตอาจเปลี่ยนเป็น Toast notification)
|
|
alert(res.error || 'Failed to enroll')
|
|
}
|
|
|
|
isEnrolling.value = false
|
|
}
|
|
|
|
|
|
const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => {
|
|
if (!text) return ''
|
|
if (typeof text === 'string') return text
|
|
// @ts-ignore
|
|
return text.th || text.en || ''
|
|
}
|
|
|
|
|
|
useHead({
|
|
title: computed(() => course.value ? `${getLocalizedText(course.value.title)} - E-Learning` : 'Course Details')
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="container mx-auto max-w-7xl px-4 py-8">
|
|
|
|
|
|
<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-bold text-slate-900 dark:text-white mb-4 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 class="flex justify-between py-2">
|
|
<span>ระดับ</span>
|
|
<span class="font-bold text-slate-900 dark:text-black">ทั้งหมด</span>
|
|
</div>
|
|
<div class="flex justify-between py-2">
|
|
<span>การเข้าถึง</span>
|
|
<span class="font-bold text-slate-900 dark:text-black">ตลอดชีพ</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Loading / Error State -->
|
|
<div v-else class="text-center py-20">
|
|
<div v-if="error" class="text-red-500 mb-4">
|
|
<p class="font-bold">เกิดข้อผิดพลาดในการโหลดข้อมูล</p>
|
|
<p class="text-sm opacity-80">{{ error.message }}</p>
|
|
</div>
|
|
<div v-else class="flex flex-col items-center gap-4 text-slate-400">
|
|
<div class="w-10 h-10 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin"/>
|
|
<p>กำลังโหลด...</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@keyframes pulse-slow {
|
|
0%, 100% { opacity: 0.1; transform: scale(1); }
|
|
50% { opacity: 0.15; transform: scale(1.15); }
|
|
}
|
|
|
|
.animate-pulse-slow {
|
|
animation: pulse-slow 10s linear infinite;
|
|
}
|
|
</style>
|