feat: Add initial pages and components for user dashboard, profile, 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 47s
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 47s
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
f26a94076c
commit
e3873f616e
11 changed files with 1046 additions and 685 deletions
|
|
@ -21,15 +21,40 @@ const emit = defineEmits<{
|
|||
|
||||
const { locale } = useI18n()
|
||||
|
||||
// State for expansion items
|
||||
const chapterOpenState = ref<Record<string, boolean>>({})
|
||||
|
||||
// Helper for localization
|
||||
const getLocalizedText = (text: any) => {
|
||||
if (!text) return ''
|
||||
if (typeof text === 'string') return text
|
||||
|
||||
const currentLocale = locale.value as 'th' | 'en'
|
||||
// Safe locale access
|
||||
const currentLocale = (locale?.value || 'th') as 'th' | 'en'
|
||||
return text[currentLocale] || text.th || text.en || ''
|
||||
}
|
||||
|
||||
// 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)
|
||||
const chapterCompletionStatus = computed(() => {
|
||||
const status: Record<string, boolean> = {}
|
||||
if (!props.courseData || !props.courseData.chapters) return status
|
||||
|
||||
props.courseData.chapters.forEach((chapter: any) => {
|
||||
if (chapter.lessons && chapter.lessons.length > 0) {
|
||||
status[chapter.id] = chapter.lessons.every((l: any) => isLessonCompleted(l))
|
||||
} else {
|
||||
status[chapter.id] = false
|
||||
}
|
||||
})
|
||||
return status
|
||||
})
|
||||
|
||||
// Local Progress Calculation
|
||||
const progressPercentage = computed(() => {
|
||||
if (!props.courseData || !props.courseData.chapters) return 0
|
||||
|
|
@ -38,11 +63,34 @@ const progressPercentage = computed(() => {
|
|||
props.courseData.chapters.forEach((c: any) => {
|
||||
c.lessons.forEach((l: any) => {
|
||||
total++
|
||||
if (l.is_completed || l.progress?.is_completed) completed++
|
||||
if (isLessonCompleted(l)) completed++
|
||||
})
|
||||
})
|
||||
return total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
})
|
||||
|
||||
// Auto-expand chapter containing current lesson
|
||||
watch(() => props.currentLessonId, (newId) => {
|
||||
if (newId && props.courseData?.chapters) {
|
||||
props.courseData.chapters.forEach((chapter: any) => {
|
||||
const hasLesson = chapter.lessons.some((l: any) => l.id === newId)
|
||||
if (hasLesson) {
|
||||
chapterOpenState.value[chapter.id] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Initialize all chapters as open by default on load
|
||||
watch(() => props.courseData, (newData) => {
|
||||
if (newData?.chapters) {
|
||||
newData.chapters.forEach((chapter: any) => {
|
||||
if (chapterOpenState.value[chapter.id] === undefined) {
|
||||
chapterOpenState.value[chapter.id] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -51,71 +99,111 @@ const progressPercentage = computed(() => {
|
|||
@update:model-value="(val) => emit('update:modelValue', val)"
|
||||
show-if-above
|
||||
bordered
|
||||
side="left"
|
||||
:width="280"
|
||||
side="right"
|
||||
:width="300"
|
||||
: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">
|
||||
<!-- 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">
|
||||
|
||||
<!-- 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 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>
|
||||
</div>
|
||||
<div class="h-2 w-full bg-slate-200 dark:bg-slate-800 rounded-full overflow-hidden shadow-inner">
|
||||
<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 shadow-[0_0_12px_rgba(37,99,235,0.3)]"
|
||||
class="h-full bg-blue-600 dark:bg-blue-500 rounded-full transition-all duration-700 ease-out"
|
||||
:style="{ width: `${progressPercentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow scroll">
|
||||
<q-list padding class="py-2">
|
||||
<!-- 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">
|
||||
<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"
|
||||
>
|
||||
<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) -->
|
||||
<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'">
|
||||
<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>
|
||||
</div>
|
||||
<!-- 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">
|
||||
{{ chapter.lessons.length }} {{ $t('course.lessonsUnit') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Lessons List -->
|
||||
<div class="bg-slate-50 dark:bg-[#0f1219]/50 border-t border-slate-100 dark:border-slate-800 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="currentLessonId === lesson.id
|
||||
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/10'
|
||||
: 'border-transparent'"
|
||||
@click="!lesson.is_locked && emit('select-lesson', lesson.id)"
|
||||
>
|
||||
<!-- Lesson Status Icon -->
|
||||
<div class="mr-3 flex-shrink-0">
|
||||
<!-- Completed (Takes Precedence) -->
|
||||
<q-icon v-if="isLessonCompleted(lesson)"
|
||||
name="check_circle"
|
||||
class="text-green-500"
|
||||
size="20px"
|
||||
/>
|
||||
<!-- 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 -->
|
||||
<q-icon v-else-if="lesson.is_locked"
|
||||
name="lock"
|
||||
class="text-slate-400 opacity-70"
|
||||
size="18px"
|
||||
/>
|
||||
<!-- Not Started -->
|
||||
<div v-else class="w-[18px] h-[18px] rounded-full border-2 border-slate-300 dark:border-slate-600"></div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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'"
|
||||
>
|
||||
{{ getLocalizedText(lesson.title) }}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isLoading" class="p-6 text-center text-slate-500">
|
||||
|
|
@ -126,31 +214,18 @@ const progressPercentage = computed(() => {
|
|||
</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 {
|
||||
/* Custom scrollbar for better aesthetics */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.scroll::-webkit-scrollbar-track {
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dark .scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,19 @@ const formatPrice = (price: number) => {
|
|||
}
|
||||
|
||||
const enrollmentLoading = ref(false);
|
||||
const activeTab = ref('curriculum');
|
||||
|
||||
const totalLessons = computed(() => {
|
||||
if (!props.course?.chapters) return 0;
|
||||
return props.course.chapters.reduce((acc: number, chapter: any) => acc + (chapter.lessons?.length || 0), 0);
|
||||
});
|
||||
|
||||
const totalDuration = computed(() => {
|
||||
if (!props.course?.chapters) return 0;
|
||||
return props.course.chapters.reduce((acc: number, chapter: any) => {
|
||||
return acc + (chapter.lessons?.reduce((lAcc: number, lesson: any) => lAcc + (lesson.duration_minutes || 0), 0) || 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
const handleEnroll = () => {
|
||||
if(!props.course) return;
|
||||
|
|
@ -42,7 +55,6 @@ const handleEnroll = () => {
|
|||
// In this pattern, we just emit.
|
||||
setTimeout(() => enrollmentLoading.value = false, 2000); // Safety timeout
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -104,45 +116,69 @@ const handleEnroll = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Curriculum Preview -->
|
||||
<div class="bg-slate-50 dark:bg-slate-900 rounded-3xl p-6 md:p-8 border border-slate-200 dark:border-white/5">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<!-- Course Detail - Single Page Layout -->
|
||||
<div class="space-y-10">
|
||||
|
||||
<!-- Instructor Info -->
|
||||
<div class="flex flex-col sm:flex-row gap-6 items-start sm:items-center pb-8 border-b border-slate-200 dark:border-slate-800">
|
||||
<q-avatar size="64px">
|
||||
<img :src="course.instructor?.profile?.avatar_url || 'https://cdn.quasar.dev/img/boy-avatar.png'" />
|
||||
</q-avatar>
|
||||
<div>
|
||||
<h3 class="text-xl font-black text-slate-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<div class="text-sm text-slate-500 mb-1 font-bold uppercase tracking-wider">{{ $t('course.instructor') }}</div>
|
||||
<div class="font-bold text-xl text-slate-800 dark:text-white">
|
||||
{{ course.instructor?.profile?.first_name || 'Unknown' }} {{ course.instructor?.profile?.last_name || 'Instructor' }}
|
||||
</div>
|
||||
<div class="text-slate-500 text-sm mt-1">{{ course.instructor?.email || 'No contact info' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Curriculum / Lesson Details -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white">
|
||||
{{ $t('course.courseContent') }}
|
||||
</h3>
|
||||
<div class="text-sm font-bold text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-white/5 px-4 py-2 rounded-full">
|
||||
{{ totalLessons }} {{ $t('course.lessons') }} • {{ totalDuration }} {{ $t('quiz.minutes') }}
|
||||
</div>
|
||||
</div>
|
||||
<q-icon name="keyboard_command_key" class="text-slate-200 dark:text-slate-800" size="32px" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="(chapter, idx) in course.chapters" :key="chapter.id" class="group">
|
||||
<div class="px-6 py-4 bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-white/5 font-black text-slate-800 dark:text-white flex justify-between items-center mb-2 shadow-sm">
|
||||
<span class="flex items-center gap-3">
|
||||
<span class="w-7 h-7 flex items-center justify-center bg-slate-100 dark:bg-white/10 rounded-lg text-xs font-bold font-mono">{{ Number(idx) + 1 }}</span>
|
||||
{{ getLocalizedText(chapter.title) }}
|
||||
</span>
|
||||
<span class="text-[10px] uppercase font-black tracking-widest text-slate-400 opacity-60">{{ chapter.lessons?.length || 0 }} {{ $t('course.lessonsUnit') }}</span>
|
||||
</div>
|
||||
<div class="ml-4 pl-4 border-l-2 border-slate-100 dark:border-slate-800 space-y-1 mt-3">
|
||||
<div v-for="lesson in chapter.lessons" :key="lesson.id" class="px-5 py-3 flex items-center gap-3 text-sm text-slate-600 dark:text-slate-400 hover:bg-white dark:hover:bg-white/5 rounded-xl transition-all hover:translate-x-1">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center" :class="lesson.type === 'VIDEO' ? 'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400' : 'bg-orange-50 text-orange-600 dark:bg-orange-500/10 dark:text-orange-400'">
|
||||
<q-icon
|
||||
:name="lesson.type === 'VIDEO' ? 'play_arrow' : 'article'"
|
||||
size="16px"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex-1 font-bold">{{ getLocalizedText(lesson.title) }}</span>
|
||||
<span v-if="lesson.duration_minutes" class="text-slate-400 dark:text-slate-500 text-[10px] font-bold">{{ lesson.duration_minutes }} {{ $t('quiz.minutes') }}</span>
|
||||
<q-icon v-if="lesson.is_locked !== false" name="lock" size="14px" class="text-slate-300 dark:text-slate-600" />
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="(chapter, idx) in course.chapters" :key="chapter.id" class="group">
|
||||
<!-- Chapter Header -->
|
||||
<div class="px-6 py-4 bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-white/5 font-black text-slate-800 dark:text-white flex justify-between items-center mb-2 shadow-sm">
|
||||
<span class="flex items-center gap-3">
|
||||
<span class="w-7 h-7 flex items-center justify-center bg-slate-100 dark:bg-white/10 rounded-lg text-xs font-bold font-mono">{{ Number(idx) + 1 }}</span>
|
||||
{{ getLocalizedText(chapter.title) }}
|
||||
</span>
|
||||
<span class="text-[10px] uppercase font-black tracking-widest text-slate-400 opacity-60">{{ chapter.lessons?.length || 0 }} {{ $t('course.lessonsUnit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!course.chapters || course.chapters.length === 0" class="flex flex-col items-center justify-center py-12 text-slate-400 dark:text-slate-500 bg-white/50 dark:bg-slate-900/50 rounded-2xl border-2 border-dashed border-slate-200 dark:border-slate-800">
|
||||
<q-icon name="menu_book" size="40px" class="mb-2 opacity-50" />
|
||||
<p class="text-sm font-medium">{{ $t('course.noContent') }}</p>
|
||||
|
||||
<!-- Lessons List -->
|
||||
<div class="ml-4 pl-4 border-l-2 border-slate-100 dark:border-slate-800 space-y-1 mt-3">
|
||||
<div v-for="lesson in chapter.lessons" :key="lesson.id" class="px-5 py-3 flex items-center gap-3 text-sm text-slate-600 dark:text-slate-400 hover:bg-white dark:hover:bg-white/5 rounded-xl transition-all hover:translate-x-1">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center shrink-0" :class="lesson.type === 'VIDEO' ? 'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400' : 'bg-orange-50 text-orange-600 dark:bg-orange-500/10 dark:text-orange-400'">
|
||||
<q-icon
|
||||
:name="lesson.type === 'VIDEO' ? 'play_arrow' : 'article'"
|
||||
size="16px"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex-1 font-bold truncate">{{ getLocalizedText(lesson.title) }}</span>
|
||||
<span v-if="lesson.duration_minutes" class="text-slate-400 dark:text-slate-500 text-[10px] font-bold shrink-0">{{ lesson.duration_minutes }} {{ $t('quiz.minutes') }}</span>
|
||||
<q-icon v-if="lesson.is_locked !== false" name="lock" size="14px" class="text-slate-300 dark:text-slate-600 shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!course.chapters || course.chapters.length === 0" class="flex flex-col items-center justify-center py-12 text-slate-400 dark:text-slate-500 bg-white/50 dark:bg-slate-900/50 rounded-2xl border-2 border-dashed border-slate-200 dark:border-slate-800">
|
||||
<q-icon name="menu_book" size="40px" class="mb-2 opacity-50" />
|
||||
<p class="text-sm font-medium">{{ $t('course.noContent') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,39 +5,43 @@
|
|||
* Uses Quasar QToolbar.
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
/** Controls visibility of the search bar */
|
||||
showSearch?: boolean
|
||||
showSearch?: boolean;
|
||||
/** Controls visibility of the sidebar toggle button */
|
||||
showSidebarToggle?: boolean
|
||||
showSidebarToggle?: boolean;
|
||||
/** Type of navigation links to display */
|
||||
navType?: 'public' | 'learner'
|
||||
}>()
|
||||
navType?: "public" | "learner";
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** Emitted when the hamburger menu is clicked */
|
||||
toggleSidebar: []
|
||||
}>()
|
||||
toggleSidebar: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
// Automatically determine navType based on route if not explicitly passed
|
||||
const navTypeComputed = computed(() => {
|
||||
if (props.navType) return props.navType
|
||||
if (props.navType) return props.navType;
|
||||
// Show learner nav for dashboard, browse, classroom, and course details
|
||||
const learnerRoutes = ['/dashboard', '/browse', '/classroom', '/course']
|
||||
return learnerRoutes.some(r => route.path.startsWith(r)) ? 'learner' : 'public'
|
||||
})
|
||||
const learnerRoutes = ["/dashboard", "/browse", "/classroom", "/course"];
|
||||
return learnerRoutes.some((r) => route.path.startsWith(r))
|
||||
? "learner"
|
||||
: "public";
|
||||
});
|
||||
|
||||
const searchText = ref('')
|
||||
const searchText = ref("");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-toolbar class="bg-white text-slate-900 h-16 shadow-sm border-none p-0">
|
||||
<div class="container mx-auto w-full px-6 md:px-12 flex items-center h-full">
|
||||
<div
|
||||
class="container mx-auto w-full px-6 md:px-12 flex items-center h-full"
|
||||
>
|
||||
<!-- Mobile Menu Toggle -->
|
||||
<q-btn
|
||||
v-if="showSidebarToggle !== false && navTypeComputed !== 'learner'"
|
||||
|
|
@ -50,22 +54,35 @@ const searchText = ref('')
|
|||
/>
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="flex items-center gap-3 cursor-pointer group" @click="navigateTo('/dashboard')">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white font-black shadow-lg shadow-blue-600/30 group-hover:scale-110 transition-transform">
|
||||
<div
|
||||
class="flex items-center gap-3 cursor-pointer group"
|
||||
@click="navigateTo('/dashboard')"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white font-black shadow-lg shadow-blue-600/30 group-hover:scale-110 transition-transform"
|
||||
>
|
||||
E
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-black text-lg leading-none tracking-tight text-slate-900 group-hover:text-blue-600 transition-colors">E-Learning</span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-[0.2em] leading-none mt-1 text-slate-500">Platform</span>
|
||||
<span
|
||||
class="font-black text-lg leading-none tracking-tight text-slate-900 group-hover:text-blue-600 transition-colors"
|
||||
>E-Learning</span
|
||||
>
|
||||
<span
|
||||
class="text-[10px] font-bold uppercase tracking-[0.2em] leading-none mt-1 text-slate-500"
|
||||
>Platform</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center min-w-[200px] md:min-w-[320px] max-w-sm ml-8 mr-8 h-10">
|
||||
<div
|
||||
class="flex items-center min-w-[200px] md:min-w-[320px] max-w-sm ml-8 mr-8 h-10"
|
||||
>
|
||||
<q-input
|
||||
v-model="searchText"
|
||||
dense
|
||||
borderless
|
||||
placeholder="ค้นหาคอร์สเรียน..."
|
||||
:placeholder="$t('menu.searchCourses')"
|
||||
class="search-input w-full bg-slate-100/60 px-4 rounded-full transition-all duration-300 focus-within:bg-white focus-within:ring-1 focus-within:ring-blue-100 h-full flex items-center"
|
||||
@keyup.enter="navigateTo(`/browse/discovery?search=${searchText}`)"
|
||||
>
|
||||
|
|
@ -77,42 +94,66 @@ const searchText = ref('')
|
|||
|
||||
<!-- Desktop Navigation -->
|
||||
<nav class="flex items-center gap-8 text-[14px] font-bold text-slate-600">
|
||||
|
||||
<!-- Learner Navigation (Dashboard Mode) -->
|
||||
<template v-if="navTypeComputed === 'learner'">
|
||||
<NuxtLink to="/dashboard" class="hover:text-blue-600 transition-colors uppercase tracking-wider" active-class="text-blue-600">
|
||||
หน้าหลัก
|
||||
<NuxtLink
|
||||
to="/dashboard"
|
||||
class="hover:text-blue-600 transition-colors uppercase tracking-wider"
|
||||
active-class="text-blue-600"
|
||||
>
|
||||
{{ $t("sidebar.overview") }}
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/browse/discovery" class="hover:text-blue-600 transition-colors uppercase tracking-wider" active-class="text-blue-600">
|
||||
คอร์สเรียนทั้งหมด
|
||||
<NuxtLink
|
||||
to="/browse/discovery"
|
||||
class="hover:text-blue-600 transition-colors uppercase tracking-wider"
|
||||
active-class="text-blue-600"
|
||||
>
|
||||
{{ $t("landing.allCourses") }}
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/dashboard/my-courses" class="hover:text-blue-600 transition-colors uppercase tracking-wider" active-class="text-blue-600">
|
||||
{{ $t('sidebar.myCourses') || 'คอร์สเรียนของฉัน' }}
|
||||
<NuxtLink
|
||||
to="/dashboard/my-courses"
|
||||
class="hover:text-blue-600 transition-colors uppercase tracking-wider"
|
||||
active-class="text-blue-600"
|
||||
>
|
||||
{{ $t("sidebar.myCourses") || "คอร์สเรียนของฉัน" }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<!-- Public Navigation (Default) -->
|
||||
<template v-else>
|
||||
<div class="cursor-pointer hover:text-purple-600 flex items-center gap-1 transition-colors">
|
||||
<div
|
||||
class="cursor-pointer hover:text-purple-600 flex items-center gap-1 transition-colors"
|
||||
>
|
||||
คอร์สเรียนทั้งหมด <q-icon name="keyboard_arrow_down" />
|
||||
<q-menu>
|
||||
<q-list dense style="min-width: 150px">
|
||||
<q-item clickable v-close-popup to="/browse">
|
||||
<q-item-section>ทั้งหมด</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-list dense style="min-width: 150px">
|
||||
<q-item clickable v-close-popup to="/browse">
|
||||
<q-item-section>ทั้งหมด</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 flex items-center gap-1 transition-colors">
|
||||
<div
|
||||
class="cursor-pointer hover:text-purple-600 flex items-center gap-1 transition-colors"
|
||||
>
|
||||
หลักสูตร Onsite <q-icon name="keyboard_arrow_down" />
|
||||
</div>
|
||||
<NuxtLink to="/browse/recommended" class="hover:text-purple-600 transition-colors">
|
||||
<NuxtLink
|
||||
to="/browse/recommended"
|
||||
class="hover:text-purple-600 transition-colors"
|
||||
>
|
||||
คอร์สแนะนำ
|
||||
</NuxtLink>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">บทความ</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">สมาชิกรายปี</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">สำหรับองค์กร</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">
|
||||
บทความ
|
||||
</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">
|
||||
สมาชิกรายปี
|
||||
</div>
|
||||
<div class="cursor-pointer hover:text-purple-600 transition-colors">
|
||||
สำหรับองค์กร
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
|
|
@ -122,10 +163,9 @@ const searchText = ref('')
|
|||
<div class="flex items-center gap-2 sm:gap-4 text-gray-500">
|
||||
<!-- Search Icon -->
|
||||
|
||||
|
||||
<!-- Language -->
|
||||
<!-- Language -->
|
||||
<LanguageSwitcher />
|
||||
|
||||
|
||||
<!-- User Profile -->
|
||||
<UserMenu />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue