feat: implement e-learning classroom page with video player, lesson navigation, progress tracking, and announcements.
This commit is contained in:
parent
be5b9756be
commit
42b7399868
5 changed files with 74 additions and 23 deletions
|
|
@ -124,18 +124,33 @@ const stopYTTracking = () => {
|
||||||
if (ytInterval) clearInterval(ytInterval);
|
if (ytInterval) clearInterval(ytInterval);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const destroyYoutubePlayer = () => {
|
||||||
|
stopYTTracking();
|
||||||
|
if (ytPlayer) {
|
||||||
|
try {
|
||||||
|
if (ytPlayer.destroy) ytPlayer.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error destroying YT player:', e);
|
||||||
|
}
|
||||||
|
ytPlayer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isYoutube.value) initYoutubeAPI();
|
if (isYoutube.value) initYoutubeAPI();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopYTTracking();
|
destroyYoutubePlayer();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for src change to re-init
|
// Watch for src change to re-init
|
||||||
watch(() => props.src, () => {
|
watch(() => props.src, (newSrc, oldSrc) => {
|
||||||
if (isYoutube.value) {
|
if (newSrc !== oldSrc) {
|
||||||
setTimeout(initYoutubeAPI, 500);
|
destroyYoutubePlayer();
|
||||||
|
if (isYoutube.value) {
|
||||||
|
setTimeout(initYoutubeAPI, 300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ const navItems = computed(() => [
|
||||||
isSvg: false
|
isSvg: false
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const handleNavigate = (path: string) => {
|
||||||
|
if (import.meta.client) {
|
||||||
|
window.location.href = path
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -51,10 +57,11 @@ const navItems = computed(() => [
|
||||||
<q-item
|
<q-item
|
||||||
v-for="item in navItems"
|
v-for="item in navItems"
|
||||||
:key="item.to"
|
:key="item.to"
|
||||||
:to="item.to"
|
|
||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
|
@click="handleNavigate(item.to)"
|
||||||
class="rounded-r-full mr-2 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-white/5"
|
class="rounded-r-full mr-2 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-white/5"
|
||||||
|
:class="{ 'q-router-link--active': $route.path === item.to || ($route.path === '/dashboard' && item.to === '/dashboard') }"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon :name="item.icon" />
|
<q-icon :name="item.icon" />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ const navItems = [
|
||||||
{ to: '/browse/discovery', icon: 'explore', label: 'รายการคอร์ส' },
|
{ to: '/browse/discovery', icon: 'explore', label: 'รายการคอร์ส' },
|
||||||
{ to: '/dashboard/my-courses', icon: 'school', label: 'คอร์สของฉัน' }
|
{ to: '/dashboard/my-courses', icon: 'school', label: 'คอร์สของฉัน' }
|
||||||
]
|
]
|
||||||
|
const handleNavigate = (path: string) => {
|
||||||
|
if (import.meta.client) {
|
||||||
|
window.location.href = path
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -14,14 +19,15 @@ const navItems = [
|
||||||
align="justify"
|
align="justify"
|
||||||
dense
|
dense
|
||||||
>
|
>
|
||||||
<q-route-tab
|
<q-tab
|
||||||
v-for="item in navItems"
|
v-for="item in navItems"
|
||||||
:key="item.to"
|
:key="item.to"
|
||||||
:to="item.to"
|
@click="handleNavigate(item.to)"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
no-caps
|
no-caps
|
||||||
class="py-2"
|
class="py-2"
|
||||||
|
:class="{ 'q-tab--active text-primary': $route.path === item.to }"
|
||||||
/>
|
/>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -452,18 +452,28 @@ export const useAuth = () => {
|
||||||
refreshToken.value = null // ลบ Refresh Token
|
refreshToken.value = null // ลบ Refresh Token
|
||||||
user.value = null
|
user.value = null
|
||||||
|
|
||||||
// Reset client-side storage (Keep remembered_email)
|
// Reset client-side storage (Keep remembered_email and theme)
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
// ลบเฉพาะข้อมูลที่ไม่ใช่อีเมลที่จำไว้
|
|
||||||
const rememberedEmail = localStorage.getItem('remembered_email')
|
const rememberedEmail = localStorage.getItem('remembered_email')
|
||||||
|
const theme = localStorage.getItem('theme')
|
||||||
|
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
|
|
||||||
if (rememberedEmail) {
|
if (rememberedEmail) {
|
||||||
localStorage.setItem('remembered_email', rememberedEmail)
|
localStorage.setItem('remembered_email', rememberedEmail)
|
||||||
}
|
}
|
||||||
|
if (theme) {
|
||||||
|
localStorage.setItem('theme', theme)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
// Clear and Redirect using hard reload for complete state reset
|
||||||
router.push('/auth/login')
|
if (import.meta.client) {
|
||||||
|
window.location.href = '/auth/login'
|
||||||
|
} else {
|
||||||
|
const router = useRouter()
|
||||||
|
router.push('/auth/login')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,11 @@ const handleLessonSelect = (lessonId: number) => {
|
||||||
const loadCourseData = async () => {
|
const loadCourseData = async () => {
|
||||||
if (!courseId.value) return
|
if (!courseId.value) return
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
// Reset states before loading new course
|
||||||
|
courseData.value = null
|
||||||
|
currentLesson.value = null
|
||||||
|
announcements.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetchCourseLearningInfo(courseId.value)
|
const res = await fetchCourseLearningInfo(courseId.value)
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
|
@ -155,14 +160,11 @@ const loadCourseData = async () => {
|
||||||
const loadLesson = async (lessonId: number) => {
|
const loadLesson = async (lessonId: number) => {
|
||||||
if (currentLesson.value?.id === lessonId) return
|
if (currentLesson.value?.id === lessonId) return
|
||||||
|
|
||||||
// Clear previous video state
|
// Clear previous video state & unload component to force reset
|
||||||
isPlaying.value = false
|
isPlaying.value = false
|
||||||
videoProgress.value = 0
|
videoProgress.value = 0
|
||||||
currentTime.value = 0
|
currentTime.value = 0
|
||||||
if (videoRef.value) {
|
currentLesson.value = null // This will unmount VideoPlayer and hide content
|
||||||
videoRef.value.pause()
|
|
||||||
videoRef.value.currentTime = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
isLessonLoading.value = true
|
isLessonLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|
@ -450,7 +452,9 @@ onMounted(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
// Clear state when leaving the page to ensure fresh start on return
|
||||||
|
courseData.value = null
|
||||||
|
currentLesson.value = null
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -462,13 +466,13 @@ onBeforeUnmount(() => {
|
||||||
<q-toolbar>
|
<q-toolbar>
|
||||||
<q-btn flat round dense icon="menu" class="lg:hidden mr-2 text-slate-900 dark:text-white" @click="toggleSidebar" />
|
<q-btn flat round dense icon="menu" class="lg:hidden mr-2 text-slate-900 dark:text-white" @click="toggleSidebar" />
|
||||||
|
|
||||||
<NuxtLink
|
<a
|
||||||
to="/dashboard/my-courses"
|
href="/dashboard/my-courses"
|
||||||
class="inline-flex items-center gap-2 text-slate-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-300 transition-all font-black text-sm md:text-base group mr-4"
|
class="inline-flex items-center gap-2 text-slate-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-300 transition-all font-black text-sm md:text-base group mr-4 no-underline"
|
||||||
>
|
>
|
||||||
<q-icon name="arrow_back" size="24px" class="transition-transform group-hover:-translate-x-1" />
|
<q-icon name="arrow_back" size="24px" class="transition-transform group-hover:-translate-x-1" />
|
||||||
<span>{{ $t('classroom.backToDashboard') }}</span>
|
<span>{{ $t('classroom.backToDashboard') }}</span>
|
||||||
</NuxtLink>
|
</a>
|
||||||
|
|
||||||
<q-toolbar-title class="text-base font-bold text-center lg:text-left truncate text-slate-900 dark:text-white">
|
<q-toolbar-title class="text-base font-bold text-center lg:text-left truncate text-slate-900 dark:text-white">
|
||||||
{{ courseData ? getLocalizedText(courseData.course.title) : $t('classroom.loadingTitle') }}
|
{{ courseData ? getLocalizedText(courseData.course.title) : $t('classroom.loadingTitle') }}
|
||||||
|
|
@ -499,7 +503,7 @@ onBeforeUnmount(() => {
|
||||||
<div class="w-full max-w-7xl mx-auto p-4 md:p-6 flex-grow">
|
<div class="w-full max-w-7xl mx-auto p-4 md:p-6 flex-grow">
|
||||||
<!-- Video Player -->
|
<!-- Video Player -->
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
v-if="currentLesson && videoSrc"
|
v-if="currentLesson && videoSrc && !isLessonLoading"
|
||||||
ref="videoPlayerComp"
|
ref="videoPlayerComp"
|
||||||
:src="videoSrc"
|
:src="videoSrc"
|
||||||
:initialSeekTime="initialSeekTime"
|
:initialSeekTime="initialSeekTime"
|
||||||
|
|
@ -508,6 +512,15 @@ onBeforeUnmount(() => {
|
||||||
@loadedmetadata="(d: number) => onVideoMetadataLoaded(d)"
|
@loadedmetadata="(d: number) => onVideoMetadataLoaded(d)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Skeleton Loader for Video/Content -->
|
||||||
|
<div v-if="isLessonLoading" class="aspect-video bg-slate-200 dark:bg-slate-800 rounded-2xl animate-pulse flex items-center justify-center mb-6 overflow-hidden relative">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-slate-200 to-slate-300 dark:from-slate-700 dark:to-slate-800 opacity-50"></div>
|
||||||
|
<div class="z-10 flex flex-col items-center">
|
||||||
|
<q-spinner size="4rem" color="primary" :thickness="4" />
|
||||||
|
<p class="mt-4 text-slate-500 dark:text-slate-400 font-bold animate-bounce">{{ $t('common.loading') }}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Lesson Info -->
|
<!-- Lesson Info -->
|
||||||
<div v-if="currentLesson" class="bg-[var(--bg-surface)] p-6 md:p-8 rounded-3xl shadow-sm border border-[var(--border-color)]">
|
<div v-if="currentLesson" class="bg-[var(--bg-surface)] p-6 md:p-8 rounded-3xl shadow-sm border border-[var(--border-color)]">
|
||||||
<!-- ใช้สีจากตัวแปรกลาง: จะแยกโหมดให้อัตโนมัติ (สว่าง=ดำ / มืด=ขาว) -->
|
<!-- ใช้สีจากตัวแปรกลาง: จะแยกโหมดให้อัตโนมัติ (สว่าง=ดำ / มืด=ขาว) -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue