feat: Implement core e-learning platform features including quiz, dashboard, course discovery, and classroom learning with i18n support.
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 35s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 4s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 35s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 4s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s
This commit is contained in:
parent
d787412036
commit
a0ca6f7e6b
13 changed files with 429 additions and 246 deletions
|
|
@ -29,6 +29,20 @@ const getLocalizedText = (text: any) => {
|
|||
const currentLocale = locale.value as 'th' | 'en'
|
||||
return text[currentLocale] || text.th || text.en || ''
|
||||
}
|
||||
|
||||
// Local Progress Calculation
|
||||
const progressPercentage = computed(() => {
|
||||
if (!props.courseData || !props.courseData.chapters) return 0
|
||||
let total = 0
|
||||
let completed = 0
|
||||
props.courseData.chapters.forEach((c: any) => {
|
||||
c.lessons.forEach((l: any) => {
|
||||
total++
|
||||
if (l.is_completed || l.progress?.is_completed) completed++
|
||||
})
|
||||
})
|
||||
return total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -38,13 +52,29 @@ const getLocalizedText = (text: any) => {
|
|||
show-if-above
|
||||
bordered
|
||||
side="left"
|
||||
:width="320"
|
||||
:width="280"
|
||||
:breakpoint="1024"
|
||||
class="bg-[var(--bg-surface)]"
|
||||
content-class="bg-[var(--bg-surface)]"
|
||||
class="bg-slate-50 dark:bg-slate-900 shadow-xl"
|
||||
content-class="flex flex-col h-full"
|
||||
>
|
||||
<div v-if="courseData" class="h-full scroll">
|
||||
<q-list class="pb-10">
|
||||
<div v-if="courseData" class="flex flex-col h-full overflow-hidden">
|
||||
<!-- Course Progress Header -->
|
||||
<div class="p-5 border-b border-gray-200 dark:border-white/5 bg-white dark:bg-slate-900">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-xs font-black uppercase tracking-widest text-slate-500 dark:text-slate-400">{{ $t('course.progress', 'ความคืบหน้า') }}</span>
|
||||
<span class="text-sm font-black text-blue-600 dark:text-blue-400">{{ progressPercentage }}%</span>
|
||||
</div>
|
||||
<q-linear-progress
|
||||
:value="progressPercentage / 100"
|
||||
color="primary"
|
||||
track-color="blue-1"
|
||||
class="rounded-full h-2"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow scroll">
|
||||
<q-list padding class="py-2">
|
||||
|
||||
|
||||
<template v-for="chapter in courseData.chapters" :key="chapter.id">
|
||||
|
|
@ -58,8 +88,8 @@ const getLocalizedText = (text: any) => {
|
|||
clickable
|
||||
v-ripple
|
||||
:active="currentLessonId === lesson.id"
|
||||
active-class="bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-200 font-medium"
|
||||
class="border-b border-gray-50 dark:border-white/5"
|
||||
active-class="bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 active-lesson-indicator"
|
||||
class="px-5 py-3 transition-all duration-200 group relative border-b border-gray-100/50 dark:border-white/5"
|
||||
@click="!lesson.is_locked && emit('select-lesson', lesson.id)"
|
||||
:disable="lesson.is_locked"
|
||||
>
|
||||
|
|
@ -68,19 +98,26 @@ const getLocalizedText = (text: any) => {
|
|||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label class="text-sm md:text-base line-clamp-2 text-[var(--text-main)]">
|
||||
<q-item-label
|
||||
class="text-sm font-bold line-clamp-2 transition-colors"
|
||||
:class="currentLessonId === lesson.id ? 'text-blue-700 dark:text-blue-300' : 'text-slate-700 dark:text-slate-300'"
|
||||
>
|
||||
{{ getLocalizedText(lesson.title) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-icon v-if="lesson.is_completed || lesson.progress?.is_completed" name="check_circle" color="positive" size="xs" />
|
||||
<q-icon v-else-if="currentLessonId === lesson.id" name="play_circle" color="primary" size="xs" />
|
||||
<q-icon v-else name="radio_button_unchecked" color="grey-4" size="xs" />
|
||||
<div class="flex items-center">
|
||||
<q-icon v-if="lesson.is_completed || lesson.progress?.is_completed" name="check_circle" color="positive" size="18px" />
|
||||
<q-icon v-else-if="currentLessonId === lesson.id" name="play_arrow" color="primary" size="18px" class="animate-pulse" />
|
||||
<q-icon v-else-if="lesson.is_locked" name="lock" color="grey-4" size="18px" />
|
||||
<q-icon v-else name="radio_button_unchecked" color="grey-3" size="18px" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isLoading" class="p-6 text-center text-slate-500">
|
||||
<q-spinner color="primary" size="2em" />
|
||||
|
|
@ -88,3 +125,33 @@ const getLocalizedText = (text: any) => {
|
|||
</div>
|
||||
</q-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.active-lesson-indicator {
|
||||
position: relative;
|
||||
}
|
||||
.active-lesson-indicator::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: #2563eb; /* blue-600 */
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.dark .scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
const props = defineProps<{
|
||||
src: string;
|
||||
poster?: string;
|
||||
initialSeekTime?: number;
|
||||
}>();
|
||||
|
||||
|
|
@ -241,6 +242,7 @@ watch([volume, isMuted], () => {
|
|||
<video
|
||||
ref="videoRef"
|
||||
:src="src"
|
||||
:poster="poster"
|
||||
class="w-full h-full object-contain"
|
||||
@click="togglePlay"
|
||||
@timeupdate="handleTimeUpdate"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue