Skip to content

Commit

Permalink
Merge pull request #551 from upb-uc4/feature/profile_picture
Browse files Browse the repository at this point in the history
Feature/profile picture
  • Loading branch information
tobias-wiese authored Oct 2, 2020
2 parents 818a3cb + 98d97ce commit 0245eca
Show file tree
Hide file tree
Showing 30 changed files with 1,012 additions and 135 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# [WIP v.0.9.1](https://github.com/upb-uc4/ui-web/compare/v0.9.0...v0.9.1) (2020-XX-XX)

## Feature
- introduce profile pictures (set, update, delete, display) [#503](https://github.com/upb-uc4/ui-web/issues/503)

## Bugfix
- remove stubs from the student's desktop navigation [#537](https://github.com/upb-uc4/ui-web/pull/537)
- error feedback is given on posting an account without role [#535](https://github.com/upb-uc4/ui-web/issues/535)
Expand Down
190 changes: 125 additions & 65 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
"@cypress/code-coverage": "^3.8.1",
"@tailwindcss/custom-forms": "^0.2.1",
"@types/jest": "^26.0.14",
"@types/node": "^14.11.2",
"@types/lodash": "^4.14.161",
"@types/node": "^14.11.2",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "^4.5.6",
Expand All @@ -74,6 +74,7 @@
"babel-plugin-istanbul": "^6.0.0",
"cross-conf-env": "^1.2.0",
"cypress": "^5.3.0",
"cypress-file-upload": "^4.1.1",
"cypress-plugin-snapshots": "^1.4.4",
"dotenv": "^8.2.0",
"eslint": "^6.7.2",
Expand All @@ -90,6 +91,11 @@
"vue-cli-plugin-vue-next": "~0.1.4"
},
"jest": {
"globals": {
"ts-jest": {
"tsConfig": "tests/unit/tsconfig.json"
}
},
"preset": "@vue/cli-plugin-unit-jest/presets/typescript-and-babel",
"setupFiles": [
"<rootDir>/.jest/env.ts"
Expand Down
124 changes: 124 additions & 0 deletions src/api/UserManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,4 +414,128 @@ export default class UserManagement extends Common {
}
return endpoint;
}

async deleteProfilePicture(username: string): Promise<APIResponse<boolean>> {
return await this._axios
.delete(`/users/${username}/image`)
.then((response: AxiosResponse) => {
return {
error: {} as APIError,
networkError: false,
statusCode: response.status,
returnValue: true,
};
})
.catch(async (error: AxiosError) => {
if (error.response) {
if (
await handleAuthenticationError({
statusCode: error.response.status,
error: error.response.data as APIError,
returnValue: false,
networkError: false,
})
) {
return await this.deleteProfilePicture(username);
}
return {
returnValue: false,
statusCode: error.response.status,
error: error.response.data as APIError,
networkError: false,
};
} else {
return {
returnValue: false,
statusCode: 0,
error: {} as APIError,
networkError: true,
};
}
});
}

async getProfilePicture(username: string): Promise<APIResponse<File>> {
return await this._axios
.get(`/users/${username}/image`, {
responseType: "arraybuffer",
})
.then((response: AxiosResponse) => {
let blob: Blob = new Blob([response.data], { type: response.headers["content-type"] });
const file: File = new File([blob], "image.png", { type: response.headers["content-type"] });
return {
error: {} as APIError,
networkError: false,
statusCode: response.status,
returnValue: file,
};
})
.catch(async (error: AxiosError) => {
if (error.response) {
if (
await handleAuthenticationError({
statusCode: error.response.status,
error: error.response.data as APIError,
returnValue: {} as File,
networkError: false,
})
) {
return await this.getProfilePicture(username);
}
return {
returnValue: {} as File,
statusCode: error.response.status,
error: error.response.data as APIError,
networkError: false,
};
} else {
return {
returnValue: {} as File,
statusCode: 0,
error: {} as APIError,
networkError: true,
};
}
});
}

async updateProfilePicture(username: string, picture: File): Promise<APIResponse<boolean>> {
return await this._axios
.put(`/users/${username}/image`, picture)
.then((response: AxiosResponse) => {
return {
error: {} as APIError,
networkError: false,
statusCode: response.status,
returnValue: true,
};
})
.catch(async (error: AxiosError) => {
if (error.response) {
if (
await handleAuthenticationError({
statusCode: error.response.status,
error: error.response.data as APIError,
returnValue: false,
networkError: false,
})
) {
return await this.updateProfilePicture(username, picture);
}
return {
returnValue: false,
statusCode: error.response.status,
error: error.response.data as APIError,
networkError: false,
};
} else {
return {
returnValue: false,
statusCode: 0,
error: {} as APIError,
networkError: true,
};
}
});
}
}
1 change: 0 additions & 1 deletion src/api/api_models/user_management/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export default interface User {
address: Address;
firstName: string;
lastName: string;
picture: string;
email: string;
birthDate: string;
phoneNumber: string;
Expand Down
197 changes: 197 additions & 0 deletions src/components/account/edit/sections/ProfilePictureSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<template>
<section class="py-8 border-t-2 border-gray-400">
<div class="lg:flex">
<div class="flex flex-col w-full mb-4 mr-12 lg:w-1/3 lg:block">
<label class="block mb-2 text-lg font-medium text-gray-700">Profile Picture</label>
</div>
<div class="flex flex-col">
<img id="picture" class="h-48 w-48 object-cover mb-5 rounded-full border border-gray-500" :src="selectedPicture" />
<input id="uploadFile" hidden type="file" accept=".jpeg, .png, .jpg" @change="uploadPicture" />
<div class="flex">
<button id="uploadPicture" :disabled="busy" class="btn btn-blue-primary w-48" @click="triggerFileUpload">
Select Image
</button>
<button
v-if="!pictureChanged"
id="deletePicture"
:disabled="busy"
title="Delete Profile Picture"
class="btn btn-icon-red ml-3 text-xl w-10"
@click="confirmDeletePicture"
>
<i class="far fa-trash-alt"></i>
</button>
<div v-else class="flex">
<button
v-if="pictureChanged"
id="confirmPicture"
:disabled="busy"
title="Confirm Profile Picture"
class="btn btn-icon-green ml-3 text-xl w-10"
@click="confirmPicture"
>
<i class="fas fa-check"></i>
</button>
<button
v-if="pictureChanged"
id="resetPicture"
:disabled="busy"
title="Reset Profile Picture"
class="btn btn-icon-red ml-3 text-xl w-10"
@click="resetPicture"
>
<i class="far fa-trash-alt"></i>
</button>
</div>
</div>
<p v-if="errorBag.hasNested('profilePicture')" class="error-message">
{{ errorBag.getNested("profilePicture") }}
</p>
</div>
</div>
</section>
<delete-profile-picture-modal ref="deletePictureModal" />
</template>

<script lang="ts">
import { Role } from "@/entities/Role";
import { useModelWrapper } from "@/use/helpers/ModelWrapper";
import ErrorBag from "@/use/helpers/ErrorBag";
import { computed, onBeforeMount, ref, watch } from "vue";
import { cloneDeep } from "lodash";
import UserManagement from "@/api/UserManagement";
import GenericResponseHandler from "@/use/helpers/GenericResponseHandler";
import ProfilePictureUpdateResponseHandler from "@/use/helpers/ProfilePictureUpdateResponseHandler";
import Router from "@/use/router/";
import DeleteProfilePictureModal from "@/components/modals/DeleteProfilePictureModal.vue";
export default {
name: "ProfilePictureSection",
components: {
DeleteProfilePictureModal,
},
setup(props: any, { emit }: any) {
const username: string = Router.currentRoute.value.params.username as string;
const selectedPicture = ref();
let fileToUpload: File = {} as File;
const fallbackPicture = ref();
const busy = ref(false);
const errorBag = ref(new ErrorBag());
const deletePictureModal = ref();
onBeforeMount(() => {
getProfilePicture();
});
async function getProfilePicture() {
busy.value = true;
const userManagement = new UserManagement();
const response = await userManagement.getProfilePicture(username);
const handler = new GenericResponseHandler();
const result = handler.handleResponse(response);
if (result.arrayBuffer != undefined) {
const reader = new FileReader();
reader.readAsDataURL(result);
reader.onload = (e) => {
selectedPicture.value = e.target?.result;
fallbackPicture.value = selectedPicture.value;
};
} else {
//TODO Show Toast
console.log("Error: Loading Profile Picture Failed");
selectedPicture.value = "";
fallbackPicture.value = selectedPicture.value;
}
busy.value = false;
}
const pictureChanged = computed(() => {
const value = selectedPicture.value !== fallbackPicture.value;
return value;
});
function triggerFileUpload() {
(document.getElementById("uploadFile") as any).click();
}
function uploadPicture(e: Event) {
const files: FileList | null = (e.target as HTMLInputElement)?.files;
if (files == null || files?.length == 0) return;
const file = files[0];
fileToUpload = file;
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
selectedPicture.value = e.target?.result;
};
}
async function confirmPicture() {
busy.value = true;
const userManagement = new UserManagement();
const response = await userManagement.updateProfilePicture(username, fileToUpload);
const handler = new ProfilePictureUpdateResponseHandler();
const result = await handler.handleResponse(response);
if (result) {
fallbackPicture.value = selectedPicture.value;
errorBag.value = new ErrorBag();
} else {
errorBag.value = new ErrorBag(handler.errorList);
}
busy.value = false;
}
function resetPicture() {
selectedPicture.value = fallbackPicture.value;
}
async function confirmDeletePicture() {
let modal = deletePictureModal.value;
let action = modal.action;
modal.show().then((response: typeof action) => {
switch (response) {
case action.CANCEL: {
//do nothing
break;
}
case action.DELETE: {
deleteProfilePicture();
break;
}
}
});
}
async function deleteProfilePicture() {
busy.value = true;
const userManagement = new UserManagement();
const response = await userManagement.deleteProfilePicture(username);
const handler = new GenericResponseHandler();
const result = await handler.handleResponse(response);
if (result) {
getProfilePicture();
} else {
// TODO: show toast
}
busy.value = false;
}
return {
busy,
uploadPicture,
selectedPicture,
triggerFileUpload,
resetPicture,
pictureChanged,
confirmPicture,
errorBag,
fileToUpload,
deletePictureModal,
confirmDeletePicture,
};
},
};
</script>
9 changes: 1 addition & 8 deletions src/components/account/list/UserRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,7 @@
<div class="flex items-center justify-between">
<div class="flex items-center justify-between">
<div class="flex">
<div class="hidden sm:flex flex-shrink-0 w-12 h-12">
<img
class="w-12 h-12 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt="profile_picture"
/>
</div>
<div class="sm:ml-8">
<div class="sm:ml-1">
<div class="text leading-5 font-medium text-blue-900 mb-1 lg:w-48 w-32 truncate">
{{ user.firstName }} {{ user.lastName }}
</div>
Expand Down
Loading

0 comments on commit 0245eca

Please sign in to comment.