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..56bef37c1e9db83f78f09316b7de6ed2324523e4 100644 --- a/src/components/BaseComponents/NavBar.vue +++ b/src/components/BaseComponents/NavBar.vue @@ -53,9 +53,9 @@ export default { async loadProfile() { if (this.$store.state.user.token !== null) { let user = parseUserFromToken(this.$store.state.user.token); - console.log(user); + //console.log(user); let id = user.accountId; - console.log(id); + //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/ChatComponents/ChatMessage.vue b/src/components/ChatComponents/ChatMessage.vue index c0a900feca080cea0d326d8bbadcb75df6197495..555460a1290deaa41476582c15e71af3dd0f7642 100644 --- a/src/components/ChatComponents/ChatMessage.vue +++ b/src/components/ChatComponents/ChatMessage.vue @@ -51,7 +51,7 @@ export default { }, methods: { color() { - console.log(this.userID); + //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 8d5c5bf09877fc57a18bab3b3ca4f73573f0cb4f..8c309a4aed96ad91b05fc8af0ffc3b1b50109300 100644 --- a/src/components/ChatComponents/ChatProfile.vue +++ b/src/components/ChatComponents/ChatProfile.vue @@ -52,8 +52,13 @@ export default { }, methods: { selectUser() { +<<<<<<< HEAD console.log(this.conversation?.recipient.userId); this.$emit("recipient", this.conversation?.recipient.userId); +======= + //console.log(this.conversation.recipient.userId); + this.$emit("recipient", this.conversation.recipient.userId); +>>>>>>> 3fd875332a013afe5b731aa96a2e2c2ce2351891 }, }, created() { diff --git a/src/components/CommunityComponents/CommunityHamburger.vue b/src/components/CommunityComponents/CommunityHamburger.vue index 34c82b645697dce95fc6d42ce3e6ea75db622323..f9d4b41f9ca426b890e492b28c169c080954d804 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("/"); }, }, + mounted() { + this.admin = 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..7fa4ed118326044995dcf63b771b46cc0ba6ea30 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 + ); + let members = await CommunityService.getCommunityMembers( + this.$route.params.communityID + ); + for (let mem in members) { + if (mem === 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..c3bf82cba6a7ea5ecaf82b2a0f33a2fa7534132a 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 = []; @@ -93,12 +119,19 @@ export default { 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..f260d3b475014d6c247a12b73543f8364a9801ba 100644 --- a/src/components/CommunityComponents/CommunityListItem.vue +++ b/src/components/CommunityComponents/CommunityListItem.vue @@ -8,12 +8,20 @@ <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 +104,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..d0b58ae3aab0a895f1444060ba7b0d36653ce313 --- /dev/null +++ b/src/components/CommunityComponents/CommunityRequestForm.vue @@ -0,0 +1,109 @@ +<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; + console.log("Dette er community id =" + this.communityID) + this.community = await GetCommunity(this.communityID); + console.log(this.community) + } + }, + 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..0dd0328534c890eb348d4e02f6d16fb7da791762 --- /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 AdminService 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"); + // AdminService.deleteCommunity(this.$route.params.communityID); + }, + }, +}; +</script> diff --git a/src/components/CommunityComponents/MemberList.vue b/src/components/CommunityComponents/MemberList.vue index 56c8bbe02a6683c4b5dc9bb2df22f1b3ad9809ec..bf2d75aeb736678ba47b4ba1106107e7aa44e899 100644 --- a/src/components/CommunityComponents/MemberList.vue +++ b/src/components/CommunityComponents/MemberList.vue @@ -1,50 +1,46 @@ <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 - ); - }, - getCommunity: async function () { - this.community = await GetCommunity( - this.$router.currentRoute.value.params.id - ); - }, + async load() {}, }, - beforeMount() { - this.getAllMembersOfCommunity(); - this.getCommunity(); + async created() { + this.loading = true; + if(this.requests){ + this.members = await GetMemberRequestsOfCommunity( + this.$route.params.communityID + ); + } else { + this.members = await CommunityService.getCommunityMembers( + this.$route.params.communityID + );} + this.loading = false; }, }; </script> diff --git a/src/components/FormComponents/LoginForm.vue b/src/components/FormComponents/LoginForm.vue index bbb849a1008414ed2abc3899b7216c9a80d7d562..03c6482b6272c6f1ffaa3461906e1260044c0ea1 100644 --- a/src/components/FormComponents/LoginForm.vue +++ b/src/components/FormComponents/LoginForm.vue @@ -137,7 +137,7 @@ export default { this.v$.user.$touch(); if (this.v$.user.$invalid) { - console.log("Ugyldig, avslutter..."); + //console.log("Ugyldig, avslutter..."); return; } @@ -154,7 +154,7 @@ export default { this.$store.commit("saveToken", loginResponse.token); await this.$router.push("/"); } else { - console.log("Something went wrong"); + //console.log("Something went wrong"); } }, }, diff --git a/src/components/FormComponents/NewPasswordForm.vue b/src/components/FormComponents/NewPasswordForm.vue index d13c08349360250ff4f0cf96b5f3faab00d7a841..fbe662109e67a2ec5fbebac903c60e2131115234 100644 --- a/src/components/FormComponents/NewPasswordForm.vue +++ b/src/components/FormComponents/NewPasswordForm.vue @@ -130,25 +130,34 @@ export default { this.v$.user.$touch(); if (this.v$.user.$invalid) { - console.log("Invalid, exiting..."); + //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) { + if (newPasswordResponse != null) { console.log("New password set"); + this.$store.commit("saveToken", newPasswordResponse); await this.$router.push("/"); - } else if (newPasswordResponse.newPasswordSet === false) { + } else { console.log("Couldn't set new password"); + } + + /* + + if (newPasswordResponse.newPasswordSet === true) { + //console.log("New password set"); + await this.$router.push("/"); + } else if (newPasswordResponse.newPasswordSet === false) { + //console.log("Couldn't set new password"); } else { - console.log("Something went wrong"); + //console.log("Something went wrong"); } + + */ }, validate() { this.$refs.form.validate(); diff --git a/src/components/FormComponents/ResetPasswordForm.vue b/src/components/FormComponents/ResetPasswordForm.vue index a4c2d80e1533e2c7be82804041bd49208b4ab6be..9f5457d9569dcd1c4d0f8f0a1b1eda7d30cfb3b3 100644 --- a/src/components/FormComponents/ResetPasswordForm.vue +++ b/src/components/FormComponents/ResetPasswordForm.vue @@ -83,7 +83,7 @@ export default { this.v$.email.$touch(); if (this.v$.email.$invalid) { - console.log("Ugyldig, avslutter..."); + //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..d0ff2bde33b2ad7da50e1fe7ede41171c38cdbf2 100644 --- a/src/components/ItemComponents/NewItemForm.vue +++ b/src/components/ItemComponents/NewItemForm.vue @@ -311,36 +311,36 @@ export default { }, methods: { checkValidation: function () { - console.log("sjekker validering"); + //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..."); + //console.log("Invalid, avslutter..."); return false; } - console.log("validert!"); + //console.log("validert!"); return true; }, async saveClicked() { - console.log("Attempting to save item"); + //console.log("Attempting to save item"); if (this.checkValidation()) { - console.log("validert, videre..."); + //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); + //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, @@ -352,11 +352,11 @@ export default { communityIDs: this.item.selectedGroups, }; - console.log(itemInfo); + //console.log(itemInfo); - const postRequest = await postNewItem(itemInfo); + /* const postRequest = */ await postNewItem(itemInfo); - console.log("posted: " + postRequest); + //console.log("posted: " + postRequest); this.$router.push("/"); } @@ -368,7 +368,7 @@ export default { }, addImage: function (event) { - console.log(event.target.files); + //console.log(event.target.files); this.item.images.push(URL.createObjectURL(event.target.files[0])); }, @@ -379,7 +379,7 @@ export default { onChangeGroup: function (e) { this.selectedGroupId = e.target.value; let alreadyInGroupList = false; - console.log("selected clicked"); + //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/TimepickerComponents/DatepickerRange/MonthSelector.vue b/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue index a16a3b840b9efd770d970b0f7313f5e1d0198ec9..bfcbaef6757e67706693a0680345aaf40f36b68a 100644 --- a/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue +++ b/src/components/TimepickerComponents/DatepickerRange/MonthSelector.vue @@ -81,7 +81,7 @@ export default { this.$emit("back", this.type); }, forward() { - console.log(this.type); + //console.log(this.type); this.$emit("forward", this.type); }, }, diff --git a/src/components/UserProfileComponents/UserListItemCard.vue b/src/components/UserProfileComponents/UserListItemCard.vue index a69c38b9931e1eb3c08d4c21290a33a48f91a5de..b60ef198a9c76157694f67bfe694bfbc24ee2f70 100644 --- a/src/components/UserProfileComponents/UserListItemCard.vue +++ b/src/components/UserProfileComponents/UserListItemCard.vue @@ -2,67 +2,131 @@ <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 + ); + }, + 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/router/index.js b/src/router/index.js index f9bc42df2a48bfc2c41262b4bcefcb4cebbeec86..2f0d2e5ac5f619305ebf5431e4c0267a20dbe201 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -65,7 +65,7 @@ const routes = [ beforeEnter: guardRoute, }, { - path: "/community/:id/memberlist", + path: "/community/:communityID/memberlist", name: "memberlist", component: () => import("../views/CommunityViews/MemberListView.vue"), beforeEnter: guardRoute, @@ -94,16 +94,28 @@ 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, }, ]; 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..091fd94aa59808dd7919b3afc564cfc866312128 --- /dev/null +++ b/src/services/community-admin.service.js @@ -0,0 +1,69 @@ +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", + { params: { userId: userID } } + ); + } + + //TODO + async rejectUserFromCommunity(communityID, userID) { + return await axios.patch( + API_URL + "communitites/" + communityID + "/requests/reject", + { + 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, + { + 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..d2bf0954527324357777de183dda46891b321418 --- /dev/null +++ b/src/services/community.service.js @@ -0,0 +1,45 @@ +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 getPublicCommunities() { + return await axios + .get(API_URL + "communities") + .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..d7ae98a92cec8a73dd0c5194260566bd73cdde82 --- /dev/null +++ b/src/services/user.service.js @@ -0,0 +1,32 @@ +// 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") + .then((res) => { + return res.data; + }) + .catch((err) => console.log(err)); + } + + async getUserRatingAverage(userId) { + return await axios + .get(API_URL + "rating/" + userId + "/average") + .then((res) => { + return res.data; + }) + .catch((err) => console.log(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..b9adba0dd168d0e183dc6b151b95fff38d806ef4 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -17,18 +17,18 @@ const ws = (function () { const onMessageReceived = (payload) => { const data = JSON.parse(payload.body); - console.log("New message!"); + //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); + //console.log("Received message: " + payload); }; const onConnected = () => { - console.log("Websocket Connected"); + //console.log("Websocket Connected"); stompClient.subscribe( "/user/" + parseCurrentUser().accountId + "/queue/messages", onMessageReceived @@ -56,8 +56,8 @@ const ws = (function () { throw new Error("No handler for event: " + event); } }, - sendMessage: ({ sender, recipient, status }) => { - if (status) console.log(status); + sendMessage: ({ sender, recipient /* , status */ }) => { + //if (status) console.log(status); stompClient.send( "/app/chat", {}, diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index a49864af94949186bb90b327a95ad74cc4dc9d37..7193b8a6cf707dbfec4527ae6bd94c060272c920 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -23,7 +23,7 @@ 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, }) @@ -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.log(error); + }); + return res.data; } export function postNewItem(itemInfo) { @@ -101,7 +108,7 @@ export function postNewItem(itemInfo) { headers: tokenHeader(), }) .then((response) => { - console.log("poster: " + response.data); + //console.log("poster: " + response.data); return response; }) .catch((error) => { @@ -127,7 +134,7 @@ export function postNewRent(rentInfo) { headers: tokenHeader(), }) .then((response) => { - console.log("poster: " + response.data); + //console.log("poster: " + response.data); return response; }) .catch((error) => { @@ -190,7 +197,7 @@ export async function getItemPictures(itemid) { } export async function GetCommunity(communityID) { - return axios + return await axios .get(API_URL + "community/" + communityID, { headers: tokenHeader(), }) @@ -215,6 +222,8 @@ export async function GetListingsInCommunity(communityID) { }); } + + export async function GetMembersOfCommunity(communityID) { return axios .get(API_URL + "community/" + communityID + "/members", { @@ -228,9 +237,22 @@ 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å!"); + //console.log("ikke logget på!"); return "Login to join any community"; } diff --git a/src/views/ChatViews/ChatView.vue b/src/views/ChatViews/ChatView.vue index a8c5eef7738240b1779ff766608661cd8bdbb953..b260cfd92c6b9d1e5862ae56f27c977d3c68b642 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,14 @@ export default { this.selected = this.conversations.find( (conversation) => conversation.recipient.userId == userid ).recipient; - console.log(this.selected); + //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..4c7fa00a00973f9b2818ad15dc6a7e7256dad19b --- /dev/null +++ b/src/views/CommunityViews/AdminView.vue @@ -0,0 +1,53 @@ +<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..8755c39e632108ccca0f9ed1ce84f5db986e8105 100644 --- a/src/views/CommunityViews/CommunityView.vue +++ b/src/views/CommunityViews/CommunityView.vue @@ -42,11 +42,19 @@ export default { if (!this.loggedIn) return; this.myCommunities = await getMyGroups(); - - // Remove all of the user's communities from the public communities arrays - this.publicCommunities = this.publicCommunities.filter( - (val) => !this.myCommunities.includes(val) - ); + }, + mounted() { + // Double loop is bad; find a better way to do this + 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); + } + } + } }, }; </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/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/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..8a7211fcc06388357a4646aa8219cb285eeffcf6 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,10 +1,36 @@ import { shallowMount } from "@vue/test-utils"; import CommunityHamburger from "@/components/CommunityComponents/CommunityHamburger.vue"; +import { route, router, $route, $router } from "../../mock-router"; describe("CommunityHamburger elements rendering", () => { - it("renders all li fields", () => { - const wrapper = shallowMount(CommunityHamburger); + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(CommunityHamburger, { + //passing prop to component + props: { + adminStatus: true, + community: { + communityId: 1, + name: "String", + description: "String", + visibility: 0, + location: "String", + picture: "String", + }, + }, + global: { + mocks: { + 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"); 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> +`;