feat: Implement core e-learning features including video playback, user profile management, and course discovery.
This commit is contained in:
parent
2cd1d481aa
commit
a648c41b72
11 changed files with 1085 additions and 747 deletions
|
|
@ -27,9 +27,10 @@ const selectedCourse = ref<any>(null);
|
|||
const isLoading = ref(false);
|
||||
const isLoadingDetail = ref(false);
|
||||
const isEnrolling = ref(false);
|
||||
const showAllCategories = ref(false);
|
||||
|
||||
|
||||
const { t } = useI18n();
|
||||
const { currentUser } = useAuth();
|
||||
const { fetchCategories } = useCategory();
|
||||
const { fetchCourses, fetchCourseById, enrollCourse } = useCourse();
|
||||
|
||||
|
|
@ -142,59 +143,10 @@ onMounted(() => {
|
|||
|
||||
<!-- LEFT SIDEBAR: Category Filter -->
|
||||
<div class="w-full lg:w-64 flex-shrink-0 lg:sticky lg:top-24">
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<q-expansion-item
|
||||
expand-separator
|
||||
:label="`หมวดหมู่ (${selectedCategoryIds.length})`"
|
||||
class="font-bold text-slate-900"
|
||||
header-class="bg-white"
|
||||
text-color="slate-900"
|
||||
:header-style="{ color: '#0f172a' }"
|
||||
default-opened
|
||||
>
|
||||
<q-list class="bg-white border-t border-slate-200">
|
||||
<q-item
|
||||
v-for="cat in (showAllCategories ? categories : categories.slice(0, 4))"
|
||||
:key="cat.id"
|
||||
tag="label"
|
||||
clickable
|
||||
v-ripple
|
||||
dense
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
v-model="selectedCategoryIds"
|
||||
:val="cat.id"
|
||||
color="primary"
|
||||
dense
|
||||
class="checkbox-visible"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-sm font-medium text-slate-900">{{ getLocalizedText(cat.name) }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Show More/Less Button -->
|
||||
<q-item
|
||||
v-if="categories.length > 4"
|
||||
clickable
|
||||
v-ripple
|
||||
@click="showAllCategories = !showAllCategories"
|
||||
class="text-blue-600 font-bold text-sm"
|
||||
>
|
||||
<q-item-section>
|
||||
<div class="flex items-center gap-1">
|
||||
{{ showAllCategories ? 'แสดงน้อยลง' : 'แสดงเพิ่มเติม' }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" :class="showAllCategories ? 'rotate-180' : ''">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
<CategorySidebar
|
||||
:categories="categories"
|
||||
v-model="selectedCategoryIds"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CONTENT: Course Grid -->
|
||||
|
|
@ -248,95 +200,13 @@ onMounted(() => {
|
|||
<q-spinner size="3rem" color="primary" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedCourse" class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<!-- Main Content (Left Column) -->
|
||||
<div class="lg:col-span-8">
|
||||
<!-- Hero Video Placeholder -->
|
||||
<div
|
||||
class="w-full aspect-video bg-slate-900 rounded-3xl overflow-hidden relative shadow-lg mb-8 group"
|
||||
>
|
||||
<img
|
||||
v-if="selectedCourse.thumbnail_url"
|
||||
:src="selectedCourse.thumbnail_url"
|
||||
class="absolute inset-0 w-full h-full object-cover opacity-60 group-hover:opacity-40 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-4 text-slate-900 dark:text-white tracking-tight">
|
||||
{{ getLocalizedText(selectedCourse.title) }}
|
||||
</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 text-lg leading-relaxed mb-8">
|
||||
{{ getLocalizedText(selectedCourse.description) }}
|
||||
</p>
|
||||
|
||||
<!-- Course Syllabus -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-3xl p-8 shadow-sm border border-slate-100 dark:border-slate-700">
|
||||
<h3 class="text-xl font-bold mb-6 text-slate-900 flex items-center gap-2">
|
||||
<span class="w-1 h-6 bg-blue-500 rounded-full"></span>
|
||||
{{ $t('course.courseContent') }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div v-for="(chapter, index) in selectedCourse.chapters" :key="chapter.id" class="border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="bg-slate-50 dark:bg-slate-700/50 p-4 flex justify-between items-center cursor-pointer">
|
||||
<div class="font-bold text-slate-800 dark:text-slate-100">
|
||||
<span class="opacity-50 mr-2">Chapter {{ Number(index) + 1 }}</span>
|
||||
{{ getLocalizedText(chapter.title) }}
|
||||
</div>
|
||||
<span class="text-xs font-bold px-3 py-1 bg-white rounded-full border border-slate-200">
|
||||
{{ chapter.lessons ? chapter.lessons.length : 0 }} Lessons
|
||||
</span>
|
||||
</div>
|
||||
<!-- Lessons -->
|
||||
<div class="divide-y divide-slate-100 dark:divide-slate-700 bg-white dark:bg-slate-800">
|
||||
<div v-for="(lesson, lIndex) in chapter.lessons" :key="lesson.id" class="p-4 flex justify-between items-center hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-6 h-6 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-[10px] font-bold text-slate-500 dark:text-slate-400">
|
||||
{{ Number(lIndex) + 1 }}
|
||||
</div>
|
||||
<span class="text-slate-700 dark:text-slate-200 font-medium text-sm">{{ getLocalizedText(lesson.title) }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-slate-400">{{ lesson.duration_minutes || 0 }}:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar (Right Column) -->
|
||||
<div class="lg:col-span-4">
|
||||
<div class="bg-white rounded-3xl p-6 shadow-lg border border-slate-100 sticky top-24">
|
||||
<q-btn
|
||||
@click="handleEnroll(selectedCourse.id)"
|
||||
|
||||
unelevated
|
||||
rounded
|
||||
class="w-full py-3 text-lg font-bold bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg"
|
||||
:loading="isEnrolling"
|
||||
:label="$t('course.enrollNow')"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<q-spinner-facebook />
|
||||
</template>
|
||||
</q-btn>
|
||||
|
||||
<div class="text-sm space-y-3 pt-4 border-t border-slate-100">
|
||||
<div class="flex justify-between text-slate-600 dark:text-black">
|
||||
<span>{{ $t('course.certificate') }}</span>
|
||||
<span class="font-bold text-slate-900 dark:text-black">{{ selectedCourse.have_certificate ? 'มีใบประกาศฯ' : 'ไม่มี' }}</span>
|
||||
</div>
|
||||
<div v-if="selectedCourse.instructor_name" class="flex justify-between text-slate-600 dark:text-black">
|
||||
<span>ผู้สอน</span>
|
||||
<span class="font-bold text-slate-900 dark:text-black">{{ selectedCourse.instructor_name }}</span>
|
||||
</div>
|
||||
<div v-if="selectedCourse.level" class="flex justify-between text-slate-600 dark:text-black">
|
||||
<span>ระดับ</span>
|
||||
<span class="font-bold text-slate-900 dark:text-black">{{ selectedCourse.level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CourseDetailView
|
||||
v-else-if="selectedCourse"
|
||||
:course="selectedCourse"
|
||||
:user="currentUser"
|
||||
@back="showDetail = false"
|
||||
@enroll="handleEnroll"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue