feat: Add user profile management page including personal info editing, avatar upload, and password change.
This commit is contained in:
parent
cf12ef965e
commit
70d2dfa4c7
4 changed files with 185 additions and 31 deletions
|
|
@ -8,7 +8,7 @@ useHead({
|
|||
title: 'ตั้งค่าบัญชี - e-Learning'
|
||||
})
|
||||
|
||||
const { currentUser, updateUserProfile, changePassword } = useAuth()
|
||||
const { currentUser, updateUserProfile, changePassword, uploadAvatar } = useAuth()
|
||||
const { t } = useI18n()
|
||||
const { errors, validate, clearFieldError } = useFormValidation()
|
||||
|
||||
|
|
@ -79,17 +79,69 @@ const triggerUpload = () => {
|
|||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const handleFileUpload = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files && target.files[0]) {
|
||||
const file = target.files[0]
|
||||
|
||||
// แสดงรูป Preview ก่อนอัปโหลด
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
userData.value.photoURL = e.target?.result as string
|
||||
}
|
||||
reader.readAsDataURL(target.files[0])
|
||||
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
|
||||
} else {
|
||||
// ถ้า error (เช่น 500) ก็ยังคงรูป preview ไว้ หรือจะ alert ก็ได้
|
||||
console.error('Upload failed:', result.error)
|
||||
alert(result.error || t('profile.updateError'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAvatar = async () => {
|
||||
// 1. Clear local preview
|
||||
userData.value.photoURL = ''
|
||||
|
||||
// 2. Update Auth State
|
||||
if (currentUser.value?.role) {
|
||||
// Direct manipulation since useAuth is shared state
|
||||
if (currentUser.value.photoURL) {
|
||||
// We can't set currentUser directly as it is computed.
|
||||
// We must access the underlying user state from useAuth?
|
||||
// But we only destructured what we needed.
|
||||
}
|
||||
}
|
||||
|
||||
// Access the user raw ref from useAuth would be better but we didn't destructure it.
|
||||
// However, we can use a workaround: call updateUserProfile with empty avatar if supported OR just assume local state.
|
||||
// The user requirement is: "If delete is clicked ... display q-avatar__content".
|
||||
// Since we don't have a confirmed DELETE API, we will just update the visual state.
|
||||
// To make it persist somewhat (until refresh), we update the cookie via a helper if possible,
|
||||
// BUT since we don't have access to `user` ref here (only currentUser computed),
|
||||
// let's grab `user` from useAuth() again or add it to destructure.
|
||||
|
||||
// Actually, I'll allow this component to just visual clear for now,
|
||||
// until we confirm API. But wait, `uploadAvatar` updates the global user state.
|
||||
// We should probably export `user` from useAuth or Add a `clearAvatar` method to useAuth.
|
||||
// For now, let's keep it simple in the component:
|
||||
|
||||
// TODO: Call API to delete avatar if endpoint exists.
|
||||
// For now, visual clear.
|
||||
|
||||
const { user } = useAuth() // Re-access to get the raw user ref
|
||||
if (user.value && user.value.profile) {
|
||||
user.value.profile.avatar_url = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateProfile = async () => {
|
||||
isProfileSaving.value = true
|
||||
|
||||
|
|
@ -145,6 +197,18 @@ const handleUpdatePassword = async () => {
|
|||
isPasswordSaving.value = false
|
||||
}
|
||||
|
||||
// Watch for changes in global user state (e.g. after avatar upload)
|
||||
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 || ''
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
isHydrated.value = true
|
||||
})
|
||||
|
|
@ -233,7 +297,7 @@ onMounted(() => {
|
|||
{{ $t('profile.editPersonalDesc') }}
|
||||
</h2>
|
||||
|
||||
<q-form @submit="handleUpdateProfile" class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="relative group cursor-pointer" @click="triggerUpload">
|
||||
|
|
@ -244,27 +308,53 @@ onMounted(() => {
|
|||
size="80"
|
||||
class="rounded-2xl border-2 border-slate-100 dark:border-white/10"
|
||||
/>
|
||||
|
||||
<!-- Hover Overlay only if has photo (optional) or always? User didn't specify, keeping simple clickable -->
|
||||
<div class="absolute inset-0 bg-black/40 rounded-2xl flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<q-icon name="camera_alt" class="text-white text-xl" />
|
||||
</div>
|
||||
<!-- Hidden Input -->
|
||||
<input ref="fileInput" type="file" class="hidden" accept="image/*" @change="handleFileUpload" >
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="font-bold text-slate-900 dark:text-white mb-1">{{ $t('profile.yourAvatar') }}</div>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
color="primary"
|
||||
:label="$t('profile.uploadNew')"
|
||||
size="sm"
|
||||
@click="triggerUpload"
|
||||
/>
|
||||
|
||||
<!-- Buttons Row -->
|
||||
<div class="flex items-center gap-3">
|
||||
<template v-if="userData.photoURL">
|
||||
<q-btn
|
||||
unelevated
|
||||
rounded
|
||||
color="primary"
|
||||
:label="$t('profile.changeAvatar')"
|
||||
class="font-bold shadow-lg shadow-blue-500/30"
|
||||
@click="triggerUpload"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<q-btn
|
||||
unelevated
|
||||
rounded
|
||||
color="primary"
|
||||
:label="$t('profile.uploadNew')"
|
||||
class="font-bold shadow-lg shadow-blue-500/30"
|
||||
@click="triggerUpload"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add Limit Text -->
|
||||
<div class="mt-1 text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ $t('profile.uploadLimit') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="bg-slate-100 dark:bg-white/5" />
|
||||
|
||||
<q-form @submit="handleUpdateProfile" class="flex flex-col gap-6">
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-bold text-slate-500 dark:text-slate-400 ml-1 uppercase">{{ $t('profile.prefix') }}</label>
|
||||
|
|
@ -340,6 +430,8 @@ onMounted(() => {
|
|||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
|
||||
</div> <!-- Close the wrapper div -->
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue