diff --git a/src/components/UserProfileComponents/Rating.vue b/src/components/UserProfileComponents/RatingComponents/Rating.vue similarity index 100% rename from src/components/UserProfileComponents/Rating.vue rename to src/components/UserProfileComponents/RatingComponents/Rating.vue diff --git a/src/components/BaseComponents/RatingModal.vue b/src/components/UserProfileComponents/RatingComponents/RatingModal.vue similarity index 98% rename from src/components/BaseComponents/RatingModal.vue rename to src/components/UserProfileComponents/RatingComponents/RatingModal.vue index ee6efd60754b44dedaca4ab525ed53dd3750dd40..419d4f9e4d6ec8cdb2545efc701f4f602ae26bf3 100644 --- a/src/components/BaseComponents/RatingModal.vue +++ b/src/components/UserProfileComponents/RatingComponents/RatingModal.vue @@ -32,9 +32,7 @@ </div> <!-- Modal body --> <div class="p-6 space-y-6"> - <p - class="text-lg text-base leading-relaxed text-gray-500 dark:text-gray-400" - > + <p class="text-lg leading-relaxed text-gray-500 dark:text-gray-400"> {{ title }} </p> </div> @@ -190,7 +188,7 @@ export default { }; await postNewRating(ratingInfo); - this.$router.push("/"); + this.$emit("reload"); }, }, }; diff --git a/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue b/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue new file mode 100644 index 0000000000000000000000000000000000000000..33835dd8495e0e401982b33da58b1cf95c8ff229 --- /dev/null +++ b/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue @@ -0,0 +1,123 @@ +<template> + <div + class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50" + > + <p class="font-bold mx-4" id="title"> + {{ historyItem.listing.title }} + </p> + <div class="flex flex-row items-center"> + <div class="flex flex-col px-4 flex-1"> + <router-link :to="{ path: '/profile/' + user.userId }"> + Leid til: {{ user.firstName }} {{ user.lastName }} + </router-link> + </div> + <div class="flex flex-col flex-1"> + <div> + Fra: + {{ this.getDateString(historyItem.fromTime, isMultipleDays) }} + </div> + <div> + Til: {{ this.getDateString(historyItem.toTime, isMultipleDays) }} + </div> + </div> + <colored-button + v-if="!isRated" + :text="'Vurder'" + class="px-4 flex-1" + @click=" + $emit('rate', { + show: true, + name: user.firstName + ' ' + user.lastName, + title: historyItem.listing.title, + rentID: historyItem.rentId, + renterIsReceiverOfRating: !currentUserIsRenter, + }) + " + /> + </div> + </div> +</template> + +<script> +import ColoredButton from "@/components/BaseComponents/ColoredButton.vue"; +import { parseCurrentUser } from "@/utils/token-utils"; +import userService from "@/services/user.service"; + +export default { + name: "RentHistoryItem", + components: { + ColoredButton, + }, + data() { + return { + user: {}, + isRated: true, + }; + }, + props: { + historyItem: { + rentId: Number, + fromTime: Number, + toTime: Number, + isAccepted: Boolean, + listing: { + listingID: Number, + title: String, + pricePerDay: Number, + address: String, + userID: Number, + }, + renterId: Number, + }, + }, + computed: { + currentUser() { + return parseCurrentUser(); + }, + isMultipleDays() { + if (this.historyItem.toTime - this.historyItem.fromTime < 86400000) { + return false; + } + return true; + }, + price() { + if (this.isMultipleDays) { + let numDays = Math.round( + (this.historyItem.toTime - this.historyItem.fromTime) / 86400000 + ); + return this.historyItem.listing.pricePerDay * numDays; + } + return this.historyItem.listing.pricePerDay; + }, + currentUserIsRenter() { + return this.isCurrentUser(this.historyItem.renterId); + }, + }, + methods: { + getDateString(milliseconds) { + let today = new Date(); + let date = new Date(milliseconds); + let dateString = date.getDate() + "." + (date.getMonth()+1); + + if (date.getFullYear() != today.getFullYear()) { + dateString += "." + date.getFullYear(); + } + return dateString; + }, + isCurrentUser(id) { + return id == this.currentUser.accountId; + }, + }, + async beforeCreate() { + if (this.currentUserIsRenter) { + this.user = await userService.getUserFromId( + this.historyItem.listing.userID + ); + } else { + this.user = await userService.getUserFromId(this.historyItem.renterId); + } + this.isRated = await userService.isRated(this.historyItem.rentId); + + }, +}; +</script> diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue index 202c397d9889c534dcb860d878e2adafe59f6b90..bfb9be5cebddf013c7df9fc757c1eb3761a99eec 100644 --- a/src/components/UserProfileComponents/UserListItemCard.vue +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -60,7 +60,7 @@ </template> <script> -import RatingComponent from "@/components/UserProfileComponents/Rating.vue"; +import RatingComponent from "@/components/UserProfileComponents/RatingComponents/Rating.vue"; import IconButton from "@/components/BaseComponents/IconButton.vue"; import UserService from "@/services/user.service"; import CommunityAdminService from "@/services/community-admin.service"; diff --git a/src/components/UserProfileComponents/UserProfile.vue b/src/components/UserProfileComponents/UserProfile.vue index 29f072b62a00f733c8e4d642f94822bef3d715dc..c16d35413982c07f759bb06b216085858c42078a 100644 --- a/src/components/UserProfileComponents/UserProfile.vue +++ b/src/components/UserProfileComponents/UserProfile.vue @@ -47,7 +47,7 @@ </li> <li> <router-link - to="" + to="/profile/history" 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 > @@ -104,7 +104,7 @@ </template> <script> -import RatingComponent from "@/components/UserProfileComponents/Rating.vue"; +import RatingComponent from "@/components/UserProfileComponents/RatingComponents/Rating.vue"; import { parseCurrentUser } from "@/utils/token-utils"; import { getUser } from "@/utils/apiutil"; import UserService from "@/services/user.service"; diff --git a/src/router/index.js b/src/router/index.js index 348a23804a588b34073f754d15a3fad2a348c1fb..5fbb09f51e59dee764bcc21d650ddcc56930bed3 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -20,12 +20,23 @@ const routes = [ name: "home", component: () => import("../views/CommunityViews/CommunityView.vue"), }, + { + path: "/help", + name: "help", + component: () => import("../views/HelpView.vue"), + }, { path: "/profile/:id", name: "profile", component: () => import("../views/UserProfileViews/ProfileView.vue"), beforeEnter: guardRoute, }, + { + path: "/profile/history", + name: "history", + component: () => import("../views/UserProfileViews/RentHistoryView.vue"), + beforeEnter: guardRoute, + }, { path: "/register", name: "register", diff --git a/src/services/user.service.js b/src/services/user.service.js index 073f1a4d45d41278a6434b65b6dab640e0b8e89f..be2ac621f527a5891dbd9161117ea468e6bf7193 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -37,27 +37,68 @@ class UserService { }) .catch((err) => console.error(err)); } + + + async getRenterHistory() { + return await axios + .get(API_URL + "user/profile/rent/history", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => { + console.error(err); + return []; + }); + } - async getUserRatingAsOwner(userId) { + async getOwnerHistory() { return await axios - .get(API_URL + "rating/" + userId + "/average/owner", { + .get(API_URL + "user/profile/rent/history/owner", { headers: tokenHeader(), }) .then((res) => { return res.data; }) - .catch((err) => console.error(err)); + .catch((err) => { + console.error(err); + }); } - async getUserRatingAsRenter(userId) { + async isRated(rentID) { return await axios - .get(API_URL + "rating/" + userId + "/average/renter", { + .get(API_URL + "rating/" + rentID + "/israted", { headers: tokenHeader(), }) .then((res) => { return res.data; }) - .catch((err) => console.error(err)); + .catch((err) => { + console.error(err); + }); + } + + async getUserRatingAsRenter(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average/renter", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)) + } + + async getUserRatingAsOwner(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average/owner", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)) } } export default new UserService(); diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue new file mode 100644 index 0000000000000000000000000000000000000000..7870d1e63ba55975d46634b639797c751b1a3d75 --- /dev/null +++ b/src/views/HelpView.vue @@ -0,0 +1,129 @@ +<template> + <div class="mt-6 bg-white justify-center w-screen"> + <div id="contact" class="grid place-items-center w-full"> + <div class="lg:mb-0 text-gray-700 w-full"> + <div class="mx-4"> + <h2 class="text-primary-dark mb-6 uppercase font-bold text-2xl"> + Kontakt oss + </h2> + <p class="text-gray-500 leading-relaxed mb-9"> + {{ contact.description }} + </p> + </div> + <div class="flex mb-8 ml-2 w-full"> + <div class="max-w-14 max-h-14 min-h-14 min-w-14"> + <LocationMarkerIcon + class="w-14 h-14 text-primary-dark rounded mr-4" + /> + </div> + <div class=""> + <h4 class="font-bold text-gray-800 text-xl mb-1">Lokaler</h4> + <p>{{ contact.address }},</p> + <p>{{ contact.city }},</p> + <p>{{ contact.country }}</p> + </div> + </div> + <div class="flex mb-8 ml-2 max-w-[370px] w-full"> + <div class="max-w-14 max-h-14 min-h-14 min-w-14"> + <MailIcon class="w-14 h-14 text-primary-dark rounded mr-4" /> + </div> + <div class=""> + <h4 class="font-bold text-gray-800 text-xl mb-1">Epost Addresse</h4> + <p>{{ contact.email }}</p> + </div> + </div> + </div> + </div> + <div id="faq"> + <div + class="mx-auto text-center px-4 text-2xl text-primary-dark font-semibold" + > + Frequently Asked Questions + </div> + <div + class="mt-8 mx-auto max-w-screen-sm lg:max-w-screen-lg flex flex-col lg:grid lg:grid-cols-2" + > + <dl v-for="(faqItem, index) in FAQ" :key="index"> + <div id="faqItem" class=""> + <div + id="question-and-answer" + class="select-none cursor-pointer border-2 mx-8 my-3 px-6 py-4 rounded-lg text-sm group" + > + <dt id="question"> + <div class="flex justify-between text-gray-800"> + <div class="font-bold"> + {{ faqItem.question }} + </div> + <div> + <div v-if="!faqItem.toggle" @click="toggle(faqItem)"> + <ChevronDoubleDownIcon + class="h-5 w-5 block rounded-full p-1" + /> + </div> + <div v-else @click="toggle(faqItem)"> + <ChevronDoubleUpIcon + class="h-5 w-5 block rounded-full p-1" + /> + </div> + </div> + </div> + </dt> + <dd v-if="faqItem.toggle" class="mt-2 leading-snug text-gray-700"> + {{ faqItem.answer }} + </dd> + </div> + </div> + </dl> + </div> + </div> + </div> +</template> + +<script> +import { + ChevronDoubleDownIcon, + LocationMarkerIcon, + ChevronDoubleUpIcon, + MailIcon, +} from "@heroicons/vue/outline"; + +export default { + data() { + return { + contact: { + description: + "BoCo (Borrow Community) er et norsk selskap som tilbyr en plattform for utlån av gjenstander for privatpersoner og bedrifter. BoCo streber for å bli den foretrukne plattformen for lån for privatpersoner og bedrifter i Norge.", + email: "BorrowCompany@BorrowCommunity.com", + address: "O. S. Bragstads Plass 2G", + city: "Trondheim", + country: "Norge", + }, + FAQ: [ + { + question: "Hvordan kan jeg legge ut en gjenstand til leie?", + answer: + "Ved å trykke på plus-ikonet i navigasjons-baren øverst på siden. Dette vil ta deg til et skjema for å legge ut en ny gjenstand.", + toggle: false, + }, + { + question: "Hvordan kan jeg bli med i en gruppe?", + answer: + "Fra hovedsiden kan man trykke på pluss-person-ikonet ved siden av mine grupper som vil ta deg til skjemaet for å lage en ny gruppe.", + toggle: false, + }, + ], + }; + }, + components: { + ChevronDoubleDownIcon, + ChevronDoubleUpIcon, + MailIcon, + LocationMarkerIcon, + }, + methods: { + toggle(faqItem) { + faqItem.toggle = !faqItem.toggle; + }, + }, +}; +</script> diff --git a/src/views/UserProfileViews/RentHistoryView.vue b/src/views/UserProfileViews/RentHistoryView.vue new file mode 100644 index 0000000000000000000000000000000000000000..45e7859306a8e3ede90ca30d039cb1f7840bb777 --- /dev/null +++ b/src/views/UserProfileViews/RentHistoryView.vue @@ -0,0 +1,75 @@ +<template> + <rating-modal + :visible="modal.show" + :name="modal.name" + :title="modal.title" + :rentID="modal.rentID" + :renterIsReceiverOfRating="modal.renterIsReceiverOfRating" + @close="modal.show = false" + @reload="this.$forceUpdate()" + /> + <ul> + <li v-for="historyItem in fullHistory" :key="historyItem.rentId"> + <rent-history-item + :historyItem="historyItem" + @rate="(modal) => openModal(modal)" + /> + </li> + </ul> +</template> + +<script> +import RentHistoryItem from "@/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue"; +import RatingModal from "@/components/UserProfileComponents/RatingComponents/RatingModal.vue"; +import UserService from "@/services/user.service"; + +export default { + components: { + RentHistoryItem, + RatingModal, + }, + data() { + return { + renterHistory: [], + ownerHistory: [], + modal: { + show: false, + name: "", + title: "", + rentID: -1, + renterIsReceiverOfRating: false, + }, + }; + }, + computed: { + fullHistory() { + function compareHistoryItems(itemA, itemB) { + if (itemA.fromTime > itemB.fromTime) { + return -1; + } + if (itemA.fromTime < itemB.fromTime) { + return 1; + } + return 0; + } + + let fullHistory = this.renterHistory.concat(this.ownerHistory); + fullHistory.filter((item) => item.isAccepted); + fullHistory.sort(compareHistoryItems); + return fullHistory; + }, + hasNoHistory() { + return this.renterHistory.length == 0 && this.ownerHistory.length == 0; + }, + }, + methods: { + openModal(modal) { + this.modal = modal; + }, + }, + async beforeCreate() { + this.renterHistory = await UserService.getRenterHistory(); + this.ownerHistory = await UserService.getOwnerHistory(); + }, +}; +</script> diff --git a/tests/unit/apiutil-communityHome-mock.spec.js b/tests/unit/apiutil-communityHome-mock.spec.js deleted file mode 100644 index 4ab75d6469cf79e31546054eebae45ce5976284d..0000000000000000000000000000000000000000 --- a/tests/unit/apiutil-communityHome-mock.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import { GetCommunity, GetListingsInCommunity } from "@/utils/apiutil"; -import axios from "axios"; - -jest.mock("axios"); - -describe("testing mocking of apiutil.js", () => { - it("check that existing group returns correctly", async () => { - const expectedResponse = { - communityId: 4040, - name: "Fisken i vannet", - description: "For vi som liker fjell fisk", - visibility: 1, - location: "Bergen brygge", - picture: "fish blub blub", - }; - - axios.get.mockImplementation(() => - Promise.resolve({ data: expectedResponse }) - ); - - const communityResponse = await GetCommunity(4040); - expect(communityResponse.name).toBe(expectedResponse.name); - }); - - it("check that existing group returns correct listings", async () => { - const expectedResponse = { - item1: { - title: "Fiskekurs", - description: "Fisking og sånn", - pricePerDay: 200, - address: "Vannet", - userID: 6, - categoryNames: null, - communityIDs: null, - }, - - item2: { - title: "TestFraFrontend", - description: "oslo", - pricePerDay: 500, - address: "oslo", - userID: 1, - categoryNames: null, - communityIDs: null, - }, - }; - - axios.get.mockImplementation(() => - Promise.resolve({ data: expectedResponse }) - ); - - const communityItemResponse = await GetListingsInCommunity(4040); - expect(communityItemResponse).toBe(expectedResponse); - }); -}); diff --git a/tests/unit/component-tests/user-component-tests/rating.spec.js b/tests/unit/component-tests/user-component-tests/rating.spec.js index cdbcee60fc44be8c4e04b9f2dc4a42756f5a6699..8fb44c69188cd302c40fbfb73d1a4706c448fb09 100644 --- a/tests/unit/component-tests/user-component-tests/rating.spec.js +++ b/tests/unit/component-tests/user-component-tests/rating.spec.js @@ -1,5 +1,5 @@ import { mount } from "@vue/test-utils"; -import Rating from "@/components/UserProfileComponents/Rating.vue"; +import Rating from "@/components/UserProfileComponents/RatingComponents/Rating.vue"; describe("Rating component", () => { let wrapper; diff --git a/vue.config.js b/vue.config.js index e77733af29c6ce1ad2798aee7bb9f3b932f7635f..aa4620c0c16d9c54ffa288ae9ea5b6a31d6a8bee 100644 --- a/vue.config.js +++ b/vue.config.js @@ -4,7 +4,7 @@ module.exports = defineConfig({ transpileDependencies: true, chainWebpack: (config) => { config.plugin("html").tap((args) => { - args[0].title = "Borrow Community"; + args[0].title = "BoCo - Borrow Community"; return args; }); },