Skip to content
Snippets Groups Projects
Commit 68ee67e1 authored by Titus Netland's avatar Titus Netland
Browse files

Merge branch 'user-profile-view' into 'main'

User profile view

See merge request !13
parents cdc292f6 ad919aaa
No related branches found
No related tags found
1 merge request!13User profile view
Pipeline #176121 passed
......@@ -14,13 +14,14 @@
"axios": "^0.26.1",
"core-js": "^3.8.3",
"cssom": "^0.5.0",
"jwt-decode": "^3.1.2",
"roboto-fontface": "*",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuelidate": "^0.7.7",
"vuex": "^4.0.0",
"vuex-persistedstate": "^4.1.0",
"webfontloader": "^1.0.0"
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@babel/core": "^7.12.16",
......@@ -11057,6 +11058,11 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"node_modules/kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
......@@ -24450,6 +24456,11 @@
"universalify": "^2.0.0"
}
},
"jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
......@@ -15,13 +15,14 @@
"axios": "^0.26.1",
"core-js": "^3.8.3",
"cssom": "^0.5.0",
"jwt-decode": "^3.1.2",
"roboto-fontface": "*",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuelidate": "^0.7.7",
"vuex": "^4.0.0",
"vuex-persistedstate": "^4.1.0",
"webfontloader": "^1.0.0"
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@babel/core": "^7.12.16",
......
src/assets/defaultUserProfileImage.jpg

4.95 KiB

