feat: Implement course discovery page with English and Thai internationalization.

This commit is contained in:
supalerk-ar66 2026-01-26 16:24:40 +07:00
parent 6d7e78fb33
commit 78a26fc2e1
3 changed files with 64 additions and 118 deletions

View file

@ -23,6 +23,14 @@
"introduction": "Introduction", "introduction": "Introduction",
"lessons": "Lessons", "lessons": "Lessons",
"lessonsUnit": "Lessons", "lessonsUnit": "Lessons",
"chapter": "Chapter",
"price": "Price",
"coursePrice": "Course Price",
"instructor": "Instructor",
"level": "Level",
"allLevels": "All Levels",
"haveCertificate": "Certificate Available",
"noCertificate": "No Certificate",
"enrollNow": "Enroll Now", "enrollNow": "Enroll Now",
"free": "Free", "free": "Free",
"certificate": "Certificate", "certificate": "Certificate",

View file

@ -23,6 +23,14 @@
"introduction": "บทนำ", "introduction": "บทนำ",
"lessons": "บทเรียน", "lessons": "บทเรียน",
"lessonsUnit": "บทเรียน", "lessonsUnit": "บทเรียน",
"chapter": "บทที่",
"price": "ราคา",
"coursePrice": "ราคาคอร์ส",
"instructor": "ผู้สอน",
"level": "ระดับ",
"allLevels": "ทุกระดับ",
"haveCertificate": "มีใบประกาศฯ",
"noCertificate": "ไม่มีใบประกาศฯ",
"enrollNow": "ลงทะเบียนเรียนทันที", "enrollNow": "ลงทะเบียนเรียนทันที",
"free": "ฟรี", "free": "ฟรี",
"certificate": "ใบประกาศ", "certificate": "ใบประกาศ",

View file

@ -16,87 +16,64 @@ useHead({
}); });
// ========================================== // ==========================================
// 1. State ( UI) // 1. State Management
// ==========================================
// showDetail: (true = , false = )
const showDetail = ref(false); const showDetail = ref(false);
// searchQuery:
const searchQuery = ref(""); const searchQuery = ref("");
const sortOption = ref('เรียงตาม: ล่าสุด') const selectedCategoryIds = ref<number[]>([]);
const sortOptions = ['เรียงตาม: ล่าสุด']
// isCategoryOpen: /
const isCategoryOpen = ref(true);
// ==========================================
// 2. (Helpers)
// ==========================================
// getLocalizedText:
// object {th, en} th , en
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
if (!text) return ''
if (typeof text === 'string') return text
return text.th || text.en || ''
}
// ==========================================
// 3. (Categories)
// ==========================================
// useCategory composable API
const { fetchCategories } = useCategory();
const categories = ref<any[]>([]); const categories = ref<any[]>([]);
const showAllCategories = ref(false); // (Show More/Less) const courses = ref<any[]>([]);
const selectedCourse = ref<any>(null);
// const isLoading = ref(false);
const isLoadingDetail = ref(false);
const isEnrolling = ref(false);
const showAllCategories = ref(false);
const { t } = useI18n();
const { fetchCategories } = useCategory();
const { fetchCourses, fetchCourseById, enrollCourse } = useCourse();
const { currentUser } = useAuth();
// 2. Computed Properties
const sortOption = ref(computed(() => t('discovery.sortRecent')));
const sortOptions = computed(() => [t('discovery.sortRecent')]);
const filteredCourses = computed(() => {
let result = courses.value;
if (selectedCategoryIds.value.length > 0) {
result = result.filter(c => selectedCategoryIds.value.includes(c.category_id));
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
result = result.filter(c => {
const title = getLocalizedText(c.title).toLowerCase();
const desc = getLocalizedText(c.description).toLowerCase();
return title.includes(query) || (desc && desc.includes(query));
});
}
return result;
});
// 3. Helper Functions
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
if (!text) return '';
if (typeof text === 'string') return text;
return text.th || text.en || '';
};
// 4. API Actions
const loadCategories = async () => { const loadCategories = async () => {
const res = await fetchCategories(); const res = await fetchCategories();
if (res.success) { if (res.success) categories.value = res.data || [];
categories.value = res.data || [];
}
}; };
// ( Show More , 8 )
const visibleCategories = computed(() => {
return showAllCategories.value ? categories.value : categories.value.slice(0, 8);
});
// Options QSelect Dropdown
const categoryOptions = computed(() => {
return categories.value.map(c => ({
id: c.id,
label: getLocalizedText(c.name),
value: c.id // q-select uses value by default if not constrained
}))
});
// Toggle Category Chip Remove
const toggleCategory = (id: number) => {
if (selectedCategoryIds.value.includes(id)) {
selectedCategoryIds.value = selectedCategoryIds.value.filter(cid => cid !== id);
} else {
selectedCategoryIds.value.push(id);
}
};
// ==========================================
// 4. (Courses)
// ==========================================
// useCourse composable (, )
const { fetchCourses, fetchCourseById, enrollCourse } = useCourse();
const courses = ref<any[]>([]);
const isLoading = ref(false); //
const selectedCourse = ref<any>(null); //
const isLoadingDetail = ref(false); //
const isEnrolling = ref(false); //
//
const loadCourses = async () => { const loadCourses = async () => {
isLoading.value = true; isLoading.value = true;
const res = await fetchCourses(); const res = await fetchCourses();
if (res.success) { if (res.success) {
//
courses.value = (res.data || []).map((c: any) => ({ courses.value = (res.data || []).map((c: any) => ({
...c, ...c,
rating: "0.0", rating: "0.0",
lessons: "0", lessons: "0",
levelType: c.levelType || "neutral" levelType: c.levelType || "neutral"
})); }));
@ -104,78 +81,31 @@ const loadCourses = async () => {
isLoading.value = false; isLoading.value = false;
}; };
//
const selectCourse = async (id: number) => { const selectCourse = async (id: number) => {
isLoadingDetail.value = true; isLoadingDetail.value = true;
selectedCourse.value = null; selectedCourse.value = null;
showDetail.value = true; // Detail View showDetail.value = true;
// API ID
const res = await fetchCourseById(id); const res = await fetchCourseById(id);
if (res.success) { if (res.success) selectedCourse.value = res.data;
selectedCourse.value = res.data;
}
isLoadingDetail.value = false; isLoadingDetail.value = false;
}; };
// (Enroll)
const handleEnroll = async (id: number) => { const handleEnroll = async (id: number) => {
if (isEnrolling.value) return; // if (isEnrolling.value) return;
isEnrolling.value = true; isEnrolling.value = true;
const res = await enrollCourse(id); const res = await enrollCourse(id);
if (res.success) { if (res.success) {
// "" parameter enrolled=true popup
return navigateTo('/dashboard/my-courses?enrolled=true'); return navigateTo('/dashboard/my-courses?enrolled=true');
} else { } else {
alert(res.error || 'Failed to enroll'); alert(res.error || 'Failed to enroll');
} }
isEnrolling.value = false; isEnrolling.value = false;
}; };
onMounted(() => { onMounted(() => {
//
loadCategories(); loadCategories();
loadCourses(); loadCourses();
}); });
// Filter Logic based on search query
// ==========================================
// 5. (Filter & Search)
// ==========================================
// selectedCategoryIds: ID
const selectedCategoryIds = ref<number[]>([]);
// (Filter Logic)
const filteredCourses = computed(() => {
let result = courses.value;
// 1. (Category Filter)
if (selectedCategoryIds.value.length > 0) {
result = result.filter(c => selectedCategoryIds.value.includes(c.category_id));
}
// 2. (Search Query) -
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
result = result.filter(
(c) => {
const title = getLocalizedText(c.title).toLowerCase();
const desc = getLocalizedText(c.description).toLowerCase();
return title.includes(query) || (desc && desc.includes(query));
}
);
}
return result;
});
// ==========================================
// 6. User & Helpers
// ==========================================
const { currentUser } = useAuth();
</script> </script>
<template> <template>
@ -200,7 +130,7 @@ const { currentUser } = useAuth();
dense dense
outlined outlined
rounded rounded
placeholder="ค้นหาคอร์ส..." :placeholder="$t('discovery.searchPlaceholder')"
class="bg-white dark:bg-slate-800 w-full md:w-64" class="bg-white dark:bg-slate-800 w-full md:w-64"
bg-color="white" bg-color="white"
color="slate-900" color="slate-900"