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

Commit

Permalink
feat(profile-picture): upload
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinKanera committed Feb 10, 2021
1 parent 5b23676 commit d839308
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 9 deletions.
35 changes: 34 additions & 1 deletion components/navbar/navbar.sass
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@

.avatar-wrap
@apply bg-ps-primary relative rounded-full
@apply w-54 h-54 mr-2
@apply w-54 h-54 mr-2 cursor-pointer

@screen md
@apply h-72 w-72
Expand All @@ -99,6 +99,17 @@
@screen md
@apply h-66 w-66

.change-wrap
@apply absolute w-full bg-ps-white rounded-full
@apply w-48 h-48 ps-center-absolute opacity-50
@apply flex items-center justify-center

@screen md
@apply h-66 w-66

.icon
@apply opacity-100

.user-info
@screen md
@apply pl-5
Expand All @@ -119,3 +130,25 @@

.microsoft-btn
border-bottom: 1px solid black

.picture-modal-wrapper
@apply text-ps-white

.title
@apply text-ps-green text-2xl

.placeholder
@apply my-6 mx-auto
@apply flex items-center justify-center

img
@apply rounded-full
@apply border-solid border-4 border-ps-green

width: 100px
height: 100px

object-fit: cover

.actions
@apply flex justify-end
76 changes: 70 additions & 6 deletions components/navbar/navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
.navbar
.user
.user-wrap
.avatar-wrap(v-if='mainStore.isLoggedIn')
.avatar-wrap(v-if='mainStore.isLoggedIn', @click='pictureModal = !pictureModal', @mouseover='pictureHover = true', @mouseleave='pictureHover = false')
img.avatar(:src='mainStore.profilePicture')/
.change-wrap(v-if='pictureHover')
edit-icon.icon
.user-info.flex(@click='toggleSettings', v-on-clickaway='closeSettings')
.user-text(v-if='mainStore.isLoggedIn')
span.user-name {{ mainStore.displayName }}
Expand All @@ -29,12 +31,20 @@
.menu-btn(v-if='!isDesktop', @click='toggleBurger')
.burger(:class='{ active: burger }')
ps-modal(v-model='loginModal')
.flex.justify-center
.flex.justify-center
ps-login-form(@login-complete='toggleLoginModal')
ps-modal(v-model='pictureModal')
.picture-modal-wrapper
span.title Změna profilového obrázku
.placeholder
img(:src='placeholderImage')
ps-drag-drop(tile, :draggable='false', accept='.jpg,.jpeg,.gif,.png', v-model='selectedPicture', @input='changePlaceholder', :disabled='uploading')
.actions
ps-btn(@click='uploadPicture', :disabled='uploading', :loading='uploading') nahrát
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, computed, watchEffect } from '@nuxtjs/composition-api';
import { defineComponent, ref, onMounted, computed, watchEffect, unref } from '@nuxtjs/composition-api';
import { useMainStore } from '@/store';
Expand All @@ -44,10 +54,13 @@ import user from 'vue-material-design-icons/Account.vue';
import logout from 'vue-material-design-icons/Logout.vue';
import microsoftLogo from 'vue-material-design-icons/Microsoft.vue';
import arrowRight from 'vue-material-design-icons/ChevronRight.vue';
import editIcon from 'vue-material-design-icons/ImageEdit.vue';
import { directive as onClickaway } from 'vue-clickaway';
import * as firebase from 'firebase/app';
import firebase from 'firebase/app';
import 'firebase/auth';
import axios from 'axios';
type Props = {
value: boolean;
Expand All @@ -61,6 +74,7 @@ export default defineComponent({
logout,
microsoftLogo,
arrowRight,
editIcon,
},
// @ts-ignore
Expand Down Expand Up @@ -114,8 +128,6 @@ export default defineComponent({
const closeNotifications = () => (displayNotifications.value = false);
const logOut = async () => {
await require('firebase/auth');
try {
await firebase.auth().signOut();
mainStore.reset();
Expand All @@ -137,6 +149,51 @@ export default defineComponent({
notificationsLength.value = length;
};
const pictureHover = ref(false);
const pictureModal = ref(false);
const selectedPicture = ref([]);
const placeholderImage = ref(unref(mainStore.state.user.profilePicture));
const uploading = ref(false);
const uploadPicture = async () => {
uploading.value = true;
const fd = new FormData();
fd.append('avatar', selectedPicture.value[0]);
try {
const response = await axios.patch('/api/user/picture', fd, {
headers: {
authorization: `Bearer ${await firebase.auth().currentUser?.getIdToken()}`,
},
});
mainStore.patch({ user: { profilePicture: response.data } });
} catch (e) {
console.error(e);
}
uploading.value = false;
pictureModal.value = false;
};
const changePlaceholder = () => {
if (!selectedPicture.value.length) {
placeholderImage.value = unref(mainStore.state.user.profilePicture);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
// @ts-ignore
placeholderImage.value = e.target?.result;
};
reader.readAsDataURL(selectedPicture.value[0]);
};
return {
burger,
toggleBurger,
Expand All @@ -153,6 +210,13 @@ export default defineComponent({
updateNotifications,
notificationsLength,
toggleLoginModal,
pictureModal,
uploadPicture,
pictureHover,
selectedPicture,
changePlaceholder,
placeholderImage,
uploading,
};
},
});
Expand Down
4 changes: 2 additions & 2 deletions components/notifications-list/notifications-list.sass
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
max-height: 325px

.wrapper
@apply p-2
@apply p-1 w-full
overflow-y: overlay

.wrapper::-webkit-scrollbar, .wrapper::-webkit-scrollbar-thumb
width: 23px
width: 24px
border-radius: 13px
background-clip: padding-box
border: 10px solid transparent
Expand Down
1 change: 1 addition & 0 deletions server/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ if (!admin.apps.length)
// user
app.post('/user/create', async (req, res) => (await import('./user/create-user')).default(req, res));
app.put('/user/year', async (req, res) => (await import('./user/update-year')).default(req, res));
app.patch('/user/picture', uploader.single('avatar'), async (req, res) => (await import('./user/profile-picture')).default(req, res));

// proposal
app.put('/proposal/accept/:id', async (req, res) => (await import('./proposal/accept')).default(req, res));
Expand Down
75 changes: 75 additions & 0 deletions server/api/user/profile-picture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Request, Response } from 'express';
import admin from 'firebase-admin';

import short from 'short-uuid';

export default async (req: Request, res: Response) => {
const idToken = req.headers.authorization?.split(' ')[1] ?? '';
// @ts-ignore
const avatarImg = req.file;

const splittedName = avatarImg.originalname.split('.');
const extension = splittedName[splittedName.length - 1];

const acceptedExtensions = ['jpeg', 'jpg', 'png', 'gif'];

if (!avatarImg.mimetype.includes('image/') || !acceptedExtensions.some((acceptedExtension) => acceptedExtension === extension)) return res.status(400).send();

try {
const userAuth = await admin.auth().verifyIdToken(idToken);

try {
const bucket = admin.storage().bucket('ps-profile-pictures');
const userRef = admin.firestore().collection('users').doc(userAuth.uid);

const fileName = `${short().new()}.${extension}`;

const uploadedUrl = await new Promise((resolve, reject) => {
const blob = bucket.file(fileName);

const blobStream = blob.createWriteStream({
metadata: {
contentType: avatarImg.mimetype,
},
resumable: false,
});

blobStream.on('finish', async () => {
try {
await admin.firestore().runTransaction(async (transaction) => {
const sfDoc = await transaction.get(userRef);

if (!sfDoc.data()?.profilePicture.includes('empty')) {
const splittedUrl = sfDoc.data()?.profilePicture.split('/');
await bucket.file(splittedUrl[splittedUrl.length - 1]).delete();
}

transaction.update(userRef, {
profilePicture: `https://storage.googleapis.com/ps-profile-pictures/${fileName}`,
});

return transaction;
});
} catch (e) {
reject(e);
}

resolve(`https://storage.googleapis.com/ps-profile-pictures/${fileName}`);
});

blobStream.on('error', (e) => {
reject(e);
});

blobStream.end(avatarImg.buffer);
});

return res.status(200).send(uploadedUrl);
} catch (e) {
console.error(e);
return res.status(500).send(e);
}
} catch (_) {
return res.status(401).send();
}
};

0 comments on commit d839308

Please sign in to comment.