33 <v-row >
44 <v-col cols =" 12" md =" 6" lg =" 4" >
55 <v-card class =" pa-4" >
6- <v-avatar size =" 80" class =" mb-4" >
7- <v-img :src =" user.image || defaultAvatar" alt =" User avatar" />
8- </v-avatar >
6+ <div style =" position : relative ; display : inline-block ;" >
7+ <v-avatar size =" 150" class =" mb-4" >
8+ <v-img :src =" user.image || defaultAvatar" alt =" User avatar" />
9+ </v-avatar >
10+ <v-btn
11+ icon
12+ size =" x-small"
13+ color =" secondary"
14+ style =" position : absolute ; bottom : 8px ; right : 8px ; z-index : 2 ;"
15+ @click =" editAvatarDialog = true"
16+ >
17+ <v-icon >mdi-pencil</v-icon >
18+ </v-btn >
19+ </div >
920 <h2 class =" mb-1" >{{ user.firstName }} {{ user.lastName }}</h2 >
10- <div class =" text--secondary mb-2" >@ {{ user.username }}</div >
21+ <div class =" text-h5 font-weight-bold mb-2" >{{ user.username }}</div >
1122 <div class =" mb-2" >{{ user.email }}</div >
12- <v-chip v-if =" user.role" color =" primary" class =" mb-2" >{{ user.role.name }}</v-chip >
1323 <div class =" text--secondary" >Last login: <span v-if =" user.lastLogin" >{{ new Date(user.lastLogin).toLocaleString() }}</span ><span v-else >-</span ></div >
24+ <v-dialog v-model =" editAvatarDialog" max-width =" 400px" >
25+ <v-card >
26+ <v-card-title >Edit Avatar</v-card-title >
27+ <v-card-text >
28+ <v-alert type =" warning" density =" compact" class =" mb-2" >
29+ The image must not exceed 100KB.
30+ </v-alert >
31+ <v-file-input
32+ v-model =" avatarFile"
33+ label =" Upload new avatar"
34+ accept =" image/*"
35+ prepend-icon =" mdi-image"
36+ ></v-file-input >
37+ </v-card-text >
38+ <v-card-actions >
39+ <v-spacer />
40+ <v-btn text @click =" editAvatarDialog = false" >Cancel</v-btn >
41+ <v-btn color =" primary" @click =" saveAvatar" >Save</v-btn >
42+ </v-card-actions >
43+ </v-card >
44+ </v-dialog >
1445 </v-card >
1546 </v-col >
1647 <v-col cols =" 12" md =" 6" lg =" 8" >
3566 </v-list-item >
3667 <v-list-item >
3768 <v-list-item-title >Role</v-list-item-title >
38- <v-list-item-subtitle >{{ user.role ? user.role.name : '-' }}</v-list-item-subtitle >
69+
70+ <v-chip
71+ class =" ma-2"
72+ color =" primary"
73+ label
74+ v-if =" user.role"
75+ >
76+ <v-icon icon =" mdi-account-circle-outline" start ></v-icon >
77+ {{ user.role.name}}
78+ </v-chip >
79+ <span v-else >-</span >
3980 </v-list-item >
4081 <v-list-item >
4182 <v-list-item-title >Groups</v-list-item-title >
42- <v-list-item-subtitle >
43- <v-chip v-for =" group in user.userGroups" :key =" group.id" class =" ma-1" color =" secondary" >{{ group.name }}</v-chip >
83+ <v-chip v-for =" group in user.userGroups" :key =" group.id" class =" ma-1" color =" grey" >{{ group.name }}</v-chip >
4484 <span v-if =" !user.userGroups || user.userGroups.length === 0" >-</span >
45- </v-list-item-subtitle >
4685 </v-list-item >
4786 <v-list-item >
4887 <v-list-item-title >Provider</v-list-item-title >
@@ -113,6 +152,8 @@ export default defineComponent({
113152 })
114153 const defaultAvatar = ' /avatar.svg'
115154 const tokens = ref <any []>([])
155+ const editAvatarDialog = ref (false )
156+ const avatarFile = ref <File | null >(null )
116157
117158 const loadProfile = async () => {
118159 try {
@@ -141,6 +182,21 @@ export default defineComponent({
141182 }
142183 }
143184
185+ const saveAvatar = async () => {
186+ if (! avatarFile .value ) return
187+ const formData = new FormData ()
188+ formData .append (' avatar' , avatarFile .value )
189+ try {
190+ await axios .post (' /api/users/profile/avatar' , formData , {
191+ headers: { ' Content-Type' : ' multipart/form-data' },
192+ })
193+ editAvatarDialog .value = false
194+ await loadProfile ()
195+ } catch (e ) {
196+ // error handling
197+ }
198+ }
199+
144200 onMounted (() => {
145201 loadProfile ()
146202 loadTokens ()
@@ -151,6 +207,9 @@ export default defineComponent({
151207 defaultAvatar ,
152208 tokens ,
153209 deleteToken ,
210+ editAvatarDialog ,
211+ avatarFile ,
212+ saveAvatar ,
154213 }
155214 },
156215})
0 commit comments