feat: Implement initial e-learning platform frontend structure including dashboard, course management, authentication, and common UI components.

This commit is contained in:
supalerk-ar66 2026-02-27 10:05:33 +07:00
parent aceeb80d9a
commit ad11c6b7c5
44 changed files with 720 additions and 578 deletions

View file

@ -1,12 +1,12 @@
<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.
* @description คอมโพเนนตแถบเมนานขางสำหรบแสดงหลกสตรของคอรสเรยน (บทเรยน & ตอนตางๆ)
* ดการการนำทางไปยงบทเรยน, แสดงสถานะการลอค, และแจงเตอนประกาศทงไมไดาน
*/
const props = defineProps<{
modelValue: boolean; // Sidebar open state (v-model)
modelValue: boolean; // / Sidebar (Sidebar open state - v-model)
courseData: any;
currentLessonId?: number;
isLoading: boolean;
@ -21,10 +21,10 @@ const emit = defineEmits<{
const { locale } = useI18n()
// State for expansion items
// (State for expansion items)
const chapterOpenState = ref<Record<string, boolean>>({})
// Helper for localization
// (Helper for localization)
const getLocalizedText = (text: any) => {
if (!text) return ''
if (typeof text === 'string') return text
@ -34,13 +34,13 @@ const getLocalizedText = (text: any) => {
return text[currentLocale] || text.th || text.en || ''
}
// Helper: Check if lesson is completed
// (Helper: Check if lesson is completed)
const isLessonCompleted = (lesson: any) => {
return lesson.is_completed === true || lesson.progress?.is_completed === true
}
// Reactive Chapter Completion Status
// Computes a map of chapterId -> boolean (true if all lessons are completed)
// (Reactive Chapter Completion Status)
// Map chapterId -> boolean (true )
const chapterCompletionStatus = computed(() => {
const status: Record<string, boolean> = {}
if (!props.courseData || !props.courseData.chapters) return status
@ -55,7 +55,7 @@ const chapterCompletionStatus = computed(() => {
return status
})
// Local Progress Calculation
// Local (Local Progress Calculation)
const progressPercentage = computed(() => {
if (!props.courseData || !props.courseData.chapters) return 0
let total = 0
@ -69,7 +69,7 @@ const progressPercentage = computed(() => {
return total > 0 ? Math.round((completed / total) * 100) : 0
})
// Auto-expand chapter containing current lesson
// (Auto-expand chapter containing current lesson)
watch(() => props.currentLessonId, (newId) => {
if (newId && props.courseData?.chapters) {
props.courseData.chapters.forEach((chapter: any) => {
@ -81,7 +81,7 @@ watch(() => props.currentLessonId, (newId) => {
}
}, { immediate: true })
// Initialize all chapters as open by default on load
// (Initialize all chapters as open by default on load)
watch(() => props.courseData, (newData) => {
if (newData?.chapters) {
newData.chapters.forEach((chapter: any) => {
@ -104,10 +104,10 @@ watch(() => props.courseData, (newData) => {
:breakpoint="1024"
class="bg-slate-50 dark:!bg-slate-900 shadow-xl"
>
<!-- Main Container: Enforce Column Layout and Full Width -->
<!-- คอนเทนเนอรหลกบงคบใชความกวางเตมท (Main Container: Enforce Column Layout and Full Width) -->
<div v-if="courseData" class="flex flex-col w-full h-full overflow-hidden text-slate-900 dark:!text-white relative bg-slate-50 dark:!bg-slate-900">
<!-- 1. Header Section (Fixed at Top) -->
<!-- 1. วนห านบนคงท (Header Section - Fixed at Top) -->
<div class="flex-none p-5 border-b border-slate-200 dark:border-white/10 bg-white dark:!bg-slate-900 z-10 w-full">
<h2 class="text-sm font-bold mb-4 line-clamp-2 leading-snug block w-full text-slate-900 dark:!text-white">{{ getLocalizedText(courseData.course.title) }}</h2>
@ -123,11 +123,11 @@ watch(() => props.courseData, (newData) => {
</div>
</div>
<!-- 2. Curriculum List (Scrollable Area) -->
<!-- 2. รายการหลกสตร นทเลอนได (Curriculum List - Scrollable Area) -->
<div class="flex-1 overflow-y-auto bg-slate-50 dark:!bg-slate-900 w-full p-4 space-y-3">
<q-list class="block w-full">
<div v-for="(chapter, idx) in courseData.chapters" :key="chapter.id" class="block w-full mb-3">
<!-- Chapter Accordion -->
<!-- กลองขอมลของบท (Chapter Accordion) -->
<q-expansion-item
v-model="chapterOpenState[chapter.id]"
class="bg-white dark:!bg-slate-800 rounded-xl overflow-hidden shadow-sm border border-slate-200 dark:border-slate-700 w-full"
@ -137,7 +137,7 @@ watch(() => props.courseData, (newData) => {
<template v-slot:header>
<div class="flex items-center w-full py-3 text-slate-900 dark:!text-white">
<div class="mr-3 flex-shrink-0">
<!-- Chapter Indicator (Check or Number) -->
<!-- วบงชบทเรยน เครองหมายถกหรอตวเลข (Chapter Indicator - Check or Number) -->
<div class="w-7 h-7 rounded-full border-2 flex items-center justify-center transition-colors font-bold"
:class="chapterCompletionStatus[chapter.id]
? 'border-green-500 text-green-500 bg-green-50 dark:!bg-green-500/10'
@ -146,7 +146,7 @@ watch(() => props.courseData, (newData) => {
<span v-else class="text-[10px]">{{ Number(idx) + 1 }}</span>
</div>
</div>
<!-- Explicitly handle text overflow -->
<!-- ดการตวอกษรทนเกนอยางชดเจน (Explicitly handle text overflow) -->
<div class="flex-1 min-w-0 pr-2 overflow-hidden">
<div class="font-bold text-sm leading-tight mb-0.5 truncate block w-full">{{ getLocalizedText(chapter.title) }}</div>
<div class="text-[10px] text-slate-500 dark:!text-slate-400 font-normal truncate block w-full">
@ -156,7 +156,7 @@ watch(() => props.courseData, (newData) => {
</div>
</template>
<!-- Lessons List -->
<!-- รายการบทเรยนยอย (Lessons List) -->
<div class="bg-slate-50 dark:!bg-slate-800/50 border-t border-slate-100 dark:border-slate-700 w-full">
<div
v-for="(lesson, lIdx) in chapter.lessons"
@ -167,27 +167,27 @@ watch(() => props.courseData, (newData) => {
: 'border-transparent'"
@click="!lesson.is_locked && emit('select-lesson', lesson.id)"
>
<!-- Lesson Status Icon -->
<!-- ไอคอนสถานะของบทเรยน (Lesson Status Icon) -->
<div class="mr-3 flex-shrink-0">
<!-- Completed (Takes Precedence) -->
<!-- เรยนจบแล (สำคญท) (Completed - Takes Precedence) -->
<q-icon v-if="isLessonCompleted(lesson)"
name="check_circle"
class="text-green-500"
size="20px"
/>
<!-- Active/Playing (If not completed) -->
<!-- กำลงเรยนอย (Active/Playing - If not completed) -->
<q-icon v-else-if="currentLessonId === lesson.id"
name="play_circle_filled"
class="text-blue-600 dark:!text-blue-400 animate-pulse"
size="20px"
/>
<!-- Locked -->
<!-- กลอคอย (Locked) -->
<q-icon v-else-if="lesson.is_locked"
name="lock"
class="text-slate-400 dark:!text-slate-500 opacity-70"
size="18px"
/>
<!-- Not Started -->
<!-- งไมไดเร (Not Started) -->
<div v-else class="w-[18px] h-[18px] rounded-full border-2 border-slate-300 dark:border-slate-600"></div>
</div>
@ -214,7 +214,7 @@ watch(() => props.courseData, (newData) => {
</template>
<style scoped>
/* Custom scrollbar for better aesthetics */
/* สครอลบาร์ปรับแต่งเพื่อความสวยงาม (Custom scrollbar for better aesthetics) */
::-webkit-scrollbar {
width: 4px;
}