feat: add user dashboard, profile management, and password reset pages.
This commit is contained in:
parent
ec67c7c6b6
commit
0ec99b140a
3 changed files with 59 additions and 39 deletions
|
|
@ -78,40 +78,42 @@ onMounted(async () => {
|
|||
|
||||
<!-- Main Content Area -->
|
||||
<div class="lg:col-span-12">
|
||||
<!-- Section: Recommended Courses -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-black flex items-center gap-3 tracking-tight text-slate-900 dark:text-white">
|
||||
<span class="w-1.5 h-8 bg-emerald-500 rounded-full shadow-[0_0_15px_rgba(16,185,129,0.5)]"/>
|
||||
{{ $t('menu.recommendedCourses') }}
|
||||
</h2>
|
||||
<!-- Recommended Courses Container -->
|
||||
<div class="bg-white dark:bg-[#1e293b] rounded-[2rem] p-8 md:p-10 border border-slate-200 dark:border-white/5 shadow-sm">
|
||||
<!-- Section Header -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-black flex items-center gap-3 tracking-tight text-slate-900 dark:text-white">
|
||||
<span class="w-1.5 h-8 bg-emerald-500 rounded-full shadow-[0_0_15px_rgba(16,185,129,0.5)]"/>
|
||||
{{ $t('menu.recommendedCourses') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Recommended Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<NuxtLink v-for="(course, idx) in recommendedCourses" :key="course.id" :to="`/course/${course.id}`" class="p-0 overflow-hidden group border border-slate-200 dark:border-white/5 rounded-3xl shadow-sm dark:shadow-xl transition-all hover:-translate-y-1 dark:hover:-translate-y-1 block bg-slate-50 dark:bg-[#0f172a]">
|
||||
<div class="h-48 overflow-hidden relative rounded-t-3xl">
|
||||
<img v-if="course.image" :src="course.image" :alt="course.title" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" >
|
||||
<div class="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-slate-900/20 dark:from-[#1e293b] to-transparent"/>
|
||||
</div>
|
||||
<div class="p-7">
|
||||
<div class="text-[10px] font-black text-slate-700 dark:text-slate-500 uppercase tracking-[0.2em] mb-2">{{ course.category }}</div>
|
||||
<h4 class="font-black text-xl mb-6 text-slate-900 dark:text-white group-hover:text-blue-700 dark:group-hover:text-blue-400 transition-colors line-clamp-2">{{ course.title }}</h4>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-slate-700 dark:text-slate-400 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-600 dark:text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{{ course.duration }}
|
||||
</span>
|
||||
<span class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-md">{{ $t('menu.viewDetails') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Loading State for Recommended -->
|
||||
<div v-if="recommendedCourses.length === 0" class="flex justify-center py-10 opacity-50">
|
||||
<div class="animate-pulse">Loading recommendations...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommended Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<NuxtLink v-for="(course, idx) in recommendedCourses" :key="course.id" :to="`/course/${course.id}`" class="p-0 overflow-hidden group border border-slate-200 dark:border-white/5 rounded-3xl shadow-sm dark:shadow-xl transition-all hover:-translate-y-1 dark:hover:-translate-y-1 block" style="background-color: var(--bg-surface);">
|
||||
<div class="h-48 overflow-hidden relative rounded-t-3xl">
|
||||
<img v-if="course.image" :src="course.image" :alt="course.title" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" >
|
||||
<div class="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-slate-900/20 dark:from-[#1e293b] to-transparent"/>
|
||||
</div>
|
||||
<div class="p-7" style="background-color: var(--bg-surface);">
|
||||
<div class="text-[10px] font-black text-slate-700 dark:text-slate-500 uppercase tracking-[0.2em] mb-2">{{ course.category }}</div>
|
||||
<h4 class="font-black text-xl mb-6 text-slate-900 dark:text-white group-hover:text-blue-700 dark:group-hover:text-blue-400 transition-colors line-clamp-2">{{ course.title }}</h4>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-slate-700 dark:text-slate-400 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-600 dark:text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{{ course.duration }}
|
||||
</span>
|
||||
<span class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-md">{{ $t('menu.viewDetails') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Loading State for Recommended -->
|
||||
<div v-if="recommendedCourses.length === 0" class="flex justify-center py-10 opacity-50">
|
||||
<div class="animate-pulse">Loading recommendations...</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ const phoneRules = [
|
|||
]
|
||||
const passwordRules = [
|
||||
(val: string) => !!val || t('common.required'),
|
||||
(val: string) => val.length >= 6 || t('common.passwordTooShort')
|
||||
(val: string) => val.length >= 6 || t('common.passwordTooShort'),
|
||||
(val: string) => val !== passwordForm.currentPassword || 'รหัสผ่านใหม่ต้องไม่ซ้ำกับรหัสผ่านปัจจุบัน'
|
||||
]
|
||||
const confirmPasswordRules = [
|
||||
(val: string) => !!val || t('common.required'),
|
||||
|
|
@ -442,7 +443,7 @@ onMounted(() => {
|
|||
:type="showCurrentPassword ? 'text' : 'password'"
|
||||
outlined dense rounded
|
||||
class="premium-q-input"
|
||||
placeholder="••••••••"
|
||||
placeholder=""
|
||||
:rules="[val => !!val || $t('common.required')]"
|
||||
>
|
||||
<template v-slot:append>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ const resetForm = reactive({
|
|||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const showPassword = ref(false)
|
||||
const showConfirmPassword = ref(false)
|
||||
|
||||
const resetRules = {
|
||||
password: {
|
||||
rules: {
|
||||
|
|
@ -126,11 +129,18 @@ const resetPassword = async () => {
|
|||
<input
|
||||
:value="resetForm.password"
|
||||
@input="(e) => handlePasswordInput('password', (e.target as HTMLInputElement).value)"
|
||||
type="password"
|
||||
class="w-full h-12 pl-12 pr-4 rounded-xl bg-slate-50 border border-slate-200 text-slate-900 placeholder-slate-400 text-base focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all font-medium"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
class="w-full h-12 pl-12 pr-12 rounded-xl bg-slate-50 border border-slate-200 text-slate-900 placeholder-slate-400 text-base focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all font-medium"
|
||||
placeholder="อย่างน้อย 8 ตัวอักษร"
|
||||
:class="{'border-red-500 focus:ring-red-500/20 focus:border-red-500': errors.password}"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors focus:outline-none flex items-center"
|
||||
>
|
||||
<span class="material-icons text-lg">{{ showPassword ? 'visibility_off' : 'visibility' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<span v-if="errors.password" class="text-xs text-red-500 font-medium ml-1 mt-1 block slide-up-sm">{{ errors.password }}</span>
|
||||
</div>
|
||||
|
|
@ -145,11 +155,18 @@ const resetPassword = async () => {
|
|||
<input
|
||||
v-model="resetForm.confirmPassword"
|
||||
@input="clearFieldError('confirmPassword')"
|
||||
type="password"
|
||||
class="w-full h-12 pl-12 pr-4 rounded-xl bg-slate-50 border border-slate-200 text-slate-900 placeholder-slate-400 text-base focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all font-medium"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
class="w-full h-12 pl-12 pr-12 rounded-xl bg-slate-50 border border-slate-200 text-slate-900 placeholder-slate-400 text-base focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all font-medium"
|
||||
placeholder="กรอกรหัสผ่านอีกครั้ง"
|
||||
:class="{'border-red-500 focus:ring-red-500/20 focus:border-red-500': errors.confirmPassword}"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="showConfirmPassword = !showConfirmPassword"
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors focus:outline-none flex items-center"
|
||||
>
|
||||
<span class="material-icons text-lg">{{ showConfirmPassword ? 'visibility_off' : 'visibility' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<span v-if="errors.confirmPassword" class="text-xs text-red-500 font-medium ml-1 mt-1 block slide-up-sm">{{ errors.confirmPassword }}</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue