2026-01-13 10:46:40 +07:00
< script setup lang = "ts" >
/ * *
* @ file reset - password . vue
2026-02-27 10:05:33 +07:00
* @ description หน ้ าต ั ้ งรห ั สผ ่ านใหม ่ ( Reset Password Page .
* อน ุ ญาตให ้ ผ ู ้ ใช ้ ต ั ้ งรห ั สผ ่ านใหม ่ หล ั งจากย ื นย ั นล ิ งก ์ อ ี เมล )
2026-01-13 10:46:40 +07:00
* /
definePageMeta ( {
layout : 'auth'
} )
useHead ( {
title : 'ตั้งรหัสผ่านใหม่ - e-Learning'
} )
2026-01-14 15:15:31 +07:00
const route = useRoute ( )
2026-01-13 10:46:40 +07:00
const router = useRouter ( )
2026-01-14 15:15:31 +07:00
const { confirmResetPassword } = useAuth ( )
2026-01-13 10:46:40 +07:00
const { errors , validate , clearFieldError } = useFormValidation ( )
const isLoading = ref ( false )
const resetForm = reactive ( {
password : '' ,
confirmPassword : ''
} )
2026-02-02 16:33:51 +07:00
const showPassword = ref ( false )
const showConfirmPassword = ref ( false )
2026-01-13 10:46:40 +07:00
const resetRules = {
2026-01-15 11:06:44 +07:00
password : {
rules : {
required : true ,
minLength : 8 ,
custom : ( val : string ) => / [ \u0E00 - \u0E7F ] / . test ( val ) ? 'ห้ามใส่ภาษาไทย' : null
} ,
label : 'รหัสผ่านใหม่'
} ,
2026-01-13 10:46:40 +07:00
confirmPassword : { rules : { required : true , match : 'password' } , label : 'ยืนยันรหัสผ่าน' }
}
2026-01-15 11:06:44 +07:00
const handlePasswordInput = ( field : keyof typeof resetForm , val : string ) => {
resetForm [ field ] = val
if ( /[\u0E00-\u0E7F]/ . test ( val ) ) {
if ( field === 'password' ) errors . value . password = 'ห้ามใส่ภาษาไทย'
2026-02-27 10:05:33 +07:00
// ไม่จำเป็นต้องแจ้งเตือน confirmPassword แยกต่างหากถ้ามันแค่ต้องตรงกัน แต่เพื่อให้สอดคล้องกันเราก็ควรทำ (We don't necessarily need to flag confirmPassword individually if it just needs to match, but let's be consistent if we want)
2026-01-15 11:06:44 +07:00
} else {
2026-02-27 10:05:33 +07:00
// ลบข้อผิดพลาดถ้าเป็น "ห้ามใส่ภาษาไทย" (Clear error if it was "Thai characters")
2026-01-15 11:06:44 +07:00
if ( field === 'password' && errors . value . password === 'ห้ามใส่ภาษาไทย' ) {
clearFieldError ( 'password' )
}
}
}
2026-01-14 15:15:31 +07:00
onMounted ( ( ) => {
2026-01-15 11:06:44 +07:00
if ( ! route . query . token ) {
2026-01-14 15:15:31 +07:00
alert ( 'ลิงก์รีเซ็ตรหัสผ่านไม่ถูกต้องหรือหมดอายุ' )
router . push ( '/auth/login' )
}
} )
2026-01-13 10:46:40 +07:00
const resetPassword = async ( ) => {
if ( ! validate ( resetForm , resetRules ) ) return
2026-02-27 10:05:33 +07:00
// ดึงโทเคนจาก URL query (Extract token from query)
2026-01-14 15:15:31 +07:00
const token = route . query . token as string
2026-01-15 11:06:44 +07:00
if ( ! token ) {
2026-01-14 15:15:31 +07:00
alert ( 'ข้อมูลสำหรับรีเซ็ตไม่ครบถ้วน' )
return
}
2026-01-13 10:46:40 +07:00
isLoading . value = true
2026-01-14 15:15:31 +07:00
const result = await confirmResetPassword ( {
token ,
password : resetForm . password
} )
2026-01-13 10:46:40 +07:00
isLoading . value = false
2026-01-14 15:15:31 +07:00
if ( result . success ) {
alert ( 'รีเซ็ตรหัสผ่านสำเร็จ! กรุณาเข้าสู่ระบบด้วยรหัสผ่านใหม่' )
router . push ( '/auth/login' )
} else {
alert ( result . error || 'เกิดข้อผิดพลาดในการรีเซ็ตรหัสผ่าน' )
}
2026-01-13 10:46:40 +07:00
}
< / script >
< template >
2026-02-02 14:37:26 +07:00
< div class = "relative min-h-screen w-full flex items-center justify-center p-4 overflow-hidden bg-slate-50 transition-colors" >
<!-- === === === === === === === === === === === === === ===
2026-02-27 10:05:33 +07:00
เอฟเฟกต ์ พ ื ้ นหล ั ง ( แสดงเฉพาะโหมดสว ่ าง ) ( BACKGROUND EFFECTS ( Light Mode Only ) )
2026-02-02 14:37:26 +07:00
=== === === === === === === === === === === === === === -- >
< div class = "fixed inset-0 overflow-hidden pointer-events-none -z-10" >
< div class = "absolute inset-0 bg-gradient-to-br from-white via-slate-50 to-blue-50/50" > < / div >
< div class = "absolute top-[-10%] right-[-5%] w-[500px] h-[500px] rounded-full bg-blue-100/50 blur-[100px] animate-pulse-slow" / >
< div class = "absolute bottom-[-10%] left-[-5%] w-[500px] h-[500px] rounded-full bg-indigo-100/50 blur-[100px] animate-pulse-slow" style = "animation-delay: 3s;" / >
2026-01-13 10:46:40 +07:00
< / div >
2026-02-02 14:37:26 +07:00
<!-- === === === === === === === === === === === === === ===
2026-02-27 10:05:33 +07:00
การ ์ ดต ั ้ งรห ั สผ ่ านใหม ่ ( RESET PASSWORD CARD )
2026-02-02 14:37:26 +07:00
=== === === === === === === === === === === === === === -- >
< div class = "w-full max-w-[460px] relative z-10 slide-up" >
2026-01-13 10:46:40 +07:00
2026-02-27 10:05:33 +07:00
<!-- ห ั วข ้ อ / โลโก ้ ( Header / Logo ) -- >
2026-02-02 14:37:26 +07:00
< div class = "text-center mb-8" >
< div class = "inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gradient-to-tr from-blue-600 to-indigo-600 text-white shadow-lg shadow-blue-600/20 mb-6" >
< span class = "font-black text-2xl" > E < / span >
< / div >
< h1 class = "text-3xl font-black text-slate-900 mb-2" > ต ั ้ งรห ั สผ ่ านใหม ่ < / h1 >
< p class = "text-slate-600 text-base" > กร ุ ณากรอกรห ั สผ ่ านใหม ่ ท ี ่ ค ุ ณต ้ องการ < / p >
< / div >
< div class = "bg-white rounded-[2rem] p-8 md:p-10 shadow-xl shadow-slate-200/50 border border-slate-100 relative overflow-hidden" >
2026-02-27 10:05:33 +07:00
<!-- ฟอร ์ ม ( Form ) -- >
2026-02-02 14:37:26 +07:00
< form @submit.prevent ="resetPassword" class = "flex flex-col gap-6" >
2026-02-27 10:05:33 +07:00
<!-- รห ั สผ ่ านใหม ่ ( New Password ) -- >
2026-02-02 14:37:26 +07:00
< div >
< label class = "block text-sm font-semibold text-slate-700 mb-2 ml-1" > รห ั สผ ่ านใหม ่ < span class = "text-red-500" > * < / span > < / label >
< div class = "relative group" >
< div class = "absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 transition-colors group-focus-within:text-blue-500 pointer-events-none" >
< span class = "material-icons text-xl" > lock < / span >
< / div >
< input
: value = "resetForm.password"
@ input = "(e) => handlePasswordInput('password', (e.target as HTMLInputElement).value)"
2026-02-02 16:33:51 +07:00
: 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"
2026-02-02 14:37:26 +07:00
placeholder = "อย่างน้อย 8 ตัวอักษร"
: class = "{'border-red-500 focus:ring-red-500/20 focus:border-red-500': errors.password}"
/ >
2026-02-02 16:33:51 +07:00
< 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 >
2026-02-02 14:37:26 +07:00
< / 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 >
2026-02-27 10:05:33 +07:00
<!-- ย ื นย ั นรห ั สผ ่ านใหม ่ ( Confirm Password ) -- >
2026-02-02 14:37:26 +07:00
< div >
< label class = "block text-sm font-semibold text-slate-700 mb-2 ml-1" > ย ื นย ั นรห ั สผ ่ านใหม ่ < span class = "text-red-500" > * < / span > < / label >
< div class = "relative group" >
< div class = "absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 transition-colors group-focus-within:text-blue-500 pointer-events-none" >
< span class = "material-icons text-xl" > lock _clock < / span >
< / div >
< input
v - model = "resetForm.confirmPassword"
@ input = "clearFieldError('confirmPassword')"
2026-02-02 16:33:51 +07:00
: 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"
2026-02-02 14:37:26 +07:00
placeholder = "กรอกรหัสผ่านอีกครั้ง"
: class = "{'border-red-500 focus:ring-red-500/20 focus:border-red-500': errors.confirmPassword}"
/ >
2026-02-02 16:33:51 +07:00
< 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 >
2026-02-02 14:37:26 +07:00
< / 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 >
2026-02-27 10:05:33 +07:00
<!-- ป ุ ่ มย ื นย ั น ( Submit Button ) -- >
2026-02-02 14:37:26 +07:00
< button
type = "submit"
: disabled = "isLoading"
class = "w-full py-3.5 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-lg font-bold shadow-lg shadow-blue-600/30 transform active:scale-[0.98] transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed mt-2"
>
< span v-if = "!isLoading" > บันทึกรหัสผ่านใหม่ < / span >
< div v -else class = "w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" > < / div >
< / button >
< / form >
2026-01-13 10:46:40 +07:00
< / div >
2026-02-02 14:37:26 +07:00
2026-02-27 10:05:33 +07:00
<!-- ล ิ งก ์ ย ้ อนกล ั บ ( Back Link ) -- >
2026-02-02 14:37:26 +07:00
< div class = "mt-8 text-center text-slate-500" >
< NuxtLink to = "/auth/login" class = "inline-flex items-center gap-2 text-sm font-medium hover:text-slate-800 transition-colors group px-4 py-2 rounded-lg hover:bg-white/50" >
< span class = "group-hover:-translate-x-1 transition-transform" > ← < / span > กล ั บไปหน ้ าเข ้ าส ู ่ ระบบ
< / NuxtLink >
< / div >
< / div >
2026-01-13 10:46:40 +07:00
< / div >
< / template >
2026-02-02 14:37:26 +07:00
< style scoped >
/* Animations */
@ keyframes pulse - slow {
0 % , 100 % { opacity : 0.3 ; transform : scale ( 1 ) ; }
50 % { opacity : 0.5 ; transform : scale ( 1.15 ) ; }
}
. animate - pulse - slow {
animation : pulse - slow 8 s ease - in - out infinite ;
}
@ keyframes slide - up {
from { opacity : 0 ; transform : translateY ( 20 px ) ; }
to { opacity : 1 ; transform : translateY ( 0 ) ; }
}
. slide - up {
animation : slide - up 0.6 s cubic - bezier ( 0.16 , 1 , 0.3 , 1 ) forwards ;
}
. slide - up - sm {
animation : slide - up 0.3 s cubic - bezier ( 0.16 , 1 , 0.3 , 1 ) forwards ;
}
< / style >