feat: Implement course discovery page with browsing, filtering, and detailed course views, supported by new course card and category sidebar components and a course composable.
All checks were successful
Build and Deploy Frontend Learner / Build Frontend Learner Docker Image (push) Successful in 38s
Build and Deploy Frontend Learner / Deploy E-learning Frontend Learner to Dev Server (push) Successful in 2s
Build and Deploy Frontend Learner / Notify Deployment Status (push) Successful in 1s

This commit is contained in:
supalerk-ar66 2026-02-10 14:02:45 +07:00
parent 0691ca40cd
commit 4955bc9e5c
4 changed files with 13 additions and 8 deletions

View file

@ -39,10 +39,11 @@ const emit = defineEmits<{
viewCertificate: [] viewCertificate: []
}>() }>()
const { locale } = useI18n()
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => { const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
if (!text) return '' if (!text) return ''
if (typeof text === 'string') return text if (typeof text === 'string') return text
return text.th || text.en || '' return text[locale.value] || text.th || text.en || ''
} }
const displayTitle = computed(() => getLocalizedText(props.title)) const displayTitle = computed(() => getLocalizedText(props.title))

View file

@ -13,12 +13,13 @@ const emit = defineEmits<{
(e: "update:modelValue", value: number[]): void; (e: "update:modelValue", value: number[]): void;
}>(); }>();
const { locale, t } = useI18n();
const showAllCategories = ref(false); const showAllCategories = ref(false);
const getLocalizedText = (text: any) => { const getLocalizedText = (text: any) => {
if (!text) return ""; if (!text) return "";
if (typeof text === "string") return text; if (typeof text === "string") return text;
return text.th || text.en || ""; return text[locale.value] || text.th || text.en || "";
}; };
</script> </script>
@ -26,7 +27,7 @@ const getLocalizedText = (text: any) => {
<div class="category-sidebar-root border rounded-2xl overflow-hidden shadow-sm"> <div class="category-sidebar-root border rounded-2xl overflow-hidden shadow-sm">
<q-expansion-item <q-expansion-item
expand-separator expand-separator
:label="`หมวดหมู่ (${modelValue.length})`" :label="`${$t('discovery.categoryTitle')} (${modelValue.length})`"
class="category-sidebar-expansion" class="category-sidebar-expansion"
header-class="category-sidebar-header" header-class="category-sidebar-header"
default-opened default-opened
@ -67,7 +68,7 @@ const getLocalizedText = (text: any) => {
> >
<q-item-section> <q-item-section>
<div class="flex items-center gap-1 text-blue-600 dark:text-blue-400"> <div class="flex items-center gap-1 text-blue-600 dark:text-blue-400">
{{ showAllCategories ? "แสดงน้อยลง" : "แสดงเพิ่มเติม" }} {{ showAllCategories ? $t('discovery.showLess') : $t('discovery.showMore') }}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4" class="h-4 w-4"

View file

@ -647,11 +647,12 @@ export const useCourse = () => {
/** /**
* Helper: แปลงข้อมูล 2 locale * Helper: แปลงข้อมูล 2 locale
*/ */
const { locale } = useI18n()
const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => { const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => {
if (!text) return '' if (!text) return ''
if (typeof text === 'string') return text if (typeof text === 'string') return text
// @ts-ignore // @ts-ignore
return text.th || text.en || '' return text[locale.value] || text.th || text.en || ''
} }
return { return {

View file

@ -11,8 +11,10 @@ definePageMeta({
middleware: "auth", middleware: "auth",
}); });
const { t, locale } = useI18n();
useHead({ useHead({
title: "รายการคอร์ส - e-Learning", title: `${t('sidebar.browseCourses')} - E-Learning Platform`,
}); });
// ========================================== // ==========================================
@ -34,14 +36,14 @@ const totalPages = ref(1);
const itemsPerPage = 12; const itemsPerPage = 12;
const { t } = useI18n();
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const { fetchCategories } = useCategory(); const { fetchCategories } = useCategory();
const { fetchCourses, fetchCourseById, enrollCourse, getLocalizedText } = useCourse(); const { fetchCourses, fetchCourseById, enrollCourse, getLocalizedText } = useCourse();
// 2. Computed Properties // 2. Computed Properties
const sortOption = ref(computed(() => t('discovery.sortRecent'))); const sortOption = computed(() => t('discovery.sortRecent'));
const sortOptions = computed(() => [t('discovery.sortRecent')]); const sortOptions = computed(() => [t('discovery.sortRecent')]);
const filteredCourses = computed(() => { const filteredCourses = computed(() => {