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 >
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 >
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