diff --git a/src/components/ItemComponents/EditItemForm.vue b/src/components/ItemComponents/EditItemForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..2b72e2a4e4cbcb9832e5ecb104fe9b869931cc23 --- /dev/null +++ b/src/components/ItemComponents/EditItemForm.vue @@ -0,0 +1,454 @@ +<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 --> + <h3 class="text-xl font-medium text-center text-primary-light mt-4 mb-8"> + Rediger gjenstand + </h3> + + <!-- Title --> + <div class="mb-6" :class="{ error: v$.updatedItem.title.$errors.length }"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="titleLabel" + >Tittel</label + > + <input + type="text" + id="title" + class="block w-full px-4 py-2 mt-2 text-gray-700 placeholder-gray-500 bg-white border rounded-md dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 focus:border-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + v-model="v$.updatedItem.title.$model" + required + /> + + <!-- error message for title--> + <div + class="text-error-medium" + v-for="(error, index) of v$.updatedItem.title.$errors" + :key="index" + > + <div class="text-error-medium text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- 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 + v-model="v$.updatedItem.selectedCategory.$model" + id="categories" + 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" + > + <option class="text-gray-400" value="" disabled> + Velg en kategori + </option> + <option + :selected="category == updatedItem.selectedCategory" + v-for="category in categories" + :key="category" + class="text-gray-900 text-sm" + > + {{ category }} + </option> + </select> + + <!-- error message for select box --> + <div + class="text-error-medium" + v-for="(error, index) of v$.updatedItem.selectedCategory.$errors" + :key="index" + > + <div class="text-error-medium text-sm"> + {{ error.$message }} + </div> + </div> + </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 class="py-1" aria-labelledby="dropdownDefault"> + <li> + <div + class="form-check" + v-for="community in communities" + :key="community" + > + <input + class="form-check-input appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white checked:bg-primary-medium focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer" + type="checkbox" + :checked="isInSelectedCommunity(community.communityId)" + :value="community.communityId" + @change="onChangeCommunity($event)" + /> + <label class="form-check-label inline-block text-gray-800"> + {{ community.name }} + </label> + </div> + </li> + </ul> + </div> + <label class="text-error-medium text-sm block">{{ + communityErrorMessage + }}</label> + </div> + + <!-- price --> + <div + class="mb-6 mt-4" + :class="{ error: v$.updatedItem.price.$errors.length }" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="priceLabel" + >Pris</label + > + <input + type="number" + v-model="v$.updatedItem.price.$model" + id="price" + 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 + /> + + <!-- error message for price --> + <div + class="text-error" + v-for="(error, index) of v$.updatedItem.price.$errors" + :key="index" + > + <div class="text-error-medium text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- Description --> + <div + class="mb-6" + :class="{ error: v$.updatedItem.description.$errors.length }" + > + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="descriptionLabel" + >Beskrivelse</label + > + <textarea + id="description" + rows="4" + v-model="v$.updatedItem.description.$model" + class="block w-full px-4 py-2 mt-2 text-gray-700 placeholder-gray-500 bg-white border rounded-md dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 focus:border-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 description --> + <div + class="text-error" + v-for="(error, index) of v$.updatedItem.description.$errors" + :key="index" + > + <div class="text-error-medium text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- Address --> + <div class="mb-6" :class="{ error: v$.updatedItem.address.$errors.length }"> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300" + id="addressLabel" + >Adresse</label + > + <input + type="text" + class="block w-full px-4 py-2 mt-2 text-gray-700 placeholder-gray-500 bg-white border rounded-md dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 focus:border-primary-light dark:focus:border-primary-light focus:ring-opacity-40 focus:outline-none focus:ring focus:ring-primary-light" + v-model="v$.updatedItem.address.$model" + id="adress" + required + /> + + <!-- error message for address--> + <div + class="text-error" + v-for="(error, index) of v$.updatedItem.address.$errors" + :key="index" + > + <div class="text-error-medium text-sm"> + {{ error.$message }} + </div> + </div> + </div> + + <!-- Images --> + <div> + <label + class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400" + id="imageLabel" + > + Bilder + </label> + + <input + type="file" + ref="file" + style="display: none" + @change="addImage" + multiple + accept="image/png, image/jpeg" + /> + + <Button :text="'Velg bilde'" @click="$refs.file.click()" /> + + <div v-for="image in updatedItem.images" :key="image" class="m-2"> + <img :src="image" class="w-2/5 inline" alt="Bilde av gjenstanden" /> + </div> + </div> + + <!-- Save item button --> + <div class="float-right"> + <Button :text="'Lagre'" @click="saveClicked" id="saveButton" /> + </div> + </div> +</template> + +<script> +import useVuelidate from "@vuelidate/core"; +import Button from "@/components/BaseComponents/ColoredButton"; +import ListingService from "@/services/listing.service"; +import CommunityService from "@/services/community.service"; +import { parseCurrentUser } from "@/utils/token-utils"; + +import { + required, + helpers, + maxLength, + between, + minLength, +} from "@vuelidate/validators"; + +export default { + name: "EditNewItem", + + components: { + Button, + }, + + setup() { + return { v$: useVuelidate() }; + }, + + validations() { + return { + updatedItem: { + title: { + required: helpers.withMessage( + () => "Tittelen kan ikke være tom", + required + ), + max: helpers.withMessage( + () => `Tittelen kan inneholde max 50 tegn`, + maxLength(50) + ), + }, + description: { + required: helpers.withMessage( + () => "Beskrivelsen kan ikke være tom", + required + ), + max: helpers.withMessage( + () => `Beskrivelsen kan inneholde max 200 tegn`, + maxLength(200) + ), + min: helpers.withMessage( + () => `Beskrivelsen kan ikke være tom`, + minLength(0) + ), + }, + price: { + required, + between: helpers.withMessage( + () => `Leieprisen kan ikke være større enn 25000`, + between(0, 25000) + ), + }, + selectedCategory: { + required: helpers.withMessage(() => `Velg en kategori`, required), + }, + address: { + required: helpers.withMessage( + () => "Addressen kan ikke være tom", + required + ), + max: helpers.withMessage( + () => `Addressen kan inneholde max 50 tegn`, + maxLength(50) + ), + }, + }, + }; + }, + + data() { + return { + updatedItem: { + title: "", + description: "", + address: "", + price: "", + category: "", + selectedCategory: "", + selectedCategories: [], + images: [], + userId: -1, + selectedCommunityId: -1, + selectedCommunities: [], + }, + categories: [ + "Antikviteter og kunst", + "Dyr og utstyr", + "Elektronikk og hvitevarer", + "Foreldre og barn", + "Fritid, hobby og underholdning", + "Hage, oppussing og hus", + "Klær, kosmetikk og tilbehør", + "Møbler og interiør", + "Næringsvirksomhet", + "Sport og friluftsliv", + "Utstyr til bil, båt og MC", + ], + initialItem: {}, + communities: [], + communityErrorMessage: "", + images: [], + }; + }, + + methods: { + checkValidation() { + console.log(this.updatedItem); + this.v$.updatedItem.$touch(); + if ( + this.v$.updatedItem.$invalid || + this.updatedItem.selectedCommunities.length === 0 + ) { + if (this.updatedItem.selectedCommunities.length === 0) { + this.communityErrorMessage = "Velg gruppe/grupper"; + } + return false; + } + return true; + }, + + async saveClicked() { + if (this.checkValidation()) { + let itemInfo = { + listingID: parseInt(this.initialItem.listingID), + title: this.updatedItem.title, + description: this.updatedItem.description, + pricePerDay: this.updatedItem.price, + address: this.updatedItem.address, + userID: this.updatedItem.userId, + categoryNames: this.updatedItem.selectedCategories, + communityIDs: this.updatedItem.selectedCommunities, + }; + await ListingService.putItem(itemInfo); + this.$router.push("/itempage/" + this.initialItem.listingID); + } + }, + + addImage(event) { + this.updatedItem.images.push(URL.createObjectURL(event.target.files[0])); + }, + + onChangeCommunity(e) { + console.log(e); + this.selectedCommunityId = e.target.value; + let alreadyInGroupList = false; + + for (let i = 0; i <= this.updatedItem.selectedCommunities.length; i++) { + if ( + this.selectedCommunityId == this.updatedItem.selectedCommunities[i] + ) { + const index = this.updatedItem.selectedCommunities.indexOf( + this.selectedCommunityId + ); + if (index > -1) { + this.item.selectedCommunities.splice(index, 1); + } + alreadyInGroupList = true; + } + } + + if (!alreadyInGroupList) { + this.updatedItem.selectedCommunities.push(this.selectedCommunityId); + this.communityErrorMessage = ""; + } + }, + + isInSelectedCommunity(id) { + for (let i in this.updatedItem.selectedCommunities) { + if (this.updatedItem.selectedCommunities[i] == id) { + return true; + } + } + return false; + }, + }, + + async beforeMount() { + let itemID = await this.$router.currentRoute.value.params.id; + let item = await ListingService.getItem(itemID); + + console.log(item); + + // Check if user is the owner of the item + let userID = await parseCurrentUser().userId; + if (item.userID == userID) { + this.$router.push(this.$router.options.history.state.back); + } + + this.initialItem = item; + this.communities = await CommunityService.getUserCommunities(); + this.images = ListingService.getItemPictures(itemID); + + let initialCategories = []; + for (let i in this.initialItem.categoryNames) { + initialCategories.push(this.initialItem.categoryNames[i]); + } + let selectedCategory = + initialCategories.length > 0 ? initialCategories[0] : ""; + + let initialCommunities = []; + for (let i in this.initialItem.communityIDs) { + initialCommunities.push(this.initialItem.communityIDs[i]); + } + + this.updatedItem = { + title: this.initialItem.title, + description: this.initialItem.description, + address: this.initialItem.address, + price: this.initialItem.pricePerDay, + selectedCategories: initialCategories, + selectedCategory: selectedCategory, + images: [], + userId: this.initialItem.userID, + selectedCommunityId: 0, + selectedCommunities: initialCommunities, + }; + console.log(this.updatedItem); + }, +}; +</script> diff --git a/src/router/index.js b/src/router/index.js index 5fbb09f51e59dee764bcc21d650ddcc56930bed3..2fcae0a5d5ffb4431d271e4a610fbdc2bbc9cdd3 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -132,6 +132,13 @@ const routes = [ path: "/user/userItems", name: "UserItems", component: () => import("../views/UserProfileViews/UserItemsView.vue"), + beforeEnter: guardRoute, + }, + { + path: "/user/userItems/:id/edit", + name: "UserItems", + component: () => import("../views/ItemViews/EditItemView.vue"), + beforeEnter: guardRoute, }, ]; diff --git a/src/services/listing.service.js b/src/services/listing.service.js new file mode 100644 index 0000000000000000000000000000000000000000..d0bfb6ca24e2494ef85e524a3f9bd830a07bc57c --- /dev/null +++ b/src/services/listing.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 ListingService { + async putItem(itemInfo) { + return await axios + .put(API_URL + "listing/change", itemInfo, { + headers: tokenHeader(), + }) + .then((res) => { + return res.data; + }) + .catch((err) => console.error(err)); + } + + async getItem(itemid) { + return await axios + .get(API_URL + "listing/" + itemid, { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } + + async getItemPictures(itemid) { + return await axios + .get(API_URL + "listing/" + itemid + "/pictures", { + headers: tokenHeader(), + }) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); + } +} + +export default new ListingService(); diff --git a/src/views/ItemViews/EditItemView.vue b/src/views/ItemViews/EditItemView.vue new file mode 100644 index 0000000000000000000000000000000000000000..763ecdafeb6145a16e7518c4630cf1ebdb79e5d0 --- /dev/null +++ b/src/views/ItemViews/EditItemView.vue @@ -0,0 +1,16 @@ +<template> + <div class="h-screen grid md:mt-8"> + <edit-item-form :initialItem="initialItem" :communities="communities" /> + </div> +</template> + +<script> +import EditItemForm from "@/components/ItemComponents/EditItemForm.vue"; + +export default { + name: "EditItemView", + components: { + EditItemForm, + }, +}; +</script>