feat: Introduce useCourse and useAuth composables, and add new pages for user registration and dynamic course details.
This commit is contained in:
parent
ffd2d55e33
commit
7b22699b13
4 changed files with 7 additions and 46 deletions
|
|
@ -1,9 +1,4 @@
|
||||||
// Shared global state for current user
|
|
||||||
import type { H3Event } from 'h3'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Types based on API responses
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
|
|
@ -64,9 +59,8 @@ export const useAuth = () => {
|
||||||
secure: false
|
secure: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// ... (previous code)
|
|
||||||
|
|
||||||
// Login
|
|
||||||
const login = async (credentials: { email: string; password: string }) => {
|
const login = async (credentials: { email: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch<loginResponse>(`${API_BASE_URL}/auth/login`, {
|
const data = await $fetch<loginResponse>(`${API_BASE_URL}/auth/login`, {
|
||||||
|
|
@ -98,7 +92,6 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register
|
|
||||||
const register = async (payload: RegisterPayload) => {
|
const register = async (payload: RegisterPayload) => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch(`${API_BASE_URL}/auth/register-learner`, {
|
const data = await $fetch(`${API_BASE_URL}/auth/register-learner`, {
|
||||||
|
|
@ -117,7 +110,6 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch User Profile (/api/user/me)
|
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
if (!token.value) return
|
if (!token.value) return
|
||||||
|
|
||||||
|
|
@ -160,7 +152,6 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update User Profile
|
|
||||||
const updateUserProfile = async (payload: {
|
const updateUserProfile = async (payload: {
|
||||||
first_name: string
|
first_name: string
|
||||||
last_name: string
|
last_name: string
|
||||||
|
|
@ -188,7 +179,6 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request Password Reset
|
|
||||||
const requestPasswordReset = async (email: string) => {
|
const requestPasswordReset = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
await $fetch(`${API_BASE_URL}/auth/reset-request`, {
|
await $fetch(`${API_BASE_URL}/auth/reset-request`, {
|
||||||
|
|
@ -202,7 +192,6 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm Reset Password
|
|
||||||
const confirmResetPassword = async (payload: { token: string; password: string }) => {
|
const confirmResetPassword = async (payload: { token: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
await $fetch(`${API_BASE_URL}/auth/reset-password`, {
|
await $fetch(`${API_BASE_URL}/auth/reset-password`, {
|
||||||
|
|
@ -216,7 +205,6 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change Password
|
|
||||||
const changePassword = async (payload: { oldPassword: string, newPassword: string }) => {
|
const changePassword = async (payload: { oldPassword: string, newPassword: string }) => {
|
||||||
if (!token.value) return { success: false, error: 'ไม่พบ Token การใช้งาน' }
|
if (!token.value) return { success: false, error: 'ไม่พบ Token การใช้งาน' }
|
||||||
|
|
||||||
|
|
@ -235,7 +223,6 @@ export const useAuth = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh Access Token
|
|
||||||
const refreshAccessToken = async () => {
|
const refreshAccessToken = async () => {
|
||||||
if (!refreshToken.value) return false
|
if (!refreshToken.value) return false
|
||||||
|
|
||||||
|
|
@ -258,7 +245,6 @@ export const useAuth = () => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
token.value = null
|
token.value = null
|
||||||
refreshToken.value = null // Clear refresh token
|
refreshToken.value = null // Clear refresh token
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import type { H3Event } from 'h3'
|
|
||||||
|
|
||||||
// Types based on API responses
|
|
||||||
export interface Course {
|
export interface Course {
|
||||||
id: number
|
id: number
|
||||||
title: string | { th: string; en: string }
|
title: string | { th: string; en: string }
|
||||||
|
|
@ -10,7 +7,7 @@ export interface Course {
|
||||||
price: string
|
price: string
|
||||||
is_free: boolean
|
is_free: boolean
|
||||||
have_certificate: boolean
|
have_certificate: boolean
|
||||||
status: string // 'DRAFT' | 'PUBLISHED' | ...
|
status: string
|
||||||
category_id: number
|
category_id: number
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
|
|
@ -20,10 +17,10 @@ export interface Course {
|
||||||
approved_by?: number
|
approved_by?: number
|
||||||
rejection_reason?: string
|
rejection_reason?: string
|
||||||
|
|
||||||
// Helper properties for UI (may be computed or mapped)
|
|
||||||
rating?: string
|
rating?: string
|
||||||
lessons?: number | string
|
lessons?: number | string
|
||||||
levelType?: 'neutral' | 'warning' | 'success' // For UI badging
|
levelType?: 'neutral' | 'warning' | 'success'
|
||||||
|
|
||||||
chapters?: {
|
chapters?: {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -45,7 +42,7 @@ interface CourseResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnrolledCourse {
|
export interface EnrolledCourse {
|
||||||
id: number // enrollment_id
|
id: number
|
||||||
course_id: number
|
course_id: number
|
||||||
course: Course
|
course: Course
|
||||||
status: 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED'
|
status: 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED'
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const { errors, validate, clearFieldError } = useFormValidation();
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// Reactive form state
|
|
||||||
const registerForm = reactive({
|
const registerForm = reactive({
|
||||||
prefix: "นาย",
|
prefix: "นาย",
|
||||||
username: "",
|
username: "",
|
||||||
|
|
@ -32,7 +32,7 @@ const registerForm = reactive({
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validation rules
|
|
||||||
const registerRules = {
|
const registerRules = {
|
||||||
username: {
|
username: {
|
||||||
rules: {
|
rules: {
|
||||||
|
|
@ -197,11 +197,7 @@ const handleRegister = async () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs Removed -->
|
|
||||||
|
|
||||||
<!-- REGISTER FORM -->
|
|
||||||
<form @submit.prevent="handleRegister">
|
<form @submit.prevent="handleRegister">
|
||||||
<!-- Username -->
|
|
||||||
<FormInput
|
<FormInput
|
||||||
:model-value="registerForm.username"
|
:model-value="registerForm.username"
|
||||||
label="ชื่อผู้ใช้"
|
label="ชื่อผู้ใช้"
|
||||||
|
|
@ -211,7 +207,6 @@ const handleRegister = async () => {
|
||||||
class="dark-form-input"
|
class="dark-form-input"
|
||||||
@update:model-value="onUsernameInput"
|
@update:model-value="onUsernameInput"
|
||||||
/>
|
/>
|
||||||
<!-- Email -->
|
|
||||||
<FormInput
|
<FormInput
|
||||||
:model-value="registerForm.email"
|
:model-value="registerForm.email"
|
||||||
label="อีเมล"
|
label="อีเมล"
|
||||||
|
|
@ -223,7 +218,6 @@ const handleRegister = async () => {
|
||||||
@update:model-value="onEmailInput"
|
@update:model-value="onEmailInput"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Password Fields -->
|
|
||||||
<FormInput
|
<FormInput
|
||||||
:model-value="registerForm.password"
|
:model-value="registerForm.password"
|
||||||
label="รหัสผ่าน"
|
label="รหัสผ่าน"
|
||||||
|
|
@ -246,7 +240,6 @@ const handleRegister = async () => {
|
||||||
@update:model-value="clearFieldError('confirmPassword')"
|
@update:model-value="clearFieldError('confirmPassword')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Name Fields (Split Row) -->
|
|
||||||
<div class="grid-12" style="gap: 16px; margin-bottom: 0">
|
<div class="grid-12" style="gap: 16px; margin-bottom: 0">
|
||||||
<div class="col-span-4">
|
<div class="col-span-4">
|
||||||
<label class="input-label text-gray-300">คำนำหน้า</label>
|
<label class="input-label text-gray-300">คำนำหน้า</label>
|
||||||
|
|
@ -294,7 +287,6 @@ const handleRegister = async () => {
|
||||||
@update:model-value="onPhoneInput"
|
@update:model-value="onPhoneInput"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary w-full mb-4 mt-2"
|
class="btn btn-primary w-full mb-4 mt-2"
|
||||||
|
|
@ -305,7 +297,6 @@ const handleRegister = async () => {
|
||||||
<span v-else>สร้างบัญชี</span>
|
<span v-else>สร้างบัญชี</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Toggle to Login -->
|
|
||||||
<div class="text-center mt-6 text-sm">
|
<div class="text-center mt-6 text-sm">
|
||||||
<span class="text-muted">มีบัญชีอยู่แล้ว? </span>
|
<span class="text-muted">มีบัญชีอยู่แล้ว? </span>
|
||||||
<NuxtLink to="/auth/login" class="font-bold text-primary hover:underline">เข้าสู่ระบบ</NuxtLink>
|
<NuxtLink to="/auth/login" class="font-bold text-primary hover:underline">เข้าสู่ระบบ</NuxtLink>
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,12 @@ const route = useRoute()
|
||||||
const courseId = computed(() => parseInt(route.params.id as string))
|
const courseId = computed(() => parseInt(route.params.id as string))
|
||||||
const { fetchCourseById, enrollCourse } = useCourse()
|
const { fetchCourseById, enrollCourse } = useCourse()
|
||||||
|
|
||||||
// Fetch course data
|
|
||||||
const { data: courseData, error, refresh } = await useAsyncData(`course-${courseId.value}`, () => fetchCourseById(courseId.value))
|
const { data: courseData, error, refresh } = await useAsyncData(`course-${courseId.value}`, () => fetchCourseById(courseId.value))
|
||||||
|
|
||||||
const course = computed(() => {
|
const course = computed(() => {
|
||||||
return courseData.value?.success ? courseData.value.data : null
|
return courseData.value?.success ? courseData.value.data : null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Enroll State
|
|
||||||
const isEnrolling = ref(false)
|
const isEnrolling = ref(false)
|
||||||
|
|
||||||
const handleEnroll = async () => {
|
const handleEnroll = async () => {
|
||||||
|
|
@ -43,7 +41,6 @@ const handleEnroll = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Helper for localization
|
|
||||||
const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => {
|
const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
if (typeof text === 'string') return text
|
if (typeof text === 'string') return text
|
||||||
|
|
@ -60,7 +57,6 @@ useHead({
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto max-w-7xl px-4 py-8">
|
<div class="container mx-auto max-w-7xl px-4 py-8">
|
||||||
|
|
||||||
<!-- Back Button -->
|
|
||||||
<NuxtLink to="/browse/discovery" class="inline-flex items-center gap-2 text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white mb-6 transition-colors font-medium">
|
<NuxtLink to="/browse/discovery" class="inline-flex items-center gap-2 text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white mb-6 transition-colors font-medium">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
|
@ -70,9 +66,7 @@ useHead({
|
||||||
|
|
||||||
<div v-if="course" class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
<div v-if="course" class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||||
|
|
||||||
<!-- Main Content (Left Column) -->
|
|
||||||
<div class="lg:col-span-8">
|
<div class="lg:col-span-8">
|
||||||
<!-- Hero Video Placeholder / Thumbnail -->
|
|
||||||
<div class="relative aspect-video bg-slate-900 rounded-2xl overflow-hidden shadow-lg mb-8 group cursor-pointer border border-slate-200 dark:border-slate-700">
|
<div class="relative aspect-video bg-slate-900 rounded-2xl overflow-hidden shadow-lg mb-8 group cursor-pointer border border-slate-200 dark:border-slate-700">
|
||||||
<div v-if="course.thumbnail_url" class="absolute inset-0">
|
<div v-if="course.thumbnail_url" class="absolute inset-0">
|
||||||
<img :src="course.thumbnail_url" class="w-full h-full object-cover opacity-90 group-hover:opacity-75 transition-opacity" />
|
<img :src="course.thumbnail_url" class="w-full h-full object-cover opacity-90 group-hover:opacity-75 transition-opacity" />
|
||||||
|
|
@ -93,7 +87,6 @@ useHead({
|
||||||
<p class="text-lg leading-relaxed">{{ getLocalizedText(course.description) }}</p>
|
<p class="text-lg leading-relaxed">{{ getLocalizedText(course.description) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Course Syllabus (Dynamic) -->
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-3xl p-6 md:p-8 shadow-sm border border-slate-100 dark:border-slate-700/50">
|
<div class="bg-white dark:bg-slate-800 rounded-3xl p-6 md:p-8 shadow-sm border border-slate-100 dark:border-slate-700/50">
|
||||||
<h3 class="font-bold text-xl text-slate-900 dark:text-white mb-6 flex items-center gap-2">
|
<h3 class="font-bold text-xl text-slate-900 dark:text-white mb-6 flex items-center gap-2">
|
||||||
<span class="w-1 h-6 bg-blue-500 rounded-full"></span>
|
<span class="w-1 h-6 bg-blue-500 rounded-full"></span>
|
||||||
|
|
@ -129,7 +122,6 @@ useHead({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar (Right Column): Sticky CTA -->
|
|
||||||
<div class="lg:col-span-4">
|
<div class="lg:col-span-4">
|
||||||
<div class="sticky top-24 bg-white dark:bg-slate-800 rounded-3xl p-6 md:p-8 shadow-lg border border-slate-100 dark:border-slate-700/50">
|
<div class="sticky top-24 bg-white dark:bg-slate-800 rounded-3xl p-6 md:p-8 shadow-lg border border-slate-100 dark:border-slate-700/50">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
|
|
@ -139,7 +131,6 @@ useHead({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Enroll Button -->
|
|
||||||
<button
|
<button
|
||||||
@click="handleEnroll"
|
@click="handleEnroll"
|
||||||
:disabled="isEnrolling"
|
:disabled="isEnrolling"
|
||||||
|
|
@ -164,10 +155,6 @@ useHead({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share / Wishlist placeholder -->
|
|
||||||
<!-- <div class="mt-6 flex gap-3">
|
|
||||||
<button class="flex-1 py-2 rounded-lg border border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-400 font-bold hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors">แชร์</button>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue