2026-01-13 10:46:40 +07:00
< script setup lang = "ts" >
definePageMeta ( {
layout : 'default' ,
middleware : 'auth'
} )
2026-02-26 15:20:44 +07:00
const { locale , t } = useI18n ( )
2026-02-03 11:01:33 +07:00
const { currentUser , updateUserProfile , changePassword , uploadAvatar , sendVerifyEmail , fetchUserProfile } = useAuth ( )
2026-02-10 16:53:56 +07:00
const { getLocalizedText } = useCourse ( )
2026-02-26 15:20:44 +07:00
import { useQuasar } from 'quasar'
const $q = useQuasar ( )
useHead ( {
title : ` ${ t ( 'userMenu.settings' ) } - e-Learning `
} )
2026-02-02 15:34:40 +07:00
2026-01-30 14:42:08 +07:00
2026-01-26 10:40:50 +07:00
2026-01-13 10:46:40 +07:00
const isEditing = ref ( false )
2026-02-19 17:37:28 +07:00
const activeTab = ref < 'general' | 'security' > ( 'general' )
2026-01-26 10:40:50 +07:00
const isProfileSaving = ref ( false )
const isPasswordSaving = ref ( false )
2026-02-02 11:11:25 +07:00
const isSendingVerify = ref ( false )
2026-01-26 10:40:50 +07:00
const isHydrated = ref ( false )
2026-01-13 10:46:40 +07:00
2026-02-10 16:53:56 +07:00
2026-02-09 11:40:41 +07:00
2026-01-16 10:03:04 +07:00
const formatDate = ( dateString ? : string ) => {
if ( ! dateString ) return '-'
try {
const date = new Date ( dateString )
2026-02-09 16:22:37 +07:00
return new Intl . DateTimeFormat ( locale . value === 'th' ? 'th-TH' : 'en-US' , {
2026-01-16 10:03:04 +07:00
day : 'numeric' ,
month : 'short' ,
year : 'numeric'
} ) . format ( date )
} catch ( e ) {
return dateString
}
}
2026-01-13 10:46:40 +07:00
const userData = ref ( {
2026-01-14 15:15:31 +07:00
firstName : currentUser . value ? . firstName || '' ,
lastName : currentUser . value ? . lastName || '' ,
email : currentUser . value ? . email || '' ,
2026-01-16 10:03:04 +07:00
phone : currentUser . value ? . phone || '' ,
2026-02-09 16:22:37 +07:00
createdAt : currentUser . value ? . createdAt || '' ,
2026-01-16 10:03:04 +07:00
photoURL : currentUser . value ? . photoURL || '' ,
2026-02-03 11:01:33 +07:00
prefix : currentUser . value ? . prefix ? . th || '' ,
emailVerifiedAt : currentUser . value ? . emailVerifiedAt
2026-01-13 10:46:40 +07:00
} )
2026-02-03 11:01:33 +07:00
// ...
2026-01-14 10:20:06 +07:00
const passwordForm = reactive ( {
currentPassword : '' ,
newPassword : '' ,
confirmPassword : ''
} )
2026-02-26 15:20:44 +07:00
const showPasswordModal = ref ( false )
const showPassword = reactive ( {
current : false ,
new : false ,
confirm : false
} )
2026-02-02 11:11:25 +07:00
2026-02-27 10:05:33 +07:00
// กฎต่างๆ ถูกย้ายไปที่คอมโพเนนต์แล้ว (Rules have been moved to components)
2026-02-02 17:13:58 +07:00
2026-02-27 10:05:33 +07:00
const fileInput = ref < HTMLInputElement | null > ( null ) // ใช้ในโหมดมุมมอง (อยู่นอกคอมโพเนนต์) (Used in view mode (outside component))
2026-01-13 10:46:40 +07:00
const toggleEdit = ( edit : boolean ) => {
isEditing . value = edit
}
2026-02-27 10:05:33 +07:00
// อัปเดตให้รับออบเจ็กต์ File ได้โดยตรง (หรือ Event สำหรับความเข้ากันได้ของโหมดมุมมองหากจำเป็น) (Updated to accept File object directly (or Event for view mode compatibility if needed))
2026-02-02 17:13:58 +07:00
const handleFileUpload = async ( fileOrEvent : File | Event ) => {
let file : File | null = null
2026-01-13 10:46:40 +07:00
2026-02-02 17:13:58 +07:00
if ( fileOrEvent instanceof File ) {
file = fileOrEvent
} else {
2026-02-27 10:05:33 +07:00
// การทำงานสำรองสำหรับอีเวนต์ input change แบบเนทีฟ (Fallback for native input change event)
2026-02-02 17:13:58 +07:00
const target = ( fileOrEvent as Event ) . target as HTMLInputElement
if ( target . files && target . files [ 0 ] ) {
file = target . files [ 0 ]
}
}
2026-01-28 14:44:41 +07:00
2026-02-02 17:13:58 +07:00
if ( file ) {
2026-01-28 14:44:41 +07:00
// แสดงรูป Preview ก่อนอัปโหลด
2026-01-13 10:46:40 +07:00
const reader = new FileReader ( )
reader . onload = ( e ) => {
userData . value . photoURL = e . target ? . result as string
}
2026-01-28 14:44:41 +07:00
reader . readAsDataURL ( file )
// เรียกฟังก์ชัน uploadAvatar จาก useAuth
isProfileSaving . value = true
const result = await uploadAvatar ( file )
isProfileSaving . value = false
if ( result . success && result . data ? . avatar _url ) {
userData . value . photoURL = result . data . avatar _url
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'positive' , message : 'อัปเดตรูปโปรไฟล์สำเร็จ' , position : 'top' } )
2026-01-28 14:44:41 +07:00
} else {
console . error ( 'Upload failed:' , result . error )
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'negative' , message : result . error || t ( 'profile.updateError' ) || 'อัปเดตรูปโปรไฟล์ไม่สำเร็จ' , position : 'top' } )
2026-01-28 14:44:41 +07:00
}
2026-01-13 10:46:40 +07:00
}
}
2026-02-27 10:05:33 +07:00
// เรียกการอัปโหลดเมื่อคลิกที่รูปโปรไฟล์ในโหมด View (Trigger upload for VIEW mode avatar click)
2026-02-02 17:13:58 +07:00
const triggerUpload = ( ) => {
fileInput . value ? . click ( )
}
2026-01-30 14:42:08 +07:00
2026-01-28 14:44:41 +07:00
2026-01-26 10:40:50 +07:00
const handleUpdateProfile = async ( ) => {
isProfileSaving . value = true
2026-01-16 10:03:04 +07:00
const prefixMap : Record < string , string > = {
'นาย' : 'Mr.' ,
'นาง' : 'Mrs.' ,
'นางสาว' : 'Ms.'
}
2026-01-26 10:40:50 +07:00
const payload = {
first _name : userData . value . firstName ,
last _name : userData . value . lastName ,
phone : userData . value . phone ,
prefix : {
th : userData . value . prefix ,
en : prefixMap [ userData . value . prefix ] || ''
}
}
const result = await updateUserProfile ( payload )
if ( result ? . success ) {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'positive' , message : t ( 'profile.updateSuccess' ) , position : 'top' } )
2026-01-26 10:40:50 +07:00
} else {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'negative' , message : result ? . error || t ( 'profile.updateError' ) , position : 'top' } )
2026-01-26 10:40:50 +07:00
}
isProfileSaving . value = false
}
2026-02-02 11:11:25 +07:00
const handleSendVerifyEmail = async ( ) => {
isSendingVerify . value = true
const result = await sendVerifyEmail ( )
isSendingVerify . value = false
if ( result . success ) {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'positive' , message : result . message || t ( 'profile.verifyEmailSuccess' ) || 'ส่งอีเมลยืนยันสำเร็จ' , position : 'top' } )
2026-02-02 11:11:25 +07:00
} else {
if ( result . code === 400 ) {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'warning' , message : t ( 'profile.emailAlreadyVerified' ) || 'อีเมลของคุณได้รับการยืนยันแล้ว' , position : 'top' } )
2026-02-02 11:11:25 +07:00
} else {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'negative' , message : result . error || t ( 'profile.verifyEmailError' ) || 'ส่งอีเมลไม่สำเร็จ' , position : 'top' } )
2026-02-02 11:11:25 +07:00
}
}
}
2026-01-26 10:40:50 +07:00
const handleUpdatePassword = async ( ) => {
if ( passwordForm . newPassword !== passwordForm . confirmPassword ) {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'negative' , message : 'รหัสผ่านใหม่ไม่ตรงกัน' , position : 'top' } )
2026-01-26 10:40:50 +07:00
return
}
isPasswordSaving . value = true
const result = await changePassword ( {
oldPassword : passwordForm . currentPassword ,
newPassword : passwordForm . newPassword
} )
if ( result . success ) {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'positive' , message : t ( 'profile.passwordSuccess' ) || 'เปลี่ยนรหัสผ่านสำเร็จ' , position : 'top' } )
2026-01-26 10:40:50 +07:00
passwordForm . currentPassword = ''
passwordForm . newPassword = ''
passwordForm . confirmPassword = ''
2026-02-26 15:20:44 +07:00
showPasswordModal . value = false
2026-01-26 10:40:50 +07:00
} else {
2026-02-26 15:20:44 +07:00
$q . notify ( { type : 'negative' , message : result . error || t ( 'profile.passwordError' ) || 'เปลี่ยนรหัสผ่านไม่สำเร็จ' , position : 'top' } )
2026-01-16 10:03:04 +07:00
}
2026-01-26 10:40:50 +07:00
isPasswordSaving . value = false
2026-01-13 10:46:40 +07:00
}
2026-01-26 10:40:50 +07:00
2026-02-27 10:05:33 +07:00
// เฝ้าดูการเปลี่ยนแปลงในสถานะผู้ใช้ส่วนกลาง (เช่น หลังจากอัปโหลดรูปโปรไฟล์หรืออัปเดตข้อมูลส่วนตัว) (Watch for changes in global user state (e.g. after avatar upload or profile update))
2026-01-28 14:44:41 +07:00
watch ( ( ) => currentUser . value , ( newUser ) => {
if ( newUser ) {
userData . value . photoURL = newUser . photoURL || ''
userData . value . firstName = newUser . firstName
userData . value . lastName = newUser . lastName
userData . value . prefix = newUser . prefix ? . th || ''
userData . value . email = newUser . email
userData . value . phone = newUser . phone || ''
2026-02-09 16:22:37 +07:00
userData . value . createdAt = newUser . createdAt || ''
2026-02-09 11:40:41 +07:00
userData . value . emailVerifiedAt = newUser . emailVerifiedAt
2026-01-28 14:44:41 +07:00
}
} , { deep : true } )
2026-02-09 11:40:41 +07:00
2026-02-03 11:01:33 +07:00
onMounted ( async ( ) => {
await fetchUserProfile ( true )
2026-01-26 10:40:50 +07:00
isHydrated . value = true
} )
2026-01-13 10:46:40 +07:00
< / script >
< template >
2026-02-26 15:20:44 +07:00
< div class = "page-container bg-[#F8F9FA] dark:bg-[#020617] transition-colors duration-300 min-h-screen" >
2026-01-26 10:40:50 +07:00
< div v-if = "!isHydrated" class="flex justify-center py-20" >
< q-spinner size = "3rem" color = "primary" / >
2026-01-13 10:46:40 +07:00
< / div >
2026-02-27 10:05:33 +07:00
<!-- การต ั ้ งค ่ าโปรไฟล ์ หล ั ก ( MAIN PROFILE SETTINGS ) -- >
2026-02-26 15:20:44 +07:00
< div v -else class = "max-w-5xl mx-auto pb-20 fade-in pt-4" >
2026-02-20 14:58:18 +07:00
2026-02-26 15:20:44 +07:00
<!-- บ ั ตรข ้ อม ู ลโปรไฟล ์ ( Profile Card ) -- >
< div class = "bg-white dark:!bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl shadow-sm mb-6 overflow-hidden" >
< div class = "p-8 border-b border-slate-200 dark:border-slate-800" >
< h2 class = "text-xl font-bold text-slate-900 dark:text-white" > { { $t ( 'profile.myProfile' ) } } < / h2 >
< p class = "text-slate-500 dark:text-slate-400 text-sm mt-1" > { { $t ( 'profile.publicInfo' ) } } < / p >
< / div >
2026-02-19 17:37:28 +07:00
2026-02-26 15:20:44 +07:00
< div class = "p-8" >
<!-- ส ่ วนอ ั ปโหลดร ู ปโปรไฟล ์ -- >
< div class = "flex flex-col sm:flex-row items-center sm:items-center gap-6 mb-10 text-center sm:text-left" >
< div class = "relative cursor-pointer" @click ="triggerUpload" >
< UserAvatar
: photo - u - r - l = "userData.photoURL"
: first - name = "userData.firstName"
: last - name = "userData.lastName"
size = "100"
class = "rounded-full bg-slate-100 dark:bg-slate-800 object-cover border-4 border-slate-50 dark:border-slate-800"
/ >
< div class = "absolute bottom-0 right-0 bg-[#3B6BE8] text-white p-1.5 rounded-full border-2 border-white dark:border-slate-900 hover:bg-blue-700 transition flex items-center justify-center" >
< q-icon name = "edit" size = "14px" / >
< / div >
< input type = "file" ref = "fileInput" class = "hidden" accept = "image/jpeg, image/png, image/gif" @change ="handleFileUpload" / >
2026-02-19 17:37:28 +07:00
< / div >
2026-02-26 15:20:44 +07:00
< div class = "flex flex-col items-center sm:items-start" >
< button @click ="triggerUpload" class = "bg-[#3B6BE8] hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg text-sm font-bold transition mb-2 shadow-sm whitespace-nowrap" >
< span v-if = "isProfileSaving"><q-spinner size="18px" / > { { $t ( 'profile.uploading' ) } } < / span >
< span v-else > {{ $ t ( ' profile.changeAvatar ' ) }} < / span >
< / button >
< p class = "text-slate-500 dark:text-slate-400 text-xs mt-1" > { { $t ( 'profile.avatarHint' ) } } < / p >
< / div >
< / div >
2026-01-13 10:46:40 +07:00
2026-02-27 10:05:33 +07:00
<!-- ฟ ิ ลด ์ ข ้ อม ู ลฟอร ์ ม ( แบ ่ งเป ็ น 2 คอล ั มน ์ ) ( Form Inputs ( 2 Column Grid ) ) -- >
2026-02-26 15:20:44 +07:00
< div class = "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-4" >
< div class = "md:col-span-2 relative" >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300 mb-2" > { { $t ( 'profile.prefix' ) } } < / label >
< select v-model = "userData.prefix" class="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-4 py-3 rounded-lg focus:outline-none focus:border-[#3B6BE8] transition text-sm font-medium text-slate-900 dark:text-white appearance-none cursor-pointer" >
< option value = "" disabled > { { $t ( 'profile.selectPrefix' ) } } < / option >
< option value = "นาย" > { { $t ( 'profile.mr' ) } } < / option >
< option value = "นาง" > { { $t ( 'profile.mrs' ) } } < / option >
< option value = "นางสาว" > { { $t ( 'profile.miss' ) } } < / option >
< / select >
< div class = "pointer-events-none absolute bottom-0 right-0 flex items-center px-4 h-11 text-slate-500" >
< q-icon name = "arrow_drop_down" size = "24px" / >
< / div >
< / div >
< div >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300 mb-2" > { { $t ( 'profile.firstName' ) } } - { { $t ( 'profile.lastName' ) } } < / label >
< div class = "flex gap-3" >
< input type = "text" v-model = "userData.firstName" class="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-4 py-3 rounded-lg focus:outline-none focus:border-[#3B6BE8] transition text-sm font-medium text-slate-900 dark:text-white" :placeholder="$t('profile.firstName')" / >
< input type = "text" v-model = "userData.lastName" class="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-4 py-3 rounded-lg focus:outline-none focus:border-[#3B6BE8] transition text-sm font-medium text-slate-900 dark:text-white" :placeholder="$t('profile.lastName')" / >
< / div >
< / div >
< div >
< div class = "flex items-center justify-between mb-2" >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300" > { { $t ( 'profile.email' ) } } < / label >
< div v-if = "userData.emailVerifiedAt" class="flex items-center gap-1 text-green-500 text-xs font-bold" >
< q-icon name = "verified_user" size = "14px" / > { { $t ( 'profile.emailVerified' ) } }
< / div >
< button v -else @click ="handleSendVerifyEmail" :disabled = "isSendingVerify" class = "flex items-center gap-1 text-amber-500 hover:text-amber-600 text-xs font-bold transition" >
< q-icon name = "warning" size = "14px" / > < span v-if = "isSendingVerify"><q-spinner size="xs" /> {{ $t('profile.verifying') }}</span><span v-else class="underline" > {{ $ t ( ' profile.verifyNow ' ) }} < / span >
< / button >
< / div >
< input type = "email" v-model = "userData.email" class="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-4 py-3 rounded-lg focus:outline-none transition text-sm font-medium text-slate-500 dark:text-slate-400" disabled / >
2026-02-20 14:58:18 +07:00
< / div >
2026-02-26 15:20:44 +07:00
< div >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300 mb-2" > { { $t ( 'profile.phone' ) } } < / label >
< input type = "tel" v-model = "userData.phone" class="w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-4 py-3 rounded-lg focus:outline-none focus:border-[#3B6BE8] transition text-sm font-medium text-slate-900 dark:text-white" placeholder="08x-xxx-xxxx" / >
< / div >
< div >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300 mb-2" > { { $t ( 'profile.joinedAt' ) } } < / label >
< input type = "text" :value = "formatDate(userData.createdAt)" class = "w-full bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 px-4 py-3 rounded-lg focus:outline-none transition text-sm font-medium text-slate-500 dark:text-slate-400" disabled / >
< / div >
2026-02-20 14:58:18 +07:00
< / div >
2026-02-26 15:20:44 +07:00
< / div >
2026-02-27 10:05:33 +07:00
<!-- ป ุ ่ มกดย ื นย ั นต ่ างๆ ( Footer Buttons ) -- >
2026-02-26 15:20:44 +07:00
< div class = "px-6 sm:px-8 py-5 border-t border-slate-200 dark:border-slate-800 flex flex-col sm:flex-row justify-center sm:justify-end gap-3 items-center bg-white dark:!bg-slate-900" >
< button class = "w-full sm:w-auto text-[13px] font-bold text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white px-4 py-2 transition order-2 sm:order-1" @click ="fetchUserProfile(true)" > {{ $ t ( ' common.cancel ' ) }} < / button >
< button @click ="handleUpdateProfile" :disabled = "isProfileSaving" class = "w-full sm:w-auto bg-[#3B6BE8] hover:bg-blue-700 text-white px-6 py-2.5 rounded-lg text-[13px] font-bold transition shadow-sm disabled:opacity-50 order-1 sm:order-2" >
< span v-if = "isProfileSaving"><q-spinner size="18px" color="white" class="mr-1" / > { { $t ( 'profile.saving' ) } } < / span >
< span v-else > {{ $ t ( ' common.saveChanges ' ) }} < / span >
< / button >
2026-02-20 14:58:18 +07:00
< / div >
< / div >
2026-02-27 10:05:33 +07:00
<!-- การ ์ ดความปลอดภ ั ย ( Security Card ) -- >
2026-02-26 15:20:44 +07:00
< div class = "bg-white dark:!bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl shadow-sm overflow-hidden" >
< div class = "p-8 border-b border-slate-200 dark:border-slate-800" >
< h2 class = "text-xl font-bold text-slate-900 dark:text-white" > { { $t ( 'profile.security' ) } } < / h2 >
< p class = "text-slate-500 dark:text-slate-400 text-sm mt-1" > { { $t ( 'profile.securitySubtitle' ) } } < / p >
< / div >
< div class = "px-6 sm:px-8 py-8 sm:py-10" >
< div class = "p-5 sm:p-6 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800 flex flex-col sm:flex-row items-center sm:items-center justify-between gap-6 text-center sm:text-left" >
< div class = "flex flex-col sm:flex-row items-center gap-4" >
< div class = "w-12 h-12 rounded-xl bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 flex items-center justify-center shrink-0" >
< q-icon name = "key" size = "24px" / >
2026-02-20 14:58:18 +07:00
< / div >
2026-02-26 15:20:44 +07:00
< div >
< h3 class = "font-bold text-slate-900 dark:text-white text-[15px] sm:text-[16px]" > { { $t ( 'profile.password' ) } } < / h3 >
< p class = "text-slate-500 dark:text-slate-400 text-xs mt-0.5 sm:mt-1" > { { $t ( 'profile.securitySubtitle' ) } } < / p >
2026-02-20 14:58:18 +07:00
< / div >
2026-02-26 15:20:44 +07:00
< / div >
< button @ click = "showPasswordModal = true" class = "w-full sm:w-auto bg-[#3B6BE8] hover:bg-blue-700 text-white px-6 py-2.5 rounded-lg text-sm font-bold transition shadow-sm shadow-blue-500/10" >
{ { $t ( 'profile.changePasswordBtn' ) } }
< / button >
2026-02-19 17:37:28 +07:00
< / div >
2026-02-26 15:20:44 +07:00
< / div >
2026-01-13 10:46:40 +07:00
< / div >
2026-02-20 14:58:18 +07:00
2026-01-13 10:46:40 +07:00
< / div >
2026-01-26 10:40:50 +07:00
2026-02-27 10:05:33 +07:00
<!-- โมดอลเปล ี ่ ยนรห ั สผ ่ าน ( Password Modal ) -- >
2026-02-26 15:20:44 +07:00
< q-dialog v-model = "showPasswordModal" >
< q-card class = "w-full max-w-md rounded-2xl p-2 dark:bg-slate-900 shadow-xl" >
< q-form @submit ="handleUpdatePassword" >
< q-card-section class = "flex items-center justify-between pb-2" >
< div class = "text-xl font-bold text-slate-900 dark:text-white" > { { $t ( 'profile.changePasswordBtn' ) } } < / div >
< q-btn icon = "close" flat round dense v -close -popup class = "text-slate-500" / >
< / q-card-section >
< q-card-section class = "pt-2" >
< div class = "space-y-1" >
< div >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300 mb-1" > { { $t ( 'profile.currentPassword' ) } } < / label >
< q-input
v - model = "passwordForm.currentPassword"
: type = "showPassword.current ? 'text' : 'password'"
outlined
dense
class = "custom-pwd-input"
: rules = "[val => !!val || $t('common.required')]"
>
< template v -slot : append >
< q-icon
: name = "showPassword.current ? 'visibility_off' : 'visibility'"
class = "cursor-pointer text-slate-400"
@ click = "showPassword.current = !showPassword.current"
/ >
< / template >
< / q-input >
< / div >
< div >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300 mb-1" > { { $t ( 'profile.newPassword' ) } } < / label >
< q-input
v - model = "passwordForm.newPassword"
: type = "showPassword.new ? 'text' : 'password'"
outlined
dense
class = "custom-pwd-input"
: rules = " [
val => ! ! val || $t ( 'common.required' ) ,
val => val . length >= 6 || $t ( 'profile.newPasswordHint' )
] "
>
< template v -slot : append >
< q-icon
: name = "showPassword.new ? 'visibility_off' : 'visibility'"
class = "cursor-pointer text-slate-400"
@ click = "showPassword.new = !showPassword.new"
/ >
< / template >
< / q-input >
< / div >
< div >
< label class = "block text-[13px] font-bold text-slate-700 dark:text-slate-300 mb-1" > { { $t ( 'profile.confirmNewPassword' ) } } < / label >
< q-input
v - model = "passwordForm.confirmPassword"
: type = "showPassword.confirm ? 'text' : 'password'"
outlined
dense
class = "custom-pwd-input"
: rules = " [
val => ! ! val || $t ( 'common.required' ) ,
val => val === passwordForm . newPassword || $t ( 'common.passwordsDoNotMatch' )
] "
>
< template v -slot : append >
< q-icon
: name = "showPassword.confirm ? 'visibility_off' : 'visibility'"
class = "cursor-pointer text-slate-400"
@ click = "showPassword.confirm = !showPassword.confirm"
/ >
< / template >
< / q-input >
< / div >
< / div >
< / q-card-section >
< q-card-actions align = "right" class = "pt-2 pb-2 px-4" >
< q-btn flat :label = "$t('common.cancel')" color = "grey-7" v -close -popup class = "font-bold text-[13px]" / >
< q-btn type = "submit" unelevated color = "primary" :label = "$t('common.save')" :loading = "isPasswordSaving" class = "font-bold rounded-lg px-4 text-[13px]" / >
< / q-card-actions >
< / q-form >
< / q-card >
< / q-dialog >
2026-01-13 10:46:40 +07:00
< / div >
< / template >
< style scoped >
. text - main {
color : white ;
}
2026-02-26 15:20:44 +07:00
. custom - pwd - input : deep ( . q - field _ _control ) {
border - radius : 8 px ;
background - color : # f8fafc ;
}
. dark . custom - pwd - input : deep ( . q - field _ _control ) {
background - color : # 1 e293b ;
}
2026-01-13 10:46:40 +07:00
2026-01-26 10:40:50 +07:00
. fade - in {
animation : fadeIn 0.4 s ease - out forwards ;
}
@ keyframes fadeIn {
from { opacity : 0 ; transform : translateY ( 10 px ) ; }
to { opacity : 1 ; transform : translateY ( 0 ) ; }
2026-01-13 10:46:40 +07:00
}
< / style >