feat: Implement initial e-learning platform frontend including landing page, course discovery, dashboard, and foundational UI components with i18n.

This commit is contained in:
supalerk-ar66 2026-02-26 15:20:44 +07:00
parent 5b9cf72046
commit 3a9da1007b
17 changed files with 1631 additions and 1524 deletions

View file

@ -102,20 +102,20 @@ watch(() => props.courseData, (newData) => {
side="right"
:width="300"
:breakpoint="1024"
class="bg-slate-50 dark:bg-slate-900 shadow-xl"
class="bg-slate-50 dark:!bg-slate-900 shadow-xl"
>
<!-- 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">
<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) -->
<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">{{ getLocalizedText(courseData.course.title) }}</h2>
<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>
<div class="flex justify-between items-center mb-2 w-full">
<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>
<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-100 dark:bg-slate-800 rounded-full overflow-hidden">
<div class="h-2 w-full bg-slate-100 dark:!bg-slate-800 rounded-full overflow-hidden">
<div
class="h-full bg-blue-600 dark:bg-blue-500 rounded-full transition-all duration-700 ease-out"
:style="{ width: `${progressPercentage}%` }"
@ -124,24 +124,24 @@ watch(() => props.courseData, (newData) => {
</div>
<!-- 2. Curriculum List (Scrollable Area) -->
<div class="flex-1 overflow-y-auto bg-slate-50 dark:bg-[#0f1219] w-full p-4 space-y-3">
<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 -->
<q-expansion-item
v-model="chapterOpenState[chapter.id]"
class="bg-white dark:bg-[#1a1e29] rounded-xl overflow-hidden shadow-sm border border-slate-200 dark:border-slate-800 w-full"
header-class="rounded-t-xl w-full"
expand-icon-class="text-slate-400"
class="bg-white dark:!bg-slate-800 rounded-xl overflow-hidden shadow-sm border border-slate-200 dark:border-slate-700 w-full"
header-class="rounded-t-xl w-full text-slate-900 dark:!text-white"
expand-icon-class="text-slate-400 dark:!text-slate-300"
>
<template v-slot:header>
<div class="flex items-center w-full py-3 text-slate-900 dark:text-white">
<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) -->
<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'
: 'border-slate-300 dark:border-slate-600 text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-800'">
? 'border-green-500 text-green-500 bg-green-50 dark:!bg-green-500/10'
: 'border-slate-300 dark:!border-slate-600 text-slate-500 dark:!text-slate-400 bg-slate-100 dark:!bg-slate-700'">
<q-icon v-if="chapterCompletionStatus[chapter.id]" name="check" size="14px" class="font-bold" />
<span v-else class="text-[10px]">{{ Number(idx) + 1 }}</span>
</div>
@ -149,7 +149,7 @@ watch(() => props.courseData, (newData) => {
<!-- 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">
<div class="text-[10px] text-slate-500 dark:!text-slate-400 font-normal truncate block w-full">
{{ chapter.lessons.length }} {{ $t('course.lessonsUnit') }}
</div>
</div>
@ -157,13 +157,13 @@ watch(() => props.courseData, (newData) => {
</template>
<!-- Lessons List -->
<div class="bg-slate-50 dark:bg-[#0f1219]/50 border-t border-slate-100 dark:border-slate-800 w-full">
<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"
:key="lesson.id"
class="flex items-center px-4 py-3 cursor-pointer transition-all border-l-4 hover:bg-slate-100 dark:hover:bg-slate-800/50 w-full"
class="flex items-center px-4 py-3 cursor-pointer transition-all border-l-4 hover:bg-slate-100 dark:hover:!bg-slate-700/50 w-full"
:class="currentLessonId === lesson.id
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/10'
? 'border-blue-600 bg-blue-50 dark:!bg-blue-900/40'
: 'border-transparent'"
@click="!lesson.is_locked && emit('select-lesson', lesson.id)"
>
@ -178,13 +178,13 @@ watch(() => props.courseData, (newData) => {
<!-- 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"
class="text-blue-600 dark:!text-blue-400 animate-pulse"
size="20px"
/>
<!-- Locked -->
<q-icon v-else-if="lesson.is_locked"
name="lock"
class="text-slate-400 opacity-70"
class="text-slate-400 dark:!text-slate-500 opacity-70"
size="18px"
/>
<!-- Not Started -->
@ -193,7 +193,7 @@ watch(() => props.courseData, (newData) => {
<div class="flex-1 min-w-0 overflow-hidden">
<div class="text-xs font-bold truncate leading-snug block w-full"
:class="currentLessonId === lesson.id ? 'text-blue-700 dark:text-blue-300' : 'text-slate-600 dark:text-slate-300'"
:class="currentLessonId === lesson.id ? 'text-blue-700 dark:!text-blue-300' : 'text-slate-600 dark:!text-slate-300'"
>
{{ getLocalizedText(lesson.title) }}
</div>