156 lines
5.6 KiB
Vue
156 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* @file CurriculumSidebar.vue
|
|
* @description Sidebar Component for displaying course curriculum (Chapters & Lessons)
|
|
* Handles lesson navigation, locked status display, and unread announcement badge.
|
|
*/
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean; // Sidebar open state (v-model)
|
|
courseData: any;
|
|
currentLessonId?: number;
|
|
isLoading: boolean;
|
|
hasUnreadAnnouncements: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: boolean): void;
|
|
(e: 'select-lesson', lessonId: number): void;
|
|
(e: 'open-announcements'): void;
|
|
}>();
|
|
|
|
const { locale } = useI18n()
|
|
|
|
// Helper for localization
|
|
const getLocalizedText = (text: any) => {
|
|
if (!text) return ''
|
|
if (typeof text === 'string') return text
|
|
|
|
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>
|
|
<q-drawer
|
|
:model-value="modelValue"
|
|
@update:model-value="(val) => emit('update:modelValue', val)"
|
|
show-if-above
|
|
bordered
|
|
side="left"
|
|
:width="280"
|
|
:breakpoint="1024"
|
|
class="bg-slate-50 dark:bg-slate-900 shadow-xl"
|
|
content-class="flex flex-col h-full"
|
|
>
|
|
<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/10 bg-slate-50/50 dark:bg-slate-900/50">
|
|
<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>
|
|
<div class="h-2 w-full bg-slate-200 dark:bg-slate-800 rounded-full overflow-hidden shadow-inner">
|
|
<div
|
|
class="h-full bg-blue-600 dark:bg-blue-500 rounded-full transition-all duration-700 ease-out shadow-[0_0_12px_rgba(37,99,235,0.3)]"
|
|
:style="{ width: `${progressPercentage}%` }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-grow scroll">
|
|
<q-list padding class="py-2">
|
|
|
|
|
|
<template v-for="chapter in courseData.chapters" :key="chapter.id">
|
|
<q-item-label header class="bg-slate-100 dark:bg-slate-800 text-[var(--text-main)] font-bold sticky top-0 z-10 border-b dark:border-white/5 text-sm py-4">
|
|
{{ getLocalizedText(chapter.title) }}
|
|
</q-item-label>
|
|
|
|
<q-item
|
|
v-for="lesson in chapter.lessons"
|
|
:key="lesson.id"
|
|
clickable
|
|
v-ripple
|
|
:active="currentLessonId === lesson.id"
|
|
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"
|
|
>
|
|
<q-item-section avatar v-if="lesson.is_locked">
|
|
<q-icon name="lock" size="xs" color="grey" />
|
|
</q-item-section>
|
|
|
|
<q-item-section>
|
|
<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>
|
|
<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" />
|
|
<div class="mt-2 text-xs">{{ $t('classroom.loadingCurriculum') }}</div>
|
|
</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>
|