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/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/RentalMessage.vue b/src/components/ChatComponents/RentalMessage.vue index 9bfaeaaa58b610f1e1b45605e1dbcb6ff31de39e..ba86c9f14a778c7767390a9c5c54e4aa7dba7226 100644 --- a/src/components/ChatComponents/RentalMessage.vue +++ b/src/components/ChatComponents/RentalMessage.vue @@ -49,7 +49,7 @@ <script> import axios from "axios"; import { tokenHeader, parseCurrentUser } from "@/utils/token-utils"; -import { getItemPictures, } from "@/utils/apiutil"; +import { getItemPictures } from "@/utils/apiutil"; export default { props: { @@ -61,7 +61,7 @@ export default { data() { return { image: null, - } + }; }, computed: { userID() { @@ -113,8 +113,8 @@ export default { 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"; - + this.image = + "https://images.unsplash.com/photo-1453728013993-6d66e9c9123a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8dmlld3xlbnwwfHwwfHw%3D&w=1000&q=80"; } }, },