diff --git a/package-lock.json b/package-lock.json index 92415a10cf3e721007d669673ea0ebbbf5827a42..4bd9f6822075a48d6d04e75bde6c7862841d8215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "axios": "^0.26.1", "core-js": "^3.8.3", "cssom": "^0.5.0", + "heroicons": "^1.0.6", "jwt-decode": "^3.1.2", "net": "^1.0.2", "roboto-fontface": "*", @@ -7540,6 +7541,11 @@ "he": "bin/he" } }, + "node_modules/heroicons": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/heroicons/-/heroicons-1.0.6.tgz", + "integrity": "sha512-5bxTsG2hyNBF0l+BrFlZlR5YngQNMfl0ggJjIRkMSADBQbaZMoTg47OIQzq6f1mpEZ85HEIgSC4wt5AeFM9J2Q==" + }, "node_modules/highlight.js": { "version": "10.7.3", "dev": true, @@ -19967,6 +19973,11 @@ "version": "1.2.0", "dev": true }, + "heroicons": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/heroicons/-/heroicons-1.0.6.tgz", + "integrity": "sha512-5bxTsG2hyNBF0l+BrFlZlR5YngQNMfl0ggJjIRkMSADBQbaZMoTg47OIQzq6f1mpEZ85HEIgSC4wt5AeFM9J2Q==" + }, "highlight.js": { "version": "10.7.3", "dev": true diff --git a/package.json b/package.json index 82bef75d775f37cffd5e8bda14ea138904251888..97312aebe6f2e7cb32032be5bf4f7416775d6ea9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "axios": "^0.26.1", "core-js": "^3.8.3", "cssom": "^0.5.0", + "heroicons": "^1.0.6", "jwt-decode": "^3.1.2", "net": "^1.0.2", "roboto-fontface": "*", diff --git a/src/components/BaseComponents/IconButton.vue b/src/components/BaseComponents/IconButton.vue index 973f04c78f374cdb6f8e728c59bd416d1aca4cf8..095db5635e33e9a5116c0724f8a77a8a8c5acb93 100644 --- a/src/components/BaseComponents/IconButton.vue +++ b/src/components/BaseComponents/IconButton.vue @@ -1,17 +1,12 @@ <template> - <!-- Icon button --> <button - class="block w-fit text-white text-base bg-primary-medium hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-light font-medium rounded-lg text-center dark:bg-primary-medium dark:hover:bg-primary-dark dark:focus:ring-primary-dark" + 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="flex flex-row px-5 py-2.5 h-10"> - <!-- Icon slot: Default content "Ban"-icon --> - <div class="h-6 w-6"> - <slot> - <BanIcon /> - </slot> - </div> - <p>{{ text }}</p> + <div class="w-5 h-5 mx-1"> + <slot><BanIcon /></slot> </div> + <span class="mx-1">{{ text }}</span> </button> </template> @@ -22,9 +17,21 @@ export default { name: "IconButton", props: { text: String, + buttonColor: String, }, components: { BanIcon, }, + computed: { + color() { + if (this.buttonColor === "red") { + return "bg-error-medium hover:bg-error-dark focus:ring-error-light"; + } + if (this.buttonColor === "green") { + return "bg-success-medium hover:bg-success-dark focus:ring-success-light"; + } + return "bg-primary-medium hover:bg-primary-dark focus:ring-primary-light"; + }, + }, }; </script> diff --git a/src/components/BaseComponents/NavBar.vue b/src/components/BaseComponents/NavBar.vue index 3281cf2e521896053954387941ba03dc04c88281..39941c5321ede2b107b3750ae38f38d198202e67 100644 --- a/src/components/BaseComponents/NavBar.vue +++ b/src/components/BaseComponents/NavBar.vue @@ -13,21 +13,21 @@ <ul class="flex"> <li> <PlusIcon - class="m-6 cursor-pointer h-7" + class="m-6 cursor-pointer h-7 text-primary-medium" alt="Legg til" @click="$router.push('/newItem')" /> </li> <li> <ChatAlt2Icon - class="m-6 cursor-pointer h-7" + class="m-6 cursor-pointer h-7 text-primary-medium" alt="Meldinger" @click="$router.push('/messages')" /> </li> <li> <UserCircleIcon - class="m-6 cursor-pointer h-7" + class="m-6 cursor-pointer h-7 text-primary-medium" alt="Profil" @click="loadProfile" /> @@ -53,9 +53,7 @@ export default { async loadProfile() { if (this.$store.state.user.token !== null) { let user = parseUserFromToken(this.$store.state.user.token); - console.log(user); let id = user.accountId; - console.log(id); await this.$router.push("/profile/" + id); } else { await this.$router.push("/login"); diff --git a/src/components/BaseComponents/PaginationTemplate.vue b/src/components/BaseComponents/PaginationTemplate.vue new file mode 100644 index 0000000000000000000000000000000000000000..206f880770cabc4931733dd2725bc39efeb4c306 --- /dev/null +++ b/src/components/BaseComponents/PaginationTemplate.vue @@ -0,0 +1,40 @@ +<template> + <div v-if="totalPages() > 0"> + <span + v-if="showPreviousLink()" + class="cursor-pointer inline-flex items-center p-2 text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700" + @click="updatePage(currentPage - 1)" + > + Forrige + </span> + <label class="mx-2">{{ currentPage + 1 }} av {{ totalPages() }}</label> + <span + v-if="showNextLink()" + class="cursor-pointer inline-flex items-center p-2 text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700" + @click="updatePage(currentPage + 1)" + > + Neste + </span> + </div> +</template> + +<script> +export default { + name: "paginationTemplate", + props: ["items", "currentPage", "pageSize"], + methods: { + updatePage(pageNumber) { + this.$emit("page:update", pageNumber); + }, + totalPages() { + return Math.ceil(this.items.length / this.pageSize); + }, + showPreviousLink() { + return this.currentPage == 0 ? false : true; + }, + showNextLink() { + return this.currentPage == this.totalPages() - 1 ? false : true; + }, + }, +}; +</script> diff --git a/src/components/BaseComponents/RatingModal.vue b/src/components/BaseComponents/RatingModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..ee6efd60754b44dedaca4ab525ed53dd3750dd40 --- /dev/null +++ b/src/components/BaseComponents/RatingModal.vue @@ -0,0 +1,197 @@ +<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, + }; + await postNewRating(ratingInfo); + + this.$router.push("/"); + }, + }, +}; +</script> diff --git a/src/components/ChatComponents/ChatMessage.vue b/src/components/ChatComponents/ChatMessage.vue index d6996776b48e22224434b1696f64e2cdb5fa3aef..73ded12cc8407dddc21f12614e01b2b8eac71b07 100644 --- a/src/components/ChatComponents/ChatMessage.vue +++ b/src/components/ChatComponents/ChatMessage.vue @@ -33,7 +33,6 @@ export default { }, methods: { color() { - console.log(this.userID); return this?.message.from == this.userID ? "bg-gray-300" : "bg-primary-medium"; diff --git a/src/components/ChatComponents/ChatProfile.vue b/src/components/ChatComponents/ChatProfile.vue index 7e1e9aa2c20ec518844d64ebfb940f8723b84340..60c544f802a2384cf9e665c986d2fc24711ab197 100644 --- a/src/components/ChatComponents/ChatProfile.vue +++ b/src/components/ChatComponents/ChatProfile.vue @@ -52,7 +52,6 @@ export default { }, methods: { selectUser() { - console.log(this.conversation.recipient.userId); this.$emit("recipient", this.conversation.recipient.userId); }, }, diff --git a/src/components/CommunityComponents/CommunityHamburger.vue b/src/components/CommunityComponents/CommunityHamburger.vue index 34c82b645697dce95fc6d42ce3e6ea75db622323..391a9ab232be3ec69eefd513b060f274e7dcd1b4 100644 --- a/src/components/CommunityComponents/CommunityHamburger.vue +++ b/src/components/CommunityComponents/CommunityHamburger.vue @@ -18,9 +18,9 @@ >Se Medlemmer </router-link> </li> - <li id="adminGroup"> + <li id="adminGroup" v-if="admin"> <router-link - :to="'/community/' + communityID + '/memberlist'" + :to="'/community/' + communityID + '/admin'" 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" >Administrer Gruppe</router-link > @@ -39,24 +39,31 @@ <script> import { LeaveCommunity } from "@/utils/apiutil"; +import CommunityAdminService from "@/services/community-admin.service"; export default { name: "CommunityHamburger", - props: { - communityID: Number, - }, data() { return { id: -1, + admin: false, + communityID: -1, }; }, - methods: { leaveCommunity: async function () { - this.id = await this.$router.currentRoute.value.params.communityID; + this.id = this.$route.params.communityID; await LeaveCommunity(this.id); this.$router.push("/"); }, }, + async mounted() { + this.admin = await CommunityAdminService.isUserAdmin( + this.$route.params.communityID + ); + }, + created() { + this.communityID = this.$route.params.communityID; + }, }; </script> diff --git a/src/components/CommunityComponents/CommunityHeader.vue b/src/components/CommunityComponents/CommunityHeader.vue index 9be1167a1da7b4871f785ffbc460bf6c4471f7c8..4df062434b011c1515a9be2f159dc8ab200db754 100644 --- a/src/components/CommunityComponents/CommunityHeader.vue +++ b/src/components/CommunityComponents/CommunityHeader.vue @@ -1,5 +1,7 @@ <template> - <div class="flex items-center justify-between mx-4"> + <!-- TODO PUT A LOADER HERE --> + <div v-if="loading">LASTER...</div> + <div v-else class="flex items-center justify-between mx-4"> <router-link :to="'/community/' + community.communityId" class="flex-1 min-w-0" @@ -51,7 +53,7 @@ <!-- If the user is member of the community, this hamburger menu will show --> <div v-if="member"> <svg - @click="toggle" + @click="toggleHamburgerMenu()" xmlns="http://www.w3.org/2000/svg" class="w-9 h-9 cursor-pointer" fill="none" @@ -70,6 +72,7 @@ v-if="hamburgerOpen" class="origin-top-right absolute right-0" :community-i-d="community.communityId" + :admin="admin" /> <!-- class="absolute" --> </div> @@ -80,11 +83,13 @@ <script> import CommunityHamburger from "@/components/CommunityComponents/CommunityHamburger"; import ColoredButton from "@/components/BaseComponents/ColoredButton"; +import CommunityService from "@/services/community.service"; +import CustomFooterModal from "@/components/BaseComponents/CustomFooterModal"; +import { parseCurrentUser } from "@/utils/token-utils"; import { JoinOpenCommunity, - GetIfUserAlreadyInCommunity, + // GetIfUserAlreadyInCommunity, } from "@/utils/apiutil"; -import CustomFooterModal from "@/components/BaseComponents/CustomFooterModal"; export default { name: "CommunityHeader", @@ -93,31 +98,39 @@ export default { ColoredButton, CustomFooterModal, }, + computed: { + userid() { + return parseCurrentUser().accountId; + }, + }, data() { return { hamburgerOpen: false, dialogOpen: false, - member: true, + member: false, + community: {}, + loading: true, }; }, props: { - adminStatus: Boolean, - community: { - communityId: Number, - name: String, - description: String, - visibility: Number, - location: String, - picture: String, - }, + admin: Boolean, }, methods: { - //To open and close the hamburger menu - toggle: function () { - if (this.hamburgerOpen) { - this.hamburgerOpen = false; - } else { - this.hamburgerOpen = true; + toggleHamburgerMenu() { + this.hamburgerOpen = !this.hamburgerOpen; + }, + async load() { + this.community = await CommunityService.getCommunity( + this.$route.params.communityID + ); + const members = await CommunityService.getCommunityMembers( + this.$route.params.communityID + ); + for (let i = 0; i < members.length; i++) { + if (members[i].userId == this.userid) { + this.member = true; + return; + } } }, joinCommunity: async function (id) { @@ -128,18 +141,13 @@ export default { window.location.reload(); } }, - getIfUserInCommunity: async function () { - try { - this.member = await GetIfUserAlreadyInCommunity( - this.$router.currentRoute.value.params.communityID - ); - } catch (error) { - console.log(error); - } - }, }, - beforeMount() { - this.getIfUserInCommunity(); + // beforeMount() { + // this.getIfUserInCommunity(); + // }, + async created() { + await this.load(); + this.loading = false; }, }; </script> diff --git a/src/components/CommunityComponents/CommunityHome.vue b/src/components/CommunityComponents/CommunityHome.vue index 99e3e48a02cfe8ae489140457f9d8ee61f4ba37d..53fb6047e7def08264fdf5e66d51bed172a93dee 100644 --- a/src/components/CommunityComponents/CommunityHome.vue +++ b/src/components/CommunityComponents/CommunityHome.vue @@ -1,10 +1,6 @@ <template> <section class="w-full px-5 py-4 mx-auto rounded-md"> - <CommunityHeader - :admin-status="false" - :community="community" - class="mb-5" - /> + <CommunityHeader :admin="false" class="mb-5" /> <!-- Search field --> <div class="relative" id="searchComponent"> @@ -26,19 +22,48 @@ class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-primary-medium dark:focus:border-primary-medium focus:outline-none focus:ring" placeholder="Search" v-model="search" + @change="searchWritten" /> </div> - <!-- Item cards --> - <div class="absolute inset-x-0 px-6 py-3"> - <div - class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full place-items-center" - > - <ItemCard - v-for="item in searchedItems" - :key="item" - :item="item" - @click="goToItemInfoPage(item.listingID)" + <div class="absolute inset-x-0 px-5 py-3"> + <!-- ItemCards --> + <div class="flex items-center justify-center w-screen"> + <!-- Shows items based on pagination --> + <div + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" + v-if="showItems" + > + <ItemCard + v-for="item in visibleItems" + :key="item" + :item="item" + @click="goToItemInfoPage(item.listingID)" + /> + </div> + + <!-- Shows items based on search field input --> + <div + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full place-items-center" + v-if="showSearchedItems" + > + <ItemCard + v-for="item in searchedItems" + :key="item" + :item="item" + @click="goToItemInfoPage(item.listingID)" + /> + </div> + </div> + + <!-- pagination --> + <div class="flex justify-center" v-if="showItems"> + <PaginationTemplate + v-bind:items="items" + v-on:page:update="updatePage" + v-bind:currentPage="currentPage" + v-bind:pageSize="pageSize" + class="mt-10" /> </div> </div> @@ -46,8 +71,10 @@ </template> <script> -import CommunityHeader from "@/components/CommunityComponents/CommunityHeader.vue"; import ItemCard from "@/components/ItemComponents/ItemCard"; +import CommunityHeader from "@/components/CommunityComponents/CommunityHeader"; +import PaginationTemplate from "@/components/BaseComponents/PaginationTemplate"; + import { GetCommunity, GetListingsInCommunity, @@ -55,12 +82,11 @@ import { } from "@/utils/apiutil"; export default { name: "SearchItemListComponent", - components: { CommunityHeader, ItemCard, + PaginationTemplate, }, - computed: { searchedItems() { let filteredItems = []; @@ -90,15 +116,22 @@ export default { title: "", pricePerDay: 0, }, - search: "", + communityID: -1, community: {}, + + showItems: true, + showSearchedItems: false, + + //Variables connected to pagination + currentPage: 0, + pageSize: 12, + visibleItems: [], }; }, methods: { - getCommunityFromAPI: async function () { - this.communityID = await this.$router.currentRoute.value.params - .communityID; + async getCommunityFromAPI() { + this.communityID = this.$route.params.communityID; this.community = await GetCommunity(this.communityID); }, getListingsOfCommunityFromAPI: async function () { @@ -107,7 +140,6 @@ export default { this.items = await GetListingsInCommunity(this.communityID); for (var i = 0; i < this.items.length; i++) { let images = await getItemPictures(this.items[i].listingID); - console.log(images); if (images.length > 0) { this.items[i].img = images[0].picture; } @@ -120,10 +152,38 @@ export default { let res = await getItemPictures(itemid); return res; }, + searchWritten: function () { + //This method triggers when search input field is changed + if (this.search.length > 0) { + this.showItems = false; + this.showSearchedItems = true; + } else { + this.showItems = true; + this.showSearchedItems = false; + } + }, + + //Pagination + updatePage(pageNumber) { + this.currentPage = pageNumber; + this.updateVisibleTodos(); + }, + updateVisibleTodos() { + this.visibleItems = this.items.slice( + this.currentPage * this.pageSize, + this.currentPage * this.pageSize + this.pageSize + ); + + // if we have 0 visible items, go back a page + if (this.visibleItems.length === 0 && this.currentPage > 0) { + this.updatePage(this.currentPage - 1); + } + }, }, - beforeMount() { - this.getCommunityFromAPI(); //To get the id of the community before mounting the view - this.getListingsOfCommunityFromAPI(); + async beforeMount() { + await this.getCommunityFromAPI(); //To get the id of the community before mounting the view + await this.getListingsOfCommunityFromAPI(); + this.updateVisibleTodos(); }, }; </script> diff --git a/src/components/CommunityComponents/CommunityList.vue b/src/components/CommunityComponents/CommunityList.vue index 859e69d3e7f96c56935f64dd7b16beca2f403188..97d25120249632f9d395a35f80e66c338125e0e3 100644 --- a/src/components/CommunityComponents/CommunityList.vue +++ b/src/components/CommunityComponents/CommunityList.vue @@ -8,6 +8,7 @@ <script> import CommunityListItem from "@/components/CommunityComponents/CommunityListItem.vue"; +//import Join export default { name: "CommunityList", diff --git a/src/components/CommunityComponents/CommunityListItem.vue b/src/components/CommunityComponents/CommunityListItem.vue index 28e910a7c60693985ec5fdcb9fc49b6a6e7aad6d..1dc94f4e3421de11b7367b51ad8c12eae11a242b 100644 --- a/src/components/CommunityComponents/CommunityListItem.vue +++ b/src/components/CommunityComponents/CommunityListItem.vue @@ -8,12 +8,19 @@ <div class="flex justify-center p-2"> <!-- If a user is not a member in the community, this button will show --> <ColoredButton - v-if="!member" + v-if="!member && community.visibility !== 0" :text="'Bli med'" @click="goToJoin(community.communityId)" class="m-2" /> + <ColoredButton + v-if="!member && community.visibility === 0" + :text="'Spør om å bli med'" + @click="goToRequest(community.communityId)" + class="m-2" + /> + <!-- If a user is member this button will show --> <ColoredButton v-if="member" @@ -96,6 +103,9 @@ export default { this.$router.push("/community/" + id); } }, + goToRequest(id) { + this.$router.push("/community/" + id + "/private/join"); + }, toggleDialog() { this.dialogOpen = !this.dialogOpen; }, diff --git a/src/components/CommunityComponents/CommunityRequestForm.vue b/src/components/CommunityComponents/CommunityRequestForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..7c5f430106fee65e8a424de790f41ec1df7c4fc5 --- /dev/null +++ b/src/components/CommunityComponents/CommunityRequestForm.vue @@ -0,0 +1,110 @@ +<template> + <div + class="md:ring-1 ring-gray-300 rounded-xl overflow-hidden mx-auto mb-auto max-w-md w-full p-4" + > + <!-- Component heading --> + <div + class="text-xl md:text-2xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-10" + > + Bli med i: {{ community.name }} + </div> + + <!-- message --> + <div class="mt-6" :class="{ error: v$.message.$errors.length }"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="messageLabel" + > + Melding til administrator av gruppa: + </label> + <textarea + id="message" + rows="4" + v-model="message" + 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 + ></textarea> + + <!-- error message for message --> + <div + class="text-error" + v-for="(error, index) of v$.message.$errors" + :key="index" + > + <div class="text-error text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- Save item button --> + <div class="flex justify-center mt-10 float-right"> + <Button @click="saveClicked" id="saveButton" :text="'Send'"> </Button> + </div> + </div> +</template> + +<script> +import axios from "axios"; +import useVuelidate from "@vuelidate/core"; +import { required, helpers, maxLength } from "@vuelidate/validators"; +import Button from "@/components/BaseComponents/ColoredButton"; +import { tokenHeader } from "@/utils/token-utils"; +import { GetCommunity } from "@/utils/apiutil"; + +export default { + name: "CommunityRequestForm.vue", + + components: { + Button, + }, + setup() { + return { v$: useVuelidate() }; + }, + + validations() { + return { + message: { + required: helpers.withMessage( + () => "Meldingen kan ikke være tom", + required + ), + max: helpers.withMessage( + () => `Meldingen kan inneholde max 200 tegn`, + maxLength(200) + ), + }, + }; + }, + data() { + return { + message: "", + communityId: null, + community: {}, + }; + }, + computed: {}, + methods: { + //TODO fix so that community id is set (not null) + async saveClicked() { + this.communityID = await this.$router.currentRoute.value.params + .communityID; + + await axios.post( + process.env.VUE_APP_BASEURL + + `communities/${this.communityID}/private/join`, + { message: this.message }, + { headers: tokenHeader() } + ); + }, + getCommunityFromAPI: async function () { + this.communityID = await this.$router.currentRoute.value.params + .communityID; + this.community = await GetCommunity(this.communityID); + }, + }, + async created() { + await this.getCommunityFromAPI(); //To get the id of the community before mounting the view + }, +}; +</script> diff --git a/src/components/CommunityComponents/CommunitySettings.vue b/src/components/CommunityComponents/CommunitySettings.vue new file mode 100644 index 0000000000000000000000000000000000000000..8ea68e5261329fb85624ce68a201be8e02273416 --- /dev/null +++ b/src/components/CommunityComponents/CommunitySettings.vue @@ -0,0 +1,28 @@ +<template> + <div class="grid place-content-center h-48"> + <IconButton + @click="deleteCommunity" + :buttonColor="'red'" + :text="'Slett felleskap'" + /> + </div> +</template> + +<script> +// import CommunityAdminService from "@/services/community-admin.service"; +import IconButton from "@/components/BaseComponents/IconButton.vue"; + +//TODO: OPEN CONFIRMATION DIALOG WHEN DELETING + +export default { + components: { + IconButton, + }, + methods: { + deleteCommunity() { + console.log("DELETED"); + // CommunityAdminService.deleteCommunity(this.$route.params.communityID); + }, + }, +}; +</script> diff --git a/src/components/CommunityComponents/MemberList.vue b/src/components/CommunityComponents/MemberList.vue index 56c8bbe02a6683c4b5dc9bb2df22f1b3ad9809ec..336481461284383d1e4c90bcb0408b28b1a8c468 100644 --- a/src/components/CommunityComponents/MemberList.vue +++ b/src/components/CommunityComponents/MemberList.vue @@ -1,50 +1,47 @@ <template> - <CommunityHeader - :admin-status="false" - :community="community" - class="mb-5 mt-5" - /> - <ul> - <li v-for="member in memberlist" :key="member.userId"> - <user-list-item-card :admin="admin" :user="member" /> + <div v-if="loading">LASTER...</div> + <ul v-else> + <li v-for="member in members" :key="member.userId"> + <UserListItemCard :buttons="buttons" :user="member" /> </li> </ul> </template> <script> import UserListItemCard from "@/components/UserProfileComponents/UserListItemCard.vue"; -import { GetMembersOfCommunity, GetCommunity } from "@/utils/apiutil"; -import CommunityHeader from "@/components/CommunityComponents/CommunityHeader.vue"; +import CommunityService from "@/services/community.service"; +import { GetMemberRequestsOfCommunity } from "@/utils/apiutil"; export default { - data() { - return { - memberlist: [], - community: {}, - }; - }, + name: "MemberList", components: { - CommunityHeader, UserListItemCard, }, props: { - admin: Boolean, + buttons: Array, + requests: Boolean, + }, + data() { + return { + members: [], + loading: false, + }; }, methods: { - getAllMembersOfCommunity: async function () { - this.memberlist = await GetMembersOfCommunity( - this.$router.currentRoute.value.params.id + async load() {}, + }, + async created() { + this.loading = true; + if (this.requests) { + this.members = await GetMemberRequestsOfCommunity( + this.$route.params.communityID ); - }, - getCommunity: async function () { - this.community = await GetCommunity( - this.$router.currentRoute.value.params.id + } else { + this.members = await CommunityService.getCommunityMembers( + this.$route.params.communityID ); - }, - }, - beforeMount() { - this.getAllMembersOfCommunity(); - this.getCommunity(); + } + this.loading = false; }, }; </script> diff --git a/src/components/FormComponents/LoginForm.vue b/src/components/FormComponents/LoginForm.vue index bbb849a1008414ed2abc3899b7216c9a80d7d562..c1b38f5d704d78d933a3c07173ff36dae2debd05 100644 --- a/src/components/FormComponents/LoginForm.vue +++ b/src/components/FormComponents/LoginForm.vue @@ -137,7 +137,6 @@ export default { this.v$.user.$touch(); if (this.v$.user.$invalid) { - console.log("Ugyldig, avslutter..."); return; } @@ -153,8 +152,6 @@ export default { } else if (loginResponse.isLoggedIn === true) { this.$store.commit("saveToken", loginResponse.token); await this.$router.push("/"); - } else { - console.log("Something went wrong"); } }, }, diff --git a/src/components/FormComponents/NewPasswordForm.vue b/src/components/FormComponents/NewPasswordForm.vue index d13c08349360250ff4f0cf96b5f3faab00d7a841..519a3c6677ce3e85668f2567e9833b4873055f65 100644 --- a/src/components/FormComponents/NewPasswordForm.vue +++ b/src/components/FormComponents/NewPasswordForm.vue @@ -130,24 +130,16 @@ export default { this.v$.user.$touch(); if (this.v$.user.$invalid) { - console.log("Invalid, exiting..."); return; } - const newPasswordInfo = { - token: this.token, - newPassword: this.password, - }; + const newPassword = this.user.password; - const newPasswordResponse = doNewPassword(newPasswordInfo); + const newPasswordResponse = await doNewPassword(newPassword); - if (newPasswordResponse.newPasswordSet === true) { - console.log("New password set"); + if (newPasswordResponse != null) { + this.$store.commit("saveToken", newPasswordResponse); await this.$router.push("/"); - } else if (newPasswordResponse.newPasswordSet === false) { - console.log("Couldn't set new password"); - } else { - console.log("Something went wrong"); } }, validate() { diff --git a/src/components/FormComponents/ResetPasswordForm.vue b/src/components/FormComponents/ResetPasswordForm.vue index a4c2d80e1533e2c7be82804041bd49208b4ab6be..36fd9aae1f69396b55e2220439b0bb1c2d174eaf 100644 --- a/src/components/FormComponents/ResetPasswordForm.vue +++ b/src/components/FormComponents/ResetPasswordForm.vue @@ -83,7 +83,6 @@ export default { this.v$.email.$touch(); if (this.v$.email.$invalid) { - console.log("Ugyldig, avslutter..."); return; } else { this.$router.push("/"); diff --git a/src/components/ItemComponents/NewItemForm.vue b/src/components/ItemComponents/NewItemForm.vue index f9f5f06e0e6a8fc5e610ee8cfc326a276d111e18..ad87fd4358184719764cfcd3fc7a1b2f9e39222b 100644 --- a/src/components/ItemComponents/NewItemForm.vue +++ b/src/components/ItemComponents/NewItemForm.vue @@ -311,37 +311,19 @@ export default { }, methods: { checkValidation: function () { - console.log("sjekker validering"); - this.v$.item.$touch(); if (this.v$.item.$invalid || this.item.selectedGroups.length === 0) { if (this.item.selectedGroups.length === 0) { this.groupErrorMessage = "Velg gruppe/grupper"; } - console.log("Invalid, avslutter..."); return false; } - - console.log("validert!"); return true; }, async saveClicked() { - console.log("Attempting to save item"); - if (this.checkValidation()) { - console.log("validert, videre..."); - this.checkUser(); - - console.log("Tittel: " + this.item.title); - console.log("Kategori: " + this.item.select); - console.log("Beskrivelse: " + this.item.description); - console.log("Addressen: " + this.item.address); - console.log("Pris: " + this.item.price); - console.log("bilder: " + this.item.images); - console.log("gruppe: " + this.item.selectedGroups); - const itemInfo = { title: this.item.title, description: this.item.description, @@ -351,12 +333,7 @@ export default { categoryNames: [], communityIDs: this.item.selectedGroups, }; - - console.log(itemInfo); - - const postRequest = await postNewItem(itemInfo); - - console.log("posted: " + postRequest); + await postNewItem(itemInfo); this.$router.push("/"); } @@ -368,7 +345,6 @@ export default { }, addImage: function (event) { - console.log(event.target.files); this.item.images.push(URL.createObjectURL(event.target.files[0])); }, @@ -379,7 +355,6 @@ export default { onChangeGroup: function (e) { this.selectedGroupId = e.target.value; let alreadyInGroupList = false; - console.log("selected clicked"); for (let i = 0; i <= this.item.selectedGroups.length; i++) { if (this.selectedGroupId == this.item.selectedGroups[i]) { diff --git a/src/components/RentingComponents/ItemInfo.vue b/src/components/RentingComponents/ItemInfo.vue index 59308822724d89fc7749b0e1ed88afde4b316cea..18f8a558678bf4a038a8d05bcac2a39400a81326 100644 --- a/src/components/RentingComponents/ItemInfo.vue +++ b/src/components/RentingComponents/ItemInfo.vue @@ -51,7 +51,10 @@ </div> </div> <div class="mt-2"> - <UserListItemCard :user="userForId"></UserListItemCard> + <UserListItemCard + :buttons="['chat']" + :user="userForId" + ></UserListItemCard> </div> <div class="mt-4"> <h3 class="text-base font-base text-gray-900">Tidspunkter</h3> @@ -87,10 +90,11 @@ <script> import NewRent from "@/components/RentingComponents/NewRent.vue"; -import { getItem, getItemPictures, getUser } from "@/utils/apiutil"; +import { getItem, getItemPictures } from "@/utils/apiutil"; import ImageCarousel from "@/components/RentingComponents/ImageCarousel.vue"; import UserListItemCard from "@/components/UserProfileComponents/UserListItemCard.vue"; import DatepickerRange from "@/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue"; +import UserService from "@/services/user.service"; export default { name: "ItemInfo", @@ -123,7 +127,7 @@ export default { ], pictures: [], noPicture: true, - userForId: Object, + userForId: {}, rentingStartDate: null, rentingEndDate: null, totPrice: 0, @@ -181,7 +185,7 @@ export default { //TODO fixs so each image get a correct alt text. }, async getUser(userId) { - this.userForId = await getUser(userId); + this.userForId = await UserService.getUserFromId(userId); }, setDate(dateOfsomthing) { if (dateOfsomthing.startDate == null || dateOfsomthing.endDate == null) { diff --git a/src/components/RentingComponents/NewRent.vue b/src/components/RentingComponents/NewRent.vue index 32073aa24a66328935b56876748e554e8dc00925..7ced0d6da6b8575e6fa37f287ddc3ae464fa0072 100644 --- a/src/components/RentingComponents/NewRent.vue +++ b/src/components/RentingComponents/NewRent.vue @@ -32,8 +32,12 @@ <button id="cancelButton" @click="cancelRent" class="text-primary-medium"> Tilbake </button> - <div id="confirmButton"> - <colored-button @click="sendRent" :text="'Send'"></colored-button> + <div id="confirm"> + <colored-button + id="confirmButton" + @click="sendRent" + :text="'Send'" + ></colored-button> </div> </div> <div> @@ -75,7 +79,6 @@ export default { newRentBox: { renterId: Number, title: String, - description: String, fromTime: Date, toTime: Date, listingID: Number, @@ -100,40 +103,40 @@ export default { let monthString = ""; //Gives the month the proper name switch (dateMonth) { - case 1: + case 0: monthString = "Januar"; break; - case 2: + case 1: monthString = "Februar"; break; - case 3: + case 2: monthString = "Mars"; break; - case 4: + case 3: monthString = "April"; break; - case 5: + case 4: monthString = "Mai"; break; - case 6: + case 5: monthString = "Juni"; break; - case 7: + case 6: monthString = "Juli"; break; - case 8: + case 7: monthString = "August"; break; - case 9: + case 8: monthString = "September"; break; - case 10: + case 9: monthString = "Oktober"; break; - case 11: + case 10: monthString = "November"; break; - case 12: + case 11: monthString = "Desember"; break; default: @@ -209,7 +212,7 @@ export default { grid-column: 1/3; grid-row: 6/7; } -#confirmButton { +#confirm { grid-column: 1/3; grid-row: 7/8; align-content: center; diff --git a/src/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue b/src/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue index 53c92481e3318bfe86ccc699f80c6ed24ffbceb7..49fd65dc1249dc57add5e7c73d69a60bca2a1736 100644 --- a/src/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue +++ b/src/components/TimepickerComponents/DatepickerRange/DatepickerRange.vue @@ -26,22 +26,6 @@ :blockedDaysRange="blockedDaysRange" ></calendar-component> </div> - <div class="split"></div> - <div> - <month-selector - @back="back" - type="monghM" - @forward="forward('monghM')" - :month="monghM" - ></month-selector> - <calendar-component - :month="monghM" - :start="startDate" - :end="endDate" - @selectDate="selectDate" - :blockedDaysRange="blockedDaysRange" - ></calendar-component> - </div> </div> <div class="footer"> <p v-if="error" class="error">{{ errorMessage }}</p> diff --git a/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue b/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue index a16a3b840b9efd770d970b0f7313f5e1d0198ec9..6ed71add4bb27126ca6a8518b081d753063d89cc 100644 --- a/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue +++ b/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue @@ -81,7 +81,6 @@ export default { this.$emit("back", this.type); }, forward() { - console.log(this.type); this.$emit("forward", this.type); }, }, diff --git a/src/components/UserProfileComponents/UserItems.vue b/src/components/UserProfileComponents/UserItems.vue new file mode 100644 index 0000000000000000000000000000000000000000..7b9a503d06a4b5d68f2b7b0af9dfca711d5edd8a --- /dev/null +++ b/src/components/UserProfileComponents/UserItems.vue @@ -0,0 +1,157 @@ +<template> + <div + id="headline" + class="text-xl md:text-2xl text-gray-600 font-medium w-full" + > + Dine gjenstander + </div> + <!-- Search field --> + <div class="relative" id="searchComponent"> + <span class="absolute inset-y-0 left-0 flex items-center pl-3"> + <svg class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none"> + <path + d="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + ></path> + </svg> + </span> + + <input + type="text" + id="searchInput" + class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-primary-medium dark:focus:border-primary-medium focus:outline-none focus:ring" + placeholder="Search" + v-model="search" + @change="searchWritten" + /> + </div> + + <div class="absolute inset-x-0 px-5 py-3"> + <!-- ItemCards --> + <div class="flex items-center justify-center w-screen"> + <!-- Shows items based on pagination --> + <div + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full" + v-if="showItems" + > + <ItemCard v-for="item in visibleItems" :key="item" :item="item" /> + </div> + + <!-- Shows items based on search field input --> + <div + class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 lg:grid-cols-5 w-full place-items-center" + v-if="showSearchedItems" + > + <ItemCard v-for="item in searchedItems" :key="item" :item="item" /> + </div> + </div> + <!-- pagination --> + <div class="flex justify-center" v-if="showItems"> + <PaginationTemplate + v-bind:items="items" + v-on:page:update="updatePage" + v-bind:currentPage="currentPage" + v-bind:pageSize="pageSize" + class="mt-10" + /> + </div> + </div> +</template> +<script> +import { GetUserListings, getItemPictures } from "@/utils/apiutil"; +import ItemCard from "@/components/ItemComponents/ItemCard.vue"; +import PaginationTemplate from "@/components/BaseComponents/PaginationTemplate"; + +export default { + name: "UserItems", + components: { + ItemCard, + PaginationTemplate, + }, + data() { + return { + items: [], + item: { + listingID: 0, + img: "", + address: "", + title: "", + pricePerDay: 0, + }, + showItems: true, + showSearchedItems: false, + search: "", + //Variables connected to pagination + currentPage: 0, + pageSize: 12, + visibleItems: [], + }; + }, + computed: { + searchedItems() { + let filteredItems = []; + + filteredItems = this.items.filter( + (p) => + p.title.toLowerCase().includes(this.search.toLowerCase()) || + p.address.toLowerCase().includes(this.search.toLowerCase()) || + p.pricePerDay === Number(this.search) + ); + + return filteredItems; + }, + }, + methods: { + getUserListingsFromAPI: async function () { + this.items = await GetUserListings(); + for (var i = 0; i < this.items.length; i++) { + let images = await getItemPictures(this.items[i].listingID); + if (images.length > 0) { + this.items[i].img = images[0].picture; + } + } + }, + //Pagination + updatePage(pageNumber) { + this.currentPage = pageNumber; + this.updateVisibleTodos(); + }, + updateVisibleTodos() { + this.visibleItems = this.items.slice( + this.currentPage * this.pageSize, + this.currentPage * this.pageSize + this.pageSize + ); + + // if we have 0 visible items, go back a page + if (this.visibleItems.length === 0 && this.currentPage > 0) { + this.updatePage(this.currentPage - 1); + } + }, + searchWritten: function () { + //This method triggers when search input field is changed + if (this.search.length > 0) { + this.showItems = false; + this.showSearchedItems = true; + } else { + this.showItems = true; + this.showSearchedItems = false; + } + }, + }, + async beforeMount() { + await this.getUserListingsFromAPI(); + this.updateVisibleTodos(); + }, +}; +</script> + +<style> +#headline { + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +</style> diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue index a69c38b9931e1eb3c08d4c21290a33a48f91a5de..202c397d9889c534dcb860d878e2adafe59f6b90 100644 --- a/src/components/UserProfileComponents/UserListItemCard.vue +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -2,67 +2,133 @@ <div class="bg-white shadow dark:bg-gray-800 select-none cursor-pointer hover:bg-gray-50 flex items-center p-4" > + <!-- User image --> <div class="h-10 w-10 flex flex-col justify-center items-center mr-4"> <router-link :to="'/profile/' + user.userId"> - <img alt="profil" src="../../assets/defaultUserProfileImage.jpg" /> + <img alt="Profilbilde" src="../../assets/defaultUserProfileImage.jpg" /> </router-link> </div> + + <!-- User name --> <div class="flex-1 pl-1"> <div class="font-medium dark:text-white"> {{ user.firstName }} {{ user.lastName }} </div> </div> + + <!-- User rating --> <div class="hidden md:block flex-auto"> - <rating-component :rating="rating" :ratingType="'Gjennomsnitts rating'" /> + <RatingComponent :rating="rating" :ratingType="'Gjennomsnitts rating'" /> </div> - <div class="flex flex-row justify-center"> - <button - v-if="!admin" - class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-primary-medium rounded-md hover:bg-primary-light focus:outline-none focus:ring focus:ring-opacity-80" - > - Åpne chat - </button> - <button - v-if="admin" - class="px-4 py-2 font-medium tracking-wide text-white capitalize transition-colors duration-200 transform bg-blue-600 rounded-md hover:bg-blue-500 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80" + + <!-- Buttons --> + <div class="flex flex-row gap-4"> + <IconButton + v-if="buttons.includes('chat')" + @click="openChatWithUser()" + :text="'Chat'" + :buttonColor="'blue'" > - Fjern bruker - </button> + <ChatIcon + /></IconButton> + + <IconButton + v-if="buttons.includes('kick')" + @click="kickUserFromCommunity()" + :buttonColor="'red'" + :text="'Spark'" + ><BanIcon + /></IconButton> + + <IconButton + v-if="buttons.includes('accept')" + @click="acceptMemberRequest()" + :buttonColor="'green'" + :text="'Godta'" + ><CheckCircleIcon + /></IconButton> + + <IconButton + v-if="buttons.includes('reject')" + @click="rejectMemberRequest()" + :buttonColor="'red'" + :text="'Avslå'" + ><XCircleIcon + /></IconButton> </div> </div> </template> <script> -import { getAverageRating } from "@/utils/apiutil"; -import RatingComponent from "./Rating.vue"; +import RatingComponent from "@/components/UserProfileComponents/Rating.vue"; +import IconButton from "@/components/BaseComponents/IconButton.vue"; +import UserService from "@/services/user.service"; +import CommunityAdminService from "@/services/community-admin.service"; + +import { + ChatIcon, + CheckCircleIcon, + BanIcon, + XCircleIcon, +} from "@heroicons/vue/outline"; export default { name: "UserListItem", data() { return { - rating: this.getRating(), + rating: -1.0, + communityID: -1, }; }, components: { RatingComponent, + IconButton, + ChatIcon, + CheckCircleIcon, + BanIcon, + XCircleIcon, }, props: { user: Object, - admin: Boolean, + buttons: Array, }, methods: { getProfilePicture() { if (this.user.picture != "") { return this.user.picture; } - return "../assets/defaultUserProfileImage.jpg"; + return "@/assets/defaultUserProfileImage.jpg"; + }, + openChatWithUser() { + this.$router.push({ + name: "messages", + params: { userId: this.user.userId }, + }); + }, + kickUserFromCommunity() { + CommunityAdminService.removeUserFromCommunity( + this.communityID, + this.user.userId + ); + //Find a better way to do this + window.location.reload(); + }, + acceptMemberRequest() { + CommunityAdminService.acceptUserIntoCommunity( + this.communityID, + this.user.userId + ); }, - async getRating() { - this.rating = await getAverageRating(this.user.userId); + rejectMemberRequest() { + CommunityAdminService.rejectUserFromCommunity( + this.communityID, + this.user.userId + ); }, }, - beforeMount() { - this.getRating(); + async created() { + this.rating = await UserService.getUserRatingAverage(this.user.userId); + this.communityID = this.$route.params.communityID; }, }; </script> diff --git a/src/components/UserProfileComponents/UserProfile.vue b/src/components/UserProfileComponents/UserProfile.vue index 9235ab472cdd07769260c182f5acc0fddd9632a8..ff50ea7b5954b5bb66d5a1ac228bef17fefa7a5e 100644 --- a/src/components/UserProfileComponents/UserProfile.vue +++ b/src/components/UserProfileComponents/UserProfile.vue @@ -33,7 +33,7 @@ > <li> <router-link - to="" + to="/user/userItems" class="block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" >Mine gjenstander</router-link > diff --git a/src/main.js b/src/main.js index 32764da4a55678ff6ed1fbf6e21819a55990f716..47d214cad941cb90ade7d2fdbd7b9c00cdedb8aa 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,5 @@ import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; -import ws from "./services/ws"; createApp(App).use(router).use(store).mount("#app"); -console.log("WS", ws.test); diff --git a/src/router/index.js b/src/router/index.js index 89cfaf4fae33eb47ad4fb4d8c12106de335359a3..244c87c3a21f24d143895f5c674afa809f5ee80b 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -70,7 +70,7 @@ const routes = [ beforeEnter: guardRoute, }, { - path: "/community/:id/memberlist", + path: "/community/:communityID/memberlist", name: "memberlist", component: () => import("../views/CommunityViews/MemberListView.vue"), beforeEnter: guardRoute, @@ -99,16 +99,33 @@ const routes = [ name: "communityHome", component: () => import("../views/CommunityViews/CommunityHomeView.vue"), }, + { + path: "/community/:communityID/private/join", + name: "communityRequest", + component: () => import("../views/CommunityViews/CommunityRequestView.vue"), + }, { beforeEnter: guardRoute, path: "/test", name: "test", component: () => import("../views/TestView.vue"), }, + { + path: "/community/:communityID/admin", + name: "CommunityAdminView", + component: () => import("@/views/CommunityViews/AdminView.vue"), + beforeEnter: guardRoute, + }, { path: "/itempage/:id", name: "ItemInfo", component: () => import("../views/RentingViews/ItemInfoPageView.vue"), + beforeEnter: guardRoute, + }, + { + path: "/user/userItems", + name: "UserItems", + component: () => import("../views/UserProfileViews/UserItemsView.vue"), }, ]; diff --git a/src/services/chat.service.js b/src/services/chat.service.js new file mode 100644 index 0000000000000000000000000000000000000000..908763073b06e96ff429dce23678cb3207945869 --- /dev/null +++ b/src/services/chat.service.js @@ -0,0 +1,21 @@ +import axios from "axios"; +import { tokenHeader } from "@/utils/token-utils"; + +const API_URL = process.env.VUE_APP_BASEURL; + +class ChatService { + async getConversations() { + return await axios + .get(API_URL + "chats/users", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } +} + +export default new ChatService(); diff --git a/src/services/community-admin.service.js b/src/services/community-admin.service.js new file mode 100644 index 0000000000000000000000000000000000000000..c935f1181ea5d8c81306dddae21a8015d95a8d9d --- /dev/null +++ b/src/services/community-admin.service.js @@ -0,0 +1,72 @@ +import axios from "axios"; +import { tokenHeader } from "@/utils/token-utils"; + +const API_URL = process.env.VUE_APP_BASEURL; + +/** + * Service class acting as a middle layer between our components and the API + */ +class CommunityAdminService { + async isUserAdmin(communityID) { + return await axios + .get(API_URL + "communities/" + communityID + "/user/admin", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }); + } + + //TODO + async acceptUserIntoCommunity(communityID, userID) { + return await axios.post( + API_URL + "communities/" + communityID + "/requests", + null, + { headers: tokenHeader(), params: { userId: userID } } + ); + } + + //TODO + async rejectUserFromCommunity(communityID, userID) { + return await axios.patch( + API_URL + "communitites/" + communityID + "/requests/reject", + null, + { headers: tokenHeader(), params: { userId: userID } } + ); + } + + /** + * Method that kicks a user from a community + * @param {int} communityID the community to remove the user from + * @param {int} userID the user to remove + * @returns TODO + */ + async removeUserFromCommunity(communityID, userID) { + return await axios.patch( + API_URL + "communities/" + communityID + "/kick", + null, + { + headers: tokenHeader(), + params: { + userId: userID, + }, + } + ); + } + + /** + * Method to delete a community + * @param {int} communityID id of the community to delete. + * @returns TODO + */ + async deleteCommunity(communityID) { + return await axios.post( + API_URL + "communities/" + communityID + "/remove", + { + headers: tokenHeader(), + } + ); + } +} + +export default new CommunityAdminService(); diff --git a/src/services/community.service.js b/src/services/community.service.js new file mode 100644 index 0000000000000000000000000000000000000000..11bc85817db76d285c028434a7e8df2f64cc5f46 --- /dev/null +++ b/src/services/community.service.js @@ -0,0 +1,60 @@ +import { tokenHeader } from "@/utils/token-utils"; +import axios from "axios"; + +const API_URL = process.env.VUE_APP_BASEURL; + +class CommunityService { + async getCommunity(communityID) { + return await axios + .get(API_URL + "community/" + communityID, { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } + + async getAllCommunities() { + return await axios + .get(API_URL + "communities", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } + + async getUserCommunities() { + return await axios + .get(API_URL + "user/communities", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } + + async getCommunityMembers(communityID) { + return await axios + .get(API_URL + "community/" + communityID + "/members", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } +} + +export default new CommunityService(); diff --git a/src/services/user.service.js b/src/services/user.service.js new file mode 100644 index 0000000000000000000000000000000000000000..f07ef8c3acda54f54fa1189b63cbb07aa0412c87 --- /dev/null +++ b/src/services/user.service.js @@ -0,0 +1,37 @@ +// import { tokenHeader } from "@/utils/token-utils"; +import { tokenHeader } from "@/utils/token-utils"; +import axios from "axios"; + +const API_URL = process.env.VUE_APP_BASEURL; + +class UserService { + async getUserFromId(userId) { + return await axios + .get(API_URL + "users/" + userId + "/profile", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); + } + + async getUserRatingAverage(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average", { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); + } + + //TODO + async getUserRatingAsOwner() {} + + //TODO + async getUserRatingAsRenter() {} +} + +export default new UserService(); diff --git a/src/services/ws.js b/src/services/ws.js index 80951c9ea5fcc921a59c9ab8d175a5ff25d7b677..c37cc21d24e93419abb2a6d1ebe3a0e0c8be3503 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -17,18 +17,14 @@ const ws = (function () { const onMessageReceived = (payload) => { const data = JSON.parse(payload.body); - console.log("New message!"); // Fire message event fire("MESSAGE", JSON.parse(payload.body)); if (data.status == "NEW_MESSAGE") fire("NEW_MESSAGE", JSON.parse(payload.body)); - - console.log("Received message: " + payload); }; const onConnected = () => { - console.log("Websocket Connected"); stompClient.subscribe( "/user/" + parseCurrentUser().accountId + "/queue/messages", onMessageReceived @@ -56,8 +52,7 @@ const ws = (function () { throw new Error("No handler for event: " + event); } }, - sendMessage: ({ sender, recipient, status }) => { - if (status) console.log(status); + sendMessage: ({ sender, recipient /* , status */ }) => { stompClient.send( "/app/chat", {}, diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index a49864af94949186bb90b327a95ad74cc4dc9d37..00c6d6fb0345f063325fa8a76c43fac34db4cd11 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -13,7 +13,7 @@ export function doLogin(loginRequest) { return auth; }) .catch((error) => { - console.log(error.response); + console.error(error.response); return auth; }); } @@ -23,14 +23,14 @@ export function registerUser(registerInfo) { .post(API_URL + "register", { email: registerInfo.email, firstName: registerInfo.firstName, - lastname: registerInfo.lastname, + lastName: registerInfo.lastname, password: registerInfo.password, address: registerInfo.address, }) .then((response) => { return response; }) - .catch((err) => console.log(err)); + .catch((err) => console.error(err)); } export async function getUser(userid) { @@ -84,15 +84,22 @@ export function getAverageRating(userid) { console.error(error); }); } -export function doNewPassword() { - //m - //add newPasswordInfo to input - const auth = { newPasswordSet: false }; - //return axios - //.post(API_URL + "newPassword", newPasswordInfo) - //.then((response) => {auth.newPasswordSet = true;return auth;}) - //.catch((error) => {console.log(error);return auth;}); - return auth; //remove after axios is added +export async function doNewPassword(password) { + let res = await axios({ + method: "put", + url: API_URL + "user/profile/password", + headers: tokenHeader(), + data: { + password: password, + }, + }) + .then((response) => { + return response; + }) + .catch((error) => { + console.error(error); + }); + return res.data; } export function postNewItem(itemInfo) { @@ -101,11 +108,10 @@ export function postNewItem(itemInfo) { headers: tokenHeader(), }) .then((response) => { - console.log("poster: " + response.data); return response; }) .catch((error) => { - console.log(error.response); + console.error(error.response); return error; }); } @@ -117,7 +123,7 @@ export function postNewgroup(groupInfo) { return response; }) .catch((error) => { - console.log(error.response); + console.error(error.response); return error; }); } @@ -127,11 +133,10 @@ export function postNewRent(rentInfo) { headers: tokenHeader(), }) .then((response) => { - console.log("poster: " + response.data); return response; }) .catch((error) => { - console.log(error.response); + console.error(error.response); return error; }); } @@ -190,7 +195,7 @@ export async function getItemPictures(itemid) { } export async function GetCommunity(communityID) { - return axios + return await axios .get(API_URL + "community/" + communityID, { headers: tokenHeader(), }) @@ -228,9 +233,21 @@ export async function GetMembersOfCommunity(communityID) { }); } +export async function GetMemberRequestsOfCommunity(communityID) { + return axios + .get(API_URL + "communities/" + communityID + "/requests", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); +} + export function JoinOpenCommunity(communityId) { if (tokenHeader().Authorization == "Bearer " + null) { - console.log("ikke logget på!"); return "Login to join any community"; } @@ -242,7 +259,7 @@ export function JoinOpenCommunity(communityId) { return response; }) .catch((error) => { - console.log(error.response); + console.error(error.response); return error; }); } @@ -273,7 +290,34 @@ export async function LeaveCommunity(communityID) { return response.data; }) .catch((error) => { - console.log(error.data); + console.error(error.data); + return error; + }); +} + +export async function GetUserListings() { + return axios + .get(API_URL + "listing/userListings", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); +} + +export function postNewRating(ratingInfo) { + return axios + .post(API_URL + "rating/save", ratingInfo, { + headers: tokenHeader(), + }) + .then((response) => { + return response; + }) + .catch((error) => { + console.log(error.response); return error; }); } diff --git a/src/views/ChatViews/ChatView.vue b/src/views/ChatViews/ChatView.vue index a8c5eef7738240b1779ff766608661cd8bdbb953..581da8f24f289618486dc3366f4476ca14e1d3c2 100644 --- a/src/views/ChatViews/ChatView.vue +++ b/src/views/ChatViews/ChatView.vue @@ -1,32 +1,40 @@ <template> - <div class="min-h-full"> + <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> - <ChatProfile - v-for="(conversation, i) in conversations" - :conversation="conversation" - :key="i" - @recipient="selectUser" - ></ChatProfile> + <li v-if="conversations"> </li> </ul> </div> - <CurrentChat - v-if="selected" - :recipient="selected" - :key="key" - ></CurrentChat> </div> - </div> + </div> --> </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"; export default { components: { @@ -40,14 +48,14 @@ export default { }; }, computed: { - userID() { - return parseCurrentUser().accountId; - }, key() { return this.selected.userId || "ERROR"; }, }, methods: { + userID() { + return parseCurrentUser().accountId; + }, selectUser(value) { const userid = value; this.conversations.find((conversation) => { @@ -56,21 +64,13 @@ export default { this.selected = this.conversations.find( (conversation) => conversation.recipient.userId == userid ).recipient; - console.log(this.selected); }, }, async created() { - const token = this.$store.state.user.token; - // Get all conversations from api with /chats/users - const response = await fetch(`${process.env.VUE_APP_BASEURL}chats/users`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); // add error handling - const res = await response.json(); - this.conversations = res; + this.conversations = await ChatService.getConversations(this.userID()); + if (this.$route.params.userId !== null) { + this.selectUser(this.$route.params.userId); + } }, }; </script> diff --git a/src/views/CommunityViews/AdminView.vue b/src/views/CommunityViews/AdminView.vue new file mode 100644 index 0000000000000000000000000000000000000000..7912ea2b5fb332dfe0274f0531312b9c6f71ae84 --- /dev/null +++ b/src/views/CommunityViews/AdminView.vue @@ -0,0 +1,61 @@ +<template> + <CommunityHeader :admin="true" class="mb-5" /> + <div + class="flex border-b border-gray-200 dark:border-gray-700 overflow-y-hidden" + > + <button + v-for="(tab, index) in tabs" + :key="tab" + @click="changeTab(index)" + class="h-10 px-4 py-2 -mb-px text-sm text-center bg-transparent border-b-2 sm:text-base whitespace-nowrap focus:outline-none" + :class="[currentTab === index ? activeClasses : inactiveClasses]" + > + {{ tab }} + </button> + </div> + <MemberList + :requests="false" + :buttons="['chat', 'kick']" + v-if="currentTab === 0" + /> + <MemberList + :requests="true" + :buttons="['accept', 'reject']" + v-if="currentTab === 1" + /> + <CommunitySettings v-if="currentTab === 2" /> +</template> + +<script> +import MemberList from "@/components/CommunityComponents/MemberList.vue"; +import CommunitySettings from "@/components/CommunityComponents/CommunitySettings.vue"; +import CommunityHeader from "@/components/CommunityComponents/CommunityHeader.vue"; + +export default { + name: "CommunityAdminView", + components: { + CommunityHeader, + MemberList, + CommunitySettings, + }, + data() { + return { + tabs: ["Medlemsliste", "Medlemsforespørsler", "Felleskap-innstillinger"], + currentTab: 0, //Currently selected tab (default 0 "Medlemsliste") + }; + }, + methods: { + changeTab(index) { + this.currentTab = index; + }, + }, + computed: { + activeClasses() { + return "text-primary-medium border-primary-medium dark:border-primary-light dark:text-primary-light"; + }, + inactiveClasses() { + return "text-gray-700 border-transparent dark:text-white cursor-base hover:border-gray-400"; + }, + }, +}; +</script> diff --git a/src/views/CommunityViews/CommunityRequestView.vue b/src/views/CommunityViews/CommunityRequestView.vue new file mode 100644 index 0000000000000000000000000000000000000000..07ebcb36a8d75082f4a695a38b41cb9757395ad0 --- /dev/null +++ b/src/views/CommunityViews/CommunityRequestView.vue @@ -0,0 +1,13 @@ +<template> + <CommunityRequestForm /> +</template> + +<script> +import CommunityRequestForm from "@/components/CommunityComponents/CommunityRequestForm.vue"; +export default { + name: "CommunityRequestView", + components: { + CommunityRequestForm, + }, +}; +</script> diff --git a/src/views/CommunityViews/CommunityView.vue b/src/views/CommunityViews/CommunityView.vue index b6643a00e413da9d9195fd6357c82876f19d5211..bd7b2ea53207acf14628dc4aff86c896d12c275b 100644 --- a/src/views/CommunityViews/CommunityView.vue +++ b/src/views/CommunityViews/CommunityView.vue @@ -1,27 +1,76 @@ <template> + <!-- My communities, with pagination --> <div v-if="loggedIn"> <div class="flex flex-row p-4 relative"> <div class="text-xl md:text-2xl text-gray-600 font-medium w-full"> Mine grupper </div> <UserAddIcon - class="cursor-pointer max-h-6 max-w-6 float-right grow" + class="cursor-pointer max-h-6 max-w-6 float-right grow text-primary-dark" @click="$router.push('/newCommunity')" alt="Opprett ny gruppe" /> </div> - <CommunityList :communities="myCommunities" :member="true" /> + <CommunityList :communities="visibleMyCommunities" :member="true" /> + + <!-- pagination my communities --> + <div class="flex justify-center"> + <PaginationTemplate + v-bind:items="myCommunities" + v-on:page:update="updatePageMyCommunities" + v-bind:currentPage="currentPageMyCommunities" + v-bind:pageSize="pageSizeMyCommunities" + class="mt-10 mb-5" + /> + </div> </div> + + <!-- Public communities, with search and pagination --> <p class="text-xl md:text-2xl text-gray-600 font-medium w-full p-4"> Offentlige grupper </p> - <CommunityList :communities="publicCommunities" :member="false" /> + <!-- Search field --> + <div class="relative mt-1 mx-2" id="searchComponent"> + <span class="absolute inset-y-0 left-0 flex items-center pl-3"> + <div class="w-5 h-5 text-gray-400"> + <SearchIcon /> + </div> + </span> + + <input + type="text" + id="searchInput" + class="w-full py-3 pl-10 pr-4 text-gray-700 bg-white border rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-primary-medium dark:focus:border-primary-medium focus:outline-none focus:ring" + placeholder="Search" + v-model="search" + @change="searchWritten" + /> + </div> + + <!-- Public communities list, two lists, one for when it's searched and one for pagination --> + <div v-if="showPaginated"> + <CommunityList :communities="visiblePublicCommunities" :member="false" /> + </div> + <div v-if="showSearched"> + <CommunityList :communities="searchPublicCommunities" :member="false" /> + </div> + <!-- pagination Public communities --> + <div class="flex justify-center"> + <PaginationTemplate + v-bind:items="publicCommunities" + v-on:page:update="updatePagePublicCommunities" + v-bind:currentPage="currentPagePublicCommunities" + v-bind:pageSize="pageSizePublicCommunities" + class="mt-10 mb-5" + /> + </div> </template> <script> import CommunityList from "@/components/CommunityComponents/CommunityList.vue"; -import { getMyGroups, getVisibleGroups } from "@/utils/apiutil"; -import { UserAddIcon } from "@heroicons/vue/outline"; +import { UserAddIcon, SearchIcon } from "@heroicons/vue/outline"; +import PaginationTemplate from "@/components/BaseComponents/PaginationTemplate"; +import CommunityService from "@/services/community.service"; export default { name: "HomeView", @@ -30,23 +79,101 @@ export default { loggedIn: false, myCommunities: [], publicCommunities: [], + search: "", + showSearched: false, + showPaginated: true, + + //Variables connected to pagination + currentPagePublicCommunities: 0, + currentPageMyCommunities: 0, + pageSizeMyCommunities: 5, + pageSizePublicCommunities: 10, + visiblePublicCommunities: [], + visibleMyCommunities: [], }; }, components: { CommunityList, UserAddIcon, + PaginationTemplate, + SearchIcon, }, - async created() { - this.publicCommunities = await getVisibleGroups(); - this.loggedIn = this.$store.state.user.token !== null; - if (!this.loggedIn) return; + computed: { + searchPublicCommunities() { + let filteredItems = []; - this.myCommunities = await getMyGroups(); + filteredItems = this.publicCommunities.filter( + (p) => + p.name.toLowerCase().includes(this.search.toLowerCase()) || + p.location.toLowerCase().includes(this.search.toLowerCase()) + ); + + return filteredItems; + }, + }, + methods: { + //Pagination + updatePagePublicCommunities(pageNumber) { + this.currentPagePublicCommunities = pageNumber; + this.updateVisibleCommunities(); + }, + updatePageMyCommunities(pageNumber) { + this.currentPageMyCommunities = pageNumber; + this.updateVisibleCommunities(); + }, + updateVisibleCommunities() { + this.visiblePublicCommunities = this.publicCommunities.slice( + this.currentPagePublicCommunities * this.pageSizePublicCommunities, + this.currentPagePublicCommunities * this.pageSizePublicCommunities + + this.pageSizePublicCommunities + ); + this.visibleMyCommunities = this.myCommunities.slice( + this.currentPageMyCommunities * this.pageSizeMyCommunities, + this.currentPageMyCommunities * this.pageSizeMyCommunities + + this.pageSizeMyCommunities + ); + + // if we have 0 visible communities, go back a page + if ( + this.visiblePublicCommunities.length === 0 && + this.currentPagePublicCommunities > 0 + ) { + this.updatePagePublicCommunities(this.currentPagePublicCommunities - 1); + } + if ( + this.visibleMyCommunities.length === 0 && + this.currentPageMyCommunities > 0 + ) { + this.updatePageMyCommunities(this.currentPageMyCommunities - 1); + } + }, + searchWritten() { + //This method triggers when search input field is changed + this.showPaginated = this.search.length < 1; + this.showSearched = this.search.length > 0; + }, + async load() { + this.publicCommunities = await CommunityService.getAllCommunities(); + this.loggedIn = this.$store.state.user.token !== null; + if (!this.loggedIn) return; + this.myCommunities = await CommunityService.getUserCommunities(); + }, + }, + async mounted() { + await this.load(); + //Double loop not bad :) + for (var i = 0; i < this.publicCommunities.length; i++) { + for (var j = 0; j < this.myCommunities.length; j++) { + if ( + this.publicCommunities[i].communityId === + this.myCommunities[j].communityId + ) { + this.publicCommunities.splice(i, 1); + } + } + } - // Remove all of the user's communities from the public communities arrays - this.publicCommunities = this.publicCommunities.filter( - (val) => !this.myCommunities.includes(val) - ); + this.updateVisibleCommunities(); }, }; </script> diff --git a/src/views/CommunityViews/MemberListView.vue b/src/views/CommunityViews/MemberListView.vue index 9f63b476d52810f37e88b726803ca54b6ccc9266..e7bdcd94166f21e4dc214045bd73f5a1ead7e460 100644 --- a/src/views/CommunityViews/MemberListView.vue +++ b/src/views/CommunityViews/MemberListView.vue @@ -1,12 +1,15 @@ <template> - <MemberList /> + <CommunityHeader :admin="false" class="mb-5 mt-5" /> + <MemberList :buttons="['chat']" /> </template> <script> +import CommunityHeader from "@/components/CommunityComponents/CommunityHeader"; import MemberList from "@/components/CommunityComponents/MemberList.vue"; export default { components: { + CommunityHeader, MemberList, }, }; diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 7537e2d18769293327ccda2e2357aefe9973c58e..f86430c64bb07d2f6e60fe0626ac1f50371d3682 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -1,5 +1,5 @@ <template> - <div /> + <div></div> </template> <script> diff --git a/src/views/UserProfileViews/UserItemsView.vue b/src/views/UserProfileViews/UserItemsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..8ac137d470d2dd6ed213d18d9c778c1da872996b --- /dev/null +++ b/src/views/UserProfileViews/UserItemsView.vue @@ -0,0 +1,15 @@ +<template> + <user-items> </user-items> +</template> + +<script> +import UserItems from "@/components/UserProfileComponents/UserItems.vue"; +export default { + name: "UserItemsView", + components: { + UserItems, + }, +}; +</script> + +<style></style> diff --git a/tailwind.config.js b/tailwind.config.js index 0089d1c11fbc2c51520dc1501abb2dec976d7fa7..65380e429009105b923a55266f6b301464b97a88 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -30,9 +30,21 @@ module.exports = { light: "#653273", dark: "#731050", }, - error: "#E23636", - warn: "#EDB95E", - success: "#82DD55", + error: { + light: "#EF4444", + medium: "#DC2626", + dark: "#B91C1C", + }, + warn: { + light: "#FDE047", + medium: "#FACC15", + dark: "#EAB308", + }, + success: { + light: "#22C55E", + medium: "#16A34A", + dark: "#15803D", + }, }, }, plugins: [require("tw-elements/dist/plugin")], 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 new file mode 100644 index 0000000000000000000000000000000000000000..91a7495f64107bf31184e1dee68e250a959e06de --- /dev/null +++ b/tests/unit/component-tests/base-component-tests/pagination-template.spec.js @@ -0,0 +1,21 @@ +import { mount } from "@vue/test-utils"; +import Pagination from "@/components/BaseComponents/PaginationTemplate.vue"; + +describe("PaginationTemplate", () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(Pagination, { + //passing prop to component + props: { + items: [], + currentPage: 0, + pageSize: 4, + }, + }); + }); + + 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 2e1f24cb2c4a254b913019f92677f207822ad056..cecad6a17a15604f5bcf46480b439199187ab9c9 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,62 +2,16 @@ exports[`CommunityHeader component renders correctly 1`] = ` <div - class="flex items-center justify-between mx-4" + data-v-app="" > - <router-link - class="flex-1 min-w-0" - to="/community/1" + + <!-- TODO PUT A LOADER HERE --> + <div + adminstatus="true" + community="[object Object]" > - <h2 - class="text-xl md:text-2xl text-gray-600 font-medium w-full sm:truncate" - > - String - </h2> - <div - class="mt-1 flex flex-col sm:flex-row sm:flex-wrap sm:mt-0 sm:space-x-6" - > - <div - class="mt-2 flex items-center text-sm text-gray-500" - > - <svg - aria-hidden="true" - class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400" - fill="currentColor" - viewBox="0 0 20 20" - xmlns="http://www.w3.org/2000/svg" - > - <path - clip-rule="evenodd" - d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" - fill-rule="evenodd" - /> - </svg> - String - </div> - </div> - </router-link> - <div> - <!-- If the user is not a member in the community, this button will show --> - <!--v-if--> - <!-- If the user is member of the community, this hamburger menu will show --> - <div> - <svg - class="w-9 h-9 cursor-pointer" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M4 6h16M4 12h16M4 18h16" - stroke-linecap="round" - stroke-linejoin="round" - stroke-width="2" - /> - </svg> - <!--v-if--> - <!-- class="absolute" --> - </div> + LASTER... </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 new file mode 100644 index 0000000000000000000000000000000000000000..0d4999e9ea79a37b5fd6bbae85350af3f93a2a91 --- /dev/null +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/item-card.spec.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ItemCard component renders correctly 1`] = ` +<div + class="mt-5" +> + <div + class="w-4/5 rounded bg-gray-200 h-full overflow-hidden display:inline-block correct-size" + > + <img + alt="Item image" + class="w-full" + src="String" + /> + <div + class="p-1 m-1" + > + <p + class="text-gray-700 text-xs font-bold" + id="adress" + > + String + </p> + <p + class="font-bold text-sm" + id="title" + > + String + </p> + <p + class="text-gray-700 text-xs" + id="price" + > + 0 kr + </p> + </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 new file mode 100644 index 0000000000000000000000000000000000000000..a475c4e78dfe175dbf46da2eb2492f7c59588869 --- /dev/null +++ b/tests/unit/component-tests/community-component-tests/__snapshots__/new-item-form.spec.js.snap @@ -0,0 +1,201 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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" +> + <!-- Component heading --> + <h3 + class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 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 --> + <div + class="mb-6" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="selectCategoryLabel" + > + 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" + > + <option + class="text-gray-400" + disabled="" + value="" + > + Velg en kategori + </option> + + <option + class="text-gray-900 text-sm" + > + Hage + </option> + <option + class="text-gray-900 text-sm" + > + Kjøkken + </option> + <option + class="text-gray-900 text-sm" + > + Musikk + </option> + <option + class="text-gray-900 text-sm" + > + Annet + </option> + + </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> + <div + class="overflow-auto w-full h-32 mt-2 text-base list-none bg-white rounded divide-y divide-gray-100 dark:bg-gray-700" + > + <ul + aria-labelledby="dropdownDefault" + class="py-1" + > + <li> + + + </li> + </ul> + </div> + <label + class="text-error 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 + </label> + <input + accept="image/png, image/jpeg" + multiple="" + style="display: none;" + type="file" + /> + <button + class="block text-white bg-primary-medium hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-light font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-medium dark:hover:bg-primary-dark dark:focus:ring-primary-light" + > + Velg bilde + </button> + + + </div> + <!-- Save item button --> + <div + class="float-right" + > + <button + class="block text-white bg-primary-medium hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-light font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-medium dark:hover:bg-primary-dark dark:focus:ring-primary-light" + id="saveButton" + > + Lagre + </button> + </div> +</div> +`; diff --git a/tests/unit/component-tests/community-component-tests/community-hamburger.spec.js b/tests/unit/component-tests/community-component-tests/community-hamburger.spec.js index a6834e8680d1d61c29962436e568165fb8c2b937..4f610d50a19491b24345c83325ce55a5c38602a1 100644 --- a/tests/unit/component-tests/community-component-tests/community-hamburger.spec.js +++ b/tests/unit/component-tests/community-component-tests/community-hamburger.spec.js @@ -1,13 +1,30 @@ import { shallowMount } from "@vue/test-utils"; import CommunityHamburger from "@/components/CommunityComponents/CommunityHamburger.vue"; +import { route, router, $route, $router } from "../../mock-router"; +import { store, $store } from "../../mock-store"; describe("CommunityHamburger elements rendering", () => { - it("renders all li fields", () => { - const wrapper = shallowMount(CommunityHamburger); + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(CommunityHamburger, { + global: { + mocks: { + store, + $store, + route, + router, + $route, + $router, + }, + }, + }); + }); + it("renders all li fields", () => { expect(wrapper.find("#newItem").text()).toMatch("Opprett Utleie"); expect(wrapper.find("#getMembers").text()).toMatch("Se Medlemmer"); - expect(wrapper.find("#adminGroup").text()).toMatch("Administrer Gruppe"); + //expect(wrapper.find("#adminGroup").text()).toMatch("Administrer Gruppe"); expect(wrapper.find("#leaveGroup").text()).toMatch("Forlat Gruppe"); }); }); 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 new file mode 100644 index 0000000000000000000000000000000000000000..3f8ffafa68bf5dd606afdeda9214b57871ff174f --- /dev/null +++ b/tests/unit/component-tests/renting-compnents-tests/new-rent.spec.js @@ -0,0 +1,46 @@ +import { mount } from "@vue/test-utils"; +import NewRent from "@/components/RentingComponents/NewRent.vue"; + +describe("Confirm and send a rent request", () => { + let wrapper; + const route = { + params: { + id: 1, + }, + }; + const router = { + push: jest.fn(), + }; + beforeEach(() => { + wrapper = mount(NewRent, { + props: { + newRentBox: { + title: "Telt", + listingID: 1, + fromTime: "2022-09-19", + toTime: "2022-09-23", + price: 200, + renterId: 1, + isAccepted: false, + }, + }, + global: { + mocks: { + $route: route, + $router: router, + }, + }, + }); + }); + + it("Is instansiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("Check if 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"); + }); +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..2aa391a34e4b72f291cf5fe3bcb6173ea20e7e06 --- /dev/null +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/new-password-form.spec.js.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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" +> + <h3 + class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" + > + Endre passord + </h3> + <div + class="" + id="firstPasswordField" + > + <label + class="block text-sm text-gray-800 dark:text-gray-200" + for="password" + > + 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" + > + <div + class="flex items-center justify-between" + > + <label + class="block text-sm text-gray-800 dark:text-gray-200" + for="rePassword" + > + 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 --> + + + </div> + <div + class="mt-6" + id="buttonsField" + > + <button + class="block text-white bg-primary-medium hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-light font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-medium dark:hover:bg-primary-dark dark:focus:ring-primary-light float-right" + > + Sett ny passord + </button> + </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 new file mode 100644 index 0000000000000000000000000000000000000000..49cbc5a2e696228709e56dc12f1925b640d0e377 --- /dev/null +++ b/tests/unit/component-tests/user-component-tests/__snapshots__/reset-password-form.spec.js.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResetPasswordForm 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" +> + <h3 + class="text-xl font-medium text-center text-gray-600 dark:text-gray-200 mt-4 mb-8" + > + Glemt passordet ditt? + </h3> + <div + class="m-6" + id="emailField" + > + <div + class="mb-6" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + for="email" + > + E-post + </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" + id="email" + placeholder="eksempel@eksempel.no" + required="" + type="email" + /> + <!-- error message --> + + + </div> + <button + class="block text-white bg-primary-medium hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-light font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-medium dark:hover:bg-primary-dark dark:focus:ring-primary-light float-right" + > + Tilbakestill passord + </button> + </div> +</div> +`;