feat: Add course discovery page with search, category filters, and detailed course view, along with a new useCategory composable.
This commit is contained in:
parent
2a461a1e4f
commit
e6a73c836c
2 changed files with 90 additions and 35 deletions
64
Frontend-Learner/composables/useCategory.ts
Normal file
64
Frontend-Learner/composables/useCategory.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue