diff --git a/README.md b/README.md index fb1f6eb8b91e5edb9de574056e10fdd8856ebce7..d75ae0c4ca45ebdfd33b67d37211cb0f45aa10cb 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,50 @@ -# frontend +# BoCo - Frontend + + +<img src="public/favicon.ico" height="100" title="hover text"> + +## Table of contents +1. [Installation](#project-setup) +1. [Running the project](#running-the-project) +1. [Running tests](#running-tests) ## Project setup + +To run the frontend node.js is required. +- Installation links + - [node.js](https://nodejs.org/en/download/) + +Install required dependencies by running ``` npm install ``` -### Compiles and hot-reloads for development -``` -npm run serve -``` +The frontend cannot run by itself and requires a backend service. The URL to this backend service needs to be provided in the environmental variables. This can be done in these steps -### Compiles and minifies for production -``` -npm run build -``` +- Create a file with the name ``.env`` in the root folder +- If the backend service uses the default configuration insert this into the file + ``` + VUE_APP_BASEURL=http://localhost:3000/api/ + ``` -### Run your unit tests -``` -npm run test:unit -``` -### Lints and fixes files +## Running the project +- To compile and run with hot-reloads for development + ``` + npm run serve + ``` + +- To compile and minify project for prodoction + ``` + npm run build + ``` + +## Running tests +To run the tests run the command: ``` -npm run lint +npm run test:unit ``` +This will run all the tests and provide a coverage report. Due to a bug with jest and vue there are tests and files missing in the coverage report. This is due to jest skipping files with ``ES6`` import. ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/src/components/BaseComponents/ColoredButton.vue b/src/components/BaseComponents/ColoredButton.vue index fb8c6ca884739d6d6b6a196fadd496944640ef57..d36bb8d03b090ff93ca1c170617d8a08ca56e46b 100644 --- a/src/components/BaseComponents/ColoredButton.vue +++ b/src/components/BaseComponents/ColoredButton.vue @@ -1,4 +1,5 @@ <template> + <!-- Button with custom text and color --> <button class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px}" :class="color" @@ -8,6 +9,13 @@ </template> <script> +/** + * Colored button component with customizable text and 3 possible colors. + * Colors: + * blue (default), + * red, + * green + */ export default { name: "ColoredButton", props: { @@ -18,6 +26,9 @@ export default { }, }, computed: { + /** + * Formats the tailwind css tags based on the color passed from parent + */ color() { if (this.buttonColor === "red") { return "bg-error-medium hover:bg-error-dark focus:ring-error-light"; diff --git a/src/components/BaseComponents/CustomFooterModal.vue b/src/components/BaseComponents/CustomFooterModal.vue index e9b5467276a554a723968b7a48581428a9df78ea..c317995fa23e3588b170826def82b946e0d76dbe 100644 --- a/src/components/BaseComponents/CustomFooterModal.vue +++ b/src/components/BaseComponents/CustomFooterModal.vue @@ -14,22 +14,12 @@ <h3 class="text-xl font-semibold text-gray-900 dark:text-white"> {{ title }} </h3> + <!-- Close button --> <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> + <XIcon class="w-5 h-5" /> </button> </div> <!-- Modal body --> @@ -40,7 +30,7 @@ </div> <!-- Modal footer --> <div class="rounded-b border-t border-gray-200 dark:border-gray-600"> - <!-- Slot: Add any html you want here --> + <!-- Slot: Add any html you want here (Must be one div) --> <slot /> </div> </div> @@ -49,14 +39,27 @@ </template> <script> +import { XIcon } from "@heroicons/vue/outline"; + +/** + * CustomFooterModal component for adding buttons and such to the footer of the modals. + * Buttons are added to slot so must be in a single div if multiple html elements are needed. + */ export default { name: "CustomFooterModal", + emits: ["close"], + components: { + XIcon, + }, props: { visible: Boolean, title: String, message: String, }, methods: { + /** + * Method that emits close to parent. + */ close() { this.$emit("close"); }, diff --git a/src/components/BaseComponents/FooterBar.vue b/src/components/BaseComponents/FooterBar.vue index 478a86447ca15a4a5be2cbb140e6a797ed142188..c2fa67a862af7abe456fcfdca2d652ec592712dc 100644 --- a/src/components/BaseComponents/FooterBar.vue +++ b/src/components/BaseComponents/FooterBar.vue @@ -1,10 +1,13 @@ <template> + <!-- Footer --> <footer class="w-full bg-white dark:bg-gray-800 sm:flex-row border-1 border-t border-gray-600 h-10" > + <!-- Copyright --> <p class="float-left text-xs my-3 ml-4 text-primary-dark"> © BoCo 2022 - All rights reserved </p> + <!-- Icon link to help page --> <QuestionMarkCircleIcon class="md:mt-0 mt-1 mr-4 float-right cursor-pointer h-8 md:h-10 text-primary-medium" alt="Hjelp" @@ -15,9 +18,12 @@ <script> import { QuestionMarkCircleIcon } from "@heroicons/vue/outline"; + +/** + * FooterBar for homepage + */ export default { name: "FooterBar", - components: { QuestionMarkCircleIcon, }, diff --git a/src/components/BaseComponents/FormImageDisplay.vue b/src/components/BaseComponents/FormImageDisplay.vue index f0b5de3a8add044a48c7d8586ad8605b5d573598..fe2244ed30ca810141e693cfb00d928b1d7b5812 100644 --- a/src/components/BaseComponents/FormImageDisplay.vue +++ b/src/components/BaseComponents/FormImageDisplay.vue @@ -1,27 +1,38 @@ <template> + <!-- Image --> <img :src="image" class="w-2/5 inline" alt="Bilde av gjenstanden" @click="toggleModal" /> - <custom-footer-modal + <!-- Modal --> + <CustomFooterModal @close="toggleModal" :message="'Bilder som fjernes kan ikke lastes opp på nytt uten å laste opp et annet bilde først.'" :title="'Er du sikker på at du vil fjene bilde?'" :visible="show" > + <!-- Buttons for modal footer --> <div class="flex justify-center p-2"> - <colored-button :text="'Avbryt'" @click="toggleModal" class="m-2" /> - <colored-button :text="'Slett'" @click="removeImage" class="m-2" /> + <ColoredButton :text="'Avbryt'" @click="toggleModal" class="m-2" /> + <ColoredButton + :text="'Slett'" + :color="'red'" + @click="removeImage" + class="m-2" + /> </div> - </custom-footer-modal> + </CustomFooterModal> </template> <script> import ColoredButton from "@/components/BaseComponents/ColoredButton"; import CustomFooterModal from "@/components/BaseComponents/CustomFooterModal.vue"; +/** + * Displays uploaded image for forms. + */ export default { components: { ColoredButton, @@ -36,9 +47,15 @@ export default { image: String, }, methods: { + /** + * Emits remove image to parent + */ removeImage() { this.$emit("remove"); }, + /** + * Toggles modal + */ toggleModal() { this.show = !this.show; }, diff --git a/src/components/BaseComponents/IconButton.vue b/src/components/BaseComponents/IconButton.vue index 095db5635e33e9a5116c0724f8a77a8a8c5acb93..594b70aa04afe016f023592e947c3628234792e6 100644 --- a/src/components/BaseComponents/IconButton.vue +++ b/src/components/BaseComponents/IconButton.vue @@ -1,9 +1,11 @@ <template> + <!-- Button --> <button class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80" :class="color" > <div class="w-5 h-5 mx-1"> + <!-- Slot for icon (Default BanIcon) --> <slot><BanIcon /></slot> </div> <span class="mx-1">{{ text }}</span> @@ -13,6 +15,13 @@ <script> import { BanIcon } from "@heroicons/vue/outline"; +/** + * IconButton component that can be customized with text, color and a slot for passing an icon. + * Colors: + * blue (default), + * red, + * green + */ export default { name: "IconButton", props: { @@ -23,6 +32,9 @@ export default { BanIcon, }, computed: { + /** + * Formats the tailwind css tags based on the color passed from parent + */ color() { if (this.buttonColor === "red") { return "bg-error-medium hover:bg-error-dark focus:ring-error-light"; diff --git a/src/components/BaseComponents/LoaderSpinner.vue b/src/components/BaseComponents/LoaderSpinner.vue index 2b88fb6994d8958d84070ecfad870f27d3326392..819677aca74e20946858215723e77edf145b154b 100644 --- a/src/components/BaseComponents/LoaderSpinner.vue +++ b/src/components/BaseComponents/LoaderSpinner.vue @@ -1,4 +1,5 @@ <template> + <!-- PacMan for indicating loading --> <div class="loadingio-spinner-bean-eater-o5tefvffeqm"> <div class="ldio-sweozsnwol"> <div> @@ -16,6 +17,11 @@ </template> <style scoped type="text/css"> +/** + * LoadSpinner component for indicating loading in the form of PacMan. + * + * generated by https://loading.io/ + */ @keyframes ldio-sweozsnwol-1 { 0% { transform: rotate(0deg); @@ -107,10 +113,9 @@ position: relative; transform: translateZ(0) scale(1); backface-visibility: hidden; - transform-origin: 0 0; /* see note above */ + transform-origin: 0 0; } .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 ceb11cee9904acae7d910f43ed50ea23c25873d5..3adb5c39b0590e7e3630f0885b389f543a61b2f8 100644 --- a/src/components/BaseComponents/NavBar.vue +++ b/src/components/BaseComponents/NavBar.vue @@ -1,7 +1,9 @@ <template> + <!-- NavBar --> <nav 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" > + <!-- Logo reroutes to homepage --> <div class="logo"> <img class="m-1 ml-4 cursor-pointer h-9 md:h-12" @@ -11,6 +13,7 @@ /> </div> <ul class="flex justify-between"> + <!-- New listing button --> <li class="cursor-pointer" v-if="this.$store.state.user.token !== null" @@ -22,25 +25,35 @@ /> <a class="hidden md:block mt-7 text-sm float-right">Legg til</a> </li> + + <!-- My messages button --> <li class="cursor-pointer" v-if="this.$store.state.user.token !== null" @click="loadMessages" > - <div class="notification-container"> + <div class="notification-container relative"> <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> + <p + class="notification absolute bg-secondary top-0 p-1 min-w-[20px] min-h-[20px] rounded-full text-xs text-white font-bold text-center right-0 cursor-pointer" + v-if="newMessages > 0" + > + {{ notifications }} + </p> <a class="hidden md:block mt-7 text-sm float-right">Meldinger</a> </div> </li> + + <!-- User profile button --> <li class="cursor-pointer" @click="loadProfile"> <UserCircleIcon class="m-6 md:mr-2 h-7 text-primary-medium float-left" alt="Profil" /> + <!-- Shows "Profil" if user is logged in, else "Logg inn" --> <a v-if="this.$store.state.user.token !== null" class="hidden md:block mr-4 mt-7 text-sm float-right" @@ -59,16 +72,27 @@ import { parseUserFromToken } from "@/utils/token-utils"; import { PlusIcon, ChatAlt2Icon, UserCircleIcon } from "@heroicons/vue/outline"; import ws from "@/services/ws"; +/** + * NavBar component used in App + */ export default { - name: "NavBar.vue", + name: "NavBar", + components: { + PlusIcon, + ChatAlt2Icon, + UserCircleIcon, + }, data() { return { newMessages: 0, }; }, computed: { + /** + * Format method for notification number on messages. + * Shows "+99" the user has more than 99 new messages. + */ notifications() { - // if new messages is greater than 99 show +99 if (this.newMessages > 99) { return "+99"; } else { @@ -76,13 +100,10 @@ export default { } }, }, - components: { - PlusIcon, - ChatAlt2Icon, - UserCircleIcon, - }, - methods: { + /** + * Method for routing to user profile. Routes to user profile if logged in, otherwise to login. + */ async loadProfile() { if (this.$store.state.user.token !== null) { let user = parseUserFromToken(this.$store.state.user.token); @@ -92,11 +113,17 @@ export default { await this.$router.push("/login"); } }, + /** + * Method for routing to messages. + */ loadMessages() { this.newMessages = 0; this.$router.push("/messages"); }, }, + /** + * On creation of this component the websocket service checks if the user has new messages. To display by the chat button. + */ created() { ws.on( "NEW_MESSAGE", @@ -111,24 +138,8 @@ export default { </script> <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(-290%, 50%); - color: white; - font-size: 10px; - border-radius: 50%; - font-weight: bold; - text-align: center; - right: 0; - cursor: pointer; } @media (max-width: 768px) { diff --git a/src/components/BaseComponents/NotificationModal.vue b/src/components/BaseComponents/NotificationModal.vue index 1fa362f504d38cb7380d0ad26a1f8316c74fe823..3010a08a9507ca6a006a33e6a897ab5fac8680f1 100644 --- a/src/components/BaseComponents/NotificationModal.vue +++ b/src/components/BaseComponents/NotificationModal.vue @@ -14,22 +14,12 @@ <h3 class="text-xl font-semibold text-gray-900 dark:text-white"> {{ title }} </h3> + <!-- Close button --> <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> + <XIcon class="w-5 h-5" /> </button> </div> <!-- Modal body --> @@ -44,14 +34,26 @@ </template> <script> +import { XIcon } from "@heroicons/vue/outline"; + +/** + * Modal for displaying notifications. + */ export default { name: "NotificationModal", + emits: ["close"], + components: { + XIcon, + }, props: { visible: Boolean, title: String, message: String, }, methods: { + /** + * Method that emits close to parent. + */ close() { this.$emit("close"); }, diff --git a/src/components/BaseComponents/PaginationTemplate.vue b/src/components/BaseComponents/PaginationTemplate.vue index 939d7350598667c7e2710346f3ba80bb83f96748..68994fd3bdeb6be7456e9e24a3abf7b8bb446ec0 100644 --- a/src/components/BaseComponents/PaginationTemplate.vue +++ b/src/components/BaseComponents/PaginationTemplate.vue @@ -1,5 +1,7 @@ <template> + <!-- Pagination --> <div v-if="totalPages() > 0"> + <!-- Prev button --> <span v-if="showPreviousLink()" 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" @@ -7,9 +9,11 @@ > Forrige </span> + <!-- Current page --> <label class="mx-2 text-primary-light" >{{ currentPage + 1 }} av {{ totalPages() }}</label > + <!-- Next button --> <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" @@ -21,19 +25,41 @@ </template> <script> +/** + * Pagination component + */ export default { name: "paginationTemplate", - props: ["items", "currentPage", "pageSize"], + emits: ["page:update"], + props: { + items: Array, + currentPage: Number, + pageSize: Number, + }, methods: { - updatePage(pageNumber) { - this.$emit("page:update", pageNumber); - }, + /** + * Calculates the number of pages to display whole list. + */ totalPages() { return Math.ceil(this.items.length / this.pageSize); }, + /** + * Emits page update to parent. + */ + updatePage(pageNumber) { + this.$emit("page:update", pageNumber); + }, + /** + * Checks if previous button is needed. + * Returns true if not on first page. + */ showPreviousLink() { return this.currentPage == 0 ? false : true; }, + /** + * Checks if next button is needed. + * Returns true if not on last page. + */ showNextLink() { return this.currentPage == this.totalPages() - 1 ? false : true; }, diff --git a/src/components/BaseComponents/RatingModal.vue b/src/components/BaseComponents/RatingModal.vue deleted file mode 100644 index 1b09b8fceae2bc0cf2764f79199aa6342c5a7463..0000000000000000000000000000000000000000 --- a/src/components/BaseComponents/RatingModal.vue +++ /dev/null @@ -1,198 +0,0 @@ -<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 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 index 5205015a39798c6af46dae4b99af4f6fbb826172..8cac4372e2073c7bcee784919b7fd544c8c1e7bf 100644 --- a/src/components/BaseComponents/TripleDotButton.vue +++ b/src/components/BaseComponents/TripleDotButton.vue @@ -5,20 +5,19 @@ 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> + <DotsVerticalIcon class="w-6 h-6" /> </button> </template> <script> +import { DotsVerticalIcon } from "@heroicons/vue/outline"; + +/** + * Triple dot button + */ export default { name: "TripleDotButton", + components: { + DotsVerticalIcon, + }, }; </script> diff --git a/src/components/ChatComponents/ChatMessage.vue b/src/components/ChatComponents/ChatMessage.vue index 0d05a808f6f0fc95f7170144436a6332dc7672be..8b046dc79ae9664a1cc82182337e6052a43333e3 100644 --- a/src/components/ChatComponents/ChatMessage.vue +++ b/src/components/ChatComponents/ChatMessage.vue @@ -58,12 +58,18 @@ export default { calculateTime() { 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()); + if (mmOfMessage <= 9) { + mmOfMessage = "0" + mmOfMessage; + } + if (hhOfMessage <= 9) { + hhOfMessage = "0" + hhOfMessage; + } 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! + var monthOfMessage = String(date.getMonth() + 1).padStart(2, "0"); const shortMonthOfMessage = date.toLocaleString("default", { month: "short", }); @@ -71,7 +77,7 @@ export default { 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 mm = String(today.getMonth() + 1).padStart(2, "0"); var yyyy = today.getFullYear(); if (ddOfMessage == dd) { return "" + hhOfMessage + ":" + mmOfMessage + ""; diff --git a/src/components/ChatComponents/RentalMessage.vue b/src/components/ChatComponents/RentalMessage.vue index 77c178ae78a6c1e783d39b7a899267bb9e7b3fd6..c864d36bff56d845b8352e2664afcb4dd4f50ae6 100644 --- a/src/components/ChatComponents/RentalMessage.vue +++ b/src/components/ChatComponents/RentalMessage.vue @@ -1,6 +1,6 @@ <template> <div class="message-container"> - <div class="message"> + <div class="message bg-gray-100 ring-1 ring-gray-300"> <div class="info"> <div class="text"> <h2 class="header">Ny utleie forespørsel</h2> @@ -8,7 +8,7 @@ <p>Pris: {{ price }}kr</p> </div> <div class="img-container"> - <img class="img" :src="img" alt="Produkt Bilde" /> + <img class="img" :src="image" alt="Produkt Bilde" /> </div> </div> <div> @@ -19,11 +19,21 @@ </p> </div> </div> - <div class="buttons" v-if="(!rent.isAccepted && !rent.deleted && this.rent.renterId != this.userID)"> + <div + class="buttons" + v-if=" + !rent.isAccepted && !rent.deleted && this.rent.renterId != this.userID + " + > <button class="button green" @click="accept">Godta</button> <button class="button red" @click="reject">Avslå</button> </div> - <div class="waiting" v-if="!rent.isAccepted && !rent.deleted && this.rent.renterId == this.userID"> + <div + class="waiting" + v-if=" + !rent.isAccepted && !rent.deleted && this.rent.renterId == this.userID + " + > Waiting for owner to accept </div> <div class="" v-if="rent.isAccepted"> @@ -38,8 +48,8 @@ <script> import axios from "axios"; -import { tokenHeader } from "@/utils/token-utils"; -import { parseCurrentUser } from "@/utils/token-utils"; +import { tokenHeader, parseCurrentUser } from "@/utils/token-utils"; +import { getItemPictures } from "@/utils/apiutil"; export default { props: { @@ -48,6 +58,11 @@ export default { required: true, }, }, + data() { + return { + image: null, + }; + }, computed: { userID() { return parseCurrentUser().accountId; @@ -75,9 +90,8 @@ export default { return this.rent.message || "Ingen Melding"; }, side() { - return this.rent.renterId == this.userID - ? "flex-end" : "flex-start"; - } + return this.rent.renterId == this.userID ? "flex-end" : "flex-start"; + }, }, methods: { async accept() { @@ -86,13 +100,28 @@ export default { null, { headers: tokenHeader() } ); + this.$router.go(0); }, async reject() { await axios.delete( process.env.VUE_APP_BASEURL + `renting/${this.rent.rentId}/delete`, { headers: tokenHeader() } ); + this.$router.go(0); }, + async getImage() { + let images = await getItemPictures(this.rent.listingId); + + if (images.length > 0) { + this.image = images[0].picture; + } else { + this.image = + "https://images.unsplash.com/photo-1453728013993-6d66e9c9123a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8dmlld3xlbnwwfHwwfHw%3D&w=1000&q=80"; + } + }, + }, + async beforeMount() { + await this.getImage(); }, }; </script> @@ -108,7 +137,6 @@ export default { display: block; flex-direction: column; width: 100%; - background: #d1d5db; border-radius: 10px; padding: 10px; max-width: 50%; diff --git a/src/components/CommunityComponents/CommunityHamburger.vue b/src/components/CommunityComponents/CommunityHamburger.vue index 348e60a278d44a882a535860aa6976314846a5b3..80a3cfd1ef8c30d6eeeca137f47baa4bc7f2b0a1 100644 --- a/src/components/CommunityComponents/CommunityHamburger.vue +++ b/src/components/CommunityComponents/CommunityHamburger.vue @@ -1,4 +1,7 @@ <template> + <!-- Hamburger menu for community header, contains options for adding a new listing, + seeing all members in the community, administrating the community if admin and + leaving the community --> <div id="dropdown" class="z-10 w-44 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700" diff --git a/src/components/CommunityComponents/CommunityHeader.vue b/src/components/CommunityComponents/CommunityHeader.vue index d5d55e2c09c7b5c8e329444d11a3a35eb5fcd1dd..86e6fe082ee3098e0328840121823ddbd11be29c 100644 --- a/src/components/CommunityComponents/CommunityHeader.vue +++ b/src/components/CommunityComponents/CommunityHeader.vue @@ -1,5 +1,10 @@ <template> + <!-- Community header contains the community's name and address, a join button if + the user is not in the community and community hamburger menu if the user is + in the community --> <div> + <!-- A warning asking user is it is sure it wants to leave the community when leave community + from hamburger menu is clicked --> <CustomFooterModal :title="'Er du sikker på at du vil forlate felleskapet?'" :message="'Dersom felleskapet er låst er du nødt til å spørre om å bli med på nytt.'" @@ -20,9 +25,13 @@ /> </div> </CustomFooterModal> + + <!-- The load spinner will show while community and it's members are being loaded from the db --> <div v-if="loading" class="flex place-content-center mx-4"> <LoaderSpinner /> </div> + + <!-- After loaded the community header shows --> <div v-else class="flex items-center justify-between mx-4"> <router-link :to="'/community/' + community.communityId" @@ -97,7 +106,6 @@ :community-i-d="community.communityId" :admin="admin" /> - <!-- class="absolute" --> </div> </div> </div> @@ -138,7 +146,7 @@ export default { member: false, community: {}, loading: true, - openDialog: false, + dialogOpen: false, }; }, props: { diff --git a/src/components/CommunityComponents/CommunityHome.vue b/src/components/CommunityComponents/CommunityHome.vue index 49103a9a86db563b3d2efd6dc733b4ef7efc7f40..b564a97e487eea78a0d5e01e73e6718f880e1cab 100644 --- a/src/components/CommunityComponents/CommunityHome.vue +++ b/src/components/CommunityComponents/CommunityHome.vue @@ -1,8 +1,12 @@ <template> + <!-- The community home page, shows all the items in the community with the possibility of + clicking on an item to be redirected to the item info page --> <div> + <!-- Shows loading component while loading the content for the page --> <div v-if="loading" class="flex place-content-center"> <LoaderSpinner /> </div> + <!-- When finish loading the home page for community is shown --> <section v-else class="w-full px-5 py-4 mx-auto rounded-md"> <CommunityHeader :admin="false" class="mb-5" /> @@ -27,7 +31,7 @@ <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" + placeholder="Søk" v-model="search" @change="searchWritten" /> @@ -51,7 +55,7 @@ <!-- 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" + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" v-if="showSearchedItems" > <ItemCard @@ -160,7 +164,7 @@ export default { } }, goToItemInfoPage(item) { - this.$router.push("/itempage/" + item); + this.$router.push("/item/" + item); }, getItemPictures: async function (itemid) { let res = await getItemPictures(itemid); diff --git a/src/components/CommunityComponents/CommunityList.vue b/src/components/CommunityComponents/CommunityList.vue index b6641f92549da76ed1f956e600842d9b07615eb5..136e50e6b7e9e26fd999f4575e84124185dd0bf8 100644 --- a/src/components/CommunityComponents/CommunityList.vue +++ b/src/components/CommunityComponents/CommunityList.vue @@ -1,5 +1,9 @@ <template> - <p v-if="!communities.length" class="flex place-content-center text-gray-400"> + <!-- A list conatining all the communities --> + <p + v-if="!communities.length" + class="flex place-content-center text-gray-400 mt-8" + > Ingen grupper </p> <ul v-else> diff --git a/src/components/CommunityComponents/CommunityListItem.vue b/src/components/CommunityComponents/CommunityListItem.vue index 40d90645e430cd49f104c91c7def3e125b9721fb..bdbc170a9eb0dd0de0fb8a6ce1a1c6737de88325 100644 --- a/src/components/CommunityComponents/CommunityListItem.vue +++ b/src/components/CommunityComponents/CommunityListItem.vue @@ -39,7 +39,7 @@ /> </div> - <!-- If a user is not logges in and tries to join a community, this message shows --> + <!-- If a user is not logged 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 }} @@ -54,10 +54,7 @@ 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" - /> + <UserGroupIcon alt="Bilde" class="h-10 w-10 text-primary-dark" /> </div> <div v-else @@ -65,8 +62,8 @@ > <img :src="community.picture" - alt="Fellsekaps bilde" - class="rounded-md" + alt="Bilde" + class="rounded-md h-14 w-14 object-contain" /> </div> <div class="flex-1 pl-1 overflow-hidden"> diff --git a/src/components/CommunityComponents/CommunityRequestForm.vue b/src/components/CommunityComponents/CommunityRequestForm.vue index 8508efd202f46350266f1c198b5f7419aca8aac3..f00a8bf6b473e75353fae3ca159d5e1d6ef98e9e 100644 --- a/src/components/CommunityComponents/CommunityRequestForm.vue +++ b/src/components/CommunityComponents/CommunityRequestForm.vue @@ -1,4 +1,5 @@ <template> + <!-- A request form for joining a private community --> <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > @@ -99,7 +100,6 @@ export default { routeToHome() { this.$router.push("/"); }, - //TODO fix so that community id is set (not null) async saveClicked() { this.communityID = await this.$router.currentRoute.value.params .communityID; diff --git a/src/components/CommunityComponents/CommunitySettings.vue b/src/components/CommunityComponents/CommunitySettings.vue index 8ea68e5261329fb85624ce68a201be8e02273416..eade2eeb081e0f08e491aa6b723e926418de70cb 100644 --- a/src/components/CommunityComponents/CommunitySettings.vue +++ b/src/components/CommunityComponents/CommunitySettings.vue @@ -9,7 +9,7 @@ </template> <script> -// import CommunityAdminService from "@/services/community-admin.service"; +import CommunityAdminService from "@/services/community-admin.service"; import IconButton from "@/components/BaseComponents/IconButton.vue"; //TODO: OPEN CONFIRMATION DIALOG WHEN DELETING @@ -19,9 +19,12 @@ export default { IconButton, }, methods: { - deleteCommunity() { - console.log("DELETED"); - // CommunityAdminService.deleteCommunity(this.$route.params.communityID); + async deleteCommunity() { + let response = await CommunityAdminService.deleteCommunity( + this.$route.params.communityID + ); + if (response.status === 200) + this.$router.push({ name: "home", replace: true }); }, }, }; diff --git a/src/components/CommunityComponents/MemberList.vue b/src/components/CommunityComponents/MemberList.vue index 3818146ff63024c1dab742805231a0e0baa582c9..0710ccbbb46e0ee8c7aebfaa3baba4e253e7b612 100644 --- a/src/components/CommunityComponents/MemberList.vue +++ b/src/components/CommunityComponents/MemberList.vue @@ -1,9 +1,15 @@ <template> - <ul> - <li v-for="member in members" :key="member.userId"> - <UserListItemCard :buttons="buttons" :user="member" /> - </li> - </ul> + <!-- Shows all members in a community --> + <div> + <ul v-if="members.length"> + <li v-for="member in members" :key="member.userId"> + <UserListItemCard :buttons="buttons" :user="member" /> + </li> + </ul> + <div v-else class="flex place-content-center text-gray-400"> + Ingenting å vise + </div> + </div> </template> <script> diff --git a/src/components/CommunityComponents/NewCommunityForm.vue b/src/components/CommunityComponents/NewCommunityForm.vue index 7a62e90f302416559f695459aafcfddfd6f43244..9776ad8c614bc84c27b5d601a502e1131e059b08 100644 --- a/src/components/CommunityComponents/NewCommunityForm.vue +++ b/src/components/CommunityComponents/NewCommunityForm.vue @@ -1,4 +1,5 @@ <template> + <!-- A form for creating a new community --> <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > @@ -278,12 +279,13 @@ export default { location: this.group.place, picture: this.group.image, }; - - console.log(this.group.image); - const respone = await postNewgroup(groupInfo); if (respone.status === 200 || respone.status === 201) { - this.$router.push({ path: "/", replace: true }); + this.$store.commit("addAdmin", respone.data); + this.$router.push({ + path: "/community/" + respone.data, + replace: true, + }); } } }, diff --git a/src/components/ItemComponents/EditItemForm.vue b/src/components/ItemComponents/EditItemForm.vue index b5fed83d4c3eac9073d1a09ca34ae95e40599163..23f9bdc9fb01f6654f94f808567961ed0e278b71 100644 --- a/src/components/ItemComponents/EditItemForm.vue +++ b/src/components/ItemComponents/EditItemForm.vue @@ -1,4 +1,5 @@ <template> + <!-- Form for editing an item --> <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > @@ -228,6 +229,7 @@ <script> import useVuelidate from "@vuelidate/core"; import ColoredButton from "@/components/BaseComponents/ColoredButton"; +import FormImageDisplay from "@/components/BaseComponents/FormImageDisplay.vue"; import ListingService from "@/services/listing.service"; import CommunityService from "@/services/community.service"; import ImageService from "@/services/image.service"; @@ -246,6 +248,7 @@ export default { components: { ColoredButton, + FormImageDisplay, }, setup() { @@ -313,10 +316,10 @@ export default { category: "", selectedCategory: "", selectedCategories: [], - images: [], userId: -1, selectedCommunityId: -1, selectedCommunities: [], + images: [], }, categories: [ "Antikviteter og kunst", @@ -353,6 +356,10 @@ export default { return true; }, + /** + * Validation gets checked, and if it returns true + * the item and the images gets updated. + */ async saveClicked() { if (this.checkValidation()) { let itemInfo = { @@ -366,11 +373,20 @@ export default { communityIDs: this.updatedItem.selectedCommunities, }; await ListingService.putItem(itemInfo); - await ImageService.putListingImages(this.images); - this.$router.push("/itempage/" + this.initialItem.listingID); + await ImageService.putListingImages( + this.initialItem.listingID, + this.updatedItem.images + ); + this.$router.push("/item/" + this.initialItem.listingID); } }, + /** + * Adds image when an image is selected from file explorer. + * Posts it to the db and gets the id of the image posted in return. + * Adds that id to an image URL and saves it in an array. + * That array containing image URLs gets posted to the db when save is clicked. + */ async addImage(event) { var that = this; let image = event.target.files[0]; @@ -380,11 +396,16 @@ export default { const id = await ImageService.postNewImage(res); const API_URL = process.env.VUE_APP_BASEURL; - that.item.images.push(API_URL + "images/" + id); + that.updatedItem.images.push(API_URL + "images/" + id); }; fileReader.readAsArrayBuffer(image); }, + /** + * Runs every time a chech box under 'grupper' is changed(checked/unchecked). + * Finds out if it was checked or unchecked and adds/removes the community from + * the array based on that. + */ onChangeCommunity(e) { this.updatedItem.selectedCommunityId = e.target.value; let alreadyInGroupList = false; @@ -412,11 +433,19 @@ export default { } }, + /** + * Updates the selected category when it gets changed changes. + */ onChangeCategory(e) { this.updatedItem.selectedCategory = e.target.value; this.updatedItem.selectedCategories = [e.target.value]; }, + /** + * pre-selects (check marks) the community/communities the item + * is posted in so the user can see where the item already is posted and + * then change the community/communities + */ isInSelectedCommunity(id) { for (let i in this.updatedItem.selectedCommunities) { if (this.updatedItem.selectedCommunities[i] == id) { @@ -425,18 +454,26 @@ export default { } return false; }, - removeImage(image) { + + /** + * Removes image from item + */ + async removeImage(image) { let newImages = []; - for (let i in this.item.images) { - if (this.item.images[i] != image) { - newImages.push(this.item.images[i]); + for (let i in this.updatedItem.images) { + if (this.updatedItem.images[i] != image) { + newImages.push(this.images[i]); } } - this.item.images = newImages; + this.updatedItem.images = newImages; }, }, - async beforeMount() { + /** + * Gets the item before the page gets mounted so the item info + * is filled in and ready to be displayed to user. + */ + async beforeCreate() { let itemID = await this.$router.currentRoute.value.params.id; let item = await ListingService.getItem(itemID); @@ -448,7 +485,12 @@ export default { this.initialItem = item; this.communities = await CommunityService.getUserCommunities(); + this.images = await ListingService.getItemPictures(itemID); + let imageURLS = []; + for (let i in this.images) { + imageURLS.push(this.images[i].picture); + } let initialCategories = []; for (let i in this.initialItem.categoryNames) { @@ -469,7 +511,7 @@ export default { price: this.initialItem.pricePerDay, selectedCategories: initialCategories, selectedCategory: selectedCategory, - images: this.images, + images: imageURLS, userId: this.initialItem.userID, selectedCommunityId: 0, selectedCommunities: initialCommunities, diff --git a/src/components/ItemComponents/ItemCard.vue b/src/components/ItemComponents/ItemCard.vue index b8125f4f70bcdfd289c1c5437ee9a31764911793..154468723a48de6ecb13ded2a9e06ae6ccf16dce 100644 --- a/src/components/ItemComponents/ItemCard.vue +++ b/src/components/ItemComponents/ItemCard.vue @@ -1,11 +1,12 @@ <template> + <!-- Item card, displays title, address, price per day and a picture --> <div class="mt-5 px-5"> <div - class="w-full h-full rounded bg-gray-200 overflow-hidden display:inline-block correct-size" + class="w-full h-full rounded bg-gray-100 ring-1 ring-gray-200 overflow-hidden display:inline-block correct-size cursor-pointer" > <div class="relative h-0 pb-[66.7%]"> <img - class="w-full h-full absolute inset-0" + class="w-full h-full object-contain absolute" :src="item.img || require('../../assets/default-product.png')" alt="Item image" /> diff --git a/src/components/ItemComponents/NewItemForm.vue b/src/components/ItemComponents/NewItemForm.vue index 9850b9bbec00f6d7ebef0968d02527254f766953..3ef622f2dfe0e9fa3cbb8dcbca343f5bf9c3bfac 100644 --- a/src/components/ItemComponents/NewItemForm.vue +++ b/src/components/ItemComponents/NewItemForm.vue @@ -1,4 +1,5 @@ <template> + <!-- Form for adding a new item --> <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > @@ -58,7 +59,7 @@ </option> </select> - <!-- error message for select box --> + <!-- error message for select category --> <div class="text-error-medium" v-for="(error, index) of v$.item.select.$errors" @@ -70,7 +71,7 @@ </div> </div> - <!-- Grupper --> + <!-- Community --> <div class="mb-6"> <label class="block text-sm font-medium text-gray-900 dark:text-gray-400" >Grupper</label @@ -95,9 +96,9 @@ </ul> </div> <!-- Error message for community --> - <label class="text-error-medium text-sm block">{{ - groupErrorMessage - }}</label> + <label class="text-error-medium text-sm block"> + {{ groupErrorMessage }} + </label> </div> <!-- price --> @@ -105,7 +106,7 @@ <label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" id="priceLabel" - >Pris</label + >Pris per dag</label > <input type="number" @@ -198,8 +199,10 @@ accept="image/png" /> + <!-- Opens file explorer --> <colored-button :text="'Velg bilde'" @click="$refs.file.click()" /> + <!-- Shows chosen images --> <div v-for="image in item.images" :key="image" class="m-2"> <form-image-display :image="image" @remove="removeImage(image)" /> </div> @@ -307,7 +310,6 @@ export default { selectedGroupId: -1, selectedGroups: [], }, - //Kategorier skal legges inn ved api/hente fra db, her må det endres etterhvert categories: [ "Antikviteter og kunst", "Dyr og utstyr", @@ -326,6 +328,11 @@ export default { }; }, methods: { + /** + * Checks validation. Checks also if any community is selected. + * If no community is selected or any other field isn't valid + * false is returned, otherwise true is returned. + */ checkValidation: function () { this.v$.item.$touch(); if (this.v$.item.$invalid || this.item.selectedGroups.length === 0) { @@ -337,6 +344,11 @@ export default { return true; }, + /** + * When save is clicked, the validation gets checked. If validated, + * the user is parsed from the token to get the userId. Then the item + * and the pictures are posted to db. + */ async saveClicked() { if (this.checkValidation()) { this.checkUser(); @@ -352,17 +364,26 @@ export default { }; await ListingService.postNewItem(itemInfo); - await ImageService.PostImagesArrayToListing(this.item.images); + await ImageService.postImagesArrayToListing(this.item.images); - this.$router.push("/"); + this.$router.go(-1); } }, + /** + * Parses user from token and uses it when posting item in saveClciked method + */ checkUser: async function () { let user = parseUserFromToken(this.$store.state.user.token); this.item.userId = parseInt(user.accountId); }, + /** + * Adds image when an image is selected from file explorer. + * Posts it to the db and gets the id of the image posted in return. + * Adds that id to an image URL and saves it in an array. + * That array containing image URLs gets posted to the db when save is clicked. + */ addImage: async function (event) { var that = this; let image = event.target.files[0]; @@ -377,6 +398,11 @@ export default { fileReader.readAsArrayBuffer(image); }, + /** + * Runs every time a chech box under 'grupper' is changed(checked/unchecked). + * Finds out if it was checked or unchecked and adds/removes the community from + * the array based on that. + */ onChangeGroup: function (e) { this.selectedGroupId = e.target.value; let alreadyInGroupList = false; @@ -397,7 +423,7 @@ export default { } }, - removeImage(image) { + async removeImage(image) { let newImages = []; for (let i in this.item.images) { if (this.item.images[i] != image) { diff --git a/src/components/ItemComponents/SearchItemList.vue b/src/components/ItemComponents/SearchItemList.vue index 00ed0583f51be18a36e249fb9e04a599a840d862..7ba86f608346b526487791f2cdaf8c32733e2fec 100644 --- a/src/components/ItemComponents/SearchItemList.vue +++ b/src/components/ItemComponents/SearchItemList.vue @@ -1,4 +1,5 @@ <template> + <!-- A template for searching in item list --> <section class="relative w-full max-w-md px-5 py-4 mx-auto rounded-md"> <div class="relative" id="searchComponent"> <span class="absolute inset-y-0 left-0 flex items-center pl-3"> @@ -17,7 +18,7 @@ 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" + placeholder="Søk" v-model="search" /> </div> @@ -54,12 +55,6 @@ export default { }, }, - /** - * Her må det lages en metode som henter alle items (i en gruppe) fra databasen. - * De kan deretter bli pusha inn i items array, og da burde de bli displayet i lista. - * Når denne metoden er på plass kan items[] i data tømmes. Da vil alt dataen komme fra db. - */ - data() { return { items: [ diff --git a/src/components/RentingComponents/ItemInfo.vue b/src/components/RentingComponents/ItemInfo.vue index 33e0d6595225af5e34e503feac89000bd408d2ca..c8637741f83349dccec7ccb99ab25a4142fe537e 100644 --- a/src/components/RentingComponents/ItemInfo.vue +++ b/src/components/RentingComponents/ItemInfo.vue @@ -51,10 +51,11 @@ </p> </div> </div> - <div class="mt-2"> + <div class="mt-2" v-if="userForId"> <UserListItemCard :buttons="['chat']" :user="userForId" + :rating="rating" ></UserListItemCard> </div> <div class="mt-4"> @@ -75,6 +76,7 @@ <p class="text-xl font-semibold text-gray-900"> Total pris: {{ totPrice }} kr </p> + <p v-if="error" class="text-error-medium">Dato er påkrevd</p> <button class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-gray-500 rounded-md focus:outline-none focus:ring focus:ring-opacity-80" v-bind:class="{ colorChange: allowForRent }" @@ -107,6 +109,7 @@ export default { data() { return { confirm: false, + error: false, item: { listingID: 0, title: "", @@ -140,6 +143,7 @@ export default { dateMessage: "Venligst velg dato for leieperioden", allowForRent: false, nonAvailableTimes: [], + rating: 0, }; }, components: { @@ -153,6 +157,8 @@ export default { if (this.allowForRent) { this.confirm = true; this.createPushItem(); + } else { + this.error = true; } }, createPushItem() { @@ -161,6 +167,7 @@ export default { this.pushItem.toTime = this.rentingEndDate; this.pushItem.title = this.item.title; this.pushItem.price = this.totPrice; + this.pushItem.renterId = this.item.userID; }, async getItem() { let id = this.$router.currentRoute.value.params.id; @@ -211,18 +218,32 @@ export default { this.rentingEndDate = dateOfsomthing.endDate; this.calculateTotPrice(); this.allowForRent = true; + this.error = false; } }, calculateTotPrice() { let amountOfDays = this.rentingEndDate - this.rentingStartDate; - amountOfDays = amountOfDays / 86400000; + amountOfDays = Math.ceil(amountOfDays / 86400000); this.totPrice = this.item.pricePerDay * amountOfDays; }, + async getUserRating() { + let maybeRating = await UserService.getUserRatingAverage( + this.userForId.userId + ); + if (isNaN(maybeRating)) { + this.rating = NaN; + } else { + this.rating = maybeRating; + if (this.rating > 5) this.rating = 5; + else if (this.rating < 1) this.rating = 1; + } + }, }, - async beforeMount() { + async created() { await this.getItemPictures(); await this.getItem(); await this.getUser(this.item.userID); + await this.getUserRating(); await this.getAvailableTimesForListing(); }, }; diff --git a/src/components/RentingComponents/NewRent.vue b/src/components/RentingComponents/NewRent.vue index 7ced0d6da6b8575e6fa37f287ddc3ae464fa0072..5b2647e4dbe76e592887f7464862293cc07792d3 100644 --- a/src/components/RentingComponents/NewRent.vue +++ b/src/components/RentingComponents/NewRent.vue @@ -42,7 +42,8 @@ </div> <div> <notification-modal - @click="routeToHome" + id="notification-modal" + @click="routeToChat" :visible="confirmed" :title="'Vellykket'" :message="'Forespørsel sendt!'" @@ -150,8 +151,8 @@ export default { cancelRent() { this.$router.go(0); }, - routeToHome() { - this.$router.push("/"); + routeToChat() { + this.$router.push("/messages?userID=" + this.newRentBox.renterId); }, sendRent: async function () { const rent = { diff --git a/src/components/UserAuthComponents/DeleteUserModal.vue b/src/components/UserAuthComponents/DeleteUserModal.vue index df77c6e66c069e6315707f4c648f4386dc2abcab..9fc855e945f9938320d16e340651446fa2c7f3cc 100644 --- a/src/components/UserAuthComponents/DeleteUserModal.vue +++ b/src/components/UserAuthComponents/DeleteUserModal.vue @@ -1,5 +1,5 @@ <template> - <!-- Main modal --> + <!-- Main modal for deleting a user --> <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" diff --git a/src/components/UserAuthComponents/LoginForm.vue b/src/components/UserAuthComponents/LoginForm.vue index ac4ab429ff491b04f3671995e853f4a70c6d2372..05607b1554f62930d1d7b7aed3e05b1eacf4f9e7 100644 --- a/src/components/UserAuthComponents/LoginForm.vue +++ b/src/components/UserAuthComponents/LoginForm.vue @@ -1,7 +1,9 @@ <template> + <!-- Login form --> <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full" > + <!-- Header --> <div class="px-6 py-4 mt-4"> <div class="text-xl md:text-2xl font-medium text-center text-primary-light mt-4 mb-8" @@ -9,6 +11,7 @@ Logg på </div> + <!-- Email --> <div> <div class="w-full mt-6" @@ -21,7 +24,7 @@ v-model="v$.user.email.$model" required /> - <!-- error message --> + <!-- error message for email --> <div v-for="(error, index) of v$.user.email.$errors" :key="index"> <div class="text-error-medium text-sm" @@ -33,6 +36,7 @@ </div> </div> + <!-- Password --> <div class="w-full mt-6" :class="{ error: v$.user.password.$errors.length }" @@ -45,7 +49,7 @@ @keyup.enter="loginClicked" required /> - <!-- error message --> + <!-- error message for password --> <div class="text-error-medium text-sm" v-for="(error, index) of v$.user.password.$errors" @@ -61,11 +65,13 @@ </div> </div> + <!-- Router link for forgetting password. Redirects to reset password page. --> <div class="flex items-center justify-between mt-8"> <router-link to="/resetPassword" class="text-primary-medium" >Glemt passord?</router-link > + <!-- Button for logging in --> <Button class="login" @click="loginClicked" @@ -78,6 +84,7 @@ <div class="flex items-center justify-center py-4 text-center bg-gray-50 dark:bg-gray-700" > + <!-- Router link to redirect to registering a new user page --> <router-link to="/register" class="mx-2 text-sm font-bold text-primary-medium dark:text-primary-light hover:underline" @@ -85,6 +92,7 @@ > </div> <div class="flex items-center justify-center text-center bg-gray-50"> + <!-- Shows a message to user if login fails --> <label class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" >{{ message }}</label @@ -122,7 +130,6 @@ export default { }, }; }, - data() { return { message: "", @@ -136,6 +143,12 @@ export default { }, methods: { + /** + * When login clicked, the inputs gets validated. If validation goes + * through loginRequest gets posted to api. If it returns false, + * user gets a message saying the email or password is wrong. If it + * returns true, the token returned gets saved to the state. + */ async loginClicked() { this.showError = true; diff --git a/src/components/UserAuthComponents/NewPasswordForm.vue b/src/components/UserAuthComponents/NewPasswordForm.vue index 7dfd1958ee78aca0bd7e1720b939d4c340ba6e9c..6eda88be53228aa45ac942c8b251f1662fd2ed58 100644 --- a/src/components/UserAuthComponents/NewPasswordForm.vue +++ b/src/components/UserAuthComponents/NewPasswordForm.vue @@ -1,20 +1,21 @@ <template> + <!-- A form for changing password --> <div class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > + <!-- Header --> <h3 class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" > Endre passord </h3> + <!-- Input field for old password --> <div id="oldPasswordField" :class="{ error: v$.user.oldPassword.$errors.length }" > - <label - for="oldPassword" - class="block text-sm text-gray-800 dark:text-gray-200" + <label class="block text-sm text-gray-800 dark:text-gray-200" >Gammelt passord</label > <input @@ -22,7 +23,7 @@ v-model="v$.user.oldPassword.$model" 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" /> - <!-- error message --> + <!-- error message for password --> <div v-for="(error, index) of v$.user.oldPassword.$errors" :key="index"> <div class="text-error-medium text-sm" @@ -34,14 +35,13 @@ </div> </div> + <!-- New password --> <div id="firstPasswordField" class="mt-4" :class="{ error: v$.user.password.$errors.length }" > - <label - for="password" - class="block text-sm text-gray-800 dark:text-gray-200" + <label class="block text-sm text-gray-800 dark:text-gray-200" >Nytt passord</label > <input @@ -49,7 +49,7 @@ v-model="v$.user.password.$model" 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" /> - <!-- error message --> + <!-- error message for password --> <div v-for="(error, index) of v$.user.password.$errors" :key="index"> <div class="text-error-medium text-sm" @@ -61,15 +61,14 @@ </div> </div> + <!-- Repeating new password --> <div id="secondPasswordField" class="mt-4" :class="{ error: v$.user.rePassword.$errors.length }" > <div class="flex items-center justify-between"> - <label - for="rePassword" - class="block text-sm text-gray-800 dark:text-gray-200" + <label class="block text-sm text-gray-800 dark:text-gray-200" >Gjenta nytt passord</label > </div> @@ -79,7 +78,7 @@ v-model="v$.user.rePassword.$model" 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" /> - <!-- error message --> + <!-- error message for password --> <div v-for="(error, index) of v$.user.rePassword.$errors" :key="index"> <div class="text-error-medium text-sm" @@ -91,6 +90,7 @@ </div> </div> + <!-- Button --> <div id="buttonsField" class="mt-6"> <Button class="float-right" @@ -98,6 +98,8 @@ :text="'Sett ny passord'" /> </div> + + <!-- Message for user --> <div class="flex items-center justify-center text-center bg-gray-50"> <label class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" @@ -162,6 +164,11 @@ export default { token: (state) => state.user.token, }), methods: { + /** + * Validates the form. If it goes through, sends a password change + * request to api and gives user a message if it does not get changed. + * If changed, saves new token to state. + */ async setNewPassword() { this.showError = true; diff --git a/src/components/UserAuthComponents/RegisterForm.vue b/src/components/UserAuthComponents/RegisterForm.vue index da8011bdce8f479730d0e8e16b082a09842243ba..466a9ab154c78e9c970df17b960851d5b378682a 100644 --- a/src/components/UserAuthComponents/RegisterForm.vue +++ b/src/components/UserAuthComponents/RegisterForm.vue @@ -1,4 +1,5 @@ <template> + <!-- Register form for creating a new user account --> <div class="w-full max-w-md mx-auto mb-auto md:ring-1 ring-gray-300 overflow-hidden rounded-xl p-4" > @@ -12,6 +13,7 @@ <form @submit.prevent> <div class="grid grid-cols-1 gap-6 mt-4"> <div> + <!-- Email --> <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" v-model="email" @@ -19,23 +21,23 @@ type="email" placeholder="E-post adresse" /> - <!-- error message --> + <!-- error message for email --> <div class="text-error-medium text-sm" v-for="(error, index) of v$.email.$errors" :key="index" > - <div - class="text-error-medium text-sm" - v-show="showError" - id="emailErrorId" - > + <div class="text-error-medium text-sm" v-show="showError"> {{ error.$message }} </div> </div> + <div class="text-error-medium text-sm" v-show="errorMessage"> + {{ errorMessage }} + </div> </div> <div> + <!-- Password --> <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" v-model="password" @@ -84,6 +86,7 @@ </div> <div> + <!-- First name --> <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" data-test="firstNameTest" @@ -92,7 +95,7 @@ type="text" placeholder="Fornavn" /> - <!-- error message --> + <!-- error message for first name--> <div class="text-error-medium text-sm" v-for="(error, index) of v$.firstName.$errors" @@ -109,6 +112,7 @@ </div> <div> + <!-- Last name --> <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" v-model="lastName" @@ -116,7 +120,7 @@ type="text" placeholder="Etternavn" /> - <!-- error message --> + <!-- error message for last name --> <div class="text-error-medium text-sm" v-for="(error, index) of v$.lastName.$errors" @@ -133,6 +137,7 @@ </div> <div> + <!-- Address --> <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" v-model="address" @@ -140,7 +145,7 @@ type="text" placeholder="Adresse" /> - <!-- error message --> + <!-- error message for address --> <div class="text-error-medium text-sm" v-for="(error, index) of v$.address.$errors" @@ -166,7 +171,7 @@ <script> import useVuelidate from "@vuelidate/core"; -import { doLogin, registerUser } from "@/utils/apiutil"; +import { doLogin } from "@/utils/apiutil"; import { required, email, @@ -177,9 +182,6 @@ import { import Button from "@/components/BaseComponents/ColoredButton"; import UserService from "@/services/user.service"; -// const isEmailTaken = (value) => -// fetch(`/api/unique/${value}`).then((r) => r.json()); // check the email in the server - export default { components: { Button, @@ -202,7 +204,6 @@ export default { email: { required: helpers.withMessage(`Feltet må være utfylt`, required), email: helpers.withMessage("E-posten er ugyldig", email), - // isUnique: helpers.withAsync(isEmailTaken), }, password: { required: helpers.withMessage(`Feltet må være utfylt`, required), @@ -246,10 +247,7 @@ export default { //If a user is created succsessfully, try to login //If we get this far, we will be pushed anyway so there is no point updating "loading" - if (!userCreated) { - this.errorMessage = "Could not create user."; - return; - } + if (!userCreated) return; const loginRequest = { email: this.email, @@ -270,17 +268,16 @@ export default { await this.$router.push("/"); }, async sendRegisterRequest() { - const registerInfo = { + const userInfo = { email: this.email, firstName: this.firstName, - lastname: this.lastName, + lastName: this.lastName, password: this.password, address: this.address, }; - - const response = await registerUser(registerInfo); - - return response.status === 200; + const res = await UserService.registerUser(userInfo); + this.errorMessage = res; + return res.status === 200; }, }, }; diff --git a/src/components/UserProfileComponents/RatingComponents/Rating.vue b/src/components/UserProfileComponents/RatingComponents/Rating.vue index 6efa3b014f4c175b7af950ddb36119a7029f60f9..dc8eee3eefd29f3f2014d8babf87ea3a08cd47d5 100644 --- a/src/components/UserProfileComponents/RatingComponents/Rating.vue +++ b/src/components/UserProfileComponents/RatingComponents/Rating.vue @@ -1,4 +1,5 @@ <template> + <!-- Shows rating, if no rating found, tells the user that no rating is registered --> <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"> @@ -29,12 +30,6 @@ ></path> </svg> </li> - <li> - <!-- 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> </template> @@ -46,8 +41,9 @@ export default { ratingType: String, }, methods: { + //Fills the rating stars getFill(i) { - if (i <= this.rating) { + if (i <= Math.round(this.rating)) { return "w-5 h-5 text-warn-medium"; } return "w-5 h-5 text-gray-300 dark:text-gray-500"; diff --git a/src/components/UserProfileComponents/RatingComponents/RatingModal.vue b/src/components/UserProfileComponents/RatingComponents/RatingModal.vue index 419d4f9e4d6ec8cdb2545efc701f4f602ae26bf3..c73f67973335cde876b10b3dc9e269f405f6b528 100644 --- a/src/components/UserProfileComponents/RatingComponents/RatingModal.vue +++ b/src/components/UserProfileComponents/RatingComponents/RatingModal.vue @@ -1,5 +1,5 @@ <template> - <!-- Main modal --> + <!-- Main modal for rating --> <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" @@ -16,18 +16,7 @@ @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> + <XIcon class="w-5 h-5" /> </button> </div> <!-- Modal body --> @@ -58,76 +47,25 @@ /> </div> + <!-- 5 rating stars --> <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> + <StarIcon :class="getStarType(0)" @click="setRating(1)"> </StarIcon> + <StarIcon :class="getStarType(1)" @click="setRating(2)"> </StarIcon> + <StarIcon :class="getStarType(2)" @click="setRating(3)"> </StarIcon> + <StarIcon :class="getStarType(3)" @click="setRating(4)"> </StarIcon> + <StarIcon :class="getStarType(4)" @click="setRating(5)"> </StarIcon> </div> + <!-- Button for submitting the rating --> <div class="flex justify-center mb-4"> - <Button :text="'Send en vurdering'" @click="sendRating"></Button> + <ColoredButton + :text="'Send en vurdering'" + @click="sendRating" + ></ColoredButton> </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> @@ -136,19 +74,22 @@ </template> <script> -import Button from "@/components/BaseComponents/ColoredButton"; +import ColoredButton from "@/components/BaseComponents/ColoredButton"; import { postNewRating } from "@/utils/apiutil"; +import { XIcon } from "@heroicons/vue/outline"; +import { StarIcon } from "@heroicons/vue/solid"; export default { name: "RatingModal", + emits: ["close", "reload"], data() { return { score: 3, comment: "", rating: [ - "text-warn", - "text-warn", - "text-warn", + "text-warn-medium", + "text-warn-medium", + "text-warn-medium", "text-gray-300", "text-gray-300", ], @@ -163,22 +104,32 @@ export default { }, components: { - Button, + XIcon, + ColoredButton, + StarIcon, }, methods: { + //A method for setting the rating setRating(ratingNumber) { this.score = ratingNumber; for (let i = 0; i < 5; i++) { if (i < ratingNumber) { - this.rating[i] = "text-warn"; + this.rating[i] = "text-warn-medium"; } else { this.rating[i] = "text-gray-300"; } } }, + getStarType(n) { + return "w-10 h-10 cursor-pointer " + this.rating[n]; + }, close() { this.$emit("close"); }, + /** + * A method for sending the rating when button is clicked. + * It posts the rating to the db. + */ async sendRating() { const ratingInfo = { score: this.score, @@ -186,9 +137,10 @@ export default { renterIsReceiverOfRating: this.renterIsReceiverOfRating, rentID: this.rentID, }; - await postNewRating(ratingInfo); + await postNewRating(ratingInfo); this.$emit("reload"); + this.$router.go(0); }, }, }; diff --git a/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue b/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue index 675524104ec1ebe1b74410ae9114131c666f4023..c3745f5821a630462d7f553456931eca1e3ba742 100644 --- a/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue +++ b/src/components/UserProfileComponents/RentHistoryComponents/RentHistoryItem.vue @@ -1,16 +1,24 @@ <template> + <!-- A card for a rent history containing the item's title, + name of the person who rented it, dates/renting period, and a button + to rate if not already rated it. --> <div class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50" > + <!-- Title --> <p class="font-bold mx-4" id="title"> {{ historyItem.listing.title }} </p> + + <!-- Name of the user who rented --> <div class="flex flex-row items-center"> <div class="flex flex-col px-4 flex-1"> <router-link :to="{ path: '/profile/' + user.userId }"> Leid til: {{ user.firstName }} {{ user.lastName }} </router-link> </div> + + <!-- Period it was rented for --> <div class="flex flex-col flex-1"> <div> Fra: @@ -20,6 +28,8 @@ Til: {{ this.getDateString(historyItem.toTime, isMultipleDays) }} </div> </div> + + <!-- Button to rate --> <colored-button v-if="!isRated" :text="'Vurder'" @@ -74,12 +84,16 @@ export default { currentUser() { return parseCurrentUser(); }, + //Checks if the rent period was multiple days or not isMultipleDays() { if (this.historyItem.toTime - this.historyItem.fromTime < 86400000) { return false; } return true; }, + /** + * computed the total price based on how long the renting period was. + */ price() { if (this.isMultipleDays) { let numDays = Math.round( @@ -89,15 +103,24 @@ export default { } return this.historyItem.listing.pricePerDay; }, + /** + * Checks if the user rented its own item + */ currentUserIsRenter() { return this.isCurrentUser(this.historyItem.renterId); }, }, methods: { + /** + * returns a date as a string + */ getDateString(milliseconds) { let today = new Date(); let date = new Date(milliseconds); - let dateString = date.getDate() + "." + (date.getMonth() + 1); + const shortMonthOfRentHistoryItem = date.toLocaleString("default", { + month: "short", + }); + let dateString = date.getDate() + ". " + shortMonthOfRentHistoryItem; if (date.getFullYear() != today.getFullYear()) { dateString += "." + date.getFullYear(); @@ -108,6 +131,9 @@ export default { return id == this.currentUser.accountId; }, }, + /** + * Gets the user and checks if a rating is saved, before mounting the page. + */ async beforeCreate() { if (this.currentUserIsRenter) { this.user = await userService.getUserFromId( diff --git a/src/components/UserProfileComponents/UserItems.vue b/src/components/UserProfileComponents/UserItems.vue index 418a4f22fa9e684959fe6cfa2d5033c7ce5671c3..53cacdd2751b93ab3323097fa641a1f5dedcaf92 100644 --- a/src/components/UserProfileComponents/UserItems.vue +++ b/src/components/UserProfileComponents/UserItems.vue @@ -1,9 +1,11 @@ <template> + <!-- Shows all the items a user has posted with search and pagination. + Includes a dropdown menu for editing or deleting an item. --> <div id="headline" class="text-xl md:text-2xl text-primary-light font-medium"> Mine gjenstander </div> <!-- Search field --> - <div class="relative" id="searchComponent"> + <div class="relative mx-4" 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 @@ -20,12 +22,12 @@ 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" + placeholder="Søk" v-model="search" @change="searchWritten" /> </div> - <div class="absolute inset-x-0 px-5 py-3"> + <div class="absolute inset-x-0"> <!-- ItemCards --> <div class="flex items-center justify-center w-screen"> <!-- Shows items based on pagination --> @@ -39,18 +41,20 @@ v-for="item in visibleItems" :key="item" > - <ItemCard - id="ItemCardPage" - class="ItemCard w-fit h-fit" - :item="item" - /> + <div class="w-full"> + <ItemCard + id="ItemCardPage" + class="ItemCard w-full h-full" + :item="item" + /> + </div> - <TripleDotButton class="DotButton" @click="openDotMenu(item)"> - </TripleDotButton> + <!-- Dropdown menu with options for editing an item and deleting an item --> + <TripleDotButton class="DotButton" @click="openDotMenu(item)" /> <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" + class="options z-10 w-44 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 pt-4 pl-12" > <ul class="py-1 absolute bg-white ring-1 ring-gray-300 rounded-xl" @@ -61,7 +65,7 @@ @click=" this.$router.push('/item/' + item.listingID + '/edit') " - 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" + class="editButton 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> @@ -69,7 +73,7 @@ <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" + class="deleteButton 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> @@ -78,6 +82,8 @@ </div> </div> + <!-- A waring asking the user if it is sure it wants to delete the item + with options to go ahead with the deleting or to cancel the delete. --> <CustomFooterModal @close="this.readyToDelete = false" :visible="readyToDelete" @@ -86,17 +92,17 @@ > <div class="flex justify-center p-2"> <ColoredButton - id="#cancelDeleteButton" + id="#cancelDeleteButton1" :text="'Avbryt'" @click="cancelDelete" class="bg-gray-500 m-2" ></ColoredButton> <ColoredButton - id="confirmDeleteButton" + id="confirmDeleteButton1" @click="deleteItem" :text="'Slett'" - class="m-2 bg-error-medium" + class="confirmDelete m-2 bg-error-medium" > </ColoredButton> </div> @@ -105,14 +111,25 @@ <!-- 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" + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" v-if="showSearchedItems" > - <div class="cardContainer" v-for="item in searchedItems" :key="item"> - <ItemCard id="ItemCardSearch" class="ItemCard" :item="item" /> - <TripleDotButton class="DotButton" @click="openDotMenu(item)"> - </TripleDotButton> + <div + class="cardContainer" + id="item" + v-for="item in searchedItems" + :key="item" + > + <div class="w-full"> + <ItemCard + id="ItemCardSearch" + class="ItemCard w-full h-full" + :item="item" + /> + </div> + <!-- Dropdown menu with options for editing an item and deleting an item --> + <TripleDotButton class="DotButton" @click="openDotMenu(item)" /> <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" @@ -142,6 +159,9 @@ </ul> </div> </div> + + <!-- A waring asking the user if it is sure it wants to delete the item + with options to go ahead with the deleting or to cancel the delete. --> <CustomFooterModal @close="this.readyToDelete = false" :visible="readyToDelete" @@ -150,7 +170,7 @@ > <div class="flex justify-center p-2"> <ColoredButton - id="#cancelDeleteButton" + id="cancelDeleteButton" :text="'Avbryt'" @click="cancelDelete" class="bg-gray-500 m-2" @@ -167,6 +187,7 @@ </CustomFooterModal> </div> </div> + <!-- pagination --> <div class="flex justify-center" v-if="showItems"> <PaginationTemplate @@ -181,14 +202,14 @@ </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"; +import UserService from "@/services/user.service"; +import listingService from "@/services/listing.service"; + export default { name: "UserItems", components: { @@ -215,6 +236,7 @@ export default { search: "", readyToDelete: false, dropdown: false, + //Variables connected to pagination currentPage: 0, pageSize: 12, @@ -222,6 +244,9 @@ export default { }; }, computed: { + /** + * Searchs items based on their title, address and price per day. + */ searchedItems() { let filteredItems = []; @@ -247,10 +272,12 @@ export default { } }, getUserListingsFromAPI: async function () { - this.items = await GetUserListings(); + this.items = await UserService.getUserListings(); for (var i = 0; i < this.items.length; i++) { this.items[i].toggle = false; - let images = await getItemPictures(this.items[i].listingID); + let images = await listingService.getItemPictures( + this.items[i].listingID + ); if (images.length > 0) { this.items[i].img = images[0].picture; } @@ -259,7 +286,10 @@ export default { cancelDelete() { this.readyToDelete = false; }, - //Pagination + /** + * 2 methods related to pagination. Updates page and updates + * visible items on page. + */ updatePage(pageNumber) { this.currentPage = pageNumber; this.updateVisibleTodos(); @@ -275,6 +305,7 @@ export default { this.updatePage(this.currentPage - 1); } }, + goToDeleteItem(item) { this.chosenItem = item; this.readyToDelete = true; @@ -283,8 +314,11 @@ export default { await UserService.setListingToDeleted(this.chosenItem); this.$router.go(0); }, + + /** + * This method triggers when search input field is changed + */ searchWritten: function () { - //This method triggers when search input field is changed if (this.search.length > 0) { this.showItems = false; this.showSearchedItems = true; @@ -294,6 +328,11 @@ export default { } }, }, + + /** + * Gets userlistings and prepares the pagination by + * updating the items to be visible. + */ async beforeMount() { await this.getUserListingsFromAPI(); this.updateVisibleTodos(); @@ -311,6 +350,7 @@ export default { .cardContainer { position: relative; } + .DotButton { position: absolute; right: 40px; diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue index 17c002bc28fce875361dafac43ea0add48e3e9a0..2d465f8c61e9e455e89b7628ed08761077793f81 100644 --- a/src/components/UserProfileComponents/UserListItemCard.vue +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -1,4 +1,7 @@ <template> + <!-- A card for displaying a user in a list. + Displays a user's profile picture, name, rating and some + buttons based on where the list is being shown. --> <div class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-4" > @@ -24,7 +27,7 @@ <!-- Buttons --> <div class="flex flex-row gap-4"> <IconButton - v-if="buttons.includes('chat')" + v-if="buttons.includes('chat') && userID != user.userId" @click="openChatWithUser()" :text="'Chat'" :buttonColor="'blue'" @@ -33,7 +36,7 @@ /></IconButton> <IconButton - v-if="buttons.includes('kick')" + v-if="buttons.includes('kick') && userID != user.userId" @click="kickUserFromCommunity()" :buttonColor="'red'" :text="'Spark'" @@ -62,8 +65,8 @@ <script> import RatingComponent from "@/components/UserProfileComponents/RatingComponents/Rating.vue"; import IconButton from "@/components/BaseComponents/IconButton.vue"; -import UserService from "@/services/user.service"; import CommunityAdminService from "@/services/community-admin.service"; +import { parseCurrentUser } from "@/utils/token-utils"; import { ChatIcon, @@ -76,7 +79,6 @@ export default { name: "UserListItem", data() { return { - rating: -1.0, communityID: -1, profileImage: { src: require("../../assets/defaultUserProfileImage.jpg"), @@ -94,8 +96,16 @@ export default { props: { user: Object, buttons: Array, + rating: Number, }, computed: { + userID() { + return parseCurrentUser().accountId; + }, + /** + * returns the user's profile picture. If the user does not have any + * profile picture the default profile picture is returned. + */ getProfilePicture() { if (this.user.picture !== "" && this.user.picture != null) { return this.user.picture; @@ -104,18 +114,25 @@ export default { }, }, methods: { + /** + * If chat button clicked, the user's gets redirected to chat page. + */ openChatWithUser() { this.$router.push({ name: "messages", query: { userID: this.user.userId }, }); }, + + /** + * Admin related methods for kicking a user from a community, + * accepting and rejecting a user's join community request + */ kickUserFromCommunity() { CommunityAdminService.removeUserFromCommunity( this.communityID, this.user.userId ); - //Find a better way to do this window.location.reload(); }, acceptMemberRequest() { @@ -131,14 +148,7 @@ export default { ); }, }, - async created() { - 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; + async beforeMounted() { this.communityID = this.$route.params.communityID; }, }; diff --git a/src/components/UserProfileComponents/UserProfile.vue b/src/components/UserProfileComponents/UserProfile.vue index c7c39da640c00bc06ee1e4b39ee0f38b8579e721..3ad810dc8a65986670aac5e217a1fe3f91eafee6 100644 --- a/src/components/UserProfileComponents/UserProfile.vue +++ b/src/components/UserProfileComponents/UserProfile.vue @@ -1,12 +1,17 @@ <template> + <!-- User profile with the logged in user's info and a dropdown menu--> <div class="w-full max-w-xl m-auto md:ring-1 ring-gray-300 overflow-hidden rounded-xl p-4" > + <!-- A warning when deleting a user account to ask the user + if it is sure --> <DeleteUserModal :visible="show" @close="this.show = false" @deleteUser="deleteUser" /> + + <!-- dropdown icon button to toggle(open/close) the dropdown menu --> <div v-show="isCurrentUser" class="float-right px-4 pt-4"> <button id="dropdownDefault" @@ -27,6 +32,8 @@ </svg> </button> + <!-- Dropdown menu containing options for seeing user's items, user's communities, + renting history, logging out, changing password and deleting account --> <div id="dropdown" v-show="dropdown" @@ -38,7 +45,7 @@ > <li> <router-link - to="/user/userItems" + to="/profile/items" class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" >Mine gjenstander</router-link > @@ -84,6 +91,7 @@ </div> </div> + <!-- User info (name, rating and profile picture) --> <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" @@ -95,10 +103,17 @@ </h5> <div> <rating-component :rating="renterRating" :ratingType="'Leietaker'" /> - <rating-component :rating="ownerRating" :ratingType="'Utleier'" /> + <RatingComponent + :rating="ownerRating" + :ratingType="'Utleier'" + ></RatingComponent> </div> - <div v-show="!isCurrentUser" class="flex mt-4 space-x-3 lg:mt-6"> + <div + v-show="!isCurrentUser" + @click="$router.push('/messages?userID=' + id)" + class="flex mt-4 space-x-3 lg:mt-6" + > <a href="#" class="inline-flex items-center py-2 px-4 text-sm font-medium text-center text-gray-900 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-700 dark:focus:ring-gray-700" @@ -149,6 +164,9 @@ export default { }, }, methods: { + /** + * Gets the user and it's ratings + */ async getUser() { this.currentUser = await parseCurrentUser(); this.id = await this.$router.currentRoute.value.params.id; @@ -157,11 +175,13 @@ export default { this.isCurrentUser = true; this.user = this.currentUser; this.user = await UserService.getUserFromId(this.user.accountId); - return; + } else { + this.user = await getUser(this.id); } - this.user = await getUser(this.id); - let ratingAsOwner = await UserService.getUserRatingAsOwner(this.id); - let ratingAsRenter = await UserService.getUserRatingAsRenter(this.id); + let ratingAsOwner = await UserService.getUserRatingAverageOwner(this.id); + let ratingAsRenter = await UserService.getUserRatingAverageRenter( + this.id + ); if (ratingAsOwner >= 0 && ratingAsOwner <= 5) { this.ownerRating = ratingAsOwner; @@ -170,6 +190,10 @@ export default { this.renterRating = ratingAsRenter; } }, + + /** + * Logs out the user and sets token in state to be null. + */ logout() { this.$store.commit("logout"); this.$router.push("/"); @@ -182,8 +206,8 @@ export default { this.logout(); }, }, - beforeMount() { - this.getUser(); + async beforeMount() { + await this.getUser(); }, }; </script> diff --git a/src/router/index.js b/src/router/index.js index 9f7228cc993ef163a3a40f9ea84fe3ce57f3bf2d..563131c78452c42a65ef14c683a1f76abcac758c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -3,22 +3,28 @@ 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 - * login page and the register page will be accessible. + * Guard route for logged-in users. + * If token is null, no user is logged in and only the + * login, register, reset password, help and listing of public groups and items pages will be accessible. + * Without a token the user gets redirected to the login page when trying to access guard routed pages. */ -function guardRoute(to, from, next) { +function authGuardRoute(to, from, next) { let isAuthenticated = store.state.user.token != null; if (isAuthenticated) { next(); // allow to enter route } else { - next("/login"); // go to '/login'; + next("/login"); // go to the login page } } -function isAdmin(to, from, next) { +/** + * Guards route for administartors. If token is null or + * the user is not the administrator of the group, the user gets sent to the home page. + */ +function adminGuardRoute(to, from, next) { if (store.state.user.adminList.includes(parseInt(from.params.communityID))) - next(); - else next("/"); + next(); // allow to enter route + else next("/"); // go to the home page } const routes = [ @@ -28,38 +34,44 @@ const routes = [ component: () => import("../views/CommunityViews/CommunityView.vue"), }, { - path: "/help", - name: "help", - component: () => import("../views/HelpView.vue"), + path: "/community/:communityID", + name: "communityHome", + component: () => import("../views/CommunityViews/CommunityHomeView.vue"), }, { - path: "/profile/:id", - name: "profile", - component: () => import("../views/UserProfileViews/ProfileView.vue"), - beforeEnter: guardRoute, + path: "/community/:communityID/admin", + name: "communityAdminPage", + component: () => import("@/views/CommunityViews/AdminView.vue"), + beforeEnter: [authGuardRoute, adminGuardRoute], }, { - path: "/profile/history", - name: "history", - component: () => import("../views/UserProfileViews/RentHistoryView.vue"), - beforeEnter: guardRoute, + path: "/community/:communityID/memberlist", + name: "memberlist", + component: () => import("../views/CommunityViews/MemberListView.vue"), + beforeEnter: authGuardRoute, }, { - path: "/profile/communities", - name: "myCommunities", - component: () => import("../views/UserProfileViews/MyCommunitiesView.vue"), - beforeEnter: guardRoute, + path: "/community/:communityID/private/join", + name: "communityRequest", + component: () => import("../views/CommunityViews/CommunityRequestView.vue"), + beforeEnter: authGuardRoute, }, { - path: "/register", - name: "register", - component: () => import("../views/UserAuthViews/RegisterView.vue"), + path: "/help", + name: "help", + component: () => import("../views/HelpView.vue"), }, { - path: "/messages", - name: "messages", - component: () => import("../views/ChatViews/ChatView.vue"), - beforeEnter: guardRoute, + path: "/item/:id", + name: "itemInfo", + component: () => import("../views/RentingViews/ItemInfoPageView.vue"), + beforeEnter: authGuardRoute, + }, + { + path: "/item/:id/edit", + name: "editItem", + component: () => import("../views/ItemViews/EditItemView.vue"), + beforeEnter: authGuardRoute, }, { path: "/login", @@ -67,80 +79,79 @@ const routes = [ component: () => import("../views/UserAuthViews/LoginView.vue"), }, { - path: "/newPassword", - name: "newPassword", - component: () => import("../views/UserAuthViews/NewPasswordView"), - beforeEnter: guardRoute, - }, - { - path: "/searchItemList", - name: "searchItemList", - component: () => import("../views/ItemViews/SearchItemListView.vue"), - }, - { - path: "/resetPassword", - name: "resetPassword", - component: () => import("../views/UserAuthViews/ResetPasswordView.vue"), + path: "/messages", + name: "messages", + component: () => import("../views/ChatViews/ChatView.vue"), + beforeEnter: authGuardRoute, }, { path: "/newCommunity", name: "newCommunity", component: () => import("../views/CommunityViews/NewCommunityView.vue"), - beforeEnter: guardRoute, - }, - { - path: "/community/:communityID/memberlist", - name: "memberlist", - component: () => import("../views/CommunityViews/MemberListView.vue"), - beforeEnter: guardRoute, + beforeEnter: authGuardRoute, }, { path: "/newItem", name: "newItem", component: () => import("../views/ItemViews/NewItemView.vue"), - beforeEnter: guardRoute, + beforeEnter: authGuardRoute, }, { - path: "/community/:communityID", - name: "communityHome", - component: () => import("../views/CommunityViews/CommunityHomeView.vue"), + path: "/newPassword", + name: "newPassword", + component: () => import("../views/UserAuthViews/NewPasswordView"), + beforeEnter: authGuardRoute, }, { - path: "/community/:communityID/private/join", - name: "communityRequest", - component: () => import("../views/CommunityViews/CommunityRequestView.vue"), - beforeEnter: guardRoute, + path: "/profile/:id", + name: "profile", + component: () => import("../views/UserProfileViews/ProfileView.vue"), + beforeEnter: authGuardRoute, }, { - path: "/test", - name: "test", - component: () => import("../views/TestView.vue"), + path: "/profile/communities", + name: "myCommunities", + component: () => import("../views/UserProfileViews/MyCommunitiesView.vue"), + beforeEnter: authGuardRoute, }, { - path: "/community/:communityID/admin", - name: "CommunityAdminView", - component: () => import("@/views/CommunityViews/AdminView.vue"), - beforeEnter: isAdmin, + path: "/profile/history", + name: "history", + component: () => import("../views/UserProfileViews/RentHistoryView.vue"), + beforeEnter: authGuardRoute, }, { - path: "/itempage/:id", - name: "ItemInfo", - component: () => import("../views/RentingViews/ItemInfoPageView.vue"), - beforeEnter: guardRoute, + path: "/profile/items", + name: "userItems", + component: () => import("../views/UserProfileViews/UserItemsView.vue"), + beforeEnter: authGuardRoute, }, { - path: "/item/:id/edit", - name: "editItem", - component: () => import("../views/ItemViews/EditItemView.vue"), - beforeEnter: guardRoute, + path: "/register", + name: "register", + component: () => import("../views/UserAuthViews/RegisterView.vue"), }, { - path: "/user/userItems", - name: "UserItems", - component: () => import("../views/UserProfileViews/UserItemsView.vue"), - beforeEnter: guardRoute, + path: "/resetPassword", + name: "resetPassword", + component: () => import("../views/UserAuthViews/ResetPasswordView.vue"), + }, + { + path: "/searchItemList", + name: "searchItemList", + component: () => import("../views/ItemViews/SearchItemListView.vue"), }, - // Make sure it's your last route definition + { + path: "/test", + name: "test", + component: () => import("../views/TestView.vue"), + beforeEnter: authGuardRoute, + }, + + /** + * Catch all for wrong/non-existing routes + * Must be last to catch all + */ { path: "/:pathMatch(.*)*", name: "not-found", component: NotFound }, ]; diff --git a/src/services/community-admin.service.js b/src/services/community-admin.service.js index c496e0a74fa3f1b9e282a1a4146b3a793b3547d3..3e7f9cbd5e617c99e79b364eee725b27034b927b 100644 --- a/src/services/community-admin.service.js +++ b/src/services/community-admin.service.js @@ -75,7 +75,7 @@ class CommunityAdminService { * @returns TODO */ async deleteCommunity(communityID) { - return await axios.post( + return await axios.delete( API_URL + "communities/" + communityID + "/remove", { headers: tokenHeader(), diff --git a/src/services/image.service.js b/src/services/image.service.js index 7ebc1fa786bdf0a79c9c193b662777a1bcabfc7f..3104f1b6be3627b9a6c1a2b72c9957f513c89d54 100644 --- a/src/services/image.service.js +++ b/src/services/image.service.js @@ -42,6 +42,19 @@ class ImageService { console.error(error.response); }); } + + deleteImage(image) { + return axios + .delete(image, { + headers: tokenHeader(), + }) + .then((response) => { + return response; + }) + .catch((error) => { + console.error(error.response); + }); + } } export default new ImageService(); diff --git a/src/services/user.service.js b/src/services/user.service.js index b6d908ba08d23bbfc32c6229ff1c093229c57a39..5aeb0201f8a3c32d04b6eae18808d0ec2f763fb3 100644 --- a/src/services/user.service.js +++ b/src/services/user.service.js @@ -39,6 +39,28 @@ class UserService { .catch((err) => console.error(err)); } + async getUserRatingAverageOwner(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average/owner", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); + } + + async getUserRatingAverageRenter(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average/renter", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); + } + async setListingToDeleted(listingId) { return await axios .delete(API_URL + "listing/" + listingId, { @@ -124,5 +146,19 @@ class UserService { }) .catch((err) => console.log(err)); } + + async registerUser(userInfo) { + return await axios + .post(API_URL + "register", userInfo) + .then((res) => { + return res; + }) + .catch((err) => { + if (err.response) { + return err.response.data; + } + console.error(err); + }); + } } export default new UserService(); diff --git a/src/store/modules/user.js b/src/store/modules/user.js index 8c3744e58a0c4e387ddb83cfcef1a99efebbff7d..00d6665d7964d04a606aa2959e57672be8510044 100644 --- a/src/store/modules/user.js +++ b/src/store/modules/user.js @@ -6,6 +6,7 @@ const state = { const mutations = { logout(state) { state.token = null; + state.adminList = []; }, saveToken(state, token) { state.token = token; @@ -18,6 +19,10 @@ const mutations = { state.adminList.push(communityIDArray[i]); } }, + addAdmin(state, communityID) { + if (!Number(communityID)) return; + state.adminList.push(communityID); + }, }; export default { diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index 7b42b8219865c3b58a02716b8326923f1946e09d..74e9b481338da2f22eda07ebb48bdc0137b5c23c 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -18,21 +18,6 @@ export function doLogin(loginRequest) { }); } -export function registerUser(registerInfo) { - return axios - .post(API_URL + "register", { - email: registerInfo.email, - firstName: registerInfo.firstName, - lastName: registerInfo.lastname, - password: registerInfo.password, - address: registerInfo.address, - }) - .then((response) => { - return response; - }) - .catch((err) => console.error(err)); -} - export async function getUser(userid) { return axios .get(API_URL + "users/" + userid + "/profile", { diff --git a/src/views/CommunityViews/CommunityView.vue b/src/views/CommunityViews/CommunityView.vue index 329917065b0679060da6503891b05c932ebaf18a..11767faa7299f22ceb232eb8045c4441b096faa0 100644 --- a/src/views/CommunityViews/CommunityView.vue +++ b/src/views/CommunityViews/CommunityView.vue @@ -48,7 +48,7 @@ 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" + placeholder="Søk" v-model="search" @change="searchWritten" /> diff --git a/src/views/ItemViews/EditItemView.vue b/src/views/ItemViews/EditItemView.vue index 763ecdafeb6145a16e7518c4630cf1ebdb79e5d0..6ae71767e398bd1f4d768bdb4682817dcde913d3 100644 --- a/src/views/ItemViews/EditItemView.vue +++ b/src/views/ItemViews/EditItemView.vue @@ -1,6 +1,7 @@ <template> + <!-- A view for editing the item --> <div class="h-screen grid md:mt-8"> - <edit-item-form :initialItem="initialItem" :communities="communities" /> + <edit-item-form /> </div> </template> diff --git a/src/views/ItemViews/NewItemView.vue b/src/views/ItemViews/NewItemView.vue index fc9db3697ba1fe00f740bb7457f233be22e6cee5..a6213c5e7c284b2980f49a5d53e6b135ed614c34 100644 --- a/src/views/ItemViews/NewItemView.vue +++ b/src/views/ItemViews/NewItemView.vue @@ -14,5 +14,3 @@ export default { }, }; </script> - -<style scoped></style> diff --git a/src/views/ItemViews/SearchItemListView.vue b/src/views/ItemViews/SearchItemListView.vue index 7da50f9397c5dee29f471267b8cb08866edcccfb..816e04e2b43a0673bb142277e79e73cc5685347d 100644 --- a/src/views/ItemViews/SearchItemListView.vue +++ b/src/views/ItemViews/SearchItemListView.vue @@ -1,4 +1,5 @@ <template> + <!-- View for search item component --> <SearchItemListComponent></SearchItemListComponent> </template> @@ -11,5 +12,3 @@ export default { }, }; </script> - -<style scoped></style> diff --git a/src/views/UserProfileViews/MyCommunitiesView.vue b/src/views/UserProfileViews/MyCommunitiesView.vue index e474528640a45dc3254256ee8ef9afef3871f7ea..bee55d4487fbdcfdcdec30435f0ba00f70ca02bb 100644 --- a/src/views/UserProfileViews/MyCommunitiesView.vue +++ b/src/views/UserProfileViews/MyCommunitiesView.vue @@ -1,4 +1,5 @@ <template> + <!-- A view for showing all the communities a user is part of --> <div> <div v-if="loading" class="flex place-content-center p-8"> <LoaderSpinner /> @@ -24,6 +25,7 @@ import CommunityList from "@/components/CommunityComponents/CommunityList.vue"; import CommunityService from "@/services/community.service"; import { UserAddIcon } from "@heroicons/vue/outline"; +import LoaderSpinner from "@/components/BaseComponents/LoaderSpinner"; export default { data() { @@ -35,6 +37,7 @@ export default { components: { CommunityList, UserAddIcon, + LoaderSpinner, }, async beforeCreate() { this.loading = true; diff --git a/src/views/UserProfileViews/ProfileView.vue b/src/views/UserProfileViews/ProfileView.vue index a490eb09dfd233803ffd624b428fa206ec767f63..cbd3a8294c0fb345ddebd3f24fbfc9c87c8fe77a 100644 --- a/src/views/UserProfileViews/ProfileView.vue +++ b/src/views/UserProfileViews/ProfileView.vue @@ -1,4 +1,5 @@ <template> + <!-- A view for the user profile card --> <div> <LargeProfileCard :isCurrentUser="true" class="md:mt-8" /> </div> diff --git a/src/views/UserProfileViews/RentHistoryView.vue b/src/views/UserProfileViews/RentHistoryView.vue index 45e7859306a8e3ede90ca30d039cb1f7840bb777..6046627d069e7410e2c23a64d5181a5bc74399b5 100644 --- a/src/views/UserProfileViews/RentHistoryView.vue +++ b/src/views/UserProfileViews/RentHistoryView.vue @@ -1,4 +1,5 @@ <template> + <!-- A view for showing rating and rent history --> <rating-modal :visible="modal.show" :name="modal.name" @@ -58,9 +59,6 @@ export default { fullHistory.sort(compareHistoryItems); return fullHistory; }, - hasNoHistory() { - return this.renterHistory.length == 0 && this.ownerHistory.length == 0; - }, }, methods: { openModal(modal) { diff --git a/src/views/UserProfileViews/UserItemsView.vue b/src/views/UserProfileViews/UserItemsView.vue index 8ac137d470d2dd6ed213d18d9c778c1da872996b..6cf4fc43868be784e6e3cccf9cb02b2059d793d4 100644 --- a/src/views/UserProfileViews/UserItemsView.vue +++ b/src/views/UserProfileViews/UserItemsView.vue @@ -1,4 +1,5 @@ <template> + <!-- A view for showing all the items the user has posted/added --> <user-items> </user-items> </template> @@ -11,5 +12,3 @@ export default { }, }; </script> - -<style></style> diff --git a/tailwind.config.js b/tailwind.config.js index d8ef3f55c175f5952769a6b41dec929ef3533f4d..e2fb7f1be12a6a949bc1d2140855418d6d91f095 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,10 +26,7 @@ module.exports = { medium: "#004aad", dark: "#002B66", }, - secondary: { - light: "#653273", - dark: "#731050", - }, + secondary: "#ff5a5f", error: { light: "#EF4444", medium: "#DC2626", diff --git a/tests/unit/component-tests/ChatComponentsTest/RentalMessage.spec.js b/tests/unit/component-tests/ChatComponentsTest/RentalMessage.spec.js index 6625f6151a85edfc90bbe86cd680727bbebc13f9..9c520a8c89f7a1157b68f29f9c2ed77ba2fbf9a1 100644 --- a/tests/unit/component-tests/ChatComponentsTest/RentalMessage.spec.js +++ b/tests/unit/component-tests/ChatComponentsTest/RentalMessage.spec.js @@ -13,11 +13,23 @@ jest.mock("@/utils/token-utils", () => { }; }); +jest.mock("@/utils/apiutil", () => { + return { + getItemPictures: () => { + return new Promise((resolve) => { + resolve([]); + }); + }, + }; +}); jest.mock("axios"); describe("RentalMessage.vue", () => { let wrapper; + const mockRouter = { + go: jest.fn(), + }; beforeEach(() => { wrapper = shallowMount(RentalMessage, { propsData: { @@ -38,6 +50,11 @@ describe("RentalMessage.vue", () => { deleted: false, }, }, + global: { + mocks: { + $router: mockRouter, + }, + }, }); }); diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/color-button.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/color-button.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..23b4e25c2bebf3eb8fcc97d852a1024aedb26183 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/color-button.spec.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColoredButtonComponent renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- Button with custom text and color --> + <button + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" + > + hei + </button> + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/custom-footer-modal.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/custom-footer-modal.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..088e1e4160c9174e917d9a62b7472627b9223061 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/custom-footer-modal.spec.js.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IconButtonComponent renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- Main modal --> + <div + 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 items-start justify-between p-4 border-b rounded-t dark:border-gray-600" + > + <h3 + class="text-xl font-semibold text-gray-900 dark:text-white" + > + String + </h3> + <!-- Close button --> + <button + 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 + aria-hidden="true" + class="w-5 h-5" + fill="none" + stroke="currentColor" + stroke-width="2" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M6 18L18 6M6 6l12 12" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </button> + </div> + <!-- Modal body --> + <div + class="p-6 space-y-6" + > + <p + class="text-base leading-relaxed text-gray-500 dark:text-gray-400" + > + String + </p> + </div> + <!-- Modal footer --> + <div + class="rounded-b border-t border-gray-200 dark:border-gray-600" + > + <!-- Slot: Add any html you want here (Must be one div) --> + + <div + class="fake-msg" + > + String + </div> + + </div> + </div> + </div> + </div> + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/footer-bar.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/footer-bar.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..bf4ee8cc5e808ec6f563c2d10af1ec4cb33fc35b --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/footer-bar.spec.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FooterBar component renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- Footer --> + <footer + class="w-full bg-white dark:bg-gray-800 sm:flex-row border-1 border-t border-gray-600 h-10" + > + <!-- Copyright --> + <p + class="float-left text-xs my-3 ml-4 text-primary-dark" + > + © BoCo 2022 - All rights reserved + </p> + <!-- Icon link to help page --> + <svg + aria-hidden="true" + class="md:mt-0 mt-1 mr-4 float-right cursor-pointer h-8 md:h-10 text-primary-medium" + fill="none" + stroke="currentColor" + stroke-width="2" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </footer> + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/form-image-display.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/form-image-display.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..a6dc51e0b065edf6405d394c937a4e028b591409 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/form-image-display.spec.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormImageDisplay component renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- Image --> + <img + alt="Bilde av gjenstanden" + class="w-2/5 inline" + src="http://localhost:3000/api/images/5" + /> + <!-- Modal --> + + <!-- Main modal --> + <!--v-if--> + + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/icon-button.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/icon-button.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..f38c6fa8f94b7e530126dffc3820753778b5c453 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/icon-button.spec.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IconButtonComponent renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- Button --> + <button + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" + > + <div + class="w-5 h-5 mx-1" + > + <!-- Slot for icon (Default BanIcon) --> + + <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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + + </div> + <span + class="mx-1" + > + hei + </span> + </button> + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/nav-bar.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/nav-bar.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..f466c4431962725a99e10fdad9139913a0315aa7 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/nav-bar.spec.js.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NavBar component renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- NavBar --> + <nav + 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" + > + <!-- Logo reroutes to homepage --> + <div + class="logo" + > + <img + alt="BoCo logo" + class="m-1 ml-4 cursor-pointer h-9 md:h-12" + src="" + /> + </div> + <ul + class="flex justify-between" + > + <!-- New listing button --> + <li + class="cursor-pointer" + > + <plus-icon-stub + alt="Legg til" + class="m-6 md:mr-2 h-7 text-primary-medium float-left" + /> + <a + class="hidden md:block mt-7 text-sm float-right" + > + Legg til + </a> + </li> + <!-- My messages button --> + <li + class="cursor-pointer" + > + <div + class="notification-container relative" + > + <chat-alt2-icon-stub + alt="Meldinger" + class="m-6 md:mr-2 h-7 text-primary-medium float-left" + /> + <!--v-if--> + <a + class="hidden md:block mt-7 text-sm float-right" + > + Meldinger + </a> + </div> + </li> + <!-- User profile button --> + <li + class="cursor-pointer" + > + <user-circle-icon-stub + alt="Profil" + class="m-6 md:mr-2 h-7 text-primary-medium float-left" + /> + <!-- Shows "Profil" if user is logged in, else "Logg inn" --> + <a + class="hidden md:block mr-4 mt-7 text-sm float-right" + > + Profil + </a> + </li> + </ul> + </nav> + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/notification-modal.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/notification-modal.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..485fb4f230c17053c0ffdd03ebbb1a003c2a55d9 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/notification-modal.spec.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotificationModal component renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- Main modal --> + <div + 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 items-start justify-between p-4 border-b rounded-t dark:border-gray-600" + > + <h3 + class="text-xl font-semibold text-gray-900 dark:text-white" + > + String + </h3> + <!-- Close button --> + <button + 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 + aria-hidden="true" + class="w-5 h-5" + fill="none" + stroke="currentColor" + stroke-width="2" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M6 18L18 6M6 6l12 12" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </button> + </div> + <!-- Modal body --> + <div + class="p-6 space-y-6" + > + <p + class="text-base leading-relaxed text-gray-500 dark:text-gray-400" + > + String + </p> + </div> + </div> + </div> + </div> + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/pagination-template.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/pagination-template.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..e4d76ee174ab3fcbf15a5a02601274a3bb2be6c6 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/pagination-template.spec.js.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaginationTemplate renders correctly 1`] = ` +<div + data-v-app="" +> + + <!-- Pagination --> + <!--v-if--> + +</div> +`; diff --git a/tests/unit/component-tests/base-component-tests/__snapshots__/triple-dot-button.spec.js.snap b/tests/unit/component-tests/base-component-tests/__snapshots__/triple-dot-button.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..710c50c9b036381f235ee14b68201cd4668257f9 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/__snapshots__/triple-dot-button.spec.js.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TripleDotButton component renders correctly 1`] = ` +<button + 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" + data-dropdown-toggle="dropdown" + id="dropdownDefault" + type="button" +> + <svg + aria-hidden="true" + class="w-6 h-6" + fill="none" + stroke="currentColor" + stroke-width="2" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> +</button> +`; diff --git a/tests/unit/component-tests/base-component-tests/color-button.spec.js b/tests/unit/component-tests/base-component-tests/color-button.spec.js index 29300503d6aeae9159b782520be94ebb502ce44c..ca9efb412b59485756b500dcbe8fb283e2464d31 100644 --- a/tests/unit/component-tests/base-component-tests/color-button.spec.js +++ b/tests/unit/component-tests/base-component-tests/color-button.spec.js @@ -13,6 +13,10 @@ describe("ColoredButtonComponent", () => { }); }); + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + it("is instantiated", () => { expect(wrapper.exists()).toBeTruthy(); }); diff --git a/tests/unit/component-tests/base-component-tests/custom-footer-modal.spec.js b/tests/unit/component-tests/base-component-tests/custom-footer-modal.spec.js index 5afb8f9cb6f6d966856b1a59516062300c80e2b1..4d4db9169c1d7c90402aa4e78be331938ebf487f 100644 --- a/tests/unit/component-tests/base-component-tests/custom-footer-modal.spec.js +++ b/tests/unit/component-tests/base-component-tests/custom-footer-modal.spec.js @@ -18,6 +18,10 @@ describe("IconButtonComponent", () => { }); }); + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + it("is instantiated", () => { expect(wrapper.exists()).toBeTruthy(); }); diff --git a/tests/unit/component-tests/base-component-tests/footer-bar.spec.js b/tests/unit/component-tests/base-component-tests/footer-bar.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f730e97654497cd4515b8f63f68718b01b81f2c9 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/footer-bar.spec.js @@ -0,0 +1,18 @@ +import { mount } from "@vue/test-utils"; +import FooterBar from "@/components/BaseComponents/FooterBar.vue"; + +describe("FooterBar component", () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(FooterBar); + }); + + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it("is instantiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); +}); diff --git a/tests/unit/component-tests/base-component-tests/form-image-display.spec.js b/tests/unit/component-tests/base-component-tests/form-image-display.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..2e2a3483fb42d9808c1716085083ced78dfd7160 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/form-image-display.spec.js @@ -0,0 +1,23 @@ +import { mount } from "@vue/test-utils"; +import FormImageDisplay from "@/components/BaseComponents/FormImageDisplay.vue"; + +describe("FormImageDisplay component", () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(FormImageDisplay, { + //passing prop to component + props: { + image: "http://localhost:3000/api/images/5", + }, + }); + }); + + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it("is instantiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); +}); diff --git a/tests/unit/component-tests/base-component-tests/icon-button.spec.js b/tests/unit/component-tests/base-component-tests/icon-button.spec.js index 376fb18f1f3ba20d4e417f7f615c2136f908016b..57b6f0fbc8450074c308627cd845f61085cb6743 100644 --- a/tests/unit/component-tests/base-component-tests/icon-button.spec.js +++ b/tests/unit/component-tests/base-component-tests/icon-button.spec.js @@ -13,6 +13,10 @@ describe("IconButtonComponent", () => { }); }); + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + it("is instantiated", () => { expect(wrapper.exists()).toBeTruthy(); }); diff --git a/tests/unit/component-tests/base-component-tests/nav-bar.spec.js b/tests/unit/component-tests/base-component-tests/nav-bar.spec.js index c728013aea767ae263d383c9f0cf92597874512b..940507557a0acbf1c42d42aa1eaca34b654fda3c 100644 --- a/tests/unit/component-tests/base-component-tests/nav-bar.spec.js +++ b/tests/unit/component-tests/base-component-tests/nav-bar.spec.js @@ -16,6 +16,10 @@ describe("NavBar component", () => { }); }); + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + it("is instantiated", () => { expect(wrapper.exists()).toBeTruthy(); }); diff --git a/tests/unit/component-tests/base-component-tests/notification-modal.spec.js b/tests/unit/component-tests/base-component-tests/notification-modal.spec.js index c005e4f94aec3da15453735e5e1b267884104baf..0fb2c913ba0efa291df6a397a8dd0ee7370da776 100644 --- a/tests/unit/component-tests/base-component-tests/notification-modal.spec.js +++ b/tests/unit/component-tests/base-component-tests/notification-modal.spec.js @@ -15,6 +15,10 @@ describe("NotificationModal component", () => { }); }); + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + it("is instantiated", () => { expect(wrapper.exists()).toBeTruthy(); }); diff --git a/tests/unit/component-tests/base-component-tests/pagination-template.spec.js b/tests/unit/component-tests/base-component-tests/pagination-template.spec.js index 91a7495f64107bf31184e1dee68e250a959e06de..b72731817026bf0de1a62bbbc04964c5f9c1e2d9 100644 --- a/tests/unit/component-tests/base-component-tests/pagination-template.spec.js +++ b/tests/unit/component-tests/base-component-tests/pagination-template.spec.js @@ -15,6 +15,10 @@ describe("PaginationTemplate", () => { }); }); + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + it("is instantiated", () => { expect(wrapper.exists()).toBeTruthy(); }); diff --git a/tests/unit/component-tests/base-component-tests/triple-dot-button.spec.js b/tests/unit/component-tests/base-component-tests/triple-dot-button.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..12657b3bfe01c9432b99af2d20deeb802722df51 --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/triple-dot-button.spec.js @@ -0,0 +1,18 @@ +import { mount } from "@vue/test-utils"; +import TripleDotButton from "@/components/BaseComponents/TripleDotButton.vue"; + +describe("TripleDotButton component", () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(TripleDotButton); + }); + + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it("is instantiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); +}); 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 30bc58a9142e8e5b02ef7f595d25c6b495848ea0..77b8975ee9dbbcc08570939bf65238456f813b77 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,34 +2,49 @@ exports[`CommunityHeader component renders correctly 1`] = ` <div - adminstatus="true" - community="[object Object]" + data-v-app="" > - <!-- Main modal --> - <!--v-if--> - + <!-- Community header contains the community's name and address, a join button if + the user is not in the community and community hamburger menu if the user is + in the community --> <div - class="flex place-content-center mx-4" + adminstatus="true" + community="[object Object]" > + <!-- A warning asking user is it is sure it wants to leave the community when leave community + from hamburger menu is clicked --> + + <!-- Main modal --> + <!--v-if--> + + <!-- The load spinner will show while community and it's members are being loaded from the db --> <div - class="loadingio-spinner-bean-eater-o5tefvffeqm" + class="flex place-content-center mx-4" > + + <!-- PacMan for indicating loading --> <div - class="ldio-sweozsnwol" + class="loadingio-spinner-bean-eater-o5tefvffeqm" > - <div> - <div /> - <div /> - <div /> - </div> - <div> - <div /> - <div /> - <div /> + <div + class="ldio-sweozsnwol" + > + <div> + <div /> + <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 4edde89c2c743d9c2411aa4a1a5c1403b7077c70..d52b806eba9a5cd9dcc8828d4bb79948c834e6ee 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 @@ -13,8 +13,8 @@ exports[`CommunityListItem component renders correctly 1`] = ` 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" + alt="Bilde" + class="rounded-md h-14 w-14 object-contain" src="string" /> </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 a385829acb32b11671e1d9788b5b401b95c4d892..c1b47cc4abaff7cddcd9ae8b01245615c9e5b528 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,110 +1,117 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CommunityList component renders correctly 1`] = ` -<ul> +<div + data-v-app="" +> - <li> - <div - class="border-black" - > - - <!-- Main modal --> - <!--v-if--> - + <!-- A list conatining all the communities --> + <ul> + + <li> <div - class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" + class="border-black" > + + <!-- Main modal --> + <!--v-if--> + <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" + class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" > <div - class="font-medium text-gray-800 dark:text-white truncate" + class="h-3 w-14 flex flex-col justify-center items-center ml-2 mt-4 mb-4 mr-2" > - string + <img + alt="Bilde" + class="rounded-md h-14 w-14 object-contain" + src="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" + <div + class="flex-1 pl-1 overflow-hidden" > - <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 + 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> - </div> - </li> - <li> - <div - class="border-black" - > - - <!-- Main modal --> - <!--v-if--> - + </li> + <li> <div - class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" + class="border-black" > + + <!-- Main modal --> + <!--v-if--> + <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" + class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-2" > <div - class="font-medium text-gray-800 dark:text-white truncate" + class="h-3 w-14 flex flex-col justify-center items-center ml-2 mt-4 mb-4 mr-2" > - string + <img + alt="Bilde" + class="rounded-md h-14 w-14 object-contain" + src="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" + <div + class="flex-1 pl-1 overflow-hidden" > - <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 + 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> - </div> - </li> + </li> + + </ul> -</ul> +</div> `; 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 9888519e7bc965cf1feeadf34dc0c6053e58a2fc..e44c8fc5aa31e9ace1a4f5430d4b37cd0265bd48 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 @@ -2,160 +2,167 @@ exports[`CreateNewGroup elements rendering renders correctly 1`] = ` <div - class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" + data-v-app="" > - <!-- Component heading --> + + <!-- A form for creating a new community --> <div - class="text-xl md:text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-10" + class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > - Opprett ny gruppe - </div> - <!-- Radio boxes --> - <div - class="mt-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" - id="radioBoxLabel" + <!-- Component heading --> + <div + class="text-xl md:text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-10" > - Synlighet - </label> + Opprett ny gruppe + </div> + <!-- Radio boxes --> <div - class="form-check" + class="mt-6" > - <input - class="form-check-input appearance-none rounded-full h-4 w-4 border border-gray-300 bg-white checked:bg-primary-medium checked:border-primary-medium focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer" - id="flexRadioOpen" - name="flexRadioDefault" - type="radio" - value="Åpen" - /> <label - class="form-check-label inline-block text-gray-800" - for="flexRadioOpen" - id="radioBoxOpenLabel" + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="radioBoxLabel" > - Åpen + Synlighet </label> + <div + class="form-check" + > + <input + class="form-check-input appearance-none rounded-full h-4 w-4 border border-gray-300 bg-white checked:bg-primary-medium checked:border-primary-medium focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer" + id="flexRadioOpen" + name="flexRadioDefault" + type="radio" + value="Åpen" + /> + <label + class="form-check-label inline-block text-gray-800" + for="flexRadioOpen" + id="radioBoxOpenLabel" + > + Åpen + </label> + </div> + <div + class="form-check" + > + <input + class="form-check-input appearance-none rounded-full h-4 w-4 border border-gray-300 bg-white checked:bg-primary-medium checked:border-primary-medium focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer" + id="flexRadioPrivate" + name="flexRadioDefault" + type="radio" + value="Privat" + /> + <label + class="form-check-label inline-block text-gray-800" + for="flexRadioPrivate" + id="radioBoxPrivateLabel" + > + Privat + </label> + </div> </div> + <!-- Title --> <div - class="form-check" + class="mt-6" > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="titleLabel" + > + Gruppenavn + </label> <input - class="form-check-input appearance-none rounded-full h-4 w-4 border border-gray-300 bg-white checked:bg-primary-medium checked:border-primary-medium focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer" - id="flexRadioPrivate" - name="flexRadioDefault" - type="radio" - value="Privat" + 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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="title" + required="" + type="text" /> + <!-- error message for title--> + + + </div> + <!-- Place --> + <div + class="mt-6" + > <label - class="form-check-label inline-block text-gray-800" - for="flexRadioPrivate" - id="radioBoxPrivateLabel" + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="positionLabel" > - Privat + By/Sted </label> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + required="" + type="text" + /> + <!-- error message for place--> + + </div> - </div> - <!-- Title --> - <div - class="mt-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" - id="titleLabel" - > - Gruppenavn - </label> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="title" - required="" - type="text" - /> - <!-- error message for title--> - - - </div> - <!-- Place --> - <div - class="mt-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" - id="positionLabel" - > - By/Sted - </label> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - required="" - type="text" - /> - <!-- error message for place--> - - - </div> - <!-- Description --> - <div - class="mt-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" - id="descriptionLabel" - > - Beskrivelse - </label> - <textarea - 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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="description" - required="" - rows="4" - /> - <!-- error message for description --> - - - </div> - <!-- Images --> - <div - class="mt-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" - id="imageLabel" + <!-- Description --> + <div + class="mt-6" > - Bilde (bildet må være .png) - </label> - <input - accept="image/png" - multiple="" - style="display: none;" - type="file" - /> - <!-- Button for adding an image --> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="descriptionLabel" + > + Beskrivelse + </label> + <textarea + 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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="description" + required="" + rows="4" + /> + <!-- error message for description --> + + + </div> + <!-- Images --> <div - class="inline-flex rounded-md shadow-sm" + class="mt-6" > - <!--<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" + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="imageLabel" + > + Bilde (bildet må være .png) + </label> + <input + accept="image/png" + multiple="" + style="display: none;" + type="file" + /> + <!-- Button for adding an image --> + <div + class="inline-flex rounded-md shadow-sm" > - Velg bilde - </button> + <!--<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" + > + Velg bilde + </button> + </div> + <!-- Div box for showing all chosen images --> + + + </div> + <!-- Save item button --> + <div + class="flex justify-center mt-10 float-right" + > + <button-stub + buttoncolor="blue" + id="saveButton" + text="Lagre" + /> </div> - <!-- Div box for showing all chosen images --> - - - </div> - <!-- Save item button --> - <div - class="flex justify-center mt-10 float-right" - > - <button-stub - buttoncolor="blue" - id="saveButton" - text="Lagre" - /> </div> + </div> `; 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 9167369f515802cca42dd7be7026189a2fa8c4be..d77de4b0af5129621991ac5931424977ad9af958 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 @@ -2,42 +2,49 @@ exports[`ItemCard component renders correctly 1`] = ` <div - class="mt-5 px-5" + data-v-app="" > + + <!-- Item card, displays title, address, price per day and a picture --> <div - class="w-full h-full rounded bg-gray-200 overflow-hidden display:inline-block correct-size" + class="mt-5 px-5" > <div - class="relative h-0 pb-[66.7%]" + class="w-full h-full rounded bg-gray-100 ring-1 ring-gray-200 overflow-hidden display:inline-block correct-size cursor-pointer" > - <img - alt="Item image" - class="w-full h-full absolute inset-0" - src="String" - /> - </div> - <div - class="p-1 m-1 bottom-0" - > - <p - class="font-bold text-sm" - id="title" - > - String - </p> - <p - class="text-gray-700 text-xs" - id="price" + <div + class="relative h-0 pb-[66.7%]" > - 0 kr - </p> - <p - class="text-gray-700 text-xs font-bold" - id="adress" + <img + alt="Item image" + class="w-full h-full object-contain absolute" + src="String" + /> + </div> + <div + class="p-1 m-1 bottom-0" > - String - </p> + <p + class="font-bold text-sm" + id="title" + > + String + </p> + <p + class="text-gray-700 text-xs" + id="price" + > + 0 kr + </p> + <p + class="text-gray-700 text-xs font-bold" + id="adress" + > + String + </p> + </div> </div> </div> + </div> `; 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 d302a72c8efaf512cedea636c34b06d59abc050e..c72b816590a7a3a9f0cb92722cb4e88cb5287698 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 @@ -2,235 +2,250 @@ exports[`NewItemForm component renders correctly 1`] = ` <div - class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" + data-v-app="" > - <!-- Component heading --> - <h3 - class="text-xl font-medium text-center text-primary-light mt-4 mb-8" - > - Opprett ny utleie - </h3> - <!-- Title --> - <div - class="mb-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" - id="titleLabel" - > - Tittel - </label> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="title" - required="" - type="text" - /> - <!-- error message for title--> - - - </div> - <!-- Select category --> + + <!-- Form for adding a new item --> <div - class="mb-6" + class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" - id="selectCategoryLabel" + <!-- Component heading --> + <h3 + class="text-xl font-medium text-center text-primary-light mt-4 mb-8" > - Kategori - </label> - <select - 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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="categories" + Opprett ny utleie + </h3> + <!-- Title --> + <div + class="mb-6" > - <option - class="text-gray-400" - disabled="" - value="" + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="titleLabel" > - Velg en kategori - </option> + Tittel + </label> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="title" + required="" + type="text" + /> + <!-- error message for title--> - <option - class="text-gray-900 text-sm" - > - Antikviteter og kunst - </option> - <option - class="text-gray-900 text-sm" - > - Dyr og utstyr - </option> - <option - class="text-gray-900 text-sm" + + </div> + <!-- Select category --> + <div + class="mb-6" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="selectCategoryLabel" > - Elektronikk og hvitevarer - </option> - <option - class="text-gray-900 text-sm" + Kategori + </label> + <select + 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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="categories" > - Foreldre og barn - </option> - <option - class="text-gray-900 text-sm" + <option + class="text-gray-400" + disabled="" + value="" + > + Velg en kategori + </option> + + <option + class="text-gray-900 text-sm" + > + Antikviteter og kunst + </option> + <option + class="text-gray-900 text-sm" + > + Dyr og utstyr + </option> + <option + class="text-gray-900 text-sm" + > + Elektronikk og hvitevarer + </option> + <option + class="text-gray-900 text-sm" + > + Foreldre og barn + </option> + <option + class="text-gray-900 text-sm" + > + Fritid, hobby og underholdning + </option> + <option + class="text-gray-900 text-sm" + > + Hage, oppussing og hus + </option> + <option + class="text-gray-900 text-sm" + > + Klær, kosmetikk og tilbehør + </option> + <option + class="text-gray-900 text-sm" + > + Møbler og interiør + </option> + <option + class="text-gray-900 text-sm" + > + Næringsvirksomhet + </option> + <option + class="text-gray-900 text-sm" + > + Sport og friluftsliv + </option> + <option + class="text-gray-900 text-sm" + > + Utstyr til bil, båt og MC + </option> + + </select> + <!-- error message for select category --> + + + </div> + <!-- Community --> + <div + class="mb-6" + > + <label + class="block text-sm font-medium text-gray-900 dark:text-gray-400" > - Fritid, hobby og underholdning - </option> - <option - class="text-gray-900 text-sm" + Grupper + </label> + <div + class="overflow-auto w-full max-h-32 mt-2 text-base list-none bg-white rounded divide-y divide-gray-100 dark:bg-gray-700" > - Hage, oppussing og hus - </option> - <option - class="text-gray-900 text-sm" + <ul + aria-labelledby="dropdownDefault" + class="py-1" + > + <li> + + + </li> + </ul> + </div> + <!-- Error message for community --> + <label + class="text-error-medium text-sm block" + /> + </div> + <!-- price --> + <div + class="mb-6 mt-4" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="priceLabel" > - Klær, kosmetikk og tilbehør - </option> - <option - class="text-gray-900 text-sm" + Pris per dag + </label> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="price" + required="" + type="number" + /> + <!-- error message for price --> + + + </div> + <!-- Description --> + <div + class="mb-6" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="descriptionLabel" > - Møbler og interiør - </option> - <option - class="text-gray-900 text-sm" + Beskrivelse + </label> + <textarea + 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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="description" + required="" + rows="4" + /> + <!-- error message for description --> + + + </div> + <!-- Address --> + <div + class="mb-6" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="addressLabel" > - Næringsvirksomhet - </option> - <option - class="text-gray-900 text-sm" + Adresse + </label> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="adress" + required="" + type="text" + /> + <!-- error message for address--> + + + </div> + <!-- Images --> + <div> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="imageLabel" > - Sport og friluftsliv - </option> - <option - class="text-gray-900 text-sm" + Bilder (bildene må være .png) + </label> + <input + accept="image/png" + style="display: none;" + type="file" + /> + <!-- Opens file explorer --> + + <!-- Button with custom text and color --> + <button + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" > - Utstyr til bil, båt og MC - </option> + Velg bilde + </button> - </select> - <!-- error message for select box --> - - - </div> - <!-- Grupper --> - <div - class="mb-6" - > - <label - class="block text-sm font-medium text-gray-900 dark:text-gray-400" - > - Grupper - </label> + <!-- Shows chosen images --> + + + </div> + <!-- Save item button --> <div - class="overflow-auto w-full max-h-32 mt-2 text-base list-none bg-white rounded divide-y divide-gray-100 dark:bg-gray-700" + class="float-right" > - <ul - aria-labelledby="dropdownDefault" - class="py-1" + + <!-- Button with custom text and color --> + <button + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" + id="saveButton" > - <li> - - - </li> - </ul> + Lagre + </button> + </div> - <!-- Error message for community --> - <label - class="text-error-medium text-sm block" - /> - </div> - <!-- price --> - <div - class="mb-6 mt-4" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" - id="priceLabel" - > - Pris - </label> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="price" - required="" - type="number" - /> - <!-- error message for price --> - - - </div> - <!-- Description --> - <div - class="mb-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" - id="descriptionLabel" - > - Beskrivelse - </label> - <textarea - 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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="description" - required="" - rows="4" - /> - <!-- error message for description --> - - - </div> - <!-- Address --> - <div - class="mb-6" - > - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" - id="addressLabel" - > - Adresse - </label> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="adress" - required="" - type="text" - /> - <!-- error message for address--> - - - </div> - <!-- Images --> - <div> - <label - class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" - id="imageLabel" - > - Bilder (bildene må være .png) - </label> - <input - accept="image/png" - style="display: none;" - type="file" - /> - <button - class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" - > - Velg bilde - </button> - - - </div> - <!-- Save item button --> - <div - class="float-right" - > - <button - class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" - id="saveButton" - > - Lagre - </button> </div> + </div> `; diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/search-item-list.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/search-item-list.spec.js.snap index 59eecbb9003356b0debaa8228f3785d489e3ec98..9ccc19c0abc78e6bb844dc550974c7bb02cecb3d 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/search-item-list.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/search-item-list.spec.js.snap @@ -1,55 +1,62 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SearchItemListComponent elements rendering renders correctly 1`] = ` -<section - class="relative w-full max-w-md px-5 py-4 mx-auto rounded-md" +<div + data-v-app="" > - <div - class="relative" - id="searchComponent" + + <!-- A template for searching in item list --> + <section + class="relative w-full max-w-md px-5 py-4 mx-auto rounded-md" > - <span - class="absolute inset-y-0 left-0 flex items-center pl-3" + <div + class="relative" + id="searchComponent" > - <svg - class="w-5 h-5 text-gray-400" - fill="none" - viewBox="0 0 24 24" + <span + class="absolute inset-y-0 left-0 flex items-center pl-3" > - <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-linecap="round" - stroke-linejoin="round" - stroke-width="2" - /> - </svg> - </span> - <input - 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" - id="searchInput" - placeholder="Search" - type="text" - /> - </div> - <div - class="absolute inset-x-0 px-6 py-3 mt-4 border-2 border-slate-500" - > + <svg + class="w-5 h-5 text-gray-400" + fill="none" + viewBox="0 0 24 24" + > + <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-linecap="round" + stroke-linejoin="round" + stroke-width="2" + /> + </svg> + </span> + <input + 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" + id="searchInput" + placeholder="Søk" + type="text" + /> + </div> <div - class="grid grid-cols-2" + class="absolute inset-x-0 px-6 py-3 mt-4 border-2 border-slate-500" > - - <item-card-stub - item="[object Object]" - /> - <item-card-stub - item="[object Object]" - /> - <item-card-stub - item="[object Object]" - /> - + <div + class="grid grid-cols-2" + > + + <item-card-stub + item="[object Object]" + /> + <item-card-stub + item="[object Object]" + /> + <item-card-stub + item="[object Object]" + /> + + </div> </div> - </div> -</section> + </section> + +</div> `; diff --git a/tests/unit/component-tests/renting-compnents-tests/item-info.spec.js b/tests/unit/component-tests/renting-compnents-tests/item-info.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..33b2c610f69ba97491214d3fab01476b32a0e270 --- /dev/null +++ b/tests/unit/component-tests/renting-compnents-tests/item-info.spec.js @@ -0,0 +1,110 @@ +import { shallowMount } from "@vue/test-utils"; +import ItemInfo from "@/components/RentingComponents/ItemInfo.vue"; + +const mockMethod = jest.spyOn(ItemInfo.methods, "sendToConfirm"); +const mockCreatePush = jest.spyOn(ItemInfo.methods, "createPushItem"); + +jest.mock("@/utils/apiutil", () => { + return { + getItem: () => { + return new Promise((resolve) => { + resolve({ + listingID: 0, + title: "Title", + description: "Description", + pricePerDay: 100, + address: "ABC ROAD 3", + userID: 1, + categoryNames: [], + communityIDs: [], + }); + }); + }, + getItemPictures: () => { + return new Promise((resolve) => { + resolve([]); + }); + }, + }; +}); + +describe("ItemInfo component", () => { + let wrapper; + const mockRouter = { + push: jest.fn(), + currentRoute: { + value: { + params: { + id: "1", + }, + }, + }, + }; + const mockStore = { + commit: jest.fn(), + }; + + beforeEach(() => { + wrapper = shallowMount(ItemInfo, { + global: { + mocks: { + $router: mockRouter, + $store: mockStore, + }, + }, + }); + }); + + it("is instantiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("Check that title is displayed", () => { + expect(wrapper.findAll("h1")[0].text()).toBe("Title"); + }); + + it("Check that button cannot be clicked if date is not selected", async () => { + const jestCreatePushItemMock = jest.fn(); + wrapper.vm.createPushItem = jestCreatePushItemMock; + + // Click rent button + wrapper.find("button").trigger("click"); + + // wait a tick + await wrapper.vm.$nextTick(); + + // Check that jestMock was not clicked + expect(mockMethod).toHaveBeenCalledTimes(1); + expect(mockCreatePush).toHaveBeenCalledTimes(0); + // Check that the last p contains "Dato er påkrevd" + expect(wrapper.findAll("p")[wrapper.findAll("p").length - 1].text()).toBe( + "Dato er påkrevd" + ); + }); + + it("Check that send to confirm works", async () => { + // Set valid days + wrapper.vm.setDate({ + startDate: "2020-01-01", + endDate: "2020-02-01", + }); + wrapper.find("button").trigger("click"); + // wait a tick + await wrapper.vm.$nextTick(); + + // Check that method to change component was called + expect(mockCreatePush).toHaveBeenCalledTimes(1); + }); + + it("Test that price calculation works", async () => { + wrapper.vm.setDate({ + startDate: new Date("2022-01-01"), + endDate: new Date("2022-01-03"), + }); + // wait a tick + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.totPrice).toBe(200); + }); +}); diff --git a/tests/unit/component-tests/renting-compnents-tests/new-rent.spec.js b/tests/unit/component-tests/renting-compnents-tests/new-rent.spec.js index 3f8ffafa68bf5dd606afdeda9214b57871ff174f..458147edeb2d729b82043ebecec97a9158d1d876 100644 --- a/tests/unit/component-tests/renting-compnents-tests/new-rent.spec.js +++ b/tests/unit/component-tests/renting-compnents-tests/new-rent.spec.js @@ -1,16 +1,16 @@ import { mount } from "@vue/test-utils"; import NewRent from "@/components/RentingComponents/NewRent.vue"; +import axios from "axios"; + +jest.mock("axios"); +let mockRouter; describe("Confirm and send a rent request", () => { - let wrapper; - const route = { - params: { - id: 1, - }, - }; - const router = { - push: jest.fn(), + mockRouter = { + go: jest.fn(), }; + + let wrapper; beforeEach(() => { wrapper = mount(NewRent, { props: { @@ -26,8 +26,7 @@ describe("Confirm and send a rent request", () => { }, global: { mocks: { - $route: route, - $router: router, + $router: mockRouter, }, }, }); @@ -37,10 +36,26 @@ describe("Confirm and send a rent request", () => { expect(wrapper.exists()).toBeTruthy(); }); - it("Check if fields show correct informations", () => { + it("Check that fields show correct informations", () => { expect(wrapper.find("#rentTitle").text()).toEqual("Telt"); expect(wrapper.find("#fromTime").text()).toMatch("19. September 2022"); expect(wrapper.find("#toTime").text()).toMatch("23. September 2022"); expect(wrapper.find("#price").text()).toEqual("Totaltpris: 200 kr"); }); + + it("Check that clicking rent sends post request", async () => { + const button = wrapper.find("#confirmButton"); + axios.post.mockResolvedValueOnce(); + button.trigger("click"); + await wrapper.vm.$nextTick(); + expect(axios.post).toHaveBeenCalledTimes(1); + }); + + it("Checks that page is reloaded when cancelButton is press", async () => { + const button = wrapper.find("#cancelButton"); + button.trigger("click"); + await wrapper.vm.$nextTick(); + expect(mockRouter.go).toHaveBeenCalledTimes(1); + expect(mockRouter.go).toHaveBeenCalledWith(0); + }); }); 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 f6bd34180d6771f7dd2f7982bdf4989f1e9928db..0c3c3c009af9e5a933f5ed99d16db44906e178a4 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 @@ -2,76 +2,93 @@ exports[`LoginForm component renders correctly 1`] = ` <div - class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full" + data-v-app="" > + + <!-- Login form --> <div - class="px-6 py-4 mt-4" + class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full" > + <!-- Header --> <div - class="text-xl md:text-2xl font-medium text-center text-primary-light mt-4 mb-8" + class="px-6 py-4 mt-4" > - Logg på - </div> - <div> <div - class="w-full mt-6" + class="text-xl md:text-2xl font-medium text-center text-primary-light mt-4 mb-8" > - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - placeholder="Skriv inn din e-postadresse *" - required="" - type="email" - /> - <!-- error message --> - - + Logg på </div> - <div - class="w-full mt-6" - > - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - placeholder="Vennligst oppgi passordet ditt *" - required="" - type="password" - /> - <!-- error message --> - - - </div> - <div - class="flex items-center justify-between mt-8" - > - <router-link - class="text-primary-medium" - to="/resetPassword" + <!-- Email --> + <div> + <div + class="w-full mt-6" + > + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + placeholder="Skriv inn din e-postadresse *" + required="" + type="email" + /> + <!-- error message for email --> + + + </div> + <!-- Password --> + <div + class="w-full mt-6" > - Glemt passord? - </router-link> - <button - class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light login" + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + placeholder="Vennligst oppgi passordet ditt *" + required="" + type="password" + /> + <!-- error message for password --> + + + </div> + <!-- Router link for forgetting password. Redirects to reset password page. --> + <div + class="flex items-center justify-between mt-8" > - Logg på - </button> + <router-link + class="text-primary-medium" + to="/resetPassword" + > + Glemt passord? + </router-link> + <!-- Button for logging in --> + + <!-- Button with custom text and color --> + <button + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light login" + > + Logg på + </button> + + </div> </div> </div> - </div> - <div - class="flex items-center justify-center py-4 text-center bg-gray-50 dark:bg-gray-700" - > - <router-link - class="mx-2 text-sm font-bold text-primary-medium dark:text-primary-light hover:underline" - to="/register" + <div + class="flex items-center justify-center py-4 text-center bg-gray-50 dark:bg-gray-700" > - Opprette ny konto - </router-link> - </div> - <div - class="flex items-center justify-center text-center bg-gray-50" - > - <label - class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" - /> + <!-- Router link to redirect to registering a new user page --> + <router-link + class="mx-2 text-sm font-bold text-primary-medium dark:text-primary-light hover:underline" + to="/register" + > + Opprette ny konto + </router-link> + </div> + <div + class="flex items-center justify-center text-center bg-gray-50" + > + <!-- Shows a message to user if login fails --> + <label + class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" + /> + </div> </div> + </div> `; diff --git a/tests/unit/component-tests/user-component-tests/__snapshots__/new-password-form.spec.js.snap b/tests/unit/component-tests/user-component-tests/__snapshots__/new-password-form.spec.js.snap index 941fc2aaf4c6fd0b024cde59276a29a4af11813f..8d4a4752be15507de8c0a6decba0a129aca5aceb 100644 --- a/tests/unit/component-tests/user-component-tests/__snapshots__/new-password-form.spec.js.snap +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/new-password-form.spec.js.snap @@ -2,87 +2,100 @@ exports[`NewPasswordForm component renders correctly 1`] = ` <div - class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" + data-v-app="" > - <h3 - class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" - > - Endre passord - </h3> + + <!-- A form for changing password --> <div - class="" - id="oldPasswordField" + class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" > - <label - class="block text-sm text-gray-800 dark:text-gray-200" - for="oldPassword" + <!-- Header --> + <h3 + class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" > - Gammelt passord - </label> - <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" - type="password" - /> - <!-- error message --> - - - </div> - <div - class="mt-4" - id="firstPasswordField" - > - <label - class="block text-sm text-gray-800 dark:text-gray-200" - for="password" + Endre passord + </h3> + <!-- Input field for old password --> + <div + class="" + id="oldPasswordField" > - Nytt passord - </label> - <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" - type="password" - /> - <!-- error message --> - - - </div> - <div - class="mt-4" - id="secondPasswordField" - > + <label + class="block text-sm text-gray-800 dark:text-gray-200" + > + Gammelt passord + </label> + <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" + type="password" + /> + <!-- error message for password --> + + + </div> + <!-- New password --> <div - class="flex items-center justify-between" + class="mt-4" + id="firstPasswordField" > <label class="block text-sm text-gray-800 dark:text-gray-200" - for="rePassword" > - Gjenta nytt passord + Nytt passord </label> + <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" + type="password" + /> + <!-- error message for password --> + + </div> - <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" - type="password" - /> - <!-- error message --> - - - </div> - <div - class="mt-6" - id="buttonsField" - > - <button - class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light float-right" + <!-- Repeating new password --> + <div + class="mt-4" + id="secondPasswordField" > - Sett ny passord - </button> - </div> - <div - class="flex items-center justify-center text-center bg-gray-50" - > - <label - class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" - /> + <div + class="flex items-center justify-between" + > + <label + class="block text-sm text-gray-800 dark:text-gray-200" + > + Gjenta nytt passord + </label> + </div> + <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" + type="password" + /> + <!-- error message for password --> + + + </div> + <!-- Button --> + <div + class="mt-6" + id="buttonsField" + > + + <!-- Button with custom text and color --> + <button + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light float-right" + > + Sett ny passord + </button> + + </div> + <!-- Message for user --> + <div + class="flex items-center justify-center text-center bg-gray-50" + > + <label + class="mx-2 text-sm font-bold text-error-medium dark:text-primary-light hover:underline" + /> + </div> </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 f22076edecd3620957e0bbf9141873adfd3a6322..5d0cf24e39eb3292cb0d2eda697982c9c214fbda 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 @@ -1,22 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Rating component renders correctly 1`] = ` -<ul - class="flex justify-center" +<div + data-v-app="" > - <li> - <p - class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400" - > - : - </p> - </li> - <li> - <p - class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400" - > - Ingen vurderinger - </p> - </li> -</ul> + + <!-- Shows rating, if no rating found, tells the user that no rating is registered --> + <ul + class="flex justify-center" + > + <li> + <p + class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400" + > + : + </p> + </li> + <li> + <p + class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400" + > + Ingen vurderinger + </p> + </li> + </ul> + +</div> `; diff --git a/tests/unit/component-tests/user-component-tests/__snapshots__/register-user-component.spec.js.snap b/tests/unit/component-tests/user-component-tests/__snapshots__/register-user-component.spec.js.snap index 193cbfa3c50d17fb2f50c439c23fe6b0031c3d33..7ac9eff69f58987c4ffb5152ce619ff927142b62 100644 --- a/tests/unit/component-tests/user-component-tests/__snapshots__/register-user-component.spec.js.snap +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/register-user-component.spec.js.snap @@ -2,95 +2,114 @@ exports[`RegisterFormComponent renders correctly 1`] = ` <div - class="w-full max-w-md mx-auto mb-auto md:ring-1 ring-gray-300 overflow-hidden rounded-xl p-4" + data-v-app="" > + + <!-- Register form for creating a new user account --> <div - class="text-xl md:text-2xl font-medium text-center text-primary-light mt-4 mb-8" - id="registerLabel" + class="w-full max-w-md mx-auto mb-auto md:ring-1 ring-gray-300 overflow-hidden rounded-xl p-4" > - Opprett ny konto - </div> - <form> <div - class="grid grid-cols-1 gap-6 mt-4" + class="text-xl md:text-2xl font-medium text-center text-primary-light mt-4 mb-8" + id="registerLabel" > - <div> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="email" - placeholder="E-post adresse" - type="email" - /> - <!-- error message --> - - - </div> - <div> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="password" - placeholder="Passord" - type="password" - /> - <!-- error message --> - - - </div> - <div> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="confirmPassword" - placeholder="Bekreft passord" - type="password" - /> - <!-- error message --> - - - </div> - <div> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - data-test="firstNameTest" - id="firstName" - placeholder="Fornavn" - type="text" - /> - <!-- error message --> - - - </div> - <div> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="lastName" - placeholder="Etternavn" - type="text" - /> - <!-- error message --> - - + Opprett ny konto + </div> + <form> + <div + class="grid grid-cols-1 gap-6 mt-4" + > + <div> + <!-- Email --> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="email" + placeholder="E-post adresse" + type="email" + /> + <!-- error message for email --> + + + <div + class="text-error-medium text-sm" + style="display: none;" + /> + </div> + <div> + <!-- Password --> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="password" + placeholder="Passord" + type="password" + /> + <!-- error message --> + + + </div> + <div> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="confirmPassword" + placeholder="Bekreft passord" + type="password" + /> + <!-- error message --> + + + </div> + <div> + <!-- First name --> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + data-test="firstNameTest" + id="firstName" + placeholder="Fornavn" + type="text" + /> + <!-- error message for first name--> + + + </div> + <div> + <!-- Last name --> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="lastName" + placeholder="Etternavn" + type="text" + /> + <!-- error message for last name --> + + + </div> + <div> + <!-- Address --> + <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + id="address" + placeholder="Adresse" + type="text" + /> + <!-- error message for address --> + + + </div> </div> - <div> - <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-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" - id="address" - placeholder="Adresse" - type="text" - /> - <!-- error message --> + <div + class="flex justify-end mt-6" + > + <!-- Button with custom text and color --> + <button + class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" + > + Opprett + </button> </div> - </div> - <div - class="flex justify-end mt-6" - > - <button - class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light" - > - Opprett - </button> - </div> - </form> + </form> + </div> + </div> `; diff --git a/tests/unit/component-tests/user-component-tests/__snapshots__/reset-password-form.spec.js.snap b/tests/unit/component-tests/user-component-tests/__snapshots__/reset-password-form.spec.js.snap index 40a038afb49c611841f534b60c65fd7a2ee740ee..417f7973077f99b92a0933b95b54788ed5269141 100644 --- a/tests/unit/component-tests/user-component-tests/__snapshots__/reset-password-form.spec.js.snap +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/reset-password-form.spec.js.snap @@ -33,11 +33,14 @@ exports[`ResetPasswordForm component renders correctly 1`] = ` </div> + + <!-- Button with custom text and color --> <button class="flex items-center px-2 py-2 font-medium tracking-wide capitalize text-white transition-colors duration-200 transform rounded-md focus:outline-none focus:ring focus:ring-opacity-80 min-w-{20px} bg-primary-medium hover:bg-primary-dark focus:ring-primary-light float-right" > Tilbakestill passord </button> + </div> </div> `; diff --git a/tests/unit/component-tests/user-component-tests/__snapshots__/user-items.spec.js.snap b/tests/unit/component-tests/user-component-tests/__snapshots__/user-items.spec.js.snap index db5d7bf26553750dd6c9f3789f9541ffc2d2f071..344d5b6abc9dc0050a300001c685640d62b2a39f 100644 --- a/tests/unit/component-tests/user-component-tests/__snapshots__/user-items.spec.js.snap +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/user-items.spec.js.snap @@ -5,6 +5,8 @@ exports[`UserItems component renders correctly 1`] = ` data-v-app="" > + <!-- Shows all the items a user has posted with search and pagination. + Includes a dropdown menu for editing or deleting an item. --> <div class="text-xl md:text-2xl text-primary-light font-medium" id="headline" @@ -13,7 +15,7 @@ exports[`UserItems component renders correctly 1`] = ` </div> <!-- Search field --> <div - class="relative" + class="relative mx-4" id="searchComponent" > <span @@ -36,12 +38,12 @@ exports[`UserItems component renders correctly 1`] = ` <input 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" id="searchInput" - placeholder="Search" + placeholder="Søk" type="text" /> </div> <div - class="absolute inset-x-0 px-5 py-3" + class="absolute inset-x-0" > <!-- ItemCards --> <div @@ -52,7 +54,108 @@ exports[`UserItems component renders correctly 1`] = ` class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" > + <div + class="cardContainer" + id="item" + > + <div + class="w-full" + > + + <!-- Item card, displays title, address, price per day and a picture --> + <div + class="mt-5 px-5 ItemCard w-full h-full" + id="ItemCardPage" + > + <div + class="w-full h-full rounded bg-gray-100 ring-1 ring-gray-200 overflow-hidden display:inline-block correct-size cursor-pointer" + > + <div + class="relative h-0 pb-[66.7%]" + > + <img + alt="Item image" + class="w-full h-full object-contain absolute" + src="" + /> + </div> + <div + class="p-1 m-1 bottom-0" + > + <p + class="font-bold text-sm" + id="title" + > + Matboks + </p> + <p + class="text-gray-700 text-xs" + id="price" + > + 50 kr + </p> + <p + class="text-gray-700 text-xs font-bold" + id="adress" + > + Veien + </p> + </div> + </div> + </div> + + </div> + <!-- Dropdown menu with options for editing an item and deleting an item --> + <button + 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 DotButton" + data-dropdown-toggle="dropdown" + id="dropdownDefault" + type="button" + > + <svg + aria-hidden="true" + class="w-6 h-6" + fill="none" + stroke="currentColor" + stroke-width="2" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </button> + <div + class="options z-10 w-44 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 pt-4 pl-12" + style="display: none;" + > + <ul + aria-labelledby="dropdownDefault" + class="py-1 absolute bg-white ring-1 ring-gray-300 rounded-xl" + > + <li> + <button + class="editButton 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 + class="deleteButton 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> + <!-- A waring asking the user if it is sure it wants to delete the item + with options to go ahead with the deleting or to cancel the delete. --> <!-- Main modal --> <!--v-if--> @@ -65,17 +168,23 @@ exports[`UserItems component renders correctly 1`] = ` <div class="flex justify-center" > + + <!-- Pagination --> <div class="mt-10" > + <!-- Prev button --> <!--v-if--> + <!-- Current page --> <label class="mx-2 text-primary-light" > 1 av 1 </label> + <!-- Next button --> <!--v-if--> </div> + </div> </div> diff --git a/tests/unit/component-tests/user-component-tests/user-items.spec.js b/tests/unit/component-tests/user-component-tests/user-items.spec.js index 7640696dee98524a786e6f059aa1520e6934a429..b077c14482cad1d98a076ab88a5bf6afcf6a5cea 100644 --- a/tests/unit/component-tests/user-component-tests/user-items.spec.js +++ b/tests/unit/component-tests/user-component-tests/user-items.spec.js @@ -1,23 +1,59 @@ import { mount } from "@vue/test-utils"; import UserItems from "@/components/UserProfileComponents/UserItems.vue"; +import UserService from "@/services/user.service"; +import ListingService from "@/services/listing.service"; + +jest.mock("@/services/user.service", () => jest.fn()); +jest.mock("@/services/listing.service", () => jest.fn()); + +UserService.getUserListings = jest.fn(() => { + return new Promise((resolve) => { + resolve([ + { + listingID: 1, + img: "", + address: "Veien", + title: "Matboks", + pricePerDay: 50, + toggle: false, + }, + ]); + }); +}); + +UserService.setListingToDeleted = jest.fn(() => { + return new Promise((resolve) => { + resolve(); + }); +}); + +ListingService.getItemPictures = jest.fn(() => { + return new Promise((resolve) => { + resolve([]); + }); +}); + +jest.mock("axios"); + +let mockRouter; +let wrapper; describe("UserItems component", () => { - let wrapper; + mockRouter = { + push: jest.fn(), + go: jest.fn(), + }; beforeEach(() => { wrapper = mount(UserItems, { + global: { + mocks: { + $router: mockRouter, + }, + }, data() { return { - items: [ - { - listingID: 1, - img: "", - address: "Veien", - title: "Matboks", - pricePerDay: 50, - toggle: false, - }, - ], + showItems: true, }; }, }); @@ -34,4 +70,36 @@ describe("UserItems component", () => { it("Check headline", () => { expect(wrapper.find("#headline").text()).toMatch("Mine gjenstander"); }); + + it("Check if pressing 'Rediger gjenstand' pushes to right path", async () => { + const dotButton = wrapper.findAll(".DotButton")[0]; + dotButton.trigger("click"); + const editButton = wrapper.findAll(".editButton")[0]; + expect(editButton.text()).toBe("Rediger gjenstand"); + editButton.trigger("click"); + await wrapper.vm.$nextTick(); + expect(mockRouter.push).toBeCalled(); + expect(mockRouter.push).toBeCalledWith("/item/1/edit"); + }); + + it("Check if delete is called", async () => { + const dotButton = wrapper.findAll(".DotButton")[0]; + dotButton.trigger("click"); + await wrapper.vm.$nextTick(); + + const deleteButton = wrapper.findAll(".deleteButton")[0]; + expect(deleteButton.text()).toBe("Slett gjenstand"); + deleteButton.trigger("click"); + await wrapper.vm.$nextTick(); + + const confirmButton = wrapper.findAll(".confirmDelete")[0]; + expect(confirmButton.text()).toBe("Slett"); + + //Feiler herfra + confirmButton.trigger("click"); + await wrapper.vm.$nextTick(); + + expect(ListingService.getItemPictures).toBeCalled(); + expect(mockRouter.go).toBeCalled(); + }); });