......@@ -102,6 +102,7 @@
import useVuelidate from "@vuelidate/core";
import { required, email, helpers } from "@vuelidate/validators";
import { doLogin } from "@/utils/apiutil";
import { parseUserFromToken } from "@/utils/token-utils";
export default {
name: "LoginForm.vue",
......@@ -165,6 +166,10 @@ export default {
else {
console.log("Something went wrong");
}
let user = parseUserFromToken();
let id = user.account_id;
this.$router.push("/profile/" + id);
},
validate() {
......
<template>
<div
class="max-w-sm bg-white rounded-lg border border-gray-200 shadow-md dark:bg-gray-800 dark:border-gray-700"
>
<div v-show="isCurrentUser" class="flex justify-end px-4 pt-4">
<button
id="dropdownDefault"
data-dropdown-toggle="dropdown"
@click="dropdown = !dropdown"
class="hidden sm:inline-block text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-1.5"
type="button"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"
></path>
</svg>
</button>
<div
id="dropdown"
v-show="dropdown"
zindex="2"
class="z-10 w-44 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700"
>
<ul class="py-1" aria-labelledby="dropdownDefault">
<li>
<router-link
to=""
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>Mine gjenstander</router-link
>
</li>
<li>
<router-link
to=""
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>Mine grupper
</router-link>
</li>
<li>
<router-link
to=""
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>Leiehistorikk</router-link
>
</li>
<li>
<router-link
to="/newPassword"
class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>Endre passord</router-link
>
</li>
<li>
<router-link
to=""
class="block py-2 px-4 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>Slett bruker</router-link
>
</li>
</ul>
</div>
</div>
<div class="flex flex-col items-center pb-10">
<img
class="mb-3 w-24 h-24 rounded-full shadow-lg"
src="../../assets/defaultUserProfileImage.jpg"
alt="Profile picture"
/>
<h5 class="mb-1 text-xl font-medium text-gray-900 dark:text-white">
{{ user.first_name }} {{ user.last_name }}
</h5>
<div>
<rating-component :rating="renterRating" :ratingType="'Leietaker'" />
<rating-component :rating="ownerRating" :ratingType="'Utleier'" />
</div>
<div v-show="!isCurrentUser" class="flex mt-4 space-x-3 lg:mt-6">
<a
href="#"
class="inline-flex items-center py-2 px-4 text-sm font-medium text-center text-gray-900 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-700"
>Åpne chat</a
>
</div>
</div>
</div>
</template>
<script>
import RatingComponent from "@/components/UserProfileComponents/RatingComponent.vue";
import { parseUserFromToken } from "@/utils/token-utils";
import { getUser /* getRenterRating, getOwnerRating */ } from "@/utils/apiutil";
import router from "@/router";
export default {
name: "LargeProfileCard",
data() {
return {
user: {},
currentUser: {},
id: -1,
isCurrentUser: false,
renterRating: 0, //getRenterRating(this.userID),
ownerRating: 0, //getOwnerRating(this.userID),
dropdown: false,
};
},
components: {
RatingComponent,
},
methods: {
async getUser() {
this.currentUser = parseUserFromToken();
this.id = router.currentRoute.value.params.id;
if (this.id == this.currentUser.account_id) {
this.isCurrentUser = true;
this.user = this.currentUser;
return;
}
let getuser = await getUser(this.id);
this.user = {
account_id: getuser.userID,
first_name: getuser.firstName,
last_name: getuser.lastName,
};
},
getProfilePicture() {
/* if (this.user.picture != "") {
return this.user.picture;
} */
return "../assets/defaultUserProfileImage.jpg";
},
},
beforeMount() {
this.getUser();
},
};
</script>
<template>
<ul class="flex justify-center">
<li>
<p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400">
{{ ratingType }}:&nbsp;
</p>
</li>
<li v-for="i in 5" :key="i">
<svg
:class="getFill(i)"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
></path>
</svg>
</li>
<li>
<p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400">
{{ compRating }} out of 5
</p>
</li>
</ul>
</template>
<script>
export default {
name: "RatingComponent",
data() {
return {
compRating: this.rating + 0,
};
},
props: {
rating: Number,
ratingType: String,
},
methods: {
getFill(i) {
if (i <= this.rating) {
return "w-5 h-5 text-yellow-400";
}
return "w-5 h-5 text-gray-300 dark:text-gray-500";
},
},
};
</script>
<template>
<div
class="select-none cursor-pointer hover:bg-gray-50 flex flex-1 items-center p-4"
>
<div class="flex flex-col w-10 h-10 justify-center items-center mr-4">
<router-link to="">
<img alt="profil" :src="getProfilePicture" />
</router-link>
</div>
<div class="flex-1 pl-1">
<div class="font-medium dark:text-white">
{{ user.first_name }} {{ user.last_name }}
</div>
</div>
<div class="flex flex-row justify-center">
<button class="w-10 text-right flex justify-end">Åpne chat</button>
<button v-if="admin" class="w-10 text-right flex justify-end">
Fjern bruker
</button>
</div>
</div>
</template>
<script>
export default {
name: "UserListItem",
props: {
user: Object,
admin: Boolean,
},
methods: {
getProfilePicture() {
if (this.user.picture != "") {
return this.user.picture;
}
return "../assets/defaultUserProfileImage.jpg";
},
},
};
</script>
import store from "@/store";
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import LoginView from "../views/LoginView.vue";
......@@ -5,18 +6,22 @@ import NewPasswordView from "../views/NewPasswordView";
const routes = [
{
path: "/endre", //Endre før push
path: "/", //Endre før push
name: "home",
component: HomeView,
},
{
path: "/about",
name: "about",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
component: () => import("../views/AboutView.vue"),
},
{
path: "/profile/:id",
name: "profile",
component: () => import("../views/ProfileView.vue"),
beforeEnter: () => {
if (store.state.user.token == null) router.push("login");
},
},
{
path: "/register",
......@@ -28,7 +33,7 @@ const routes = [
import(/* webpackChunkName: "register" */ "../views/RegisterView.vue"),
},
{
path: "/",
path: "/login",
name: "login",
component: LoginView,
},
......
import axios from "axios";
import { tokenHeader } from "./token-utils";
const API_URL = process.env.VUE_APP_BASEURL;
......@@ -31,3 +32,42 @@ export function registerUser(registerInfo) {
})
.catch((err) => console.log(err));
}
export async function getUser(userid) {
return axios
.get(API_URL + "users/" + userid + "/profile", {
headers: tokenHeader(),
})
.then((response) => {
return response.data;
})
.catch((error) => {
console.error(error);
});
}
export function getRenterRating(userid) {
return axios
.get(API_URL + "users/" + userid + "", {
headers: tokenHeader(),
})
.then((response) => {
return response.data;
})
.catch((error) => {
console.error(error);
});
}
export function getOwnerRating(userid) {
return axios
.get(API_URL + "users/" + userid + "", {
headers: tokenHeader(),
})
.then((response) => {
return response.data;
})
.catch((error) => {
console.error(error);
});
}
import jwt_decode from "jwt-decode";
import store from "@/store";
export function tokenHeader() {
let token = store.state.user.token;
return { Authorization: token };
}
export function parseUserFromToken() {
let token = store.state.user.token;
return jwt_decode(token);
}
<!-- View for looking at different profile display methods -->
<template>
<large-profile-card :isCurrentUser="true" />
</template>
<script>
import LargeProfileCard from "@/components/UserProfileComponents/LargeProfileCard.vue";
export default {
components: {
LargeProfileCard,
},
};
</script>
import { getUser } from "@/utils/apiutil";
import axios from "axios";
jest.mock("axios");
describe("testing mocking of apiutil.js", () => {
it("check that existing user returns correctly", async () => {
const expectedResponse = {
response:
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2NvdW50X2lkIjoiNiIsImV4cCI6MTY1MTEzMDU2NywiZmlyc3RfbmFtZSI6IkFsaWRhIiwiZW1haWwiOiJhbGlkYUB0ZXN0Lm5vIn0.Cp3_qfLhA55j5yaa1WPG97LNtvAZssxo0ROP3VIrHVs",
};
axios.get.mockImplementation(() =>
Promise.resolve({ data: expectedResponse })
);
const userResponse = await getUser(1);
expect(userResponse).not.toEqual({ response: "User not found in DB" });
});
it("check that non-existing user returns 404", async () => {
const expectedResponse = { response: "User not found in DB" };
axios.get.mockImplementation(() =>
Promise.resolve({ data: expectedResponse })
);
const userResponse = await getUser(100000);
expect(userResponse).toEqual(expectedResponse);
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment