Skip to content
This repository was archived by the owner on Mar 19, 2024. It is now read-only.

Commit d839308

Browse files
committed
feat(profile-picture): upload
1 parent 5b23676 commit d839308

5 files changed

Lines changed: 182 additions & 9 deletions

File tree

components/navbar/navbar.sass

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787

8888
.avatar-wrap
8989
@apply bg-ps-primary relative rounded-full
90-
@apply w-54 h-54 mr-2
90+
@apply w-54 h-54 mr-2 cursor-pointer
9191

9292
@screen md
9393
@apply h-72 w-72
@@ -99,6 +99,17 @@
9999
@screen md
100100
@apply h-66 w-66
101101

102+
.change-wrap
103+
@apply absolute w-full bg-ps-white rounded-full
104+
@apply w-48 h-48 ps-center-absolute opacity-50
105+
@apply flex items-center justify-center
106+
107+
@screen md
108+
@apply h-66 w-66
109+
110+
.icon
111+
@apply opacity-100
112+
102113
.user-info
103114
@screen md
104115
@apply pl-5
@@ -119,3 +130,25 @@
119130

120131
.microsoft-btn
121132
border-bottom: 1px solid black
133+
134+
.picture-modal-wrapper
135+
@apply text-ps-white
136+
137+
.title
138+
@apply text-ps-green text-2xl
139+
140+
.placeholder
141+
@apply my-6 mx-auto
142+
@apply flex items-center justify-center
143+
144+
img
145+
@apply rounded-full
146+
@apply border-solid border-4 border-ps-green
147+
148+
width: 100px
149+
height: 100px
150+
151+
object-fit: cover
152+
153+
.actions
154+
@apply flex justify-end

components/navbar/navbar.vue

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
.navbar
33
.user
44
.user-wrap
5-
.avatar-wrap(v-if='mainStore.isLoggedIn')
5+
.avatar-wrap(v-if='mainStore.isLoggedIn', @click='pictureModal = !pictureModal', @mouseover='pictureHover = true', @mouseleave='pictureHover = false')
66
img.avatar(:src='mainStore.profilePicture')/
7+
.change-wrap(v-if='pictureHover')
8+
edit-icon.icon
79
.user-info.flex(@click='toggleSettings', v-on-clickaway='closeSettings')
810
.user-text(v-if='mainStore.isLoggedIn')
911
span.user-name {{ mainStore.displayName }}
@@ -29,12 +31,20 @@
2931
.menu-btn(v-if='!isDesktop', @click='toggleBurger')
3032
.burger(:class='{ active: burger }')
3133
ps-modal(v-model='loginModal')
32-
.flex.justify-center
34+
.flex.justify-center
3335
ps-login-form(@login-complete='toggleLoginModal')
36+
ps-modal(v-model='pictureModal')
37+
.picture-modal-wrapper
38+
span.title Změna profilového obrázku
39+
.placeholder
40+
img(:src='placeholderImage')
41+
ps-drag-drop(tile, :draggable='false', accept='.jpg,.jpeg,.gif,.png', v-model='selectedPicture', @input='changePlaceholder', :disabled='uploading')
42+
.actions
43+
ps-btn(@click='uploadPicture', :disabled='uploading', :loading='uploading') nahrát
3444
</template>
3545

3646
<script lang="ts">
37-
import { defineComponent, ref, onMounted, computed, watchEffect } from '@nuxtjs/composition-api';
47+
import { defineComponent, ref, onMounted, computed, watchEffect, unref } from '@nuxtjs/composition-api';
3848
3949
import { useMainStore } from '@/store';
4050
@@ -44,10 +54,13 @@ import user from 'vue-material-design-icons/Account.vue';
4454
import logout from 'vue-material-design-icons/Logout.vue';
4555
import microsoftLogo from 'vue-material-design-icons/Microsoft.vue';
4656
import arrowRight from 'vue-material-design-icons/ChevronRight.vue';
57+
import editIcon from 'vue-material-design-icons/ImageEdit.vue';
4758
4859
import { directive as onClickaway } from 'vue-clickaway';
4960
50-
import * as firebase from 'firebase/app';
61+
import firebase from 'firebase/app';
62+
import 'firebase/auth';
63+
import axios from 'axios';
5164
5265
type Props = {
5366
value: boolean;
@@ -61,6 +74,7 @@ export default defineComponent({
6174
logout,
6275
microsoftLogo,
6376
arrowRight,
77+
editIcon,
6478
},
6579
6680
// @ts-ignore
@@ -114,8 +128,6 @@ export default defineComponent({
114128
const closeNotifications = () => (displayNotifications.value = false);
115129
116130
const logOut = async () => {
117-
await require('firebase/auth');
118-
119131
try {
120132
await firebase.auth().signOut();
121133
mainStore.reset();
@@ -137,6 +149,51 @@ export default defineComponent({
137149
notificationsLength.value = length;
138150
};
139151
152+
const pictureHover = ref(false);
153+
const pictureModal = ref(false);
154+
const selectedPicture = ref([]);
155+
156+
const placeholderImage = ref(unref(mainStore.state.user.profilePicture));
157+
const uploading = ref(false);
158+
159+
const uploadPicture = async () => {
160+
uploading.value = true;
161+
const fd = new FormData();
162+
163+
fd.append('avatar', selectedPicture.value[0]);
164+
165+
try {
166+
const response = await axios.patch('/api/user/picture', fd, {
167+
headers: {
168+
authorization: `Bearer ${await firebase.auth().currentUser?.getIdToken()}`,
169+
},
170+
});
171+
172+
mainStore.patch({ user: { profilePicture: response.data } });
173+
} catch (e) {
174+
console.error(e);
175+
}
176+
177+
uploading.value = false;
178+
pictureModal.value = false;
179+
};
180+
181+
const changePlaceholder = () => {
182+
if (!selectedPicture.value.length) {
183+
placeholderImage.value = unref(mainStore.state.user.profilePicture);
184+
return;
185+
}
186+
187+
const reader = new FileReader();
188+
189+
reader.onload = (e) => {
190+
// @ts-ignore
191+
placeholderImage.value = e.target?.result;
192+
};
193+
194+
reader.readAsDataURL(selectedPicture.value[0]);
195+
};
196+
140197
return {
141198
burger,
142199
toggleBurger,
@@ -153,6 +210,13 @@ export default defineComponent({
153210
updateNotifications,
154211
notificationsLength,
155212
toggleLoginModal,
213+
pictureModal,
214+
uploadPicture,
215+
pictureHover,
216+
selectedPicture,
217+
changePlaceholder,
218+
placeholderImage,
219+
uploading,
156220
};
157221
},
158222
});

components/notifications-list/notifications-list.sass

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
max-height: 325px
66

77
.wrapper
8-
@apply p-2
8+
@apply p-1 w-full
99
overflow-y: overlay
1010

1111
.wrapper::-webkit-scrollbar, .wrapper::-webkit-scrollbar-thumb
12-
width: 23px
12+
width: 24px
1313
border-radius: 13px
1414
background-clip: padding-box
1515
border: 10px solid transparent

server/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ if (!admin.apps.length)
2121
// user
2222
app.post('/user/create', async (req, res) => (await import('./user/create-user')).default(req, res));
2323
app.put('/user/year', async (req, res) => (await import('./user/update-year')).default(req, res));
24+
app.patch('/user/picture', uploader.single('avatar'), async (req, res) => (await import('./user/profile-picture')).default(req, res));
2425

2526
// proposal
2627
app.put('/proposal/accept/:id', async (req, res) => (await import('./proposal/accept')).default(req, res));

server/api/user/profile-picture.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Request, Response } from 'express';
2+
import admin from 'firebase-admin';
3+
4+
import short from 'short-uuid';
5+
6+
export default async (req: Request, res: Response) => {
7+
const idToken = req.headers.authorization?.split(' ')[1] ?? '';
8+
// @ts-ignore
9+
const avatarImg = req.file;
10+
11+
const splittedName = avatarImg.originalname.split('.');
12+
const extension = splittedName[splittedName.length - 1];
13+
14+
const acceptedExtensions = ['jpeg', 'jpg', 'png', 'gif'];
15+
16+
if (!avatarImg.mimetype.includes('image/') || !acceptedExtensions.some((acceptedExtension) => acceptedExtension === extension)) return res.status(400).send();
17+
18+
try {
19+
const userAuth = await admin.auth().verifyIdToken(idToken);
20+
21+
try {
22+
const bucket = admin.storage().bucket('ps-profile-pictures');
23+
const userRef = admin.firestore().collection('users').doc(userAuth.uid);
24+
25+
const fileName = `${short().new()}.${extension}`;
26+
27+
const uploadedUrl = await new Promise((resolve, reject) => {
28+
const blob = bucket.file(fileName);
29+
30+
const blobStream = blob.createWriteStream({
31+
metadata: {
32+
contentType: avatarImg.mimetype,
33+
},
34+
resumable: false,
35+
});
36+
37+
blobStream.on('finish', async () => {
38+
try {
39+
await admin.firestore().runTransaction(async (transaction) => {
40+
const sfDoc = await transaction.get(userRef);
41+
42+
if (!sfDoc.data()?.profilePicture.includes('empty')) {
43+
const splittedUrl = sfDoc.data()?.profilePicture.split('/');
44+
await bucket.file(splittedUrl[splittedUrl.length - 1]).delete();
45+
}
46+
47+
transaction.update(userRef, {
48+
profilePicture: `https://storage.googleapis.com/ps-profile-pictures/${fileName}`,
49+
});
50+
51+
return transaction;
52+
});
53+
} catch (e) {
54+
reject(e);
55+
}
56+
57+
resolve(`https://storage.googleapis.com/ps-profile-pictures/${fileName}`);
58+
});
59+
60+
blobStream.on('error', (e) => {
61+
reject(e);
62+
});
63+
64+
blobStream.end(avatarImg.buffer);
65+
});
66+
67+
return res.status(200).send(uploadedUrl);
68+
} catch (e) {
69+
console.error(e);
70+
return res.status(500).send(e);
71+
}
72+
} catch (_) {
73+
return res.status(401).send();
74+
}
75+
};

0 commit comments

Comments
 (0)