diff --git a/jest.config.js b/jest.config.js index 6dda9e7a1f5f0b23dd68aceb027c2afd557f99b8..f638d89a791a7b7bc5a7e52336b00da7759e1863 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ module.exports = { preset: "@vue/cli-plugin-unit-jest", + collectCoverage: true, + collectCoverageFrom: ["src/**/*.js", "src/**/*.vue", "!**/node_modules/**"], }; diff --git a/src/components/BaseComponents/FooterBar.vue b/src/components/BaseComponents/FooterBar.vue new file mode 100644 index 0000000000000000000000000000000000000000..478a86447ca15a4a5be2cbb140e6a797ed142188 --- /dev/null +++ b/src/components/BaseComponents/FooterBar.vue @@ -0,0 +1,25 @@ +<template> + <footer + class="w-full bg-white dark:bg-gray-800 sm:flex-row border-1 border-t border-gray-600 h-10" + > + <p class="float-left text-xs my-3 ml-4 text-primary-dark"> + © BoCo 2022 - All rights reserved + </p> + <QuestionMarkCircleIcon + class="md:mt-0 mt-1 mr-4 float-right cursor-pointer h-8 md:h-10 text-primary-medium" + alt="Hjelp" + @click="$router.push('/help')" + /> + </footer> +</template> + +<script> +import { QuestionMarkCircleIcon } from "@heroicons/vue/outline"; +export default { + name: "FooterBar", + + components: { + QuestionMarkCircleIcon, + }, +}; +</script> diff --git a/src/components/BaseComponents/InputField.vue b/src/components/BaseComponents/InputField.vue deleted file mode 100644 index a6cbb32e98ed5e1f94ec9b784ea492b9b138048e..0000000000000000000000000000000000000000 --- a/src/components/BaseComponents/InputField.vue +++ /dev/null @@ -1,11 +0,0 @@ -<template> - <input - class="block w-full px-4 py-2 mt-2 text-gray-700 placeholder-gray-500 bg-white border rounded-md dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-blue-300" - /> -</template> - -<script> -export default { - name: "InputField", -}; -</script> diff --git a/src/components/BaseComponents/LoaderSpinner.vue b/src/components/BaseComponents/LoaderSpinner.vue new file mode 100644 index 0000000000000000000000000000000000000000..2b88fb6994d8958d84070ecfad870f27d3326392 --- /dev/null +++ b/src/components/BaseComponents/LoaderSpinner.vue @@ -0,0 +1,116 @@ +<template> + <div class="loadingio-spinner-bean-eater-o5tefvffeqm"> + <div class="ldio-sweozsnwol"> + <div> + <div></div> + <div></div> + <div></div> + </div> + <div> + <div></div> + <div></div> + <div></div> + </div> + </div> + </div> +</template> + +<style scoped type="text/css"> +@keyframes ldio-sweozsnwol-1 { + 0% { + transform: rotate(0deg); + } + 50% { + transform: rotate(-45deg); + } + 100% { + transform: rotate(0deg); + } +} +@keyframes ldio-sweozsnwol-2 { + 0% { + transform: rotate(180deg); + } + 50% { + transform: rotate(225deg); + } + 100% { + transform: rotate(180deg); + } +} +.ldio-sweozsnwol > div:nth-child(2) { + transform: translate(-15px, 0); +} +.ldio-sweozsnwol > div:nth-child(2) div { + position: absolute; + top: 40px; + left: 40px; + width: 120px; + height: 60px; + border-radius: 120px 120px 0 0; + background: #f8b26a; + animation: ldio-sweozsnwol-1 1s linear infinite; + transform-origin: 60px 60px; +} +.ldio-sweozsnwol > div:nth-child(2) div:nth-child(2) { + animation: ldio-sweozsnwol-2 1s linear infinite; +} +.ldio-sweozsnwol > div:nth-child(2) div:nth-child(3) { + transform: rotate(-90deg); + animation: none; +} +@keyframes ldio-sweozsnwol-3 { + 0% { + transform: translate(190px, 0); + opacity: 0; + } + 20% { + opacity: 1; + } + 100% { + transform: translate(70px, 0); + opacity: 1; + } +} +.ldio-sweozsnwol > div:nth-child(1) { + display: block; +} +.ldio-sweozsnwol > div:nth-child(1) div { + position: absolute; + top: 92px; + left: -8px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #004aad; + animation: ldio-sweozsnwol-3 1s linear infinite; +} +.ldio-sweozsnwol > div:nth-child(1) div:nth-child(1) { + animation-delay: -0.67s; +} +.ldio-sweozsnwol > div:nth-child(1) div:nth-child(2) { + animation-delay: -0.33s; +} +.ldio-sweozsnwol > div:nth-child(1) div:nth-child(3) { + animation-delay: 0s; +} +.loadingio-spinner-bean-eater-o5tefvffeqm { + width: 200px; + height: 200px; + display: inline-block; + overflow: hidden; + background: none; +} +.ldio-sweozsnwol { + width: 50%; + height: 50%; + position: relative; + transform: translateZ(0) scale(1); + backface-visibility: hidden; + transform-origin: 0 0; /* see note above */ +} +.ldio-sweozsnwol div { + box-sizing: content-box; +} +/* generated by https://loading.io/ */ +</style> diff --git a/src/components/BaseComponents/NavBar.vue b/src/components/BaseComponents/NavBar.vue index 39941c5321ede2b107b3750ae38f38d198202e67..e6f2562c9e029366d0f973a997895c83b4d4b9a6 100644 --- a/src/components/BaseComponents/NavBar.vue +++ b/src/components/BaseComponents/NavBar.vue @@ -1,36 +1,39 @@ <template> <nav - class="flex items-center bg-white justify-between h-14 border-1 border-b border-gray-300 border-solid sticky top-0 z-50" + class="flex items-center bg-white justify-between h-10 md:h-14 border-1 border-b border-gray-300 border-solid sticky top-0 z-50" > <div class="logo"> <img - class="m-1 ml-4 cursor-pointer h-12" + class="m-1 ml-4 cursor-pointer h-9 md:h-12" src="../../assets/logo3.svg" alt="BoCo logo" @click="$router.push('/')" /> </div> - <ul class="flex"> - <li> + <ul class="flex justify-between"> + <li class="cursor-pointer" @click="$router.push('/newItem')"> <PlusIcon - class="m-6 cursor-pointer h-7 text-primary-medium" + class="m-6 md:mr-2 h-7 text-primary-medium float-left" alt="Legg til" - @click="$router.push('/newItem')" /> + <a class="hidden md:block mt-7 text-sm float-right">Legg til</a> </li> - <li> - <ChatAlt2Icon - class="m-6 cursor-pointer h-7 text-primary-medium" - alt="Meldinger" - @click="$router.push('/messages')" - /> + <li class="cursor-pointer" @click="loadMessages"> + <div class="notification-container"> + <ChatAlt2Icon + class="m-6 md:mr-2 h-7 text-primary-medium float-left" + alt="Meldinger" + /> + <p class="notification" v-if="newMessages > 0">{{ notifications }}</p> + <a class="hidden md:block mt-7 text-sm float-right">Meldinger</a> + </div> </li> - <li> + <li class="cursor-pointer" @click="loadProfile"> <UserCircleIcon - class="m-6 cursor-pointer h-7 text-primary-medium" + class="m-6 md:mr-2 h-7 text-primary-medium float-left" alt="Profil" - @click="loadProfile" /> + <a class="hidden md:block mr-4 mt-7 text-sm float-right">Profil</a> </li> </ul> </nav> @@ -39,10 +42,25 @@ <script> import { parseUserFromToken } from "@/utils/token-utils"; import { PlusIcon, ChatAlt2Icon, UserCircleIcon } from "@heroicons/vue/outline"; +import ws from "@/services/ws"; export default { name: "NavBar.vue", - + data() { + return { + newMessages: 0, + }; + }, + computed: { + notifications() { + // if new messages is greater than 99 show +99 + if (this.newMessages > 99) { + return "+99"; + } else { + return this.newMessages; + } + }, + }, components: { PlusIcon, ChatAlt2Icon, @@ -59,8 +77,42 @@ export default { await this.$router.push("/login"); } }, + loadMessages() { + this.newMessages = 0; + this.$router.push("/messages"); + }, + }, + created() { + ws.on( + "NEW_MESSAGE", + () => { + if (this.$router.currentRoute.value.name == "messages") return; + this.newMessages += 1; + }, + "header" + ); }, }; </script> -<style scoped></style> +<style scoped> +.notification-container { + position: relative; +} +.notification { + position: absolute; + background-color: #ff5a5f; + top: 0; + min-width: 20px; + min-height: 20px; + padding: 0.25rem; + transform: translate(-80%, -30%); + color: white; + font-size: 10px; + border-radius: 50%; + font-weight: bold; + text-align: center; + right: 0; + cursor: pointer; +} +</style> diff --git a/src/components/BaseComponents/NotificationsForm.vue b/src/components/BaseComponents/NotificationsForm.vue deleted file mode 100644 index b142fb2a8e36ec57b52256194cc35de433f71d63..0000000000000000000000000000000000000000 --- a/src/components/BaseComponents/NotificationsForm.vue +++ /dev/null @@ -1,11 +0,0 @@ -<template> - <div>This is a notification page</div> -</template> - -<script> -export default { - name: "NotificationsForm", -}; -</script> - -<style scoped></style> diff --git a/src/components/BaseComponents/PaginationTemplate.vue b/src/components/BaseComponents/PaginationTemplate.vue index b6907f178de39c1dace54149174e972e8e764b4b..939d7350598667c7e2710346f3ba80bb83f96748 100644 --- a/src/components/BaseComponents/PaginationTemplate.vue +++ b/src/components/BaseComponents/PaginationTemplate.vue @@ -7,7 +7,9 @@ > Forrige </span> - <label class="mx-2">{{ currentPage + 1 }} av {{ totalPages() }}</label> + <label class="mx-2 text-primary-light" + >{{ currentPage + 1 }} av {{ totalPages() }}</label + > <span v-if="showNextLink()" class="cursor-pointer inline-flex items-center p-2 text-sm font-medium text-primary-light bg-white rounded-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700" diff --git a/src/components/BaseComponents/RatingModal.vue b/src/components/BaseComponents/RatingModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..370a997b86685b2474854ea94c983e8549a113dc --- /dev/null +++ b/src/components/BaseComponents/RatingModal.vue @@ -0,0 +1,200 @@ +<template> + <!-- Main modal --> + <div + v-if="visible" + class="fixed grid place-items-center bg-gray-600 bg-opacity-50 top-0 left-0 right-0 z-50 w-full overflow-x-hidden overflow-y-auto inset-0 h-full" + > + <div class="relative w-full h-full max-w-2xl p-4 md:h-auto"> + <!-- Modal content --> + <div class="relative bg-white rounded-lg shadow dark:bg-gray-700"> + <!-- Modal header --> + <div class="flex p-4 border-b rounded-t dark:border-gray-600"> + <h3 class="text-xl font-semibold text-gray-900 dark:text-white"> + {{ name }} + </h3> + <button + @click="close()" + class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" + > + <svg + class="w-5 h-5" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + fill-rule="evenodd" + d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" + clip-rule="evenodd" + ></path> + </svg> + </button> + </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" + > + {{ title }} + </p> + </div> + + <div class="ml-6 mt-4"> + <p + class="text-base leading-relaxed text-gray-500 dark:text-gray-400" + v-show="renterIsReceiverOfRating" + > + Gi en vurdering til utleieren + </p> + <p + class="text-base leading-relaxed text-gray-500 dark:text-gray-400" + v-show="!renterIsReceiverOfRating" + > + Gi en vurdering til leietakeren + </p> + </div> + + <div class="flex justify-center px-4"> + <textarea + class="w-full h-40 bg-gray-200 mb-4 ring-1 ring-gray-400 rounded-xl" + /> + </div> + + <div class="flex items-center justify-center mb-8"> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[0]" + @click="setRating(1)" + 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> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[1]" + @click="setRating(2)" + 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> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[2]" + @click="setRating(3)" + 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> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[3]" + @click="setRating(4)" + 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> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[4]" + @click="setRating(5)" + 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> + </div> + + <div class="flex justify-center mb-4"> + <Button :text="'Send en vurdering'" @click="sendRating"></Button> + </div> + + <!-- Modal footer --> + <div class="rounded-b border-t border-gray-200 dark:border-gray-600"> + <!-- Slot: Add any html you want here --> + <slot /> + </div> + </div> + </div> + </div> +</template> + +<script> +import Button from "@/components/BaseComponents/ColoredButton"; +import { postNewRating } from "@/utils/apiutil"; + +export default { + name: "RatingModal", + data() { + return { + score: 3, + comment: "", + rating: [ + "text-warn", + "text-warn", + "text-warn", + "text-gray-300", + "text-gray-300", + ], + }; + }, + props: { + visible: Boolean, + name: String, + title: String, + rentID: Number, + renterIsReceiverOfRating: Boolean, + }, + + components: { + Button, + }, + methods: { + setRating(ratingNumber) { + this.score = ratingNumber; + for (let i = 0; i < 5; i++) { + if (i < ratingNumber) { + this.rating[i] = "text-warn"; + } else { + this.rating[i] = "text-gray-300"; + } + } + }, + close() { + this.$emit("close"); + }, + async sendRating() { + const ratingInfo = { + score: this.score, + comment: this.comment, + renterIsReceiverOfRating: this.renterIsReceiverOfRating, + rentID: this.rentID, + }; + + const postResponse = await postNewRating(ratingInfo); + + console.log("posted: " + postResponse); + + this.$router.push("/"); + }, + }, +}; +</script> diff --git a/src/components/BaseComponents/TripleDotButton.vue b/src/components/BaseComponents/TripleDotButton.vue new file mode 100644 index 0000000000000000000000000000000000000000..5205015a39798c6af46dae4b99af4f6fbb826172 --- /dev/null +++ b/src/components/BaseComponents/TripleDotButton.vue @@ -0,0 +1,24 @@ +<template> + <button + id="dropdownDefault" + data-dropdown-toggle="dropdown" + class="w-10 h-10 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 float-right 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> +</template> +<script> +export default { + name: "TripleDotButton", +}; +</script> diff --git a/src/components/ChatComponents/ChatComponent.vue b/src/components/ChatComponents/ChatComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..6b2252b6a241430f9cc7e2dd91718b9817713a9f --- /dev/null +++ b/src/components/ChatComponents/ChatComponent.vue @@ -0,0 +1,275 @@ +<template> + <div class="chat-container"> + <div class="header"> + <div + v-on:click="openHamburgerMethod" + class="hamburger grid space-y-2 content-center m-3" + > + <div class="w-8 h-0.5 bg-gray-600"></div> + <div class="w-8 h-0.5 bg-gray-600"></div> + <div class="w-8 h-0.5 bg-gray-600"></div> + </div> + <div class="flex"> + <img class="pfp" :src="this.src" alt="Profile Picture" /> + <h1>{{ name }}</h1> + </div> + <div></div> + </div> + <div v-if="recipient" class="conversation"> + <div v-for="(message, i) in messages" v-bind:key="i"> + <rental-message + v-if="message?.createdAt" + :rent="message" + ></rental-message> + <ChatMessage v-else :message="message"></ChatMessage> + </div> + </div> + <div v-else class="conversation"> + <div class="not-found"> + <p>USER NOT FOUND</p> + </div> + </div> + <div + class="flex items-center justify-between w-full p-3 border-t border-gray-300" + > + <input + v-on:keyup.enter="sendMessage" + type="text" + placeholder="Message" + class="block w-full py-2 pl-4 mx-3 bg-gray-100 rounded-full outline-none focus:text-gray-700" + name="message" + v-model="message" + /> + <button v-on:click="sendMessage" style="padding: 10px; color: red"> + <svg + class="w-5 h-5 text-gray-500 origin-center transform rotate-90" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + > + <path + d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" + /> + </svg> + </button> + </div> + </div> +</template> + +<script> +import ChatMessage from "./ChatMessage.vue"; +import axios from "axios"; +import ws from "@/services/ws"; +import RentalMessage from "./RentalMessage.vue"; +export default { + props: { + openHamburger: { type: Function }, + recipientID: { + type: String, + }, + }, + data() { + return { + canScroll: true, + scrollBehavior: "", + recipient: null, + rents: [], + message: "", + msgs: [], + }; + }, + components: { + ChatMessage, + RentalMessage, + }, + computed: { + name() { + return this.recipient + ? this.recipient.firstName + " " + this.recipient.lastName + : "N/A"; + }, + src() { + return ( + this.recipient?.picture || + require("@/assets/defaultUserProfileImage.jpg") + ); + }, + messages() { + let arr = [...this.msgs, ...this.rents].sort((a, b) => { + return (a?.createdAt || a.timestamp) - (b?.createdAt || b.timestamp); + }); + return arr; + }, + }, + methods: { + openHamburgerMethod() { + this.$emit("openHamburger"); + }, + scroll() { + let container = this.$el.querySelector(".conversation"); + container.scrollTop = container.scrollHeight; + }, + async reloadMessages() { + const token = this.$store.state.user.token; + const response = await fetch( + `${process.env.VUE_APP_BASEURL}chats/users/${this.recipientID}/messages`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + this.msgs = await response.json(); + }, + async sendMessage() { + if (!this.recipient || this.message == null || this.message == "") return; + this.canScroll = true; + const token = this.$store.state.user.token; + await axios.post( + process.env.VUE_APP_BASEURL + + `chats/users/${this.recipientID}/messages`, + { + message: this.message, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + this.message = ""; + ws.sendMessage({ + sender: parseInt(this.userid), + recipient: this.recipientID, + }); + await this.reloadMessages(); + }, + async reloadRents() { + const token = this.$store.state.user.token; + const response = await fetch( + `${process.env.VUE_APP_BASEURL}renting/user/${this.recipientID}/all`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + this.rents = await response.json(); + }, + async getRecipient() { + const token = this.$store.state.user.token; + const response = await fetch( + `${process.env.VUE_APP_BASEURL}users/${this.recipientID}/profile`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + this.recipient = await response.json(); + }, + }, + watch: { + async recipientID() { + this.rents = []; + this.msgs = []; + await this.reloadMessages(); + await this.getRecipient(); + await this.reloadRents(); + }, + }, + async created() { + await this.reloadMessages(); + await this.getRecipient(); + await this.reloadRents(); + + ws.on( + "NEW_MESSAGE", + () => { + this.reloadMessages(); + }, + "chat" + ); + }, + updated() { + if (this.canScroll) this.scroll(); + this.canScroll = false; + this.scrollBehavior = "smooth"; + }, + unmounted() { + ws.end("NEW_MESSAGE", "chat"); + }, +}; +</script> + +<style scoped> +.hamburger { + display: none; +} + +.chat-container { + display: flex; + flex-direction: column; + height: 100%; + width: auto; +} + +.not-found { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.not-found p { + font-size: 2rem; + font-weight: bold; +} +.conversation { + height: 100%; + width: 100%; + padding: 1.25rem; + overflow-y: scroll; + scroll-behavior: v-bind(scrollBehavior); +} + +.header { + display: flex; + flex-direction: row; + padding: 0.75rem; + border-bottom: 1px solid black; +} + +.header h1 { + align-self: center; + margin-left: 10px; + color: #4b5563; + font-weight: bold; +} + +.pfp { + object-fit: cover; + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; +} + +@media screen and (max-width: 600px) { + .header { + justify-content: space-between; + } + + .hamburger { + display: block; + } +} +</style> diff --git a/src/components/ChatComponents/ChatMessage.vue b/src/components/ChatComponents/ChatMessage.vue index 73ded12cc8407dddc21f12614e01b2b8eac71b07..3490083e12afbff3702aabc031b2173e9803e417 100644 --- a/src/components/ChatComponents/ChatMessage.vue +++ b/src/components/ChatComponents/ChatMessage.vue @@ -1,22 +1,34 @@ <template> - <div v-bind:class="'w-full break-words flex ' + side()"> - <div - style="max-width: 70%" - v-bind:class=" - this.color() + ' rounded px-5 py-2 my-2 relative ' + this.textColor() - " - > - <span class="block" - >{{ this.message.content }} {{ this.message.from }}</span - > - <span class="block text-xs text-right">{{ this.calculateTime() }}</span> + <div v-bind:class="'blob-container ' + this.side()"> + <div v-bind:class="this.color() + ' message-container ' + this.textColor()"> + <span class="message">{{ this.message.content }}</span> + <span class="">{{ this.calculateTime() }}</span> </div> </div> </template> +<style scoped> +.blob-container { + display: flex; + max-width: 100%; +} + +.message { + word-break: break-word; + display: block; +} + +.message-container { + border-radius: 10px; + max-width: 70%; + padding: 0.75rem; + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} +</style> <script> import { parseCurrentUser } from "@/utils/token-utils"; - +//block text-xs text-right export default { props: { message: Object, @@ -46,13 +58,59 @@ export default { : "justify-end"; }, calculateTime() { - //let time = this.message.from; - // Calculate time when message was sent - let date = new Date(this.message.timestamp); - let hours = date.getHours(); - let minutes = "0" + date.getMinutes(); - let formattedTime = hours + ":" + minutes.substr(-2); - return formattedTime; + var time = this?.message.timestamp; + var date = new Date(time); + //Todo add timing for mm and hh and week of message + var mmOfMessage = String(date.getMinutes()); + var hhOfMessage = String(date.getHours()); + var ddOfMessage = String(date.getDate()).padStart(2, "0"); + var dayOfMessage = date.toLocaleString("default", { weekday: "short" }); + var monthOfMessage = String(date.getMonth() + 1).padStart(2, "0"); //January is 0! + const shortMonthOfMessage = date.toLocaleString("default", { + month: "short", + }); + var yyyyOfMessage = date.getFullYear(); + + var today = new Date(); + var dd = String(today.getDate()).padStart(2, "0"); + var mm = String(today.getMonth() + 1).padStart(2, "0"); //January is 0! + var yyyy = today.getFullYear(); + if (ddOfMessage == dd) { + return "" + hhOfMessage + ":" + mmOfMessage + ""; + } else if (this.isDateInThisWeek(date)) { + return "" + dayOfMessage + " " + hhOfMessage + ":" + mmOfMessage; + } else if (monthOfMessage == mm) { + return "" + ddOfMessage + " " + hhOfMessage + ":" + mmOfMessage; + } else if (yyyyOfMessage == yyyy) { + return "" + shortMonthOfMessage + " " + ddOfMessage; + } + return ( + shortMonthOfMessage + " " + ddOfMessage + " " + yyyyOfMessage + "" + ); + + /* + Take timestamp and display date when message was sent + If message was sent this day show time (HH:MM) (13:00) + If message was sent this week show day of the week and time (DDD HH:MM) (Mon 13:00) + If message was sent this month show day of the month, date and time (DD HH:MM) (13 13:00) + If message was sent this year show month and day of the month (MMM DD) (Jan 13) + If message was sent more than a year ago show year with date (MMM DD YYYY) (Jan 13 2020) + */ + }, + isDateInThisWeek(date) { + const todayObj = new Date(); + const todayDate = todayObj.getDate(); + const todayDay = todayObj.getDay(); + + // get first date of week + const firstDayOfWeek = new Date(todayObj.setDate(todayDate - todayDay)); + + // get last date of week + const lastDayOfWeek = new Date(firstDayOfWeek); + lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6); + + // if date is equal or within the first and last dates of the week + return date >= firstDayOfWeek && date <= lastDayOfWeek; }, }, }; diff --git a/src/components/ChatComponents/ChatProfile.vue b/src/components/ChatComponents/ChatProfile.vue index 60c544f802a2384cf9e665c986d2fc24711ab197..db4c47cad8cc00e943159362ca45f4dc1ee7df65 100644 --- a/src/components/ChatComponents/ChatProfile.vue +++ b/src/components/ChatComponents/ChatProfile.vue @@ -5,7 +5,7 @@ > <img class="h-10 w-10 rounded-full object-cover" - src="https://www.mintface.xyz/content/images/2021/08/QmTndiF423kjdXsNzsip1QQkBQqDuzDhJnGuJAXtv4XXiZ-1.png" + :src="src || 'S../../assets/defaultUserProfileImage.jpg'" :alt="{ name }" /> <div class="w-full pb-2"> @@ -47,12 +47,40 @@ export default { ); }, lastMessageTime() { - return "5 min"; + // Calculate time since last message + const time = this.conversation.lastMessage.timestamp; + const now = new Date(); + const diff = now - time; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const months = Math.floor(days / 30); + const years = Math.floor(months / 12); + + if (seconds < 60) { + return "Just now"; + } else if (minutes < 60) { + return minutes + " minutes ago"; + } else if (hours < 24) { + return hours + " hours ago"; + } else if (days < 30) { + return days + " days ago"; + } else if (months < 12) { + return months + " months ago"; + } else { + return years + " years ago"; + } + }, + src() { + return this.conversation.recipient.picture + ? this.conversation.recipient.picture + : require("@/assets/defaultUserProfileImage.jpg"); }, }, methods: { selectUser() { - this.$emit("recipient", this.conversation.recipient.userId); + this.$emit("recipient", this.conversation?.recipient.userId); }, }, }; diff --git a/src/components/ChatComponents/ChatsComponent.vue b/src/components/ChatComponents/ChatsComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..f2cc2b00b788c5154aa5e137ae3ff1f9cc1482ab --- /dev/null +++ b/src/components/ChatComponents/ChatsComponent.vue @@ -0,0 +1,181 @@ +<template> + <div class="chat"> + <div class="conversations"> + <h1>Meldinger</h1> + <hr /> + <ChatProfile + v-for="(conversation, i) in conversations" + :conversation="conversation" + :key="i" + @recipient="selectUser" + ></ChatProfile> + <!-- If no conversatiosn show no conversations found --> + <div v-if="conversations.length === 0" class="no-conversations"> + <p>Ingen samtaler</p> + </div> + </div> + <div class="current-chat"> + <ChatComponent + @openHamburger="openHamburger" + v-if="recipientID" + :recipientID="recipientID" + ></ChatComponent> + <div v-else class="nothing-selected"> + <p>Select a user to start a chat</p> + </div> + </div> + </div> +</template> + +<script> +import ChatProfile from "./ChatProfile.vue"; +import ChatComponent from "./ChatComponent.vue"; +import { parseCurrentUser } from "@/utils/token-utils"; +import ws from "@/services/ws"; + +export default { + props: {}, + watch: { + $route() { + const newVal = this.$route.query?.userID || null; + this.recipientID = newVal; + }, + }, + data: () => { + return { + messages: [], + message: "", + recipient: null, + hambuger: "none", + conversations: [], + hambugerDisplay: "none", + recipientID: null, + }; + }, + components: { ChatProfile, ChatComponent }, + computed: { + userid() { + return parseCurrentUser().accountId; + }, + name() { + return this.recipient.firstName + " " + this.recipient.lastName; + }, + }, + methods: { + selectUser(recipientID) { + this.hambugerDisplay = "none"; + this.$router.push({ path: "messages", query: { userID: recipientID } }); + }, + openHamburger() { + this.hambugerDisplay = "block"; + }, + calculateSide(from) { + return from == this.userid ? "end" : "start"; + }, + async fetchChats() { + const token = this.$store.state.user.token; + // Get all conversations from api with /chats/users + const conResponse = await fetch( + `${process.env.VUE_APP_BASEURL}chats/users`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); // add error handling + this.conversations = await conResponse.json(); + }, + }, + async created() { + await this.fetchChats(); + ws.on( + "NEW_MESSAGE", + async () => { + await this.fetchChats(); + }, + "chats" + ); + this.recipientID = this.$route.query?.userID || null; + if (!this.recipientID) this.hambugerDisplay = "block"; + }, + unmounted() { + ws.end("NEW_MESSAGE", "chats"); + }, +}; +</script> +<style scoped> +.chat { + display: flex; + flex-direction: row; + width: 100%; + height: min(100vh - 3.5rem); +} + +.nothing-selected { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.no-conversations { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.nothing-selected p { + font-size: 2rem; + font-weight: bold; + text-align: center; +} + +.current-chat { + width: 100%; + height: 100%; +} + +.conversations { + min-width: 300px; + border-right-width: 1px; + border-color: black; + height: 100%; +} + +.conversations h1 { + padding: 0.5rem; + padding-left: 0; + align-self: center; + margin-left: 10px; + color: #4b5563; + font-weight: bold; + font-size: large; +} + +@media screen and (max-width: 600px) { + .conversations { + display: v-bind(hambugerDisplay); + z-index: 99; + width: 100%; + position: absolute; + background-color: white; + } + + .conversations h1 { + text-align: center; + } +} + +.button { + display: flex; + justify-content: center; + padding: 0.75rem; +} +</style> diff --git a/src/components/ChatComponents/RentalMessage.vue b/src/components/ChatComponents/RentalMessage.vue new file mode 100644 index 0000000000000000000000000000000000000000..8dab267ac5d9db895be02f6b04bcd3102b160c28 --- /dev/null +++ b/src/components/ChatComponents/RentalMessage.vue @@ -0,0 +1,179 @@ +<template> + <div class="message" style="max-width: 70%"> + <div class="info"> + <div class="text"> + <h2 class="header">Ny utleie forespørsel</h2> + <p>Dato: {{ from }} - {{ to }}</p> + <p>Pris: {{ price }}kr</p> + </div> + <div class="img-container"> + <img class="img" :src="img" alt="Produkt Bilde" /> + </div> + </div> + <div> + <p class="more-header">Melding fra leier:</p> + <div class="more"> + <p> + {{ extra }} + </p> + </div> + </div> + <div class="buttons" v-if="!rent.isAccepted && !rent.deleted"> + <button class="button green" @click="accept">Godta</button> + <button class="button red" @click="reject">Avslå</button> + </div> + <div class="" v-if="rent.isAccepted"> + <h1 class="green">Godtatt</h1> + </div> + <div class="" v-if="rent.deleted"> + <h1 class="red">Avslått</h1> + </div> + </div> +</template> + +<script> +import axios from "axios"; +import { tokenHeader } from "@/utils/token-utils"; +export default { + props: { + rent: { + type: Object, + required: true, + }, + }, + computed: { + img() { + return "https://images.unsplash.com/photo-1453728013993-6d66e9c9123a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8dmlld3xlbnwwfHwwfHw%3D&w=1000&q=80"; //this.rent.listing.imageUrl; + }, + from() { + // take ms and turn into date and return date + return new Date(this.rent.fromTime).toLocaleDateString(); + }, + to() { + return new Date(this.rent.toTime).toLocaleDateString(); + }, + price() { + // Calculate price from price * days + return ( + this.rent.listing.pricePerDay * + Math.ceil( + (this.rent.toTime - this.rent.fromTime) / (1000 * 60 * 60 * 24) + ) + ); + }, + extra() { + return this.rent.message || "Ingen Melding"; + }, + }, + methods: { + async accept() { + await axios.put( + process.env.VUE_APP_BASEURL + `renting/${this.rent.rentId}/accept`, + null, + { headers: tokenHeader() } + ); + }, + async reject() { + await axios.delete( + process.env.VUE_APP_BASEURL + `renting/${this.rent.rentId}/delete`, + null, + { headers: tokenHeader() } + ); + }, + }, +}; +</script> + +<style scoped> +.message { + display: flex; + flex-direction: column; + width: 100%; + background: #d1d5db; + border-radius: 10px; + padding: 10px; +} + +.info { + display: flex; + flex-direction: row; + justify-content: space-around; +} +.img { + width: 100%; + height: auto; + border-radius: 10px; + max-height: 200px; +} + +.text h1 { + text-decoration: underline; +} + +@media (max-width: 1200px) { + .img-container { + max-width: 30%; + } + .text { + min-width: 70%; + } +} + +.img-container { + display: flex; + align-content: center; + justify-items: center; + max-height: 20%; +} +.header { + font-weight: bold; + text-align: center; + text-decoration: underline; +} +.more-header { + font-weight: bold; +} + +.more { + border: 1px solid black; + border-radius: 10px; +} + +.more p { + margin-left: 5px; +} + +.buttons { + margin-top: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-items: center; + justify-content: space-around; +} + +.button { + border-radius: 0.25rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + font-weight: 700; + color: white; +} + +.red { + background-color: #ff1f1f; +} + +.red:hover { + box-shadow: 0 0 0 0.2rem rgba(255, 31, 31, 0.5); +} + +.green { + background-color: #0bb610; +} +.green:hover { + box-shadow: 0 0 0 0.2rem rgba(11, 182, 16, 0.5); +} +</style> diff --git a/src/components/CommunityComponents/CommunityHamburger.vue b/src/components/CommunityComponents/CommunityHamburger.vue index 391a9ab232be3ec69eefd513b060f274e7dcd1b4..7754ce6eb97658c51124f5bd2b476e6b14a32c0e 100644 --- a/src/components/CommunityComponents/CommunityHamburger.vue +++ b/src/components/CommunityComponents/CommunityHamburger.vue @@ -27,7 +27,7 @@ </li> <li id="leaveGroup"> <div - class="cursor-pointer block py-2 px-4 text-sm text-error hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + class="cursor-pointer block py-2 px-4 text-sm text-error-medium hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" @click="leaveCommunity" > Forlat Gruppe diff --git a/src/components/CommunityComponents/CommunityHeader.vue b/src/components/CommunityComponents/CommunityHeader.vue index 4df062434b011c1515a9be2f159dc8ab200db754..eba31718a765b9adfc08bd6b449dbff4c3de28a1 100644 --- a/src/components/CommunityComponents/CommunityHeader.vue +++ b/src/components/CommunityComponents/CommunityHeader.vue @@ -1,6 +1,7 @@ <template> - <!-- TODO PUT A LOADER HERE --> - <div v-if="loading">LASTER...</div> + <div v-if="loading" class="flex place-content-center mx-4"> + <LoaderSpinner /> + </div> <div v-else class="flex items-center justify-between mx-4"> <router-link :to="'/community/' + community.communityId" @@ -83,6 +84,7 @@ <script> import CommunityHamburger from "@/components/CommunityComponents/CommunityHamburger"; import ColoredButton from "@/components/BaseComponents/ColoredButton"; +import LoaderSpinner from "@/components/BaseComponents/LoaderSpinner"; import CommunityService from "@/services/community.service"; import CustomFooterModal from "@/components/BaseComponents/CustomFooterModal"; import { parseCurrentUser } from "@/utils/token-utils"; @@ -97,6 +99,7 @@ export default { CommunityHamburger, ColoredButton, CustomFooterModal, + LoaderSpinner, }, computed: { userid() { @@ -142,9 +145,6 @@ export default { } }, }, - // beforeMount() { - // this.getIfUserInCommunity(); - // }, async created() { await this.load(); this.loading = false; diff --git a/src/components/CommunityComponents/CommunityHome.vue b/src/components/CommunityComponents/CommunityHome.vue index 53fb6047e7def08264fdf5e66d51bed172a93dee..5ba88fc1c7d94bcb9c69fbe4f17c322cc50a13e9 100644 --- a/src/components/CommunityComponents/CommunityHome.vue +++ b/src/components/CommunityComponents/CommunityHome.vue @@ -1,79 +1,89 @@ <template> - <section class="w-full px-5 py-4 mx-auto rounded-md"> - <CommunityHeader :admin="false" class="mb-5" /> - - <!-- Search field --> - <div class="relative" id="searchComponent"> - <span class="absolute inset-y-0 left-0 flex items-center pl-3"> - <svg class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none"> - <path - d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" - stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" - ></path> - </svg> - </span> - - <input - type="text" - id="searchInput" - class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-primary-medium dark:focus:border-primary-medium focus:outline-none focus:ring" - placeholder="Search" - v-model="search" - @change="searchWritten" - /> + <div> + <div v-if="loading" class="flex place-content-center"> + <LoaderSpinner /> </div> + <section v-else class="w-full px-5 py-4 mx-auto rounded-md"> + <CommunityHeader :admin="false" class="mb-5" /> - <div class="absolute inset-x-0 px-5 py-3"> - <!-- ItemCards --> - <div class="flex items-center justify-center w-screen"> - <!-- Shows items based on pagination --> - <div - class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" - v-if="showItems" - > - <ItemCard - v-for="item in visibleItems" - :key="item" - :item="item" - @click="goToItemInfoPage(item.listingID)" - /> - </div> + <div v-if="!items.length" class="flex place-content-center text-gray-400"> + Ingen gjenstander å vise 🥺 👉👈 + </div> + <div v-else> + <!-- Search field --> + <div class="relative" id="searchComponent"> + <span class="absolute inset-y-0 left-0 flex items-center pl-3"> + <svg class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none"> + <path + d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + ></path> + </svg> + </span> - <!-- Shows items based on search field input --> - <div - class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full place-items-center" - v-if="showSearchedItems" - > - <ItemCard - v-for="item in searchedItems" - :key="item" - :item="item" - @click="goToItemInfoPage(item.listingID)" + <input + type="text" + class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-primary-medium dark:focus:border-primary-medium focus:outline-none focus:ring" + placeholder="Search" + v-model="search" + @change="searchWritten" /> </div> - </div> - <!-- pagination --> - <div class="flex justify-center" v-if="showItems"> - <PaginationTemplate - v-bind:items="items" - v-on:page:update="updatePage" - v-bind:currentPage="currentPage" - v-bind:pageSize="pageSize" - class="mt-10" - /> + <div class="absolute inset-x-0 px-5 py-3"> + <!-- ItemCards --> + <div class="flex items-center justify-center w-screen"> + <!-- Shows items based on pagination --> + <div + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" + v-if="showItems" + > + <ItemCard + v-for="item in visibleItems" + :key="item" + :item="item" + @click="goToItemInfoPage(item.listingID)" + /> + </div> + + <!-- Shows items based on search field input --> + <div + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full place-items-center" + v-if="showSearchedItems" + > + <ItemCard + v-for="item in searchedItems" + :key="item" + :item="item" + @click="goToItemInfoPage(item.listingID)" + /> + </div> + </div> + + <!-- pagination --> + <div class="flex justify-center" v-if="showItems"> + <PaginationTemplate + v-bind:items="items" + v-on:page:update="updatePage" + v-bind:currentPage="currentPage" + v-bind:pageSize="pageSize" + class="mt-10" + /> + </div> + </div> </div> - </div> - </section> + </section> + </div> </template> <script> import ItemCard from "@/components/ItemComponents/ItemCard"; import CommunityHeader from "@/components/CommunityComponents/CommunityHeader"; import PaginationTemplate from "@/components/BaseComponents/PaginationTemplate"; +import LoaderSpinner from "@/components/BaseComponents/LoaderSpinner"; import { GetCommunity, @@ -81,11 +91,12 @@ import { getItemPictures, } from "@/utils/apiutil"; export default { - name: "SearchItemListComponent", + name: "CommunityHome", components: { CommunityHeader, ItemCard, PaginationTemplate, + LoaderSpinner, }, computed: { searchedItems() { @@ -108,6 +119,7 @@ export default { }, data() { return { + search: "", items: [], item: { listingID: 0, @@ -123,6 +135,8 @@ export default { showItems: true, showSearchedItems: false, + loading: false, + //Variables connected to pagination currentPage: 0, pageSize: 12, @@ -181,9 +195,11 @@ export default { }, }, async beforeMount() { + this.loading = true; await this.getCommunityFromAPI(); //To get the id of the community before mounting the view await this.getListingsOfCommunityFromAPI(); this.updateVisibleTodos(); + this.loading = false; }, }; </script> diff --git a/src/components/CommunityComponents/CommunityList.vue b/src/components/CommunityComponents/CommunityList.vue index 97d25120249632f9d395a35f80e66c338125e0e3..b6641f92549da76ed1f956e600842d9b07615eb5 100644 --- a/src/components/CommunityComponents/CommunityList.vue +++ b/src/components/CommunityComponents/CommunityList.vue @@ -1,14 +1,20 @@ <template> - <ul> + <p v-if="!communities.length" class="flex place-content-center text-gray-400"> + Ingen grupper + </p> + <ul v-else> <li v-for="community in communities" :key="community"> - <CommunityListItem :community="community" :member="member" /> + <CommunityListItem + :community="community" + :member="member" + class="border-black" + /> </li> </ul> </template> <script> import CommunityListItem from "@/components/CommunityComponents/CommunityListItem.vue"; -//import Join export default { name: "CommunityList", diff --git a/src/components/CommunityComponents/CommunityListItem.vue b/src/components/CommunityComponents/CommunityListItem.vue index 1dc94f4e3421de11b7367b51ad8c12eae11a242b..40d90645e430cd49f104c91c7def3e125b9721fb 100644 --- a/src/components/CommunityComponents/CommunityListItem.vue +++ b/src/components/CommunityComponents/CommunityListItem.vue @@ -1,68 +1,85 @@ <template> - <CustomFooterModal - @close="this.dialogOpen = false" - :visible="dialogOpen" - :title="community.name" - :message="community.description" - > - <div class="flex justify-center p-2"> - <!-- If a user is not a member in the community, this button will show --> - <ColoredButton - v-if="!member && community.visibility !== 0" - :text="'Bli med'" - @click="goToJoin(community.communityId)" - class="m-2" - /> + <div> + <CustomFooterModal + @close="this.dialogOpen = false" + :visible="dialogOpen" + :title="community.name" + :message="community.description" + > + <div class="flex justify-center p-2"> + <!-- If a user is not a member in the community, this button will show --> + <ColoredButton + v-if="!member && community.visibility !== 0" + :text="'Bli med'" + @click="goToJoin(community.communityId)" + class="m-2" + /> - <ColoredButton - v-if="!member && community.visibility === 0" - :text="'Spør om å bli med'" - @click="goToRequest(community.communityId)" - class="m-2" - /> + <ColoredButton + v-if="!member && community.visibility === 0" + :text="'Spør om å bli med'" + @click="goToRequest(community.communityId)" + class="m-2" + /> - <!-- If a user is member this button will show --> - <ColoredButton - v-if="member" - :text="'Gå til'" - @click="goToGroup(community.communityId)" - class="m-2" - /> + <!-- If a user is member this button will show --> + <ColoredButton + v-if="member" + :text="'Gå til'" + @click="goToGroup(community.communityId)" + class="m-2" + /> - <!-- If a user isn't member but the community is open, the user is able to get in to see listings(items) --> - <ColoredButton - v-if="!member && community.visibility === 1" - :text="'Gå til'" - @click="goToGroup(community.communityId)" - class="m-2" - /> - </div> + <!-- If a user isn't member but the community is open, the user is able to get in to see listings(items) --> + <ColoredButton + v-if="!member && community.visibility === 1" + :text="'Gå til'" + @click="goToGroup(community.communityId)" + class="m-2" + /> + </div> - <!-- If a user is not logges in and tries to join a community, this message shows --> - <div class="flex justify-center p-2"> - <p class="text-base leading-relaxed text-gray-500 dark:text-gray-400"> - {{ responseToUser }} - </p> - </div> - </CustomFooterModal> - <div - @click="toggleDialog()" - class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-4" - > - <div class="h-10 w-10 flex flex-col justify-center items-center mr-4"> - <UserGroupIcon v-if="!community.image" alt="Felleskapets bilde" /> - <!-- TODO: USE COMMUNITY IMAGE <img alt="Felleskapets bilde" src="@/assets/group.png" /> --> - </div> - <div class="flex-1 pl-1 overflow-hidden"> - <div class="font-medium dark:text-white truncate"> - {{ community.name }} + <!-- If a user is not logges in and tries to join a community, this message shows --> + <div class="flex justify-center p-2"> + <p class="text-base leading-relaxed text-gray-500 dark:text-gray-400"> + {{ responseToUser }} + </p> + </div> + </CustomFooterModal> + <div + @click="toggleDialog()" + class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" + > + <div + v-if="!community.picture" + class="h-10 w-10 flex flex-col justify-center items-center ml-2 mr-2" + > + <UserGroupIcon + alt="Felleskapets bilde" + class="h-10 w-10 text-primary-dark" + /> + </div> + <div + v-else + class="h-3 w-14 flex flex-col justify-center items-center ml-2 mt-4 mb-4 mr-2" + > + <img + :src="community.picture" + alt="Fellsekaps bilde" + class="rounded-md" + /> + </div> + <div class="flex-1 pl-1 overflow-hidden"> + <div class="font-medium text-gray-800 dark:text-white truncate"> + {{ community.name }} + </div> + </div> + <div class="flex flex-row justify-center items-center"> + <LockClosedIcon + v-if="community.visibility === 0" + class="max-h-6 max-w-6 shrink m-2" + /> </div> - </div> - <div class="flex flex-row justify-center items-center"> - <LockClosedIcon - v-if="community.visibility === 0" - class="max-h-6 max-w-6 shrink m-2" - /> </div> </div> </template> diff --git a/src/components/CommunityComponents/CommunityRequestForm.vue b/src/components/CommunityComponents/CommunityRequestForm.vue index 7c5f430106fee65e8a424de790f41ec1df7c4fc5..0ea59b7ccf360f9745e9e87c7b407eeb61df68ed 100644 --- a/src/components/CommunityComponents/CommunityRequestForm.vue +++ b/src/components/CommunityComponents/CommunityRequestForm.vue @@ -41,6 +41,14 @@ <div class="flex justify-center mt-10 float-right"> <Button @click="saveClicked" id="saveButton" :text="'Send'"> </Button> </div> + + <notification-modal + @click="routeToHome" + :visible="sendRequestClicked" + :title="'Vellykket'" + :message="'Forespørsel sendt!'" + > + </notification-modal> </div> </template> @@ -51,12 +59,14 @@ import { required, helpers, maxLength } from "@vuelidate/validators"; import Button from "@/components/BaseComponents/ColoredButton"; import { tokenHeader } from "@/utils/token-utils"; import { GetCommunity } from "@/utils/apiutil"; +import NotificationModal from "@/components/BaseComponents/NotificationModal"; export default { name: "CommunityRequestForm.vue", components: { Button, + NotificationModal, }, setup() { return { v$: useVuelidate() }; @@ -81,10 +91,14 @@ export default { message: "", communityId: null, community: {}, + sendRequestClicked: false, }; }, computed: {}, methods: { + routeToHome() { + this.$router.push("/"); + }, //TODO fix so that community id is set (not null) async saveClicked() { this.communityID = await this.$router.currentRoute.value.params @@ -96,6 +110,8 @@ export default { { message: this.message }, { headers: tokenHeader() } ); + + this.sendRequestClicked = true; }, getCommunityFromAPI: async function () { this.communityID = await this.$router.currentRoute.value.params diff --git a/src/components/CommunityComponents/MemberList.vue b/src/components/CommunityComponents/MemberList.vue index 336481461284383d1e4c90bcb0408b28b1a8c468..3818146ff63024c1dab742805231a0e0baa582c9 100644 --- a/src/components/CommunityComponents/MemberList.vue +++ b/src/components/CommunityComponents/MemberList.vue @@ -1,6 +1,5 @@ <template> - <div v-if="loading">LASTER...</div> - <ul v-else> + <ul> <li v-for="member in members" :key="member.userId"> <UserListItemCard :buttons="buttons" :user="member" /> </li> diff --git a/src/components/CommunityComponents/NewCommunityForm.vue b/src/components/CommunityComponents/NewCommunityForm.vue index 54f3ba8e608a6a9041f97406552ffb82f08604f9..7a62e90f302416559f695459aafcfddfd6f43244 100644 --- a/src/components/CommunityComponents/NewCommunityForm.vue +++ b/src/components/CommunityComponents/NewCommunityForm.vue @@ -4,7 +4,7 @@ > <!-- Component heading --> <div - class="text-xl md:text-2xl font-medium text-center text-primary-light mt-4 mb-10" + class="text-xl md:text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-10" > Opprett ny gruppe </div> @@ -70,11 +70,11 @@ <!-- error message for title--> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.group.name.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -96,11 +96,11 @@ <!-- error message for place--> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.group.place.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -123,11 +123,11 @@ <!-- error message for description --> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.group.description.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -136,10 +136,10 @@ <!-- Images --> <div class="mt-6"> <label - class="block mb-2 text-xl font-medium text-gray-900 dark:text-gray-400" + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="imageLabel" > - Bilde + Bilde (bildet må være .png) </label> <input @@ -148,21 +148,19 @@ style="display: none" @change="addImage" multiple - accept="image/png, image/jpeg" + accept="image/png" /> <!-- Button for adding an image --> <div class="inline-flex rounded-md shadow-sm"> - <div class="text-error uppercase text-center">midlertidig fjernet</div> - <!-- <button + <!--<div class="text-error uppercase text-center">Midlertidig fjernet</div> --> + <button @click="$refs.file.click()" class="text-black bg-gray-200 hover:bg-grey-800 focus:ring-4 focus:outline-none focus:ring-grey-300 font-medium rounded-lg text-sm sm:w-auto px-5 py-2.5 text-center dark:bg-grey-600 dark:hover:bg-grey-700 dark:focus:ring-grey-800 disabled:opacity-50" :disabled="imageAdded" > Velg bilde - </button> --> - - <!-- Button for removing an image --> + </button> </div> <!-- Div box for showing all chosen images --> @@ -181,7 +179,7 @@ <script> import useVuelidate from "@vuelidate/core"; import { required, helpers, maxLength } from "@vuelidate/validators"; -import { postNewgroup } from "@/utils/apiutil"; +import { postNewgroup, postNewImageCommunity } from "@/utils/apiutil"; import Button from "@/components/BaseComponents/ColoredButton"; export default { @@ -199,7 +197,7 @@ export default { group: { name: { required: helpers.withMessage( - () => "Navnt kan ikke være tom", + () => "Navn kan ikke være tom", required ), max: helpers.withMessage( @@ -239,6 +237,7 @@ export default { radio: null, place: "", visibility: 1, + image: "", }, imageThere: false, }; @@ -253,10 +252,6 @@ export default { }, }, methods: { - removeImage: function () { - this.group.images.pop(); - this.imageThere = false; - }, checkRadioButton: function (event) { this.group.radio = event.target.value; @@ -281,15 +276,32 @@ export default { description: this.group.description, visibility: this.group.visibility, location: this.group.place, - picture: "", + picture: this.group.image, }; - await postNewgroup(groupInfo); + console.log(this.group.image); + + const respone = await postNewgroup(groupInfo); + if (respone.status === 200 || respone.status === 201) { + this.$router.push({ path: "/", replace: true }); + } } }, - addImage: function (event) { + addImage: async function (event) { this.group.images.push(URL.createObjectURL(event.target.files[0])); + + var that = this; + let image = event.target.files[0]; + let fileReader = new FileReader(); + fileReader.onloadend = async function () { + const res = fileReader.result; + const id = await postNewImageCommunity(res); + + const API_URL = process.env.VUE_APP_BASEURL; + that.group.image = API_URL + "images/" + id; + }; + fileReader.readAsArrayBuffer(image); this.imageThere = true; }, }, diff --git a/src/components/ItemComponents/ItemCard.vue b/src/components/ItemComponents/ItemCard.vue index ac79861f7bf9eb7bbed3592b4420d440bc98e587..0d81988d6ae5dca8c620b69c0eadeb7a6fb7cc7f 100644 --- a/src/components/ItemComponents/ItemCard.vue +++ b/src/components/ItemComponents/ItemCard.vue @@ -4,11 +4,11 @@ class="w-4/5 rounded bg-gray-200 h-full overflow-hidden display:inline-block correct-size" > <img - class="w-full" + class="h-3/4" :src="item.img || require('../../assets/default-product.png')" alt="Item image" /> - <div class="p-1 m-1"> + <div class="p-1 m-1 bottom-0"> <p class="text-gray-700 text-xs font-bold" id="adress"> {{ item.address }} </p> diff --git a/src/components/ItemComponents/NewItemForm.vue b/src/components/ItemComponents/NewItemForm.vue index b92f23e2e83e052bad971f79f699ac35cba373dc..244d24434ca753ffd3ecd2f3e76ebd0929fb3dcd 100644 --- a/src/components/ItemComponents/NewItemForm.vue +++ b/src/components/ItemComponents/NewItemForm.vue @@ -3,9 +3,7 @@ class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > <!-- Component heading --> - <h3 - class="text-xl font-medium text-center text-primary-light mt-4 mb-8" - > + <h3 class="text-xl font-medium text-center text-primary-light mt-4 mb-8"> Opprett ny utleie </h3> @@ -26,11 +24,11 @@ <!-- error message for title--> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.item.title.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -62,11 +60,11 @@ <!-- error message for select box --> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.item.select.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -96,7 +94,10 @@ </li> </ul> </div> - <label class="text-error text-sm block">{{ groupErrorMessage }}</label> + <!-- Error message for community --> + <label class="text-error-medium text-sm block">{{ + groupErrorMessage + }}</label> </div> <!-- price --> @@ -116,11 +117,11 @@ <!-- error message for price --> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.item.price.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -143,11 +144,11 @@ <!-- error message for description --> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.item.description.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -170,11 +171,11 @@ <!-- error message for address--> <div - class="text-error" + class="text-error-medium" v-for="(error, index) of v$.item.address.$errors" :key="index" > - <div class="text-error text-sm"> + <div class="text-error-medium text-sm"> {{ error.$message }} </div> </div> @@ -186,7 +187,7 @@ class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="imageLabel" > - Bilder + Bilder (bildene må være .png) </label> <input @@ -195,7 +196,7 @@ style="display: none" @change="addImage" multiple - accept="image/png, image/jpeg" + accept="image/png" /> <Button :text="'Velg bilde'" @click="$refs.file.click()" /> @@ -215,7 +216,12 @@ <script> import useVuelidate from "@vuelidate/core"; import { parseUserFromToken } from "@/utils/token-utils"; -import { postNewItem, getMyGroups } from "@/utils/apiutil"; +import { + postNewItem, + getMyGroups, + postNewImageCommunity, + PostImagesArrayToListing, +} from "@/utils/apiutil"; import Button from "@/components/BaseComponents/ColoredButton"; import { @@ -302,6 +308,7 @@ export default { userId: -1, selectedGroupId: -1, selectedGroups: [], + imagesToSend: [], }, //Kategorier skal legges inn ved api/hente fra db, her må det endres etterhvert categories: ["Hage", "Kjøkken", "Musikk", "Annet"], @@ -324,6 +331,7 @@ export default { async saveClicked() { if (this.checkValidation()) { this.checkUser(); + const itemInfo = { title: this.item.title, description: this.item.description, @@ -335,6 +343,8 @@ export default { }; await postNewItem(itemInfo); + await PostImagesArrayToListing(this.item.imagesToSend); + this.$router.push("/"); } }, @@ -344,8 +354,20 @@ export default { this.item.userId = parseInt(user.accountId); }, - addImage: function (event) { + addImage: async function (event) { this.item.images.push(URL.createObjectURL(event.target.files[0])); + + var that = this; + let image = event.target.files[0]; + let fileReader = new FileReader(); + fileReader.onloadend = async function () { + const res = fileReader.result; + const id = await postNewImageCommunity(res); + + const API_URL = process.env.VUE_APP_BASEURL; + that.item.imagesToSend.push(API_URL + "images/" + id); + }; + fileReader.readAsArrayBuffer(image); }, getGroups: async function () { diff --git a/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue b/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue index 5279dc8265d9273154150ff7398a40912bcae196..7ff418c29bec9efc5c40b2d424c1c0751563cd42 100644 --- a/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue +++ b/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue @@ -5,7 +5,7 @@ > <div class="months" v-for="day in days" :key="day">{{ day }}</div> </div> - <div class="grid grid-cols-7 gap-y-0.5 my-1"> + <div class="daysList grid grid-cols-7 gap-y-0.5 my-1"> <div class="days blocked" v-for="index in daysBeforeStartOfMonth" @@ -192,14 +192,6 @@ export default { isBlocked(day) { return this.blockedDays.includes(day); }, - isDayBlocked(day) { - return this.blockedDays.includes(day); - }, - isDayBetweenBlocked(day) { - return this.blockedDaysRange.some(([start, end]) => { - return start <= day && day <= end; - }); - }, // Get date from day and month and check if it is between start and end isDayBetweenStartAndEndDate(day) { if (!this.startDate || !this.endDate) return false; diff --git a/src/components/FormComponents/LoginForm.vue b/src/components/UserAuthComponents/LoginForm.vue similarity index 94% rename from src/components/FormComponents/LoginForm.vue rename to src/components/UserAuthComponents/LoginForm.vue index 362dec931a8512d52aeea7ec1ba5a20bd1a9d9ea..4b52c2efef1be63592ab7843f55bcf7a945f4e0d 100644 --- a/src/components/FormComponents/LoginForm.vue +++ b/src/components/UserAuthComponents/LoginForm.vue @@ -24,7 +24,7 @@ <!-- error message --> <div v-for="(error, index) of v$.user.email.$errors" :key="index"> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="emailErrorId" > @@ -47,12 +47,12 @@ /> <!-- error message --> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-for="(error, index) of v$.user.password.$errors" :key="index" > <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="passwordErrorId" > @@ -82,7 +82,7 @@ </div> <div class="flex items-center justify-center text-center bg-gray-50"> <label - class="mx-2 text-sm font-bold text-red-500 dark:text-primary-light hover:underline" + class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" >{{ message }}</label > </div> diff --git a/src/components/FormComponents/NewPasswordForm.vue b/src/components/UserAuthComponents/NewPasswordForm.vue similarity index 97% rename from src/components/FormComponents/NewPasswordForm.vue rename to src/components/UserAuthComponents/NewPasswordForm.vue index d32c15d65f1d4938f01c9c862fe70cc49f58929c..7dfd1958ee78aca0bd7e1720b939d4c340ba6e9c 100644 --- a/src/components/FormComponents/NewPasswordForm.vue +++ b/src/components/UserAuthComponents/NewPasswordForm.vue @@ -25,7 +25,7 @@ <!-- error message --> <div v-for="(error, index) of v$.user.oldPassword.$errors" :key="index"> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="oldPasswordErrorId" > @@ -52,7 +52,7 @@ <!-- error message --> <div v-for="(error, index) of v$.user.password.$errors" :key="index"> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="passwordErrorId" > @@ -82,7 +82,7 @@ <!-- error message --> <div v-for="(error, index) of v$.user.rePassword.$errors" :key="index"> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="rePasswordErrorId" > diff --git a/src/components/FormComponents/RegisterForm.vue b/src/components/UserAuthComponents/RegisterForm.vue similarity index 93% rename from src/components/FormComponents/RegisterForm.vue rename to src/components/UserAuthComponents/RegisterForm.vue index 12c4a1227379f44305217b4c594c31e2ed703952..17cd3cf736afe4e9a3c53d30af6527e7a3d8602a 100644 --- a/src/components/FormComponents/RegisterForm.vue +++ b/src/components/UserAuthComponents/RegisterForm.vue @@ -21,12 +21,12 @@ /> <!-- error message --> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-for="(error, index) of v$.email.$errors" :key="index" > <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="emailErrorId" > @@ -45,12 +45,12 @@ /> <!-- error message --> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-for="(error, index) of v$.password.$errors" :key="index" > <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="passwordErrorId" > @@ -69,12 +69,12 @@ /> <!-- error message --> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-for="(error, index) of v$.confirmPassword.$errors" :key="index" > <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="confirmPasswordErrorId" > @@ -94,12 +94,12 @@ /> <!-- error message --> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-for="(error, index) of v$.firstName.$errors" :key="index" > <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="firstNameErrorId" > @@ -118,12 +118,12 @@ /> <!-- error message --> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-for="(error, index) of v$.lastName.$errors" :key="index" > <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="lastNameErrorId" > @@ -142,12 +142,12 @@ /> <!-- error message --> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-for="(error, index) of v$.address.$errors" :key="index" > <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="addressErrorId" > diff --git a/src/components/FormComponents/ResetPasswordForm.vue b/src/components/UserAuthComponents/ResetPasswordForm.vue similarity index 94% rename from src/components/FormComponents/ResetPasswordForm.vue rename to src/components/UserAuthComponents/ResetPasswordForm.vue index 6f95c5210690720b88148f5babcc73702e54ce6c..714e615ddf080f6fb7da79f72c831efd403ae9e1 100644 --- a/src/components/FormComponents/ResetPasswordForm.vue +++ b/src/components/UserAuthComponents/ResetPasswordForm.vue @@ -2,9 +2,7 @@ <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > - <h3 - class="text-xl font-medium text-center text-primary-light mt-4 mb-8" - > + <h3 class="text-xl font-medium text-center text-primary-light mt-4 mb-8"> Glemt passordet ditt? </h3> @@ -31,7 +29,7 @@ <!-- error message --> <div v-for="(error, index) of v$.email.$errors" :key="index"> <div - class="text-red-600 text-sm" + class="text-error-medium text-sm" v-show="showError" id="emailErrorId" > diff --git a/src/components/UserProfileComponents/RatingComponents/Rating.vue b/src/components/UserProfileComponents/RatingComponents/Rating.vue index 1db98bfb010248d1cf3d554df1f946a6867e6dc9..6efa3b014f4c175b7af950ddb36119a7029f60f9 100644 --- a/src/components/UserProfileComponents/RatingComponents/Rating.vue +++ b/src/components/UserProfileComponents/RatingComponents/Rating.vue @@ -1,5 +1,17 @@ <template> - <ul v-if="rating != -1" class="flex justify-center"> + <ul v-if="isNaN(rating)" class="flex justify-center"> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + {{ ratingType }}: + </p> + </li> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + Ingen vurderinger + </p> + </li> + </ul> + <ul v-else class="flex justify-center"> <li> <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> {{ ratingType }}: @@ -18,21 +30,10 @@ </svg> </li> <li> - <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + <!-- Trenger vi å vise i tekst når vi har stjerner? --> + <!-- <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> {{ rating }} out of 5 - </p> - </li> - </ul> - <ul v-else class="flex justify-center"> - <li> - <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> - {{ ratingType }}: - </p> - </li> - <li> - <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> - Ingen vurderinger - </p> + </p> --> </li> </ul> </template> diff --git a/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue b/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue index 33835dd8495e0e401982b33da58b1cf95c8ff229..675524104ec1ebe1b74410ae9114131c666f4023 100644 --- a/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue +++ b/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue @@ -21,7 +21,7 @@ </div> </div> <colored-button - v-if="!isRated" + v-if="!isRated" :text="'Vurder'" class="px-4 flex-1" @click=" @@ -97,7 +97,7 @@ export default { getDateString(milliseconds) { let today = new Date(); let date = new Date(milliseconds); - let dateString = date.getDate() + "." + (date.getMonth()+1); + let dateString = date.getDate() + "." + (date.getMonth() + 1); if (date.getFullYear() != today.getFullYear()) { dateString += "." + date.getFullYear(); @@ -117,7 +117,6 @@ export default { this.user = await userService.getUserFromId(this.historyItem.renterId); } this.isRated = await userService.isRated(this.historyItem.rentId); - }, }; </script> diff --git a/src/components/UserProfileComponents/UserItems.vue b/src/components/UserProfileComponents/UserItems.vue index 02179d6620d3e0bf8e426ad24385c236b6f7f9d9..25038bd7e4c1af6223f5993f85cd72ef4328b299 100644 --- a/src/components/UserProfileComponents/UserItems.vue +++ b/src/components/UserProfileComponents/UserItems.vue @@ -1,9 +1,6 @@ <template> - <div - id="headline" - class="text-xl md:text-2xl text-primary-light font-medium" - > - Dine gjenstander + <div id="headline" class="text-xl md:text-2xl text-primary-light font-medium"> + Mine gjenstander </div> <!-- Search field --> <div class="relative" id="searchComponent"> @@ -28,7 +25,6 @@ @change="searchWritten" /> </div> - <div class="absolute inset-x-0 px-5 py-3"> <!-- ItemCards --> <div class="flex items-center justify-center w-screen"> @@ -37,7 +33,68 @@ class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" v-if="showItems" > - <ItemCard v-for="item in visibleItems" :key="item" :item="item" /> + <div + class="cardContainer" + id="item" + v-for="item in visibleItems" + :key="item" + > + <ItemCard class="ItemCard w-fit h-fit" :item="item" /> + + <TripleDotButton class="DotButton" @click="openDotMenu(item)"> + </TripleDotButton> + + <div + v-show="item.toggle" + class="options 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 absolute bg-white ring-1 ring-gray-300 rounded-xl" + aria-labelledby="dropdownDefault" + > + <li> + <button + to="/user/userItems" + 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" + > + Rediger gjenstand + </button> + </li> + <li> + <button + @click="goToDeleteItem(item.listingID)" + class="block py-2 px-4 text-sm text-error-medium hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + > + Slett gjenstand + </button> + </li> + </ul> + </div> + </div> + + <CustomFooterModal + @close="this.readyToDelete = false" + :visible="readyToDelete" + :title="'Sikker på at du vil slette annonsen?'" + :message="''" + > + <div class="flex justify-center p-2"> + <ColoredButton + id="#cancelDeleteButton" + :text="'Avbryt'" + @click="cancelDelete" + class="bg-gray-500 m-2" + ></ColoredButton> + + <ColoredButton + id="confirmDeleteButton" + @click="deleteItem" + :text="'Slett'" + class="m-2 bg-error-medium" + > + </ColoredButton> + </div> + </CustomFooterModal> </div> <!-- Shows items based on search field input --> @@ -45,7 +102,61 @@ class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full place-items-center" v-if="showSearchedItems" > - <ItemCard v-for="item in searchedItems" :key="item" :item="item" /> + <div class="cardContainer" v-for="item in searchedItems" :key="item"> + <ItemCard class="ItemCard" :item="item" /> + <TripleDotButton class="DotButton" @click="openDotMenu(item)"> + </TripleDotButton> + + <div + v-show="item.toggle" + class="options 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 absolute bg-white ring-1 ring-gray-300 rounded-xl" + aria-labelledby="dropdownDefault" + > + <li> + <button + to="/user/userItems" + 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" + > + Rediger gjenstand + </button> + </li> + <li> + <button + @click="goToDeleteItem(item.listingID)" + class="block py-2 px-4 text-sm text-error-medium hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" + > + Slett gjenstand + </button> + </li> + </ul> + </div> + </div> + <CustomFooterModal + @close="this.readyToDelete = false" + :visible="readyToDelete" + :title="'Sikker på at du vil slette annonsen?'" + :message="''" + > + <div class="flex justify-center p-2"> + <ColoredButton + id="#cancelDeleteButton" + :text="'Avbryt'" + @click="cancelDelete" + class="bg-gray-500 m-2" + ></ColoredButton> + + <ColoredButton + id="confirmDeleteButton" + @click="deleteItem" + :text="'Slett'" + class="m-2 bg-error-medium" + > + </ColoredButton> + </div> + </CustomFooterModal> </div> </div> <!-- pagination --> @@ -61,15 +172,23 @@ </div> </template> <script> +import TripleDotButton from "@/components/BaseComponents/TripleDotButton.vue"; import { GetUserListings, getItemPictures } from "@/utils/apiutil"; +import ColoredButton from "@/components/BaseComponents/ColoredButton.vue"; + +import UserService from "@/services/user.service"; import ItemCard from "@/components/ItemComponents/ItemCard.vue"; import PaginationTemplate from "@/components/BaseComponents/PaginationTemplate"; +import CustomFooterModal from "@/components/BaseComponents/CustomFooterModal.vue"; export default { name: "UserItems", components: { ItemCard, + TripleDotButton, PaginationTemplate, + CustomFooterModal, + ColoredButton, }, data() { return { @@ -80,10 +199,14 @@ export default { address: "", title: "", pricePerDay: 0, + toggle: false, }, + chosenItem: null, showItems: true, showSearchedItems: false, search: "", + readyToDelete: false, + dropdown: false, //Variables connected to pagination currentPage: 0, pageSize: 12, @@ -105,15 +228,29 @@ export default { }, }, methods: { + openDotMenu(item) { + if (item.toggle == false) { + for (var i = 0; i < this.visibleItems.length; i++) { + this.visibleItems[i].toggle = false; + } + item.toggle = true; + } else { + item.toggle = false; + } + }, getUserListingsFromAPI: async function () { this.items = await GetUserListings(); for (var i = 0; i < this.items.length; i++) { + this.items[i].toggle = false; let images = await getItemPictures(this.items[i].listingID); if (images.length > 0) { this.items[i].img = images[0].picture; } } }, + cancelDelete() { + this.readyToDelete = false; + }, //Pagination updatePage(pageNumber) { this.currentPage = pageNumber; @@ -130,6 +267,14 @@ export default { this.updatePage(this.currentPage - 1); } }, + goToDeleteItem(item) { + this.chosenItem = item; + this.readyToDelete = true; + }, + async deleteItem() { + await UserService.setListingToDeleted(this.chosenItem); + this.$router.go(0); + }, searchWritten: function () { //This method triggers when search input field is changed if (this.search.length > 0) { @@ -153,5 +298,17 @@ export default { display: block; margin-top: 10px; margin-bottom: 10px; + margin-left: 20px; +} +.cardContainer { + position: relative; +} +.DotButton { + position: absolute; + right: 40px; + bottom: 10px; +} +.options { + position: absolute; } </style> diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue index bfb9be5cebddf013c7df9fc757c1eb3761a99eec..17c002bc28fce875361dafac43ea0add48e3e9a0 100644 --- a/src/components/UserProfileComponents/UserListItemCard.vue +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -5,7 +5,7 @@ <!-- User image --> <div class="h-10 w-10 flex flex-col justify-center items-center mr-4"> <router-link :to="'/profile/' + user.userId"> - <img alt="Profilbilde" src="../../assets/defaultUserProfileImage.jpg" /> + <img alt="Profilbilde" :src="getProfilePicture" /> </router-link> </div> @@ -18,7 +18,7 @@ <!-- User rating --> <div class="hidden md:block flex-auto"> - <RatingComponent :rating="rating" :ratingType="'Gjennomsnitts rating'" /> + <RatingComponent :rating="rating" :ratingType="'Vurderinger'" /> </div> <!-- Buttons --> @@ -78,6 +78,9 @@ export default { return { rating: -1.0, communityID: -1, + profileImage: { + src: require("../../assets/defaultUserProfileImage.jpg"), + }, }; }, components: { @@ -92,17 +95,19 @@ export default { user: Object, buttons: Array, }, - methods: { + computed: { getProfilePicture() { - if (this.user.picture != "") { + if (this.user.picture !== "" && this.user.picture != null) { return this.user.picture; } - return "@/assets/defaultUserProfileImage.jpg"; + return this.profileImage.src; }, + }, + methods: { openChatWithUser() { this.$router.push({ name: "messages", - params: { userId: this.user.userId }, + query: { userID: this.user.userId }, }); }, kickUserFromCommunity() { @@ -127,7 +132,13 @@ export default { }, }, async created() { - this.rating = await UserService.getUserRatingAverage(this.user.userId); + const maybeRating = await UserService.getUserRatingAverage( + this.user.userId + ); + if (isNaN(maybeRating)) this.rating = NaN; + else this.rating = maybeRating; + if (this.rating > 5) this.rating = 5; + if (this.rating < 1) this.rating = 1; this.communityID = this.$route.params.communityID; }, }; diff --git a/src/components/UserProfileComponents/UserProfile.vue b/src/components/UserProfileComponents/UserProfile.vue index 3306a345483e2ea26c94444928c752065e65b239..142f3e612d45c5b27fb4c9172b6513e357cdfb66 100644 --- a/src/components/UserProfileComponents/UserProfile.vue +++ b/src/components/UserProfileComponents/UserProfile.vue @@ -40,7 +40,7 @@ </li> <li> <router-link - :to="'/user/' + id + '/communites'" + :to="'/profile/communities'" 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> @@ -81,14 +81,14 @@ <div class="flex flex-col items-center pb-10 mt-16 z-5"> <img class="mb-3 w-24 h-24 rounded-full shadow-lg" - src="../../assets/defaultUserProfileImage.jpg" + :src="getProfilePicture" alt="Profile picture" /> <h5 class="mb-1 text-xl font-medium text-gray-900 dark:text-white"> {{ user.firstName }} {{ user.lastName }} </h5> <div> - <rating-component :rating="renterRating" :ratingType="'Leietaker'"/> + <rating-component :rating="renterRating" :ratingType="'Leietaker'" /> <rating-component :rating="ownerRating" :ratingType="'Utleier'" /> </div> @@ -106,8 +106,8 @@ <script> 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" +import { getUser } from "@/utils/apiutil"; +import UserService from "@/services/user.service"; export default { name: "LargeProfileCard", @@ -120,11 +120,22 @@ export default { renterRating: -1, ownerRating: -1, dropdown: false, + profileImage: { + src: require("../../assets/defaultUserProfileImage.jpg"), + }, }; }, components: { RatingComponent, }, + computed: { + getProfilePicture() { + if (this.user.picture !== "" && this.user.picture != null) { + return this.user.picture; + } + return this.profileImage.src; + }, + }, methods: { async getUser() { this.currentUser = await parseCurrentUser(); @@ -133,25 +144,20 @@ export default { if (this.id === this.currentUser.accountId) { this.isCurrentUser = true; this.user = this.currentUser; + this.user = await UserService.getUserFromId(this.user.accountId); return; } this.user = await getUser(this.id); let ratingAsOwner = await UserService.getUserRatingAsOwner(this.id); - let ratingAsRenter = await UserService.getUserRatingAsRenter(this.id) + let ratingAsRenter = await UserService.getUserRatingAsRenter(this.id); if (ratingAsOwner >= 0 && ratingAsOwner <= 5) { this.ownerRating = ratingAsOwner; } - if (ratingAsRenter >= 0 && ratingAsRenter <= 5){ + if (ratingAsRenter >= 0 && ratingAsRenter <= 5) { this.renterRating = ratingAsRenter; } }, - getProfilePicture() { - if (this.user.picture !== "") { - return this.user.picture; - } - return "../assets/defaultUserProfileImage.jpg"; - }, logout() { this.$store.commit("logout"); this.$router.push("/"); diff --git a/src/router/index.js b/src/router/index.js index 2fcae0a5d5ffb4431d271e4a610fbdc2bbc9cdd3..c5435d22559bc65e9eb4a687a51adf70d9a8efd7 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,5 +1,6 @@ import store from "@/store"; import { createRouter, createWebHistory } from "vue-router"; +import NotFound from "@/views/NotFound.vue"; /** * Guards routes. If token is null, no user is logged in and only the @@ -37,10 +38,16 @@ const routes = [ component: () => import("../views/UserProfileViews/RentHistoryView.vue"), beforeEnter: guardRoute, }, + { + path: "/profile/communities", + name: "myCommunities", + component: () => import("../views/UserProfileViews/MyCommunitiesView.vue"), + beforeEnter: guardRoute, + }, { path: "/register", name: "register", - component: () => import("../views/FormViews/RegisterView.vue"), + component: () => import("../views/UserAuthViews/RegisterView.vue"), }, { path: "/messages", @@ -51,12 +58,12 @@ const routes = [ { path: "/login", name: "login", - component: () => import("../views/FormViews/LoginView.vue"), + component: () => import("../views/UserAuthViews/LoginView.vue"), }, { path: "/newPassword", name: "newPassword", - component: () => import("../views/FormViews/NewPasswordView"), + component: () => import("../views/UserAuthViews/NewPasswordView"), beforeEnter: guardRoute, }, { @@ -67,7 +74,7 @@ const routes = [ { path: "/resetPassword", name: "resetPassword", - component: () => import("../views/FormViews/ResetPasswordView.vue"), + component: () => import("../views/UserAuthViews/ResetPasswordView.vue"), }, { path: "/newCommunity", @@ -87,19 +94,6 @@ const routes = [ component: () => import("../views/ItemViews/NewItemView.vue"), beforeEnter: guardRoute, }, - { - path: "/notifications", - name: "notifications", - component: () => - import("../components/BaseComponents/NotificationsForm.vue"), - beforeEnter: guardRoute, - }, - { - path: "/user/:id/communities", - name: "myCommunities", - component: () => import("../views/CommunityViews/MyCommunitiesView.vue"), - beforeEnter: guardRoute, - }, { path: "/community/:communityID", name: "communityHome", @@ -140,6 +134,8 @@ const routes = [ component: () => import("../views/ItemViews/EditItemView.vue"), beforeEnter: guardRoute, }, + // Make sure it's your last route definition + { path: "/:pathMatch(.*)*", name: "not-found", component: NotFound }, ]; const router = createRouter({ @@ -147,4 +143,9 @@ const router = createRouter({ routes, }); +router.resolve({ + name: "not-found", + params: { pathMatch: ["not", "found"] }, +}).href; + export default router; diff --git a/src/services/user.service.js b/src/services/user.service.js index 5c6ebb84dd73c423d690adda4a336089bd81cd55..44b6159bdec5a16ca2f6893037ac792fa66f751c 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -1,31 +1,43 @@ -// import { tokenHeader } from "@/utils/token-utils"; import { tokenHeader } from "@/utils/token-utils"; import axios from "axios"; const API_URL = process.env.VUE_APP_BASEURL; class UserService { - async getUserFromId(userId) { - return await axios - .get(API_URL + "users/" + userId + "/profile", { - headers: tokenHeader(), - }) - .then((res) => { - return res.data; - }) - .catch((err) => console.error(err)); - } + async getUserFromId(userId) { + return await axios + .get(API_URL + "users/" + userId + "/profile", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); + } - async getUserRatingAverage(userId) { - return await axios - .get(API_URL + "rating/" + userId + "/average", { - headers: tokenHeader(), - }) - .then((res) => { - return res.data; - }) - .catch((err) => console.error(err)); - } + async getUserRatingAverage(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); + } + + async setListingToDeleted(listingId) { + return await axios + .delete(API_URL + "listing/" + listingId, { + headers: tokenHeader() + }) + .then((res) => { + return res.data; + }) + .catch((err) => { + console.error(err); + }) + } async getRenterHistory() { return await axios @@ -67,29 +79,26 @@ class UserService { }); } - 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 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)) + .get(API_URL + "rating/" + userId + "/average/owner", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); } } - export - default - new - UserService(); \ No newline at end of file +export default new UserService(); diff --git a/src/services/ws.js b/src/services/ws.js index c37cc21d24e93419abb2a6d1ebe3a0e0c8be3503..233ed883af296c9da8417e1884ca35cba91106ae 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -1,16 +1,21 @@ const Stomp = require("stompjs"); -const SockJS = new require("sockjs-client")(process.env.VUE_APP_BASEURL + "ws"); +const SockJSRequire = require("sockjs-client"); +const SockJS = new SockJSRequire(process.env.VUE_APP_BASEURL + "ws"); import { parseCurrentUser } from "@/utils/token-utils"; // Create a Singleton function // Events fired by websocket, MESSAGE const ws = (function () { // Object of all injected functions that the client may want + // These functions will be in a object const handlers = {}; const fire = function (event, data) { if (handlers[event]) { - handlers[event](data); + // for each object in object fire event + for (const key in handlers[event]) { + handlers[event][key](data); + } } }; @@ -19,7 +24,6 @@ const ws = (function () { // Fire message event fire("MESSAGE", JSON.parse(payload.body)); - if (data.status == "NEW_MESSAGE") fire("NEW_MESSAGE", JSON.parse(payload.body)); }; @@ -38,16 +42,20 @@ const ws = (function () { stompClient.connect({}, onConnected, onError); return { - on: function (event, callback) { - handlers[event] = callback; + on: function (event, callback, id = "none") { + // Generate random id + if (!handlers[event]) { + handlers[event] = {}; + } + handlers[event][id] = callback; }, fire: fire, - isActive: function (event) { - return !!handlers[event]; + isActive: function (event, id = "none") { + return !!handlers[event]?.[id]; }, - end: function (event) { + end: function (event, id = "none") { if (handlers[event]) { - delete handlers[event]; + delete handlers[event][id]; } else { throw new Error("No handler for event: " + event); } diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index 2f8fabcb74dc13da2e74888291ee8b581c721b16..86b911d547006c86e8c1fece8cc0b7ba0b3be32a 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -325,3 +325,31 @@ export function postNewRating(ratingInfo) { return error; }); } + +export function postNewImageCommunity(image) { + return axios + .post(API_URL + "images", image, { + headers: { ...tokenHeader(), "Content-Type": "image/png" }, + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.log(error); + return error; + }); +} + +export function PostImagesArrayToListing(imagesArray) { + return axios + .post(API_URL + "listing/pictures", imagesArray, { + headers: tokenHeader(), + }) + .then((response) => { + return response; + }) + .catch((error) => { + console.error(error.response); + return error; + }); +} diff --git a/src/views/ChatViews/ChatView.vue b/src/views/ChatViews/ChatView.vue index 581da8f24f289618486dc3366f4476ca14e1d3c2..98283b536f2b892499768a7351948c5f3a91299d 100644 --- a/src/views/ChatViews/ChatView.vue +++ b/src/views/ChatViews/ChatView.vue @@ -1,76 +1,17 @@ <template> - <div class="flex flex-col h-full overflow-hidden border-2"> - <div class="flex flex-row h-full border-2 bg-gray-50"> - <div class="basis-1/3"> - <h1 class="text-center text-l">Mine samtaler</h1> - <ul v-if="conversations" class="border-2"> - <li - v-for="conversation in conversations" - :key="conversation.recipient.userId" - > - <ChatProfile :conversation="conversation" @recipient="selectUser" /> - </li> - </ul> - </div> - <div class="basis-2/3"> - <CurrentChat v-if="selected" :recipient="selected" /> - </div> - </div> - </div> - <!-- <div class="min-h-full"> - <div class="border rounded grid grid-cols-3 w-full"> - <div class="border-r border-gray-300 col-span-1"> - <ul class="hidden sm:block overflow-auto h-full"> - <h2 class="my-2 mb-2 ml-2 text-lg text-gray-600">Chats</h2> - <li v-if="conversations"> - </li> - </ul> - </div> - </div> - </div> --> + <chats-component></chats-component> </template> <script> -import ChatProfile from "@/components/ChatComponents/ChatProfile.vue"; -import CurrentChat from "@/components/ChatComponents/CurrentChat.vue"; -import { parseCurrentUser } from "@/utils/token-utils"; -import ChatService from "@/services/chat.service"; - +import ChatsComponent from "@/components/ChatComponents/ChatsComponent.vue"; export default { components: { - ChatProfile, - CurrentChat, + ChatsComponent, }, data() { - return { - conversations: null, - selected: null, - }; - }, - computed: { - key() { - return this.selected.userId || "ERROR"; - }, - }, - methods: { - userID() { - return parseCurrentUser().accountId; - }, - selectUser(value) { - const userid = value; - this.conversations.find((conversation) => { - return conversation.recipient.userId == userid; - }); - this.selected = this.conversations.find( - (conversation) => conversation.recipient.userId == userid - ).recipient; - }, - }, - async created() { - this.conversations = await ChatService.getConversations(this.userID()); - if (this.$route.params.userId !== null) { - this.selectUser(this.$route.params.userId); - } + return {}; }, }; </script> + +<style></style> diff --git a/src/views/CommunityViews/CommunityView.vue b/src/views/CommunityViews/CommunityView.vue index 52e711014b45736bf30d0a21122e03eb24262b55..329917065b0679060da6503891b05c932ebaf18a 100644 --- a/src/views/CommunityViews/CommunityView.vue +++ b/src/views/CommunityViews/CommunityView.vue @@ -1,68 +1,81 @@ <template> - <!-- My communities, with pagination --> - <div v-if="loggedIn"> - <div class="flex flex-row p-4 relative"> - <div class="text-xl md:text-2xl text-primary-light font-medium w-full"> - Mine grupper - </div> - <UserAddIcon - class="cursor-pointer max-h-6 max-w-6 float-right grow text-primary-dark" - @click="$router.push('/newCommunity')" - alt="Opprett ny gruppe" - /> - </div> - <CommunityList :communities="visibleMyCommunities" :member="true" /> - - <!-- pagination my communities --> - <div class="flex justify-center"> - <PaginationTemplate - v-bind:items="myCommunities" - v-on:page:update="updatePageMyCommunities" - v-bind:currentPage="currentPageMyCommunities" - v-bind:pageSize="pageSizeMyCommunities" - class="mt-4" - /> + <div> + <div v-if="loading" class="flex place-content-center p-8 min-h-screen"> + <LoaderSpinner /> </div> - </div> + <div v-else class="min-h-screen"> + <!-- My communities, with pagination --> + <div v-if="loggedIn"> + <div class="flex flex-row p-4 relative"> + <div + class="text-xl md:text-2xl text-primary-medium font-medium w-full" + > + Mine grupper + </div> + <UserAddIcon + class="cursor-pointer max-h-6 max-w-6 float-right grow text-primary-dark" + @click="$router.push('/newCommunity')" + alt="Opprett ny gruppe" + /> + </div> + <CommunityList :communities="visibleMyCommunities" :member="true" /> - <!-- Public communities, with search and pagination --> - <p class="text-xl md:text-2xl text-primary-light font-medium w-full p-4"> - Offentlige grupper - </p> - <!-- Search field --> - <div class="relative mt-1 mx-2" id="searchComponent"> - <span class="absolute inset-y-0 left-0 flex items-center pl-3"> - <div class="w-5 h-5 text-gray-400"> - <SearchIcon /> + <!-- pagination my communities --> + <div class="flex justify-center"> + <PaginationTemplate + v-bind:items="myCommunities" + v-on:page:update="updatePageMyCommunities" + v-bind:currentPage="currentPageMyCommunities" + v-bind:pageSize="pageSizeMyCommunities" + class="mt-4" + /> + </div> </div> - </span> - <input - type="text" - id="searchInput" - class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-primary-medium dark:focus:border-primary-medium focus:outline-none focus:ring" - placeholder="Search" - v-model="search" - @change="searchWritten" - /> - </div> + <!-- Public communities, with search and pagination --> + <p class="text-xl md:text-2xl text-primary-medium font-medium w-full p-4"> + Offentlige grupper + </p> + <!-- Search field --> + <div class="relative mt-1 mx-2" id="searchComponent"> + <span class="absolute inset-y-0 left-0 flex items-center pl-3"> + <div class="w-5 h-5 text-gray-400"> + <SearchIcon /> + </div> + </span> - <!-- Public communities list, two lists, one for when it's searched and one for pagination --> - <div v-if="showPaginated"> - <CommunityList :communities="visiblePublicCommunities" :member="false" /> - </div> - <div v-if="showSearched"> - <CommunityList :communities="searchPublicCommunities" :member="false" /> - </div> - <!-- pagination Public communities --> - <div class="flex justify-center"> - <PaginationTemplate - v-bind:items="publicCommunities" - v-on:page:update="updatePagePublicCommunities" - v-bind:currentPage="currentPagePublicCommunities" - v-bind:pageSize="pageSizePublicCommunities" - class="my-4" - /> + <input + type="text" + id="searchInput" + class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-primary-medium dark:focus:border-primary-medium focus:outline-none focus:ring" + placeholder="Search" + v-model="search" + @change="searchWritten" + /> + </div> + + <!-- Public communities list, two lists, one for when it's searched and one for pagination --> + <div v-if="showPaginated"> + <CommunityList + :communities="visiblePublicCommunities" + :member="false" + /> + </div> + <div v-if="showSearched"> + <CommunityList :communities="searchPublicCommunities" :member="false" /> + </div> + <!-- pagination Public communities --> + <div class="flex justify-center"> + <PaginationTemplate + v-bind:items="publicCommunities" + v-on:page:update="updatePagePublicCommunities" + v-bind:currentPage="currentPagePublicCommunities" + v-bind:pageSize="pageSizePublicCommunities" + class="my-4" + /> + </div> + </div> + <FooterBar /> </div> </template> @@ -71,11 +84,14 @@ import CommunityList from "@/components/CommunityComponents/CommunityList.vue"; import { UserAddIcon, SearchIcon } from "@heroicons/vue/outline"; import PaginationTemplate from "@/components/BaseComponents/PaginationTemplate"; import CommunityService from "@/services/community.service"; +import LoaderSpinner from "@/components/BaseComponents/LoaderSpinner"; +import FooterBar from "@/components/BaseComponents/FooterBar"; export default { name: "HomeView", data() { return { + loading: false, loggedIn: false, myCommunities: [], publicCommunities: [], @@ -97,6 +113,8 @@ export default { UserAddIcon, PaginationTemplate, SearchIcon, + LoaderSpinner, + FooterBar, }, computed: { searchPublicCommunities() { @@ -160,20 +178,21 @@ export default { }, }, async mounted() { + this.loading = true; await this.load(); //Double loop not bad :) for (var i = 0; i < this.publicCommunities.length; i++) { for (var j = 0; j < this.myCommunities.length; j++) { if ( - this.publicCommunities[i].communityId === - this.myCommunities[j].communityId + this.publicCommunities?.[i]?.communityId === + this.myCommunities?.[j]?.communityId ) { this.publicCommunities.splice(i, 1); } } } - this.updateVisibleCommunities(); + this.loading = false; }, }; </script> diff --git a/src/views/CommunityViews/MyCommunitiesView.vue b/src/views/CommunityViews/MyCommunitiesView.vue deleted file mode 100644 index b2abf6c58e23ed50f27c12a94fbe80c1cbc9aad8..0000000000000000000000000000000000000000 --- a/src/views/CommunityViews/MyCommunitiesView.vue +++ /dev/null @@ -1,32 +0,0 @@ -<template> - <div> - <div id="myGroups"> - <div>Mine grupper:</div> - <group-list :groupList="myGroups" /> - </div> - </div> -</template> - -<script> -import GroupList from "@/components/CommunityComponents/CommunityList.vue"; -import { getMyGroups } from "@/utils/apiutil"; - -export default { - data() { - return { - myGroups: [], - }; - }, - components: { - GroupList, - }, - methods: { - async getMyGroups() { - this.myGroups = await getMyGroups(); - }, - }, - beforeMount() { - this.getMyGroups(); - }, -}; -</script> diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue index 7870d1e63ba55975d46634b639797c751b1a3d75..343f1f47c4d432352db1c3c670c293ef7c4a1fdf 100644 --- a/src/views/HelpView.vue +++ b/src/views/HelpView.vue @@ -1,16 +1,20 @@ <template> - <div class="mt-6 bg-white justify-center w-screen"> + <div class="mt-10 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"> + <h2 + class="flex justify-center text-primary-dark mb-6 uppercase font-bold text-2xl mt-4" + > Kontakt oss </h2> - <p class="text-gray-500 leading-relaxed mb-9"> + <p + class="flex justify-center mx-auto text-gray-500 leading-relaxed mb-12 max-w-md mt-8" + > {{ contact.description }} </p> </div> - <div class="flex mb-8 ml-2 w-full"> + <div class="flex justify-center mb-12 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" @@ -18,17 +22,17 @@ </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.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="flex justify-center mb-10 w-full pr-11"> <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> + <h4 class="font-bold text-gray-800 text-xl mb-1">E-postadresse</h4> <p>{{ contact.email }}</p> </div> </div> @@ -36,9 +40,9 @@ </div> <div id="faq"> <div - class="mx-auto text-center px-4 text-2xl text-primary-dark font-semibold" + class="mx-auto text-center px-4 mt-8 text-2xl text-primary-dark font-semibold" > - Frequently Asked Questions + Ofte stilte spørsmål </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" @@ -49,7 +53,7 @@ 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"> + <dt id="question" @click="toggle(faqItem)"> <div class="flex justify-between text-gray-800"> <div class="font-bold"> {{ faqItem.question }} @@ -93,7 +97,7 @@ export default { 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", + email: "kontakt@boco.no", address: "O. S. Bragstads Plass 2G", city: "Trondheim", country: "Norge", @@ -106,11 +110,24 @@ export default { toggle: false, }, { - question: "Hvordan kan jeg bli med i en gruppe?", + question: "Hvordan kan jeg opprette en ny 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, }, + { + question: "Hvordan kan jeg bli med i en gruppe?", + answer: + "På hovedsiden vil alle offentlige og lukkede grupper vises. Når du trykker på ønsket gruppe vil du få muligheten til å bli med/sende medlemsforespørsel.", + toggle: false, + }, + { + question: + "Hva vil jeg ha tilgang til ved å logge inn/opprette en bruker?", + answer: + "Uten å være logget inn vil du kunne se alle grupper, og også se gjenstander som ligger ute til lån i offentlige grupper. For å kunne låne en gjenstand må du være med i gruppen gjenstanden ligger i, og for dette må du være innlogget. Du må også være innlogget for å sende medlemsforespørsel i lukkede grupper.", + toggle: false, + }, ], }; }, diff --git a/src/views/NotFound.vue b/src/views/NotFound.vue new file mode 100644 index 0000000000000000000000000000000000000000..1051e31dded6de02a0c51f96a3994c47f7033598 --- /dev/null +++ b/src/views/NotFound.vue @@ -0,0 +1,18 @@ +<template> + <div class="flex place-content-center h-full p-8"> + <div class="bg-gray-300 p-8 rounded-2xl drop-shadow-xl"> + <h1 class="text-primary-medium text-4xl">Oisann!</h1> + <p>Her skjedde det visst en feil...</p> + <p> + Lyst å gå tilbake til + <router-link to="/" class="underline">start?</router-link> + </p> + </div> + </div> +</template> + +<script> +export default {}; +</script> + +<style></style> diff --git a/src/views/TestView.vue b/src/views/TestView.vue index f86430c64bb07d2f6e60fe0626ac1f50371d3682..b3a3ac1be4c5e41d3c9e49311443720fcedf895d 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -1,15 +1,18 @@ <template> - <div></div> + <chats-component></chats-component> </template> <script> +import ChatsComponent from "@/components/ChatComponents/ChatsComponent.vue"; export default { + components: { + ChatsComponent, + }, data() { return { show: false, }; }, - components: {}, methods: { toggleModal() { this.show = !this.show; diff --git a/src/views/FormViews/LoginView.vue b/src/views/UserAuthViews/LoginView.vue similarity index 74% rename from src/views/FormViews/LoginView.vue rename to src/views/UserAuthViews/LoginView.vue index ec6fa68a7c9b217e2d199d2bd8b9d8781556343a..6ab7e4ca537206fceb2a2b1628d374b6aee0e8fb 100644 --- a/src/views/FormViews/LoginView.vue +++ b/src/views/UserAuthViews/LoginView.vue @@ -5,7 +5,7 @@ </template> <script> -import LoginForm from "@/components/FormComponents/LoginForm"; +import LoginForm from "@/components/UserAuthComponents/LoginForm"; export default { name: "LoginView.vue", components: { diff --git a/src/views/FormViews/NewPasswordView.vue b/src/views/UserAuthViews/NewPasswordView.vue similarity index 73% rename from src/views/FormViews/NewPasswordView.vue rename to src/views/UserAuthViews/NewPasswordView.vue index 1c08586c58f04a984fa22a5af77219c79907c145..890a8f524b4490c03ba510c85c2dfc429b228143 100644 --- a/src/views/FormViews/NewPasswordView.vue +++ b/src/views/UserAuthViews/NewPasswordView.vue @@ -5,7 +5,7 @@ </template> <script> -import NewPasswordForm from "@/components/FormComponents/NewPasswordForm"; +import NewPasswordForm from "@/components/UserAuthComponents/NewPasswordForm"; export default { name: "NewPasswordView.vue", components: { diff --git a/src/views/FormViews/RegisterView.vue b/src/views/UserAuthViews/RegisterView.vue similarity index 67% rename from src/views/FormViews/RegisterView.vue rename to src/views/UserAuthViews/RegisterView.vue index 93013987e5e31384c4391768e5468a6bc77df35d..0bf15f1143c38c4bf7fd336ca82afb2a302b9073 100644 --- a/src/views/FormViews/RegisterView.vue +++ b/src/views/UserAuthViews/RegisterView.vue @@ -5,7 +5,7 @@ </template> <script> -import RegisterFormComponent from "../../components/FormComponents/RegisterForm.vue"; +import RegisterFormComponent from "../../components/UserAuthComponents/RegisterForm.vue"; export default { components: { diff --git a/src/views/FormViews/ResetPasswordView.vue b/src/views/UserAuthViews/ResetPasswordView.vue similarity index 73% rename from src/views/FormViews/ResetPasswordView.vue rename to src/views/UserAuthViews/ResetPasswordView.vue index 284c4fe3582728f817ad5fb7b6fbb98373dd38dc..3bb12c1738c08a4578fb2d381dd52cb86a7032a3 100644 --- a/src/views/FormViews/ResetPasswordView.vue +++ b/src/views/UserAuthViews/ResetPasswordView.vue @@ -5,7 +5,7 @@ </template> <script> -import ResetPassword from "@/components/FormComponents/ResetPasswordForm"; +import ResetPassword from "@/components/UserAuthComponents/ResetPasswordForm"; export default { name: "ResetPasswordView.vue", components: { diff --git a/src/views/UserProfileViews/MyCommunitiesView.vue b/src/views/UserProfileViews/MyCommunitiesView.vue new file mode 100644 index 0000000000000000000000000000000000000000..e474528640a45dc3254256ee8ef9afef3871f7ea --- /dev/null +++ b/src/views/UserProfileViews/MyCommunitiesView.vue @@ -0,0 +1,45 @@ +<template> + <div> + <div v-if="loading" class="flex place-content-center p-8"> + <LoaderSpinner /> + </div> + <div v-else> + <!-- My communities, with pagination --> + <div class="flex flex-row p-4 relative"> + <div class="text-xl md:text-2xl text-primary-light font-medium w-full"> + Mine grupper + </div> + <UserAddIcon + class="cursor-pointer max-h-6 max-w-6 float-right grow text-primary-dark" + @click="$router.push('/newCommunity')" + alt="Opprett ny gruppe" + /> + </div> + <CommunityList :communities="myCommunities" :member="true" /> + </div> + </div> +</template> + +<script> +import CommunityList from "@/components/CommunityComponents/CommunityList.vue"; +import CommunityService from "@/services/community.service"; +import { UserAddIcon } from "@heroicons/vue/outline"; + +export default { + data() { + return { + myCommunities: [], + loading: false, + }; + }, + components: { + CommunityList, + UserAddIcon, + }, + async beforeCreate() { + this.loading = true; + this.myCommunities = await CommunityService.getUserCommunities(); + this.loading = false; + }, +}; +</script> diff --git a/tailwind.config.js b/tailwind.config.js index 65380e429009105b923a55266f6b301464b97a88..d8ef3f55c175f5952769a6b41dec929ef3533f4d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -24,7 +24,7 @@ module.exports = { primary: { light: "#306EC1", medium: "#004aad", - dark: "#003884", + dark: "#002B66", }, secondary: { light: "#653273", diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/community-header.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/community-header.spec.js.snap index cecad6a17a15604f5bcf46480b439199187ab9c9..e7df24bdd8074c7762732e44fcac5daf475f1c7d 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/community-header.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/community-header.spec.js.snap @@ -2,16 +2,27 @@ exports[`CommunityHeader component renders correctly 1`] = ` <div - data-v-app="" + adminstatus="true" + class="flex place-content-center mx-4" + community="[object Object]" > - - <!-- TODO PUT A LOADER HERE --> <div - adminstatus="true" - community="[object Object]" + class="loadingio-spinner-bean-eater-o5tefvffeqm" > - LASTER... + <div + class="ldio-sweozsnwol" + > + <div> + <div /> + <div /> + <div /> + </div> + <div> + <div /> + <div /> + <div /> + </div> + </div> </div> - </div> `; diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/community-list-item.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/community-list-item.spec.js.snap index 25c734add5c0ca6ad6f94cec526e074008da6782..4edde89c2c743d9c2411aa4a1a5c1403b7077c70 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/community-list-item.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/community-list-item.spec.js.snap @@ -1,41 +1,28 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CommunityListItem component renders correctly 1`] = ` -<div - data-v-app="" -> - +<div> <!-- Main modal --> <!--v-if--> <div - class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-4" + class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" > <div - class="h-10 w-10 flex flex-col justify-center items-center mr-4" + class="h-3 w-14 flex flex-col justify-center items-center ml-2 mt-4 mb-4 mr-2" > - <svg - aria-hidden="true" - fill="none" - stroke="currentColor" - stroke-width="2" - viewBox="0 0 24 24" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" - stroke-linecap="round" - stroke-linejoin="round" - /> - </svg> - <!-- TODO: USE COMMUNITY IMAGE <img alt="Felleskapets bilde" src="@/assets/group.png" /> --> + <img + alt="Fellsekaps bilde" + class="rounded-md" + src="string" + /> </div> <div class="flex-1 pl-1 overflow-hidden" > <div - class="font-medium dark:text-white truncate" + class="font-medium text-gray-800 dark:text-white truncate" > string </div> @@ -60,6 +47,5 @@ exports[`CommunityListItem component renders correctly 1`] = ` </svg> </div> </div> - </div> `; diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/community-list.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/community-list.spec.js.snap index 0feda8234936c3b8b7a1ec27d48066f90615cdaf..a385829acb32b11671e1d9788b5b401b95c4d892 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/community-list.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/community-list.spec.js.snap @@ -1,10 +1,110 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CommunityList component renders correctly 1`] = ` -<ul - grouplist="[object Object],[object Object]" -> +<ul> + <li> + <div + class="border-black" + > + + <!-- Main modal --> + <!--v-if--> + + <div + class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" + > + <div + class="h-3 w-14 flex flex-col justify-center items-center ml-2 mt-4 mb-4 mr-2" + > + <img + alt="Fellsekaps bilde" + class="rounded-md" + src="string" + /> + </div> + <div + class="flex-1 pl-1 overflow-hidden" + > + <div + class="font-medium text-gray-800 dark:text-white truncate" + > + string + </div> + </div> + <div + class="flex flex-row justify-center items-center" + > + <svg + aria-hidden="true" + class="max-h-6 max-w-6 shrink m-2" + fill="none" + stroke="currentColor" + stroke-width="2" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </div> + </div> + </div> + </li> + <li> + <div + class="border-black" + > + + <!-- Main modal --> + <!--v-if--> + + <div + class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" + > + <div + class="h-3 w-14 flex flex-col justify-center items-center ml-2 mt-4 mb-4 mr-2" + > + <img + alt="Fellsekaps bilde" + class="rounded-md" + src="string" + /> + </div> + <div + class="flex-1 pl-1 overflow-hidden" + > + <div + class="font-medium text-gray-800 dark:text-white truncate" + > + string + </div> + </div> + <div + class="flex flex-row justify-center items-center" + > + <svg + aria-hidden="true" + class="max-h-6 max-w-6 shrink m-2" + fill="none" + stroke="currentColor" + stroke-width="2" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </div> + </div> + </div> + </li> </ul> `; diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/create-new-group.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/create-new-group.spec.js.snap index 1b82d99ed9de0f8bcd2c9cc9ed86ec7b30886061..e3a0811fb62bb0a919a31fa7a987b7bf16e599bf 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/create-new-group.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/create-new-group.spec.js.snap @@ -6,7 +6,7 @@ exports[`CreateNewGroup elements rendering renders correctly 1`] = ` > <!-- Component heading --> <div - class="text-xl md:text-2xl font-medium text-center text-primary-light mt-4 mb-10" + class="text-xl md:text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-10" > Opprett ny gruppe </div> @@ -121,13 +121,13 @@ exports[`CreateNewGroup elements rendering renders correctly 1`] = ` class="mt-6" > <label - class="block mb-2 text-xl font-medium text-gray-900 dark:text-gray-400" + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="imageLabel" > - Bilde + Bilde (bildet må være .png) </label> <input - accept="image/png, image/jpeg" + accept="image/png" multiple="" style="display: none;" type="file" @@ -136,19 +136,12 @@ exports[`CreateNewGroup elements rendering renders correctly 1`] = ` <div class="inline-flex rounded-md shadow-sm" > - <div - class="text-error uppercase text-center" + <!--<div class="text-error uppercase text-center">Midlertidig fjernet</div> --> + <button + class="text-black bg-gray-200 hover:bg-grey-800 focus:ring-4 focus:outline-none focus:ring-grey-300 font-medium rounded-lg text-sm sm:w-auto px-5 py-2.5 text-center dark:bg-grey-600 dark:hover:bg-grey-700 dark:focus:ring-grey-800 disabled:opacity-50" > - midlertidig fjernet - </div> - <!-- <button - @click="$refs.file.click()" - class="text-black bg-gray-200 hover:bg-grey-800 focus:ring-4 focus:outline-none focus:ring-grey-300 font-medium rounded-lg text-sm sm:w-auto px-5 py-2.5 text-center dark:bg-grey-600 dark:hover:bg-grey-700 dark:focus:ring-grey-800 disabled:opacity-50" - :disabled="imageAdded" - > - Velg bilde - </button> --> - <!-- Button for removing an image --> + Velg bilde + </button> </div> <!-- Div box for showing all chosen images --> diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap index 0d4999e9ea79a37b5fd6bbae85350af3f93a2a91..356fa6b63aa9ece26edbdd78e5d07728036f38b7 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap @@ -9,11 +9,11 @@ exports[`ItemCard component renders correctly 1`] = ` > <img alt="Item image" - class="w-full" + class="h-3/4" src="String" /> <div - class="p-1 m-1" + class="p-1 m-1 bottom-0" > <p class="text-gray-700 text-xs font-bold" diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap index c2789c34942918bfb9b251c3bbd6d7c71fbc0eee..c0152222de3fa62b177a362dde7b4b755e039b5b 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap @@ -100,8 +100,9 @@ exports[`NewItemForm component renders correctly 1`] = ` </li> </ul> </div> + <!-- Error message for community --> <label - class="text-error text-sm block" + class="text-error-medium text-sm block" /> </div> <!-- price --> @@ -170,10 +171,10 @@ exports[`NewItemForm component renders correctly 1`] = ` class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="imageLabel" > - Bilder + Bilder (bildene må være .png) </label> <input - accept="image/png, image/jpeg" + accept="image/png" multiple="" style="display: none;" type="file" diff --git a/tests/unit/component-tests/community-component-tests/community-list.spec.js b/tests/unit/component-tests/community-component-tests/community-list.spec.js index fbd0968070e19814478e839c92245089f09013b8..1341e301a208a570b8d3f3cb009e311129e4f002 100644 --- a/tests/unit/component-tests/community-component-tests/community-list.spec.js +++ b/tests/unit/component-tests/community-component-tests/community-list.spec.js @@ -7,7 +7,7 @@ describe("CommunityList component", () => { beforeEach(() => { wrapper = mount(CommunityList, { props: { - groupList: [ + communities: [ { communityId: 0, name: "string", diff --git a/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/CalendarComponent.spec.js b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/CalendarComponent.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..e244058b3b8821425bf1a3f582f55bbbf490d4c7 --- /dev/null +++ b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/CalendarComponent.spec.js @@ -0,0 +1,44 @@ +import { shallowMount } from "@vue/test-utils"; +import CalendarComponent from "@/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue"; + +describe("CalendarComponent tests", () => { + let wrapper; + beforeEach(() => { + wrapper = shallowMount(CalendarComponent, { + propsData: { + month: new Date(1651739228545), // May 2022 + blockedDaysRange: [ + [new Date(1651739228545)], // 5 May + [ + new Date(1652733228545), // 16 May + new Date(1652833228545), // 18 May + ], + ], + }, + }); + }); + + it("Is instansiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("Check that all week days are rendered", () => { + expect(wrapper.findAll(".months").length).toBe(7); + }); + + it("Check that the correct amount of days are rendered", () => { + // 31 days in May, 6 for start of week from last month and 5 for end of month + expect(wrapper.find(".daysList").findAll("div").length).toBe(42); + }); + + it("Check select day works", () => { + wrapper.find(".daysList").findAll("div")[7].find("button").trigger("click"); + expect(wrapper.emitted()).toHaveProperty("selectDate"); + }); + + it("Test that selecting day outside of month does not work", () => { + // Click on the first day, which is not in the month + wrapper.find(".daysList").findAll("div")[0].find("button").trigger("click"); + expect(wrapper.emitted()).not.toHaveProperty("selectDate"); + }); +}); diff --git a/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/month-selector-test.spec.js b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/month-selector-test.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..5a7204d7f0935d0cdb1201c0803ba8b23288f59e --- /dev/null +++ b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/month-selector-test.spec.js @@ -0,0 +1,46 @@ +import { shallowMount } from "@vue/test-utils"; +import monthSelector from "@/components/TimepickerComponents/DatepickerRange/MonthSelector.vue"; + +describe("MonthSelector tests", () => { + let wrapper; + beforeEach(() => { + wrapper = shallowMount(monthSelector, { + propsData: { + month: new Date(1651739228545), // 05 May 2022 UTC + type: "type", + }, + }); + }); + + it("Is instansiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("Check if fields show correct informations", () => { + // Check if container exists + expect(wrapper.find(".container-c .text")); + const children = wrapper.find(".container-c .text").findAll("div"); + + // Check if there are two children + expect(children.length).toBe(2); + + // Check children content + expect(children[0].text()).toBe("MAY"); + expect(children[1].text()).toBe("2022"); + }); + + it("Check that changing are emitted", async () => { + // Check that there are two buttons + expect(wrapper.findAll(".button").length).toBe(2); + + const buttons = wrapper.findAll(".button"); + console.log(buttons[0].html()); + // Check that the first button goes a month back + await buttons[0].trigger("click"); + expect(wrapper.emitted()).toHaveProperty("back"); + + // Check that the second button goes a month forward + await buttons[1].trigger("click"); + expect(wrapper.emitted()).toHaveProperty("forward"); + }); +}); diff --git a/tests/unit/component-tests/user-component-tests/__snapshots__/login-form.spec.js.snap b/tests/unit/component-tests/user-component-tests/__snapshots__/login-form.spec.js.snap index f79182e421a30cc8a35db92683cee3b064bbfb02..7d3875c13b81280752f1d753d824b2f830373fc9 100644 --- a/tests/unit/component-tests/user-component-tests/__snapshots__/login-form.spec.js.snap +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/login-form.spec.js.snap @@ -70,7 +70,7 @@ exports[`LoginForm component renders correctly 1`] = ` class="flex items-center justify-center text-center bg-gray-50" > <label - class="mx-2 text-sm font-bold text-red-500 dark:text-primary-light hover:underline" + class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" /> </div> </div> diff --git a/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap b/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap index e7d04a61c2d249a8bda1825ed17ed7be65bcf319..f22076edecd3620957e0bbf9141873adfd3a6322 100644 --- a/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap @@ -11,73 +11,11 @@ exports[`Rating component renders correctly 1`] = ` : </p> </li> - - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - 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" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - 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" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - 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" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - 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" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - 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" - /> - </svg> - </li> - <li> <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400" > - out of 5 + Ingen vurderinger </p> </li> </ul> diff --git a/tests/unit/component-tests/user-component-tests/login-form.spec.js b/tests/unit/component-tests/user-component-tests/login-form.spec.js index 40fa711393e969435a609474c0f550f179513135..bb0285db4c868fa9acc66080b46d0fa184cceb7e 100644 --- a/tests/unit/component-tests/user-component-tests/login-form.spec.js +++ b/tests/unit/component-tests/user-component-tests/login-form.spec.js @@ -1,5 +1,5 @@ import { mount } from "@vue/test-utils"; -import LoginForm from "@/components/FormComponents/LoginForm.vue"; +import LoginForm from "@/components/UserAuthComponents/LoginForm.vue"; describe("LoginForm component", () => { let wrapper; diff --git a/tests/unit/component-tests/user-component-tests/new-password-form.spec.js b/tests/unit/component-tests/user-component-tests/new-password-form.spec.js index 638e597bd9b130a6b2a1c69a0b724b3414436648..3d52388c9b35a953ceca35a27a87c578f603135e 100644 --- a/tests/unit/component-tests/user-component-tests/new-password-form.spec.js +++ b/tests/unit/component-tests/user-component-tests/new-password-form.spec.js @@ -1,5 +1,5 @@ import { mount } from "@vue/test-utils"; -import NewPasswordForm from "@/components/FormComponents/NewPasswordForm.vue"; +import NewPasswordForm from "@/components/UserAuthComponents/NewPasswordForm.vue"; describe("NewPasswordForm component", () => { let wrapper; diff --git a/tests/unit/component-tests/user-component-tests/register-user-component.spec.js b/tests/unit/component-tests/user-component-tests/register-user-component.spec.js index 9accd167543e4b2291397821c4aeaedf42804313..4c2ca3b84153c1a1f12b75765948b59a59dc6262 100644 --- a/tests/unit/component-tests/user-component-tests/register-user-component.spec.js +++ b/tests/unit/component-tests/user-component-tests/register-user-component.spec.js @@ -1,5 +1,5 @@ import { mount } from "@vue/test-utils"; -import RegisterFormComponent from "@/components/FormComponents/RegisterForm"; +import RegisterFormComponent from "@/components/UserAuthComponents/RegisterForm"; describe("RegisterFormComponent", () => { let wrapper; diff --git a/tests/unit/component-tests/user-component-tests/reset-password-form.spec.js b/tests/unit/component-tests/user-component-tests/reset-password-form.spec.js index 1dc9f0a4f379129d888436a42d40bf354ae56f66..45f67cef373d59a5d51850d424378a3af218490c 100644 --- a/tests/unit/component-tests/user-component-tests/reset-password-form.spec.js +++ b/tests/unit/component-tests/user-component-tests/reset-password-form.spec.js @@ -1,5 +1,5 @@ import { mount } from "@vue/test-utils"; -import ResetPasswordForm from "@/components/FormComponents/ResetPasswordForm.vue"; +import ResetPasswordForm from "@/components/UserAuthComponents/ResetPasswordForm.vue"; describe("ResetPasswordForm component", () => { let wrapper;