diff --git a/jest.config.js b/jest.config.js index 6dda9e7a1f5f0b23dd68aceb027c2afd557f99b8..dfcd35547136d3b563a43317e3b2f305b5b92518 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ module.exports = { preset: "@vue/cli-plugin-unit-jest", + collectCoverage: true, + collectCoverageFrom: ['src/**/*.js', 'src/**/*.vue', '!**/node_modules/**'] }; diff --git a/src/components/BaseComponents/NavBar.vue b/src/components/BaseComponents/NavBar.vue index e80792a22af331b12efb22fe785c06f306e994de..3b6937f7b737afc98c8f871b46104ee44aebd6e0 100644 --- a/src/components/BaseComponents/NavBar.vue +++ b/src/components/BaseComponents/NavBar.vue @@ -11,27 +11,24 @@ /> </div> <ul class="flex justify-between"> - <li class="cursor-pointer" - @click="$router.push('/newItem')" - > + <li class="cursor-pointer" @click="$router.push('/newItem')"> <PlusIcon class="m-6 md:mr-2 h-7 text-primary-medium float-left" alt="Legg til" /> <a class="hidden md:block mt-7 text-sm float-right">Legg til</a> </li> - <li class="cursor-pointer" - @click="$router.push('/messages')" - > - <ChatAlt2Icon - class="m-6 md:mr-2 h-7 text-primary-medium float-left" - alt="Meldinger" - /> - <a class="hidden md:block mt-7 text-sm float-right">Meldinger</a> + <li> + <div class="notification-container"> + <ChatAlt2Icon + class="m-6 cursor-pointer h-7" + alt="Meldinger" + @click="loadMessages()" + /> + <p @click="loadMessages()" class="notification" v-if="newMessages > 0">{{notifications}}</p> + </div> </li> - <li class="cursor-pointer" - @click="loadProfile" - > + <li class="cursor-pointer" @click="loadProfile"> <UserCircleIcon class="m-6 md:mr-2 h-7 text-primary-medium float-left" alt="Profil" @@ -45,10 +42,25 @@ <script> import { parseUserFromToken } from "@/utils/token-utils"; import { PlusIcon, ChatAlt2Icon, UserCircleIcon } from "@heroicons/vue/outline"; +import ws from '@/services/ws'; export default { name: "NavBar.vue", - + data() { + return { + newMessages: 0, + } + }, + computed: { + notifications() { + // if new messages is greater than 99 show +99 + if (this.newMessages > 99) { + return '+99' + } else { + return this.newMessages + } + } + }, components: { PlusIcon, ChatAlt2Icon, @@ -65,8 +77,38 @@ export default { await this.$router.push("/login"); } }, + loadMessages() { + this.newMessages = 0; + this.$router.push('/messages'); + } }, + created() { + ws.on('NEW_MESSAGE', () => { + if(this.$router.currentRoute.value.name == 'messages') return; + this.newMessages += 1; + }, "header"); + } }; </script> -<style scoped></style> +<style scoped> +.notification-container { + position: relative; +} +.notification { + position: absolute; + background-color: #ff5a5f; + top: 0; + min-width: 20px; + min-height: 20px; + padding: 0.25rem; + transform: translate(-80%, -30%); + color: white; + font-size: 10px; + border-radius: 50%; + font-weight: bold; + text-align: center; + right: 0; + cursor: pointer; +} +</style> diff --git a/src/components/BaseComponents/RatingModal.vue b/src/components/BaseComponents/RatingModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..370a997b86685b2474854ea94c983e8549a113dc --- /dev/null +++ b/src/components/BaseComponents/RatingModal.vue @@ -0,0 +1,200 @@ +<template> + <!-- Main modal --> + <div + v-if="visible" + class="fixed grid place-items-center bg-gray-600 bg-opacity-50 top-0 left-0 right-0 z-50 w-full overflow-x-hidden overflow-y-auto inset-0 h-full" + > + <div class="relative w-full h-full max-w-2xl p-4 md:h-auto"> + <!-- Modal content --> + <div class="relative bg-white rounded-lg shadow dark:bg-gray-700"> + <!-- Modal header --> + <div class="flex p-4 border-b rounded-t dark:border-gray-600"> + <h3 class="text-xl font-semibold text-gray-900 dark:text-white"> + {{ name }} + </h3> + <button + @click="close()" + class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" + > + <svg + class="w-5 h-5" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + fill-rule="evenodd" + d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" + clip-rule="evenodd" + ></path> + </svg> + </button> + </div> + <!-- Modal body --> + <div class="p-6 space-y-6"> + <p + class="text-lg text-base leading-relaxed text-gray-500 dark:text-gray-400" + > + {{ title }} + </p> + </div> + + <div class="ml-6 mt-4"> + <p + class="text-base leading-relaxed text-gray-500 dark:text-gray-400" + v-show="renterIsReceiverOfRating" + > + Gi en vurdering til utleieren + </p> + <p + class="text-base leading-relaxed text-gray-500 dark:text-gray-400" + v-show="!renterIsReceiverOfRating" + > + Gi en vurdering til leietakeren + </p> + </div> + + <div class="flex justify-center px-4"> + <textarea + class="w-full h-40 bg-gray-200 mb-4 ring-1 ring-gray-400 rounded-xl" + /> + </div> + + <div class="flex items-center justify-center mb-8"> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[0]" + @click="setRating(1)" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" + ></path> + </svg> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[1]" + @click="setRating(2)" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" + ></path> + </svg> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[2]" + @click="setRating(3)" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" + ></path> + </svg> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[3]" + @click="setRating(4)" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" + ></path> + </svg> + <svg + class="w-10 h-10 text-warn cursor-pointer" + :class="rating[4]" + @click="setRating(5)" + fill="currentColor" + viewBox="0 0 20 20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" + ></path> + </svg> + </div> + + <div class="flex justify-center mb-4"> + <Button :text="'Send en vurdering'" @click="sendRating"></Button> + </div> + + <!-- Modal footer --> + <div class="rounded-b border-t border-gray-200 dark:border-gray-600"> + <!-- Slot: Add any html you want here --> + <slot /> + </div> + </div> + </div> + </div> +</template> + +<script> +import Button from "@/components/BaseComponents/ColoredButton"; +import { postNewRating } from "@/utils/apiutil"; + +export default { + name: "RatingModal", + data() { + return { + score: 3, + comment: "", + rating: [ + "text-warn", + "text-warn", + "text-warn", + "text-gray-300", + "text-gray-300", + ], + }; + }, + props: { + visible: Boolean, + name: String, + title: String, + rentID: Number, + renterIsReceiverOfRating: Boolean, + }, + + components: { + Button, + }, + methods: { + setRating(ratingNumber) { + this.score = ratingNumber; + for (let i = 0; i < 5; i++) { + if (i < ratingNumber) { + this.rating[i] = "text-warn"; + } else { + this.rating[i] = "text-gray-300"; + } + } + }, + close() { + this.$emit("close"); + }, + async sendRating() { + const ratingInfo = { + score: this.score, + comment: this.comment, + renterIsReceiverOfRating: this.renterIsReceiverOfRating, + rentID: this.rentID, + }; + + const postResponse = await postNewRating(ratingInfo); + + console.log("posted: " + postResponse); + + this.$router.push("/"); + }, + }, +}; +</script> diff --git a/src/components/ChatComponents/ChatComponent.vue b/src/components/ChatComponents/ChatComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..ff6f25d4ae56e5d7175eb1dfcf8ffed2bf287fed --- /dev/null +++ b/src/components/ChatComponents/ChatComponent.vue @@ -0,0 +1,271 @@ +<template> + <div class="chat-container"> + <div class="header"> + <div + v-on:click="openHamburgerMethod" + class="hamburger grid space-y-2 content-center m-3" + > + <div class="w-8 h-0.5 bg-gray-600"></div> + <div class="w-8 h-0.5 bg-gray-600"></div> + <div class="w-8 h-0.5 bg-gray-600"></div> + </div> + <div class="flex"> + <img class="pfp" :src="this.src" alt="Profile Picture" /> + <h1>{{ name }}</h1> + </div> + <div></div> + </div> + <div v-if="recipient" class="conversation"> + <div v-for="(message, i) in messages" v-bind:key="i"> + <rental-message + v-if="message?.createdAt" + :rent="message" + ></rental-message> + <ChatMessage v-else :message="message"></ChatMessage> + </div> + </div> + <div v-else class="conversation"> + <div class="not-found"> + <p>USER NOT FOUND</p> + </div> + </div> + <div + class="flex items-center justify-between w-full p-3 border-t border-gray-300" + > + <input + v-on:keyup.enter="sendMessage" + type="text" + placeholder="Message" + class="block w-full py-2 pl-4 mx-3 bg-gray-100 rounded-full outline-none focus:text-gray-700" + name="message" + v-model="message" + /> + <button v-on:click="sendMessage" style="padding: 10px; color: red"> + <svg + class="w-5 h-5 text-gray-500 origin-center transform rotate-90" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + > + <path + d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" + /> + </svg> + </button> + </div> + </div> +</template> + +<script> +import ChatMessage from "./ChatMessage.vue"; +import axios from "axios"; +import ws from "@/services/ws"; +import RentalMessage from "./RentalMessage.vue"; +export default { + props: { + openHamburger: { type: Function }, + recipientID: { + type: String, + }, + }, + data() { + return { + canScroll: true, + scrollBehavior: "", + recipient: null, + rents: [], + message: "", + msgs: [], + }; + }, + components: { + ChatMessage, + RentalMessage, + }, + computed: { + name() { + return this.recipient + ? this.recipient.firstName + " " + this.recipient.lastName + : "N/A"; + }, + src() { + return ( + this.recipient?.picture || + require("@/assets/defaultUserProfileImage.jpg") + ); + }, + messages() { + let arr = [...this.msgs, ...this.rents].sort((a, b) => { + return (a?.createdAt || a.timestamp) - (b?.createdAt || b.timestamp); + }); + return arr; + }, + }, + methods: { + openHamburgerMethod() { + this.$emit("openHamburger"); + }, + scroll() { + let container = this.$el.querySelector(".conversation"); + container.scrollTop = container.scrollHeight; + }, + async reloadMessages() { + const token = this.$store.state.user.token; + const response = await fetch( + `${process.env.VUE_APP_BASEURL}chats/users/${this.recipientID}/messages`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + this.msgs = await response.json(); + }, + async sendMessage() { + if (!this.recipient || this.message == null || this.message == "") return; + this.canScroll = true; + const token = this.$store.state.user.token; + await axios.post( + process.env.VUE_APP_BASEURL + + `chats/users/${this.recipientID}/messages`, + { + message: this.message, + }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + this.message = ""; + ws.sendMessage({ + sender: parseInt(this.userid), + recipient: this.recipientID, + }); + await this.reloadMessages(); + }, + async reloadRents() { + const token = this.$store.state.user.token; + const response = await fetch( + `${process.env.VUE_APP_BASEURL}renting/user/${this.recipientID}/all`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + this.rents = await response.json(); + }, + async getRecipient() { + const token = this.$store.state.user.token; + const response = await fetch( + `${process.env.VUE_APP_BASEURL}users/${this.recipientID}/profile`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); + + this.recipient = await response.json(); + }, + }, + watch: { + async recipientID() { + this.rents = []; + this.msgs = []; + await this.reloadMessages(); + await this.getRecipient(); + await this.reloadRents(); + }, + }, + async created() { + await this.reloadMessages(); + await this.getRecipient(); + await this.reloadRents(); + + ws.on("NEW_MESSAGE", () => { + this.reloadMessages(); + }, "chat"); + }, + updated() { + if (this.canScroll) this.scroll(); + this.canScroll = false; + this.scrollBehavior = "smooth"; + }, + unmounted() { + ws.end("NEW_MESSAGE", "chat"); + } +}; +</script> + +<style scoped> +.hamburger { + display: none; +} + +.chat-container { + display: flex; + flex-direction: column; + height: 100%; + width: auto; +} + +.not-found { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.not-found p { + font-size: 2rem; + font-weight: bold; +} +.conversation { + height: 100%; + width: 100%; + padding: 1.25rem; + overflow-y: scroll; + scroll-behavior: v-bind(scrollBehavior); +} + +.header { + display: flex; + flex-direction: row; + padding: 0.75rem; + border-bottom: 1px solid black; +} + +.header h1 { + align-self: center; + margin-left: 10px; + color: #4b5563; + font-weight: bold; +} + +.pfp { + object-fit: cover; + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; +} + +@media screen and (max-width: 600px) { + .header { + justify-content: space-between; + } + + .hamburger { + display: block; + } +} +</style> diff --git a/src/components/ChatComponents/ChatMessage.vue b/src/components/ChatComponents/ChatMessage.vue index 73ded12cc8407dddc21f12614e01b2b8eac71b07..eb2e04459b84d3f8c48b94a421cc5c3d2e3c067b 100644 --- a/src/components/ChatComponents/ChatMessage.vue +++ b/src/components/ChatComponents/ChatMessage.vue @@ -1,22 +1,34 @@ <template> - <div v-bind:class="'w-full break-words flex ' + side()"> - <div - style="max-width: 70%" - v-bind:class=" - this.color() + ' rounded px-5 py-2 my-2 relative ' + this.textColor() - " - > - <span class="block" - >{{ this.message.content }} {{ this.message.from }}</span - > - <span class="block text-xs text-right">{{ this.calculateTime() }}</span> + <div v-bind:class="'blob-container ' + this.side()"> + <div v-bind:class="this.color() + ' message-container ' + this.textColor()"> + <span class="message">{{ this.message.content }}</span> + <span class="">{{ this.calculateTime() }}</span> </div> </div> </template> +<style scoped> +.blob-container { + display: flex; + max-width: 100%; +} + +.message { + word-break: break-word; + display: block; +} + +.message-container { + border-radius: 10px; + max-width: 70%; + padding: 0.75rem; + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} +</style> <script> import { parseCurrentUser } from "@/utils/token-utils"; - +//block text-xs text-right export default { props: { message: Object, @@ -46,14 +58,60 @@ export default { : "justify-end"; }, calculateTime() { - //let time = this.message.from; - // Calculate time when message was sent - let date = new Date(this.message.timestamp); - let hours = date.getHours(); - let minutes = "0" + date.getMinutes(); - let formattedTime = hours + ":" + minutes.substr(-2); - return formattedTime; + var time = this?.message.timestamp; + var date = new Date(time); + //Todo add timing for mm and hh and week of message + var mmOfMessage = String(date.getMinutes()); + var hhOfMessage = String(date.getHours()); + var ddOfMessage = String(date.getDate()).padStart(2, '0'); + var dayOfMessage = date.toLocaleString('default', { weekday: 'short' }); + var monthOfMessage = String(date.getMonth() + 1).padStart(2, '0'); //January is 0! + const shortMonthOfMessage = date.toLocaleString('default', { month: 'short' }); + var yyyyOfMessage = date.getFullYear(); + + var today = new Date(); + var dd = String(today.getDate()).padStart(2, '0'); + var mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! + var yyyy = today.getFullYear(); + if(ddOfMessage == dd){ + return "" + hhOfMessage + ":" + mmOfMessage + ""; + } + else if (this.isDateInThisWeek(date)){ + return "" +dayOfMessage + " " + hhOfMessage+":" + mmOfMessage; + } + else if (monthOfMessage == mm){ + return "" + ddOfMessage + " " + hhOfMessage + ":" + mmOfMessage; + } + else if (yyyyOfMessage == yyyy){ + return "" + shortMonthOfMessage + " " + ddOfMessage; + } + return shortMonthOfMessage + " " + ddOfMessage + " " + yyyyOfMessage + ""; + + /* + Take timestamp and display date when message was sent + If message was sent this day show time (HH:MM) (13:00) + If message was sent this week show day of the week and time (DDD HH:MM) (Mon 13:00) + If message was sent this month show day of the month, date and time (DD HH:MM) (13 13:00) + If message was sent this year show month and day of the month (MMM DD) (Jan 13) + If message was sent more than a year ago show year with date (MMM DD YYYY) (Jan 13 2020) + */ + }, + isDateInThisWeek(date) { + const todayObj = new Date(); + const todayDate = todayObj.getDate(); + const todayDay = todayObj.getDay(); + + // get first date of week + const firstDayOfWeek = new Date(todayObj.setDate(todayDate - todayDay)); + + // get last date of week + const lastDayOfWeek = new Date(firstDayOfWeek); + lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6); + + // if date is equal or within the first and last dates of the week + return date >= firstDayOfWeek && date <= lastDayOfWeek; +}, }, }; </script> diff --git a/src/components/ChatComponents/ChatProfile.vue b/src/components/ChatComponents/ChatProfile.vue index 60c544f802a2384cf9e665c986d2fc24711ab197..bfed6d6281e7ae81a7c2851aeb0f60fdf16bb425 100644 --- a/src/components/ChatComponents/ChatProfile.vue +++ b/src/components/ChatComponents/ChatProfile.vue @@ -5,7 +5,7 @@ > <img class="h-10 w-10 rounded-full object-cover" - src="https://www.mintface.xyz/content/images/2021/08/QmTndiF423kjdXsNzsip1QQkBQqDuzDhJnGuJAXtv4XXiZ-1.png" + :src="src || 'S../../assets/defaultUserProfileImage.jpg'" :alt="{ name }" /> <div class="w-full pb-2"> @@ -47,13 +47,41 @@ export default { ); }, lastMessageTime() { - return "5 min"; + // Calculate time since last message + const time = this.conversation.lastMessage.timestamp; + const now = new Date(); + const diff = now - time; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const months = Math.floor(days / 30); + const years = Math.floor(months / 12); + + if (seconds < 60) { + return "Just now"; + } else if (minutes < 60) { + return minutes + " minutes ago"; + } else if (hours < 24) { + return hours + " hours ago"; + } else if (days < 30) { + return days + " days ago"; + } else if (months < 12) { + return months + " months ago"; + } else { + return years + " years ago"; + } + }, + src() { + return this.conversation.recipient.picture + ? this.conversation.recipient.picture + : require("@/assets/defaultUserProfileImage.jpg"); }, }, methods: { selectUser() { - this.$emit("recipient", this.conversation.recipient.userId); + this.$emit("recipient", this.conversation?.recipient.userId); }, - }, + } }; </script> diff --git a/src/components/ChatComponents/ChatsComponent.vue b/src/components/ChatComponents/ChatsComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..b5ee4f6cf257ba47619d1ddcb68ff9a911e988a7 --- /dev/null +++ b/src/components/ChatComponents/ChatsComponent.vue @@ -0,0 +1,177 @@ +<template> + <div class="chat"> + <div class="conversations"> + <h1>Samtaler:</h1> + <hr /> + <ChatProfile + v-for="(conversation, i) in conversations" + :conversation="conversation" + :key="i" + @recipient="selectUser" + ></ChatProfile> + <!-- If no conversatiosn show no conversations found --> + <div v-if="conversations.length === 0" class="no-conversations"> + <p>Ingen samtaler</p> + </div> + </div> + <div class="current-chat"> + <ChatComponent + @openHamburger="openHamburger" + v-if="recipientID" + :recipientID="recipientID" + ></ChatComponent> + <div v-else class="nothing-selected"> + <p>Select a user to start a chat</p> + </div> + </div> + </div> +</template> + +<script> +import ChatProfile from "./ChatProfile.vue"; +import ChatComponent from "./ChatComponent.vue"; +import { parseCurrentUser } from "@/utils/token-utils"; +import ws from "@/services/ws"; + +export default { + props: {}, + watch: { + $route() { + const newVal = this.$route.query?.userID || null; + this.recipientID = newVal; + }, + }, + data: () => { + return { + messages: [], + message: "", + recipient: null, + hambuger: "none", + conversations: [], + hambugerDisplay: "none", + recipientID: null, + }; + }, + components: { ChatProfile, ChatComponent }, + computed: { + userid() { + return parseCurrentUser().accountId; + }, + name() { + return this.recipient.firstName + " " + this.recipient.lastName; + }, + }, + methods: { + selectUser(recipientID) { + this.hambugerDisplay = "none"; + this.$router.push({ path: "messages", query: { userID: recipientID } }); + }, + openHamburger() { + this.hambugerDisplay = "block"; + }, + calculateSide(from) { + return from == this.userid ? "end" : "start"; + }, + async fetchChats() { + const token = this.$store.state.user.token; + // Get all conversations from api with /chats/users + const conResponse = await fetch( + `${process.env.VUE_APP_BASEURL}chats/users`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + } + ); // add error handling + this.conversations = await conResponse.json(); + }, + }, + async created() { + await this.fetchChats(); + ws.on("NEW_MESSAGE",async () => { + await this.fetchChats() + }, "chats"); + this.recipientID = this.$route.query?.userID || null; + if (!this.recipientID) this.hambugerDisplay = "block"; + }, + unmounted() { + ws.end("NEW_MESSAGE", "chats"); + }, +}; +</script> +<style scoped> +.chat { + display: flex; + flex-direction: row; + width: 100%; + height: min(100vh - 3.5rem); +} + +.nothing-selected { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.no-conversations { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.nothing-selected p { + font-size: 2rem; + font-weight: bold; + text-align: center; +} + +.current-chat { + width: 100%; + height: 100%; +} + +.conversations { + min-width: 300px; + border-right-width: 1px; + border-color: black; + height: 100%; +} + +.conversations h1 { + padding: 0.5rem; + padding-left: 0; + align-self: center; + margin-left: 10px; + color: #4b5563; + font-weight: bold; + font-size: large; +} + +@media screen and (max-width: 600px) { + .conversations { + display: v-bind(hambugerDisplay); + z-index: 99; + width: 100%; + position: absolute; + background-color: white; + } + + .conversations h1 { + text-align: center; + } +} + +.button { + display: flex; + justify-content: center; + padding: 0.75rem; +} +</style> diff --git a/src/components/ChatComponents/RentalMessage.vue b/src/components/ChatComponents/RentalMessage.vue new file mode 100644 index 0000000000000000000000000000000000000000..e57640b14711ff8908c210e5cf9faefc9f8fd004 --- /dev/null +++ b/src/components/ChatComponents/RentalMessage.vue @@ -0,0 +1,180 @@ +<template> + <div class="message" style="max-width: 70%"> + <div class="info"> + <div class="text"> + <h2 class="header">Ny utleie forespørsel</h2> + <p>Dato: {{ from }} - {{ to }}</p> + <p>Pris: {{ price }}kr</p> + </div> + <div class="img-container"> + <img class="img" :src="img" alt="Produkt Bilde" /> + </div> + </div> + <div> + <p class="more-header">Melding fra leier:</p> + <div class="more"> + <p> + {{ extra }} + </p> + </div> + </div> + <div class="buttons" v-if = "(!rent.isAccepted && !rent.deleted)"> + <button class="button green" @click="accept">Godta</button> + <button class="button red" @click="reject">Avslå</button> + </div> + <div class="" v-if = rent.isAccepted> + <h1 class="green">Godtatt</h1> + </div> + <div class="" v-if = rent.deleted> + <h1 class="red">Avslått</h1> + </div> + </div> +</template> + +<script> +import axios from "axios"; +import { tokenHeader } from "@/utils/token-utils"; +export default { + props: { + rent: { + type: Object, + required: true + }, + }, + computed: { + img() { + return "https://images.unsplash.com/photo-1453728013993-6d66e9c9123a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8dmlld3xlbnwwfHwwfHw%3D&w=1000&q=80"; //this.rent.listing.imageUrl; + }, + from() { + // take ms and turn into date and return date + return new Date(this.rent.fromTime).toLocaleDateString(); + }, + to() { + return new Date(this.rent.toTime).toLocaleDateString(); + }, + price() { + // Calculate price from price * days + return ( + this.rent.listing.pricePerDay * + Math.ceil( + (this.rent.toTime - this.rent.fromTime) / (1000 * 60 * 60 * 24) + ) + ); + }, + extra() { + return this.rent.message || "Ingen Melding"; + }, + }, + methods: { + async accept() { + await axios.put( + process.env.VUE_APP_BASEURL + + `renting/${this.rent.rentId}/accept` ,null, + { headers: tokenHeader() } + ); + }, + async reject() { + await axios.delete( + process.env.VUE_APP_BASEURL + + `renting/${this.rent.rentId}/delete`,null, + { headers: tokenHeader() } + ); + + }, + }, +}; +</script> + +<style scoped> +.message { + display: flex; + flex-direction: column; + width: 100%; + background: #d1d5db; + border-radius: 10px; + padding: 10px; +} + +.info { + display: flex; + flex-direction: row; + justify-content: space-around; +} +.img { + width: 100%; + height: auto; + border-radius: 10px; + max-height: 200px; +} + +.text h1 { + text-decoration: underline; +} + +@media (max-width: 1200px) { + .img-container { + max-width: 30%; + } + .text { + min-width: 70%; + } +} + +.img-container { + display: flex; + align-content: center; + justify-items: center; + max-height: 20%; +} +.header { + font-weight: bold; + text-align: center; + text-decoration: underline; +} +.more-header { + font-weight: bold; +} + +.more { + border: 1px solid black; + border-radius: 10px; +} + +.more p { + margin-left: 5px; +} + +.buttons { + margin-top: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-items: center; + justify-content: space-around; +} + +.button { + border-radius: 0.25rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + font-weight: 700; + color: white; +} + +.red { + background-color: #ff1f1f; +} + +.red:hover { + box-shadow: 0 0 0 0.2rem rgba(255, 31, 31, 0.5); +} + +.green { + background-color: #0bb610; +} +.green:hover { + box-shadow: 0 0 0 0.2rem rgba(11, 182, 16, 0.5); +} +</style> diff --git a/src/components/CommunityComponents/CommunityHeader.vue b/src/components/CommunityComponents/CommunityHeader.vue index afd63a75c4fb1c7ac271c6eaaf9c94096fec4728..eba31718a765b9adfc08bd6b449dbff4c3de28a1 100644 --- a/src/components/CommunityComponents/CommunityHeader.vue +++ b/src/components/CommunityComponents/CommunityHeader.vue @@ -145,9 +145,6 @@ export default { } }, }, - // beforeMount() { - // this.getIfUserInCommunity(); - // }, async created() { await this.load(); this.loading = false; diff --git a/src/components/CommunityComponents/NewCommunityForm.vue b/src/components/CommunityComponents/NewCommunityForm.vue index 3784e50b8ee43c15553a74f49b10e55dffb33e7f..200db5b1a33be506325ffbb4fe7a15d2ec2a472b 100644 --- a/src/components/CommunityComponents/NewCommunityForm.vue +++ b/src/components/CommunityComponents/NewCommunityForm.vue @@ -136,10 +136,10 @@ <!-- Images --> <div class="mt-6"> <label - class="block mb-2 text-xl font-medium text-gray-900 dark:text-gray-400" - id="imageLabel" + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="imageLabel" > - Bilde + Bilde (bildet må være .png) </label> <input @@ -148,7 +148,7 @@ style="display: none" @change="addImage" multiple - accept="image/png, image/jpeg" + accept="image/png" /> <!-- Button for adding an image --> diff --git a/src/components/ItemComponents/ItemCard.vue b/src/components/ItemComponents/ItemCard.vue index ac79861f7bf9eb7bbed3592b4420d440bc98e587..0d81988d6ae5dca8c620b69c0eadeb7a6fb7cc7f 100644 --- a/src/components/ItemComponents/ItemCard.vue +++ b/src/components/ItemComponents/ItemCard.vue @@ -4,11 +4,11 @@ class="w-4/5 rounded bg-gray-200 h-full overflow-hidden display:inline-block correct-size" > <img - class="w-full" + class="h-3/4" :src="item.img || require('../../assets/default-product.png')" alt="Item image" /> - <div class="p-1 m-1"> + <div class="p-1 m-1 bottom-0"> <p class="text-gray-700 text-xs font-bold" id="adress"> {{ item.address }} </p> diff --git a/src/components/ItemComponents/NewItemForm.vue b/src/components/ItemComponents/NewItemForm.vue index 67ed1ebcc6ab09ca8977bb710f5f0becdd7a2149..244d24434ca753ffd3ecd2f3e76ebd0929fb3dcd 100644 --- a/src/components/ItemComponents/NewItemForm.vue +++ b/src/components/ItemComponents/NewItemForm.vue @@ -187,7 +187,7 @@ class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="imageLabel" > - Bilder + Bilder (bildene må være .png) </label> <input @@ -196,7 +196,7 @@ style="display: none" @change="addImage" multiple - accept="image/png, image/jpeg" + accept="image/png" /> <Button :text="'Velg bilde'" @click="$refs.file.click()" /> @@ -216,7 +216,12 @@ <script> import useVuelidate from "@vuelidate/core"; import { parseUserFromToken } from "@/utils/token-utils"; -import { postNewItem, getMyGroups } from "@/utils/apiutil"; +import { + postNewItem, + getMyGroups, + postNewImageCommunity, + PostImagesArrayToListing, +} from "@/utils/apiutil"; import Button from "@/components/BaseComponents/ColoredButton"; import { @@ -303,6 +308,7 @@ export default { userId: -1, selectedGroupId: -1, selectedGroups: [], + imagesToSend: [], }, //Kategorier skal legges inn ved api/hente fra db, her må det endres etterhvert categories: ["Hage", "Kjøkken", "Musikk", "Annet"], @@ -325,6 +331,7 @@ export default { async saveClicked() { if (this.checkValidation()) { this.checkUser(); + const itemInfo = { title: this.item.title, description: this.item.description, @@ -336,6 +343,8 @@ export default { }; await postNewItem(itemInfo); + await PostImagesArrayToListing(this.item.imagesToSend); + this.$router.push("/"); } }, @@ -345,8 +354,20 @@ export default { this.item.userId = parseInt(user.accountId); }, - addImage: function (event) { + addImage: async function (event) { this.item.images.push(URL.createObjectURL(event.target.files[0])); + + var that = this; + let image = event.target.files[0]; + let fileReader = new FileReader(); + fileReader.onloadend = async function () { + const res = fileReader.result; + const id = await postNewImageCommunity(res); + + const API_URL = process.env.VUE_APP_BASEURL; + that.item.imagesToSend.push(API_URL + "images/" + id); + }; + fileReader.readAsArrayBuffer(image); }, getGroups: async function () { diff --git a/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue b/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue index 5279dc8265d9273154150ff7398a40912bcae196..7ff418c29bec9efc5c40b2d424c1c0751563cd42 100644 --- a/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue +++ b/src/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue @@ -5,7 +5,7 @@ > <div class="months" v-for="day in days" :key="day">{{ day }}</div> </div> - <div class="grid grid-cols-7 gap-y-0.5 my-1"> + <div class="daysList grid grid-cols-7 gap-y-0.5 my-1"> <div class="days blocked" v-for="index in daysBeforeStartOfMonth" @@ -192,14 +192,6 @@ export default { isBlocked(day) { return this.blockedDays.includes(day); }, - isDayBlocked(day) { - return this.blockedDays.includes(day); - }, - isDayBetweenBlocked(day) { - return this.blockedDaysRange.some(([start, end]) => { - return start <= day && day <= end; - }); - }, // Get date from day and month and check if it is between start and end isDayBetweenStartAndEndDate(day) { if (!this.startDate || !this.endDate) return false; diff --git a/src/components/UserProfileComponents/RatingComponents/Rating.vue b/src/components/UserProfileComponents/RatingComponents/Rating.vue index 1db98bfb010248d1cf3d554df1f946a6867e6dc9..6efa3b014f4c175b7af950ddb36119a7029f60f9 100644 --- a/src/components/UserProfileComponents/RatingComponents/Rating.vue +++ b/src/components/UserProfileComponents/RatingComponents/Rating.vue @@ -1,5 +1,17 @@ <template> - <ul v-if="rating != -1" class="flex justify-center"> + <ul v-if="isNaN(rating)" class="flex justify-center"> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + {{ ratingType }}: + </p> + </li> + <li> + <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + Ingen vurderinger + </p> + </li> + </ul> + <ul v-else class="flex justify-center"> <li> <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> {{ ratingType }}: @@ -18,21 +30,10 @@ </svg> </li> <li> - <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> + <!-- Trenger vi å vise i tekst når vi har stjerner? --> + <!-- <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> {{ rating }} out of 5 - </p> - </li> - </ul> - <ul v-else class="flex justify-center"> - <li> - <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> - {{ ratingType }}: - </p> - </li> - <li> - <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400"> - Ingen vurderinger - </p> + </p> --> </li> </ul> </template> diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue index 1d6bd9f2472695c6427f96026a8b212ab9d130a9..17c002bc28fce875361dafac43ea0add48e3e9a0 100644 --- a/src/components/UserProfileComponents/UserListItemCard.vue +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -18,7 +18,7 @@ <!-- User rating --> <div class="hidden md:block flex-auto"> - <RatingComponent :rating="rating" :ratingType="'Gjennomsnitts rating'" /> + <RatingComponent :rating="rating" :ratingType="'Vurderinger'" /> </div> <!-- Buttons --> @@ -107,7 +107,7 @@ export default { openChatWithUser() { this.$router.push({ name: "messages", - params: { userId: this.user.userId }, + query: { userID: this.user.userId }, }); }, kickUserFromCommunity() { @@ -132,7 +132,13 @@ export default { }, }, async created() { - this.rating = await UserService.getUserRatingAverage(this.user.userId); + const maybeRating = await UserService.getUserRatingAverage( + this.user.userId + ); + if (isNaN(maybeRating)) this.rating = NaN; + else this.rating = maybeRating; + if (this.rating > 5) this.rating = 5; + if (this.rating < 1) this.rating = 1; this.communityID = this.$route.params.communityID; }, }; diff --git a/src/services/ws.js b/src/services/ws.js index c37cc21d24e93419abb2a6d1ebe3a0e0c8be3503..4a8cbbd485ee497068d73d5828a8d2e55cd039d9 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -1,16 +1,22 @@ const Stomp = require("stompjs"); -const SockJS = new require("sockjs-client")(process.env.VUE_APP_BASEURL + "ws"); +const SockJSRequire = require("sockjs-client"); +const SockJS = new SockJSRequire(process.env.VUE_APP_BASEURL + "ws"); import { parseCurrentUser } from "@/utils/token-utils"; // Create a Singleton function // Events fired by websocket, MESSAGE const ws = (function () { // Object of all injected functions that the client may want + // These functions will be in a object const handlers = {}; const fire = function (event, data) { if (handlers[event]) { - handlers[event](data); + + // for each object in object fire event + for(const key in handlers[event]) { + handlers[event][key](data); + } } }; @@ -19,7 +25,6 @@ const ws = (function () { // Fire message event fire("MESSAGE", JSON.parse(payload.body)); - if (data.status == "NEW_MESSAGE") fire("NEW_MESSAGE", JSON.parse(payload.body)); }; @@ -38,16 +43,20 @@ const ws = (function () { stompClient.connect({}, onConnected, onError); return { - on: function (event, callback) { - handlers[event] = callback; + on: function (event, callback, id = "none") { + // Generate random id + if(!handlers[event]) { + handlers[event] = {} + }; + handlers[event][id] = callback; }, fire: fire, - isActive: function (event) { - return !!handlers[event]; + isActive: function (event, id = "none") { + return !!handlers[event]?.[id]; }, - end: function (event) { + end: function (event, id = "none") { if (handlers[event]) { - delete handlers[event]; + delete handlers[event][id]; } else { throw new Error("No handler for event: " + event); } diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index 88717abceb23d9a2537ab240708f7bdaaf669b19..86b911d547006c86e8c1fece8cc0b7ba0b3be32a 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -332,7 +332,6 @@ export function postNewImageCommunity(image) { headers: { ...tokenHeader(), "Content-Type": "image/png" }, }) .then((response) => { - console.log(response.data); return response.data; }) .catch((error) => { @@ -340,3 +339,17 @@ export function postNewImageCommunity(image) { return error; }); } + +export function PostImagesArrayToListing(imagesArray) { + return axios + .post(API_URL + "listing/pictures", imagesArray, { + headers: tokenHeader(), + }) + .then((response) => { + return response; + }) + .catch((error) => { + console.error(error.response); + return error; + }); +} diff --git a/src/views/ChatViews/ChatView.vue b/src/views/ChatViews/ChatView.vue index 581da8f24f289618486dc3366f4476ca14e1d3c2..98283b536f2b892499768a7351948c5f3a91299d 100644 --- a/src/views/ChatViews/ChatView.vue +++ b/src/views/ChatViews/ChatView.vue @@ -1,76 +1,17 @@ <template> - <div class="flex flex-col h-full overflow-hidden border-2"> - <div class="flex flex-row h-full border-2 bg-gray-50"> - <div class="basis-1/3"> - <h1 class="text-center text-l">Mine samtaler</h1> - <ul v-if="conversations" class="border-2"> - <li - v-for="conversation in conversations" - :key="conversation.recipient.userId" - > - <ChatProfile :conversation="conversation" @recipient="selectUser" /> - </li> - </ul> - </div> - <div class="basis-2/3"> - <CurrentChat v-if="selected" :recipient="selected" /> - </div> - </div> - </div> - <!-- <div class="min-h-full"> - <div class="border rounded grid grid-cols-3 w-full"> - <div class="border-r border-gray-300 col-span-1"> - <ul class="hidden sm:block overflow-auto h-full"> - <h2 class="my-2 mb-2 ml-2 text-lg text-gray-600">Chats</h2> - <li v-if="conversations"> - </li> - </ul> - </div> - </div> - </div> --> + <chats-component></chats-component> </template> <script> -import ChatProfile from "@/components/ChatComponents/ChatProfile.vue"; -import CurrentChat from "@/components/ChatComponents/CurrentChat.vue"; -import { parseCurrentUser } from "@/utils/token-utils"; -import ChatService from "@/services/chat.service"; - +import ChatsComponent from "@/components/ChatComponents/ChatsComponent.vue"; export default { components: { - ChatProfile, - CurrentChat, + ChatsComponent, }, data() { - return { - conversations: null, - selected: null, - }; - }, - computed: { - key() { - return this.selected.userId || "ERROR"; - }, - }, - methods: { - userID() { - return parseCurrentUser().accountId; - }, - selectUser(value) { - const userid = value; - this.conversations.find((conversation) => { - return conversation.recipient.userId == userid; - }); - this.selected = this.conversations.find( - (conversation) => conversation.recipient.userId == userid - ).recipient; - }, - }, - async created() { - this.conversations = await ChatService.getConversations(this.userID()); - if (this.$route.params.userId !== null) { - this.selectUser(this.$route.params.userId); - } + return {}; }, }; </script> + +<style></style> diff --git a/src/views/CommunityViews/CommunityView.vue b/src/views/CommunityViews/CommunityView.vue index b709530023217c15612f51b03ec395e6833112d9..329917065b0679060da6503891b05c932ebaf18a 100644 --- a/src/views/CommunityViews/CommunityView.vue +++ b/src/views/CommunityViews/CommunityView.vue @@ -184,8 +184,8 @@ export default { for (var i = 0; i < this.publicCommunities.length; i++) { for (var j = 0; j < this.myCommunities.length; j++) { if ( - this.publicCommunities[i].communityId === - this.myCommunities[j].communityId + this.publicCommunities?.[i]?.communityId === + this.myCommunities?.[j]?.communityId ) { this.publicCommunities.splice(i, 1); } diff --git a/src/views/TestView.vue b/src/views/TestView.vue index f86430c64bb07d2f6e60fe0626ac1f50371d3682..b3a3ac1be4c5e41d3c9e49311443720fcedf895d 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -1,15 +1,18 @@ <template> - <div></div> + <chats-component></chats-component> </template> <script> +import ChatsComponent from "@/components/ChatComponents/ChatsComponent.vue"; export default { + components: { + ChatsComponent, + }, data() { return { show: false, }; }, - components: {}, methods: { toggleModal() { this.show = !this.show; diff --git a/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 1b8f2021b3218a5e802c69956cd055a0f77d6b00..e3a0811fb62bb0a919a31fa7a987b7bf16e599bf 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/create-new-group.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/create-new-group.spec.js.snap @@ -121,13 +121,13 @@ exports[`CreateNewGroup elements rendering renders correctly 1`] = ` class="mt-6" > <label - class="block mb-2 text-xl font-medium text-gray-900 dark:text-gray-400" + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="imageLabel" > - Bilde + Bilde (bildet må være .png) </label> <input - accept="image/png, image/jpeg" + accept="image/png" multiple="" style="display: none;" type="file" diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap index 0d4999e9ea79a37b5fd6bbae85350af3f93a2a91..356fa6b63aa9ece26edbdd78e5d07728036f38b7 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap @@ -9,11 +9,11 @@ exports[`ItemCard component renders correctly 1`] = ` > <img alt="Item image" - class="w-full" + class="h-3/4" src="String" /> <div - class="p-1 m-1" + class="p-1 m-1 bottom-0" > <p class="text-gray-700 text-xs font-bold" diff --git a/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap b/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap index 26181401d016fdab5f197d4f30a46c66b2ca4c1b..c0152222de3fa62b177a362dde7b4b755e039b5b 100644 --- a/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap @@ -171,10 +171,10 @@ exports[`NewItemForm component renders correctly 1`] = ` class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" id="imageLabel" > - Bilder + Bilder (bildene må være .png) </label> <input - accept="image/png, image/jpeg" + accept="image/png" multiple="" style="display: none;" type="file" diff --git a/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/CalendarComponent.spec.js b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/CalendarComponent.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..9ed6d65483a684d332ba961c31262b387ec9b1db --- /dev/null +++ b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/CalendarComponent.spec.js @@ -0,0 +1,45 @@ +import { shallowMount } from "@vue/test-utils"; +import CalendarComponent from "@/components/TimepickerComponents/DatepickerRange/CalendarComponent.vue"; + +describe("CalendarComponent tests", () => { + let wrapper; + beforeEach(() => { + wrapper = shallowMount(CalendarComponent, { + propsData: { + month: new Date(1651739228545),// May 2022 + blockedDaysRange: [ + [new Date(1651739228545)], // 5 May + [ + new Date(1652733228545), // 16 May + new Date(1652833228545) // 18 May + ]] + } + }); + }); + + it("Is instansiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("Check that all week days are rendered", () => { + expect(wrapper.findAll(".months").length).toBe(7); + }) + + it("Check that the correct amount of days are rendered", () => { + // 31 days in May, 6 for start of week from last month and 5 for end of month + expect(wrapper.find(".daysList").findAll("div").length).toBe(42); + }) + + it("Check select day works", () => { + wrapper.find(".daysList").findAll("div")[7].find("button").trigger("click"); + expect(wrapper.emitted()).toHaveProperty('selectDate') + }) + + it("Test that selecting day outside of month does not work", () => { + // Click on the first day, which is not in the month + wrapper.find(".daysList").findAll("div")[0].find("button").trigger("click"); + expect(wrapper.emitted()).not.toHaveProperty('selectDate') + }) + + +}); \ No newline at end of file diff --git a/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/month-selector-test.spec.js b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/month-selector-test.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..bba2c88846de4db57ea3535c56ff329502b6c366 --- /dev/null +++ b/tests/unit/component-tests/time-picker-components-tests/date-range-picker-tests/month-selector-test.spec.js @@ -0,0 +1,46 @@ +import { shallowMount } from "@vue/test-utils"; +import monthSelector from "@/components/TimepickerComponents/DatepickerRange/MonthSelector.vue"; + +describe("MonthSelector tests", () => { + let wrapper; + beforeEach(() => { + wrapper = shallowMount(monthSelector, { + propsData: { + month: new Date(1651739228545), // 05 May 2022 UTC + type: "type" + } + }); + }); + + it("Is instansiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("Check if fields show correct informations", () => { + // Check if container exists + expect(wrapper.find(".container-c .text")) + const children = wrapper.find(".container-c .text").findAll("div"); + + // Check if there are two children + expect(children.length).toBe(2); + + // Check children content + expect(children[0].text()).toBe("MAY"); + expect(children[1].text()).toBe("2022"); + }); + + it("Check that changing are emitted", async () => { + // Check that there are two buttons + expect(wrapper.findAll(".button").length).toBe(2); + + const buttons = wrapper.findAll(".button"); + console.log(buttons[0].html()); + // Check that the first button goes a month back + await buttons[0].trigger("click"); + expect(wrapper.emitted()).toHaveProperty('back') + + // Check that the second button goes a month forward + await buttons[1].trigger("click"); + expect(wrapper.emitted()).toHaveProperty('forward') + }) +}); \ No newline at end of file diff --git a/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap b/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap index e7d04a61c2d249a8bda1825ed17ed7be65bcf319..f22076edecd3620957e0bbf9141873adfd3a6322 100644 --- a/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/rating.spec.js.snap @@ -11,73 +11,11 @@ exports[`Rating component renders correctly 1`] = ` : </p> </li> - - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" - /> - </svg> - </li> - <li> - <svg - class="w-5 h-5 text-gray-300 dark:text-gray-500" - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" - /> - </svg> - </li> - <li> <p class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400" > - out of 5 + Ingen vurderinger </p> </li> </ul>