From 7ead98375eb34fea43cb9240b2192aaf05805653 Mon Sep 17 00:00:00 2001 From: supalerk-ar66 Date: Wed, 11 Feb 2026 11:25:55 +0700 Subject: [PATCH] feat: Implement core e-learning features including course discovery, classroom components, user profile management, and internationalization for English and Thai. --- .../classroom/AnnouncementModal.vue | 16 ++++--- .../classroom/CurriculumSidebar.vue | 6 ++- .../components/course/CourseCard.vue | 6 ++- .../components/discovery/CategorySidebar.vue | 9 ++-- .../components/discovery/CourseDetailView.vue | 43 +++++++++++-------- .../components/layout/MobileNav.vue | 13 +++--- .../components/profile/ProfileEditForm.vue | 12 ++++-- Frontend-Learner/composables/useCourse.ts | 7 ++- Frontend-Learner/i18n/locales/en.json | 16 ++++++- Frontend-Learner/i18n/locales/th.json | 16 ++++++- .../pages/dashboard/my-courses.vue | 6 ++- 11 files changed, 107 insertions(+), 43 deletions(-) diff --git a/Frontend-Learner/components/classroom/AnnouncementModal.vue b/Frontend-Learner/components/classroom/AnnouncementModal.vue index 7d937ba3..aedb1177 100644 --- a/Frontend-Learner/components/classroom/AnnouncementModal.vue +++ b/Frontend-Learner/components/classroom/AnnouncementModal.vue @@ -13,11 +13,15 @@ const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void; }>(); +const { locale, t } = useI18n() + // Helper for localization const getLocalizedText = (text: any) => { if (!text) return '' if (typeof text === 'string') return text - return text.th || text.en || '' + + const currentLocale = locale.value as 'th' | 'en' + return text[currentLocale] || text.th || text.en || '' } @@ -30,9 +34,9 @@ const getLocalizedText = (text: any) => {
- {{ $t('classroom.announcements', 'ประกาศในคอร์สเรียน') }} + {{ $t('classroom.announcements') }} -
{{ announcements.length }} รายการ
+
{{ announcements.length }} {{ $t('common.items') }}
@@ -62,16 +66,16 @@ const getLocalizedText = (text: any) => {
- {{ getLocalizedText(ann.title) || 'ประกาศ' }} + {{ getLocalizedText(ann.title) || $t('sidebar.announcements') }}
- {{ new Date(ann.created_at || Date.now()).toLocaleDateString('th-TH', { day: 'numeric', month: 'short', year: 'numeric' }) }} + {{ new Date(ann.created_at || Date.now()).toLocaleDateString(locale === 'th' ? 'th-TH' : 'en-US', { day: 'numeric', month: 'short', year: 'numeric' }) }} - {{ new Date(ann.created_at || Date.now()).toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' }) }} + {{ new Date(ann.created_at || Date.now()).toLocaleTimeString(locale === 'th' ? 'th-TH' : 'en-US', { hour: '2-digit', minute: '2-digit' }) }}
diff --git a/Frontend-Learner/components/classroom/CurriculumSidebar.vue b/Frontend-Learner/components/classroom/CurriculumSidebar.vue index 360d3dda..ccd87df4 100644 --- a/Frontend-Learner/components/classroom/CurriculumSidebar.vue +++ b/Frontend-Learner/components/classroom/CurriculumSidebar.vue @@ -19,11 +19,15 @@ const emit = defineEmits<{ (e: 'open-announcements'): void; }>(); +const { locale } = useI18n() + // Helper for localization const getLocalizedText = (text: any) => { if (!text) return '' if (typeof text === 'string') return text - return text.th || text.en || '' + + const currentLocale = locale.value as 'th' | 'en' + return text[currentLocale] || text.th || text.en || '' } diff --git a/Frontend-Learner/components/course/CourseCard.vue b/Frontend-Learner/components/course/CourseCard.vue index c3b235e2..0c4b92a0 100644 --- a/Frontend-Learner/components/course/CourseCard.vue +++ b/Frontend-Learner/components/course/CourseCard.vue @@ -39,10 +39,14 @@ const emit = defineEmits<{ viewCertificate: [] }>() +const { locale } = useI18n() + const getLocalizedText = (text: string | { th: string; en: string } | undefined) => { if (!text) return '' if (typeof text === 'string') return text - return text.th || text.en || '' + + const currentLocale = locale.value as 'th' | 'en' + return text[currentLocale] || text.th || text.en || '' } const displayTitle = computed(() => getLocalizedText(props.title)) diff --git a/Frontend-Learner/components/discovery/CategorySidebar.vue b/Frontend-Learner/components/discovery/CategorySidebar.vue index e5b8c1d6..213bd8a2 100644 --- a/Frontend-Learner/components/discovery/CategorySidebar.vue +++ b/Frontend-Learner/components/discovery/CategorySidebar.vue @@ -13,12 +13,15 @@ const emit = defineEmits<{ (e: "update:modelValue", value: number[]): void; }>(); +const { locale, t } = useI18n(); const showAllCategories = ref(false); const getLocalizedText = (text: any) => { if (!text) return ""; if (typeof text === "string") return text; - return text.th || text.en || ""; + + const currentLocale = locale.value as 'th' | 'en'; + return text[currentLocale] || text.th || text.en || ""; }; @@ -26,7 +29,7 @@ const getLocalizedText = (text: any) => {
{ >
- {{ showAllCategories ? "แสดงน้อยลง" : "แสดงเพิ่มเติม" }} + {{ showAllCategories ? $t('discovery.showLess') : $t('discovery.showMore') }} (); +const { locale, t } = useI18n(); + const getLocalizedText = (text: any) => { if (!text) return '' if (typeof text === 'string') return text - return text.th || text.en || '' + + const currentLocale = locale.value as 'th' | 'en' + return text[currentLocale] || text.th || text.en || '' } const formatPrice = (price: number) => { - return new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB' }).format(price); + return new Intl.NumberFormat(locale.value === 'th' ? 'th-TH' : 'en-US', { + style: 'currency', + currency: 'THB' + }).format(price); } const enrollmentLoading = ref(false); @@ -43,7 +50,7 @@ const handleEnroll = () => { @@ -72,7 +79,7 @@ const handleEnroll = () => { />
- +
@@ -86,11 +93,11 @@ const handleEnroll = () => {
- {{ course.category?.name?.th || course.category?.name?.en || 'ทั่วไป' }} + {{ getLocalizedText(course.category?.name) }} - {{ course.duration_minutes || 60 }} นาที + {{ course.duration_minutes || 60 }} {{ $t('course.minutes') }} @@ -107,14 +114,14 @@ const handleEnroll = () => {

- เนื้อหาบทเรียน + {{ $t('course.courseContent') }}

- {{ idx + 1 }}. {{ getLocalizedText(chapter.title) }} - {{ chapter.lessons?.length || 0 }} บทเรียน + {{ Number(idx) + 1 }}. {{ getLocalizedText(chapter.title) }} + {{ chapter.lessons?.length || 0 }} {{ $t('course.lessonsUnit') }}
@@ -124,13 +131,13 @@ const handleEnroll = () => { size="18px" /> {{ getLocalizedText(lesson.title) }} - {{ lesson.duration_minutes }} นาที + {{ lesson.duration_minutes }} {{ $t('course.minutes') }}
- ยังไม่มีเนื้อหาในขณะนี้ + {{ $t('course.noContent') }}
@@ -145,10 +152,10 @@ const handleEnroll = () => {
- {{ course.price > 0 ? formatPrice(course.price) : 'เรียนฟรี' }} + {{ course.price > 0 ? formatPrice(course.price) : $t('course.free') }}
- {{ formatPrice(course.price * 2) }} + {{ formatPrice(Number(course.price) * 2) }}
{ size="lg" color="primary" class="w-full mb-4 shadow-lg shadow-blue-600/30 font-bold" - :label="user ? (course.enrolled ? 'เข้าสู่บทเรียน' : (course.price > 0 ? 'ซื้อคอร์สเรียนนี้' : 'ลงทะเบียนเรียนฟรี')) : 'เข้าสู่ระบบเพื่อลงทะเบียน'" + :label="user ? (course.enrolled ? $t('course.startLearning') : (course.price > 0 ? $t('course.buyNow') : $t('course.enrollFree'))) : $t('course.loginToEnroll')" :loading="enrollmentLoading" @click="handleEnroll" />
- รับประกันความพึงพอใจ คืนเงินภายใน 7 วัน + {{ $t('course.satisfactionGuarantee') }}

@@ -171,15 +178,15 @@ const handleEnroll = () => {
- เข้าเรียนได้ตลอดชีพ + {{ $t('course.lifetimeAccess') }}
- ทำแบบทดสอบไม่จำกัด + {{ $t('course.unlimitedQuizzes') }}
- ใบประกาศนียบัตรเมื่อเรียนจบ + {{ $t('course.certificate') }} ({{ $t('course.available') }})
diff --git a/Frontend-Learner/components/layout/MobileNav.vue b/Frontend-Learner/components/layout/MobileNav.vue index cfe857a1..c0ee0cab 100644 --- a/Frontend-Learner/components/layout/MobileNav.vue +++ b/Frontend-Learner/components/layout/MobileNav.vue @@ -1,9 +1,12 @@