feat: Add course discovery page with search, category filters, and detailed course view, along with a new useCategory composable.

This commit is contained in:
supalerk-ar66 2026-01-20 14:47:46 +07:00
parent 2a461a1e4f
commit e6a73c836c
2 changed files with 90 additions and 35 deletions

View file

@ -0,0 +1,64 @@
export interface Category {
id: number
name: {
th: string
en: string
[key: string]: string
}
slug: string
description: {
th: string
en: string
[key: string]: string
}
icon: string
sort_order: number
is_active: boolean
created_at: string
updated_at: string
}
export interface CategoryData {
total: number
categories: Category[]
}
export interface CategoryResponse {
code: number
message: string
data: CategoryData
}
export const useCategory = () => {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBase as string
const { token } = useAuth()
const fetchCategories = async () => {
try {
const response = await $fetch<CategoryResponse>(`${API_BASE_URL}/categories`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: response.data?.categories || [],
total: response.data?.total || 0
}
} catch (err: any) {
console.error('Fetch categories failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error fetching categories'
}
}
}
return {
fetchCategories
}
}

View file

@ -20,6 +20,29 @@ const showDetail = ref(false);
const searchQuery = ref("");
const isCategoryOpen = ref(true);
// Helper to get localized text
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
if (!text) return ''
if (typeof text === 'string') return text
return text.th || text.en || ''
}
// Categories Data
const { fetchCategories } = useCategory();
const categories = ref<any[]>([]);
const showAllCategories = ref(false);
const loadCategories = async () => {
const res = await fetchCategories();
if (res.success) {
categories.value = res.data || [];
}
};
const visibleCategories = computed(() => {
return showAllCategories.value ? categories.value : categories.value.slice(0, 8);
});
// Courses Data
const { fetchCourses, fetchCourseById } = useCourse();
const courses = ref<any[]>([]);
@ -54,42 +77,10 @@ const selectCourse = async (id: number) => {
};
onMounted(() => {
loadCategories();
loadCourses();
});
// Categories Data
const categories = [
"การตลาดออนไลน์",
"ธุรกิจ",
"การเงิน & ลงทุน",
"การพัฒนาตนเอง",
"Office Productivity",
"Data",
"เขียนโปรแกรม",
"การพัฒนาซอฟต์แวร์",
"การออกแบบ",
"Art & Craft",
"การเขียน",
"ถ่ายภาพ & วิดีโอ",
"ภาษา",
"Lifestyles",
];
// Category Visibility State
const showAllCategories = ref(false);
const visibleCategories = computed(() => {
return showAllCategories.value ? categories : categories.slice(0, 8);
});
// Helper to get localized text
const getLocalizedText = (text: string | { th: string; en: string } | undefined) => {
if (!text) return ''
if (typeof text === 'string') return text
return text.th || text.en || ''
}
// Filter Logic based on search query
const filteredCourses = computed(() => {
if (!searchQuery.value) return courses.value;
@ -171,7 +162,7 @@ const filteredCourses = computed(() => {
<div v-show="isCategoryOpen" class="flex flex-col gap-1">
<div
v-for="cat in visibleCategories"
:key="cat"
:key="cat.id"
class="flex items-center justify-between py-3 border-b border-slate-100 dark:border-slate-700/50 group cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50 -mx-4 px-4 transition-colors"
>
<label
@ -181,7 +172,7 @@ const filteredCourses = computed(() => {
type="checkbox"
class="w-4 h-4 rounded border-slate-300 text-primary focus:ring-primary"
/>
{{ cat }}
{{ getLocalizedText(cat.name) }}
</label>
</div>
<button