Skip to content

Commit 4fdeca5

Browse files
authored
Merge pull request #668 from kubero-dev/feature/add-profile-editing
add profile editing
2 parents e67760a + d906df7 commit 4fdeca5

4 files changed

Lines changed: 305 additions & 53 deletions

File tree

client/src/components/accounts/users.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ export default defineComponent({
356356
357357
const saveEdit = async () => {
358358
try {
359-
await axios.put(`/api/users/${editedUser.value.id}`, editedUser.value)
359+
await axios.put(`/api/users/id/${editedUser.value.id}`, editedUser.value)
360360
await loadUsers()
361361
editDialog.value = false
362362
} catch (e) {
@@ -366,7 +366,7 @@ export default defineComponent({
366366
367367
const deleteUser = async (user: User) => {
368368
try {
369-
await axios.delete(`/api/users/${user.id}`)
369+
await axios.delete(`/api/users/id/${user.id}`)
370370
await loadUsers()
371371
} catch (e) {
372372
console.error('Error deleting user:', e)
@@ -420,7 +420,7 @@ export default defineComponent({
420420
return
421421
}
422422
try {
423-
await axios.put(`/api/users/${editedUser.value.id}/password`, {
423+
await axios.put(`/api/users/id/${editedUser.value.id}/password`, {
424424
password: editedUser.value.password,
425425
})
426426
changePasswordDialog.value = false

client/src/components/profile/index.vue

Lines changed: 202 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<h2 class="mb-1">{{ user.firstName }} {{ user.lastName }}</h2>
2121
<div class="text-h5 font-weight-bold mb-2">{{ user.username }}</div>
2222
<div class="mb-2">{{ user.email }}</div>
23-
<div class="text--secondary">Last login: <span v-if="user.lastLogin">{{ new Date(user.lastLogin).toLocaleString() }}</span><span v-else>-</span></div>
23+
<!--<div class="text--secondary">Last login: <span v-if="user.lastLogin">{{ new Date(user.lastLogin).toLocaleString() }}</span><span v-else>-</span></div>-->
2424
<v-dialog v-model="editAvatarDialog" max-width="400px">
2525
<v-card>
2626
<v-card-title>Edit Avatar</v-card-title>
@@ -46,7 +46,29 @@
4646
</v-col>
4747
<v-col cols="12" md="6" lg="8">
4848
<v-card color="cardBackground" class="pa-4">
49-
<h3 class="mb-4">Profile Details</h3>
49+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
50+
<h3>Profile Details</h3>
51+
<div>
52+
<v-btn
53+
icon
54+
size="small"
55+
color="primary"
56+
@click="openEditProfileDialog"
57+
class="mr-2"
58+
>
59+
<v-icon>mdi-pencil</v-icon>
60+
</v-btn>
61+
<v-btn
62+
icon
63+
size="small"
64+
color="primary"
65+
@click="openChangePasswordDialog"
66+
v-if="user.provider === 'local' || !user.provider"
67+
>
68+
<v-icon>mdi-lock-reset</v-icon>
69+
</v-btn>
70+
</div>
71+
</div>
5072
<v-table density="compact" class="profile-table">
5173
<tbody>
5274
<tr>
@@ -88,6 +110,93 @@
88110
</tr>
89111
</tbody>
90112
</v-table>
113+
<v-dialog v-model="editProfileDialog" max-width="500px">
114+
<v-card>
115+
<v-card-title>Edit Profile</v-card-title>
116+
<v-card-text>
117+
<v-alert
118+
v-show="profileError"
119+
type="warning"
120+
border="start"
121+
class="mb-3"
122+
:class="{ 'shaking': profileErrorShake }"
123+
>
124+
{{ profileErrorMessage }}
125+
</v-alert>
126+
<v-text-field
127+
v-model="editedUser.firstName"
128+
label="First Name"
129+
:rules="[v => !!v || 'First name is required']"
130+
></v-text-field>
131+
<v-text-field
132+
v-model="editedUser.lastName"
133+
label="Last Name"
134+
:rules="[v => !!v || 'Last name is required']"
135+
></v-text-field>
136+
<v-text-field
137+
v-model="editedUser.email"
138+
label="Email"
139+
type="email"
140+
:rules="[
141+
v => !!v || 'Email is required',
142+
v => /.+@.+\..+/.test(v) || 'Email must be valid'
143+
]"
144+
></v-text-field>
145+
</v-card-text>
146+
<v-card-actions>
147+
<v-spacer />
148+
<v-btn text @click="editProfileDialog = false">Cancel</v-btn>
149+
<v-btn color="primary" @click="saveProfile">Save</v-btn>
150+
</v-card-actions>
151+
</v-card>
152+
</v-dialog>
153+
<v-dialog v-model="changePasswordDialog" max-width="500px">
154+
<v-card>
155+
<v-card-title>Change Password</v-card-title>
156+
<v-card-text>
157+
<v-alert
158+
v-show="passwordError"
159+
type="warning"
160+
border="start"
161+
class="mb-3"
162+
:class="{ 'shaking': passwordErrorShake }"
163+
>
164+
{{ passwordErrorMessage }}
165+
</v-alert>
166+
<v-text-field
167+
v-model="passwordForm.currentPassword"
168+
label="Current Password"
169+
type="password"
170+
:rules="[v => !!v || 'Current password is required']"
171+
class="mb-2"
172+
></v-text-field>
173+
<v-text-field
174+
v-model="passwordForm.newPassword"
175+
label="New Password"
176+
type="password"
177+
:rules="[
178+
v => !!v || 'New password is required',
179+
v => v.length >= 8 || 'Password must be at least 8 characters'
180+
]"
181+
class="mb-2"
182+
></v-text-field>
183+
<v-text-field
184+
v-model="passwordForm.confirmPassword"
185+
label="Confirm New Password"
186+
type="password"
187+
:rules="[
188+
v => !!v || 'Please confirm your password',
189+
v => v === passwordForm.newPassword || 'Passwords do not match'
190+
]"
191+
></v-text-field>
192+
</v-card-text>
193+
<v-card-actions>
194+
<v-spacer />
195+
<v-btn text @click="changePasswordDialog = false">Cancel</v-btn>
196+
<v-btn color="primary" @click="savePassword">Change Password</v-btn>
197+
</v-card-actions>
198+
</v-card>
199+
</v-dialog>
91200
</v-card>
92201
</v-col>
93202
</v-row>
@@ -226,6 +335,16 @@ export default defineComponent({
226335
const tokens = ref<any[]>([])
227336
const editAvatarDialog = ref(false)
228337
const avatarFile = ref<File | null>(null)
338+
const editProfileDialog = ref(false)
339+
const editedUser = ref<any>({ firstName: '', lastName: '', email: '' })
340+
const changePasswordDialog = ref(false)
341+
const passwordForm = ref<any>({ currentPassword: '', newPassword: '', confirmPassword: '' })
342+
const profileError = ref(false)
343+
const profileErrorMessage = ref('')
344+
const profileErrorShake = ref(false)
345+
const passwordError = ref(false)
346+
const passwordErrorMessage = ref('')
347+
const passwordErrorShake = ref(false)
229348
const createDialog = ref(false)
230349
const tokenDialog = ref(false)
231350
const generatedToken = ref<any>({ name: '', expiresAt: '', token: '' })
@@ -279,6 +398,61 @@ export default defineComponent({
279398
}
280399
}
281400
401+
const openEditProfileDialog = () => {
402+
editedUser.value = {
403+
firstName: user.value.firstName,
404+
lastName: user.value.lastName,
405+
email: user.value.email
406+
}
407+
profileError.value = false
408+
editProfileDialog.value = true
409+
}
410+
411+
const saveProfile = async () => {
412+
try {
413+
await axios.put('/api/users/profile', editedUser.value)
414+
editProfileDialog.value = false
415+
profileError.value = false
416+
await loadProfile()
417+
} catch (e: any) {
418+
profileError.value = true
419+
profileErrorMessage.value = e.response?.data?.message || 'Failed to update profile'
420+
profileErrorShake.value = true
421+
setTimeout(() => {
422+
profileErrorShake.value = false
423+
}, 300)
424+
}
425+
}
426+
427+
const openChangePasswordDialog = () => {
428+
passwordForm.value = { currentPassword: '', newPassword: '', confirmPassword: '' }
429+
passwordError.value = false
430+
changePasswordDialog.value = true
431+
}
432+
433+
const savePassword = async () => {
434+
if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
435+
return // validation will handle this
436+
}
437+
438+
try {
439+
await axios.put('/api/users/profile/password', {
440+
currentPassword: passwordForm.value.currentPassword,
441+
newPassword: passwordForm.value.newPassword
442+
})
443+
changePasswordDialog.value = false
444+
passwordError.value = false
445+
passwordForm.value = { currentPassword: '', newPassword: '', confirmPassword: '' }
446+
} catch (e: any) {
447+
passwordError.value = true
448+
passwordErrorMessage.value = e.response?.data?.message || 'Failed to change password'
449+
passwordErrorShake.value = true
450+
setTimeout(() => {
451+
passwordErrorShake.value = false
452+
}, 300)
453+
}
454+
}
455+
282456
const openCreateDialog = () => {
283457
newToken.value = { name: '', expiresAt: '', token: '' }
284458
createDialog.value = true
@@ -320,6 +494,14 @@ export default defineComponent({
320494
editAvatarDialog,
321495
avatarFile,
322496
saveAvatar,
497+
editProfileDialog,
498+
editedUser,
499+
openEditProfileDialog,
500+
saveProfile,
501+
changePasswordDialog,
502+
passwordForm,
503+
openChangePasswordDialog,
504+
savePassword,
323505
createDialog,
324506
tokenDialog,
325507
generatedToken,
@@ -329,12 +511,30 @@ export default defineComponent({
329511
copyToken,
330512
textareaFlash,
331513
authStore,
514+
profileError,
515+
profileErrorMessage,
516+
profileErrorShake,
517+
passwordError,
518+
passwordErrorMessage,
519+
passwordErrorShake,
332520
}
333521
},
334522
})
335523
</script>
336524

337525
<style scoped>
526+
/* https://unused-css.com/blog/css-shake-animation/ */
527+
@keyframes horizontal-shaking {
528+
0% { transform: translateX(0) }
529+
25% { transform: translateX(5px) }
530+
50% { transform: translateX(-5px) }
531+
75% { transform: translateX(5px) }
532+
100% { transform: translateX(0) }
533+
}
534+
.shaking {
535+
animation: horizontal-shaking 0.3s ease-in-out;
536+
}
537+
338538
.flash {
339539
animation: flash-animation 3s ease-in-out;
340540
}

0 commit comments

Comments
 (0)