feat: Add course detail page, authentication composable, and course browsing page.
This commit is contained in:
parent
1e06041769
commit
327f6ec7b5
3 changed files with 230 additions and 115 deletions
|
|
@ -17,54 +17,25 @@ useHead({
|
|||
|
||||
// Reactive state for the search input
|
||||
const searchQuery = ref('')
|
||||
const { fetchCourses } = useCourse()
|
||||
|
||||
/**
|
||||
* @const courses
|
||||
* @description Mock data for available courses. In a real app, this would be fetched from an API.
|
||||
* Each course object contains: id, title, level, levelType (for badge color), price, description, rating, lessons.
|
||||
*/
|
||||
const courses = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'เบื้องต้นการออกแบบ UX/UI',
|
||||
price: 'ฟรี',
|
||||
description: 'เรียนรู้พื้นฐานการวาดโครงร่างและการใช้งานเครื่องมือออกแบบยุคใหม่',
|
||||
rating: '4.8',
|
||||
lessons: '12'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'รูปแบบ React ขั้นสูง',
|
||||
price: 'ฟรี',
|
||||
description: 'เจาะลึก HOC, Hooks และการจัดการ State ขนาดใหญ่ในแอปพลิเคชัน',
|
||||
rating: '4.9',
|
||||
lessons: '24'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'การตลาดดิจิทัล 101',
|
||||
price: 'ฟรี',
|
||||
description: 'คู่มือสมบูรณ์สำหรับการทำ SEO/SEM และการวิเคราะห์ข้อมูลผู้ใช้',
|
||||
rating: '4.7',
|
||||
lessons: '18'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Full-stack Node.js Developer',
|
||||
price: 'ฟรี',
|
||||
description: 'สร้าง API ที่ยืดหยุ่นและมีประสิทธิภาพด้วย Node.js และ Express',
|
||||
rating: '4.9',
|
||||
lessons: '32'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Full-stack Node.js Developer',
|
||||
price: 'ฟรี',
|
||||
description: 'สร้าง API ที่ยืดหยุ่นและมีประสิทธิภาพด้วย Node.js และ Express',
|
||||
rating: '4.9',
|
||||
lessons: '32'
|
||||
// Helper to handle 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 || ''
|
||||
}
|
||||
|
||||
// Fetch courses from API
|
||||
const { data: coursesResponse, error } = await useAsyncData('courses-list', () => fetchCourses())
|
||||
|
||||
// Computed property for courses list from API response
|
||||
const courses = computed(() => {
|
||||
if (coursesResponse.value?.success) {
|
||||
return coursesResponse.value.data
|
||||
}
|
||||
]
|
||||
return []
|
||||
})
|
||||
|
||||
/**
|
||||
* @computed filteredCourses
|
||||
|
|
@ -72,12 +43,15 @@ const courses = [
|
|||
* Checks both the course title and description (case-insensitive).
|
||||
*/
|
||||
const filteredCourses = computed(() => {
|
||||
if (!searchQuery.value) return courses
|
||||
const list = courses.value || []
|
||||
if (!searchQuery.value) return list
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return courses.filter(c =>
|
||||
c.title.toLowerCase().includes(query) ||
|
||||
c.description.toLowerCase().includes(query)
|
||||
)
|
||||
return list.filter(c => {
|
||||
const title = getLocalizedText(c.title).toLowerCase()
|
||||
const desc = getLocalizedText(c.description).toLowerCase()
|
||||
return title.includes(query) || desc.includes(query)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -146,46 +120,58 @@ const filteredCourses = computed(() => {
|
|||
class="glass-card group flex flex-col h-full hover:-translate-y-2 transition-transform duration-500 slide-up"
|
||||
:style="{ animationDelay: `${0.1 * (index + 1)}s` }"
|
||||
>
|
||||
<!-- Card Image / Placeholder Area -->
|
||||
<!-- Card Image -->
|
||||
<div class="h-56 bg-gradient-to-br from-slate-800 to-slate-900 relative overflow-hidden group-hover:opacity-90 transition-opacity">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<img
|
||||
v-if="course.thumbnail_url"
|
||||
:src="course.thumbnail_url"
|
||||
:alt="getLocalizedText(course.title)"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
|
||||
/>
|
||||
<div
|
||||
v-else-if="!course.thumbnail_url || true"
|
||||
class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
>
|
||||
<div class="w-20 h-20 rounded-2xl bg-white/5 flex items-center justify-center text-5xl group-hover:scale-110 transition-transform duration-500 shadow-inner border border-white/5">
|
||||
📚
|
||||
</div>
|
||||
</div>
|
||||
<!-- Level Badge (Neutral/Warning/Success variants) -->
|
||||
|
||||
<!-- Level Badge (Neutral/Warning/Success variants) - Optional based on data -->
|
||||
<div v-if="course.levelType" class="absolute top-4 right-4">
|
||||
<!-- Add badge logic if needed -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Content Body -->
|
||||
<div class="p-8 flex-1 flex flex-col border-t border-white/5">
|
||||
<h3 class="text-2xl font-bold text-white mb-3 leading-tight group-hover:text-blue-400 transition-colors">
|
||||
{{ course.title }}
|
||||
{{ getLocalizedText(course.title) }}
|
||||
</h3>
|
||||
<p class="text-slate-400 text-sm mb-8 line-clamp-2 leading-relaxed flex-1">
|
||||
{{ course.description }}
|
||||
{{ getLocalizedText(course.description) }}
|
||||
</p>
|
||||
|
||||
<!-- Meta Information (Rating, Lessons) -->
|
||||
<div class="flex items-center gap-6 mb-8 text-sm font-medium text-slate-500">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-amber-400">⭐</span>
|
||||
<span class="text-slate-300">{{ course.rating }}</span>
|
||||
<span class="text-slate-300">{{ course.rating || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="w-1 h-1 rounded-full bg-slate-700" />
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="opacity-70">📖</span>
|
||||
<span class="text-slate-300">{{ course.lessons }} บทเรียน</span>
|
||||
<span class="text-slate-300">{{ course.lessons || 0 }} บทเรียน</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Footer (Price & Action) -->
|
||||
<div class="pt-6 border-t border-white/5 flex items-center justify-between mt-auto">
|
||||
<span class="text-2xl font-black text-blue-400 tracking-tight">
|
||||
{{ course.price }}
|
||||
{{ course.is_free ? 'ฟรี' : course.price }}
|
||||
</span>
|
||||
<NuxtLink
|
||||
to="/browse/opencovery"
|
||||
:to="`/course/${course.id}`"
|
||||
class="px-6 py-3 bg-white/10 hover:bg-blue-600 text-white rounded-xl text-sm font-bold transition-all border border-white/10 hover:border-blue-500/50"
|
||||
>
|
||||
ดูรายละเอียด
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue