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 searchQuery = ref("");
|
||||||
const isCategoryOpen = ref(true);
|
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
|
// Courses Data
|
||||||
const { fetchCourses, fetchCourseById } = useCourse();
|
const { fetchCourses, fetchCourseById } = useCourse();
|
||||||
const courses = ref<any[]>([]);
|
const courses = ref<any[]>([]);
|
||||||
|
|
@ -54,42 +77,10 @@ const selectCourse = async (id: number) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadCategories();
|
||||||
loadCourses();
|
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
|
// Filter Logic based on search query
|
||||||
const filteredCourses = computed(() => {
|
const filteredCourses = computed(() => {
|
||||||
if (!searchQuery.value) return courses.value;
|
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-show="isCategoryOpen" class="flex flex-col gap-1">
|
||||||
<div
|
<div
|
||||||
v-for="cat in visibleCategories"
|
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"
|
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
|
<label
|
||||||
|
|
@ -181,7 +172,7 @@ const filteredCourses = computed(() => {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="w-4 h-4 rounded border-slate-300 text-primary focus:ring-primary"
|
class="w-4 h-4 rounded border-slate-300 text-primary focus:ring-primary"
|
||||||
/>
|
/>
|
||||||
{{ cat }}
|
{{ getLocalizedText(cat.name) }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue