Request wine front & backend talk nicer togheter.
- requestWineController validates wine object and returns helpfull error repsonse if anything is missing. - requestWine uses new endpoint and calls api from within itself. - Linting.
This commit is contained in:
		| @@ -1,10 +1,11 @@ | |||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const RequestRepository = require(path.join(__dirname, "../request")); | const requestRepository = require(path.join(__dirname, "../request")); | ||||||
|  |  | ||||||
| function addRequest(req, res) { | function addRequest(req, res) { | ||||||
|   const { wine } = req.body; |   const { wine } = req.body; | ||||||
|  |  | ||||||
|   return RequestRepository.addNew(wine) |   return verifyWineValues(wine) | ||||||
|  |     .then(_ => requestRepository.addNew(wine)) | ||||||
|     .then(wine => |     .then(wine => | ||||||
|       res.json({ |       res.json({ | ||||||
|         message: "Successfully added new request", |         message: "Successfully added new request", | ||||||
| @@ -23,7 +24,8 @@ function addRequest(req, res) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function allRequests(req, res) { | function allRequests(req, res) { | ||||||
|   return RequestRepository.getAll() |   return requestRepository | ||||||
|  |     .getAll() | ||||||
|     .then(wines => |     .then(wines => | ||||||
|       res.json({ |       res.json({ | ||||||
|         wines: wines, |         wines: wines, | ||||||
| @@ -31,12 +33,10 @@ function allRequests(req, res) { | |||||||
|       }) |       }) | ||||||
|     ) |     ) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.log("error in getAllRequests:", error); |       const { message, statusCode } = error; | ||||||
|  |       return res.status(statusCode || 500).json({ | ||||||
|       const message = "Unable to fetch all requested wines."; |  | ||||||
|       return res.status(500).json({ |  | ||||||
|         success: false, |         success: false, | ||||||
|         message: message |         message: message || "Unable to fetch all requested wines." | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| @@ -44,7 +44,8 @@ function allRequests(req, res) { | |||||||
| function deleteRequest(req, res) { | function deleteRequest(req, res) { | ||||||
|   const { id } = req.params; |   const { id } = req.params; | ||||||
|  |  | ||||||
|   return RequestRepository.deleteById(id) |   return requestRepository | ||||||
|  |     .deleteById(id) | ||||||
|     .then(_ => |     .then(_ => | ||||||
|       res.json({ |       res.json({ | ||||||
|         message: `Slettet vin med id: ${id}`, |         message: `Slettet vin med id: ${id}`, | ||||||
| @@ -61,6 +62,41 @@ function deleteRequest(req, res) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function verifyWineValues(wine) { | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     if (wine == undefined) { | ||||||
|  |       reject({ | ||||||
|  |         message: "No wine object found in request body.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (wine.id == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value id.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } else if (wine.name == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value name.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } else if (wine.vivinoLink == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value vivinoLink.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } else if (wine.image == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value image.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     resolve(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   addRequest, |   addRequest, | ||||||
|   allRequests, |   allRequests, | ||||||
|   | |||||||
| @@ -1,10 +1,6 @@ | |||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const RequestedWine = require(path.join( | const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine")); | ||||||
|   __dirname, "/schemas/RequestedWine" | const Wine = require(path.join(__dirname, "/schemas/Wine")); | ||||||
| )); |  | ||||||
| const Wine = require(path.join( |  | ||||||
|   __dirname, "/schemas/Wine" |  | ||||||
| )); |  | ||||||
|  |  | ||||||
| class RequestedWineNotFound extends Error { | class RequestedWineNotFound extends Error { | ||||||
|   constructor(message = "Wine with this id was not found.") { |   constructor(message = "Wine with this id was not found.") { | ||||||
| @@ -14,11 +10,11 @@ class RequestedWineNotFound extends Error { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| const addNew = async (wine) => { | const addNew = async wine => { | ||||||
|   let thisWineIsLOKO = await Wine.findOne({id: wine.id}) |   let foundWine = await Wine.findOne({ id: wine.id }); | ||||||
|  |  | ||||||
|   if(thisWineIsLOKO == undefined){ |   if (foundWine == undefined) { | ||||||
|     thisWineIsLOKO = new Wine({ |     foundWine = new Wine({ | ||||||
|       name: wine.name, |       name: wine.name, | ||||||
|       vivinoLink: wine.vivinoLink, |       vivinoLink: wine.vivinoLink, | ||||||
|       rating: null, |       rating: null, | ||||||
| @@ -26,44 +22,44 @@ const addNew = async (wine) => { | |||||||
|       image: wine.image, |       image: wine.image, | ||||||
|       id: wine.id |       id: wine.id | ||||||
|     }); |     }); | ||||||
|     await thisWineIsLOKO.save() |     await foundWine.save(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   let requestedWine = await RequestedWine.findOne({ "wineId": wine.id}) |   let requestedWine = await RequestedWine.findOne({ wineId: wine.id }); | ||||||
|  |  | ||||||
|   if (requestedWine == undefined) { |   if (requestedWine == undefined) { | ||||||
|     requestedWine = new RequestedWine({ |     requestedWine = new RequestedWine({ | ||||||
|       count: 1, |       count: 1, | ||||||
|       wineId: wine.id, |       wineId: wine.id, | ||||||
|       wine: thisWineIsLOKO |       wine: foundWine | ||||||
|     }) |     }); | ||||||
|   } else { |   } else { | ||||||
|     requestedWine.count += 1; |     requestedWine.count += 1; | ||||||
|   } |   } | ||||||
|   await requestedWine.save() |   await requestedWine.save(); | ||||||
|  |  | ||||||
|   return requestedWine; |   return requestedWine; | ||||||
| } | }; | ||||||
|  |  | ||||||
| const getById = (id) => { | const getById = id => { | ||||||
|   return RequestedWine.findOne({ wineId: id }).populate("wine") |   return RequestedWine.findOne({ wineId: id }) | ||||||
|  |     .populate("wine") | ||||||
|     .then(wine => { |     .then(wine => { | ||||||
|       if (wine == null) { |       if (wine == null) { | ||||||
|         throw new RequestedWineNotFound(); |         throw new RequestedWineNotFound(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       return wine; |       return wine; | ||||||
|     }) |     }); | ||||||
| } | }; | ||||||
|  |  | ||||||
| const deleteById = (id) => { | const deleteById = id => { | ||||||
|   return getById(id) |   return getById(id).then(requestedWine => RequestedWine.deleteOne({ _id: requestedWine._id })); | ||||||
|     .then(wine => RequestedWine.deleteOne({ wineId: wine.id })) | }; | ||||||
| } |  | ||||||
|  |  | ||||||
| const getAll = () => { | const getAll = () => { | ||||||
|   return RequestedWine.find({}).populate("wine"); |   return RequestedWine.find({}).populate("wine"); | ||||||
| } | }; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   addNew, |   addNew, | ||||||
|   | |||||||
| @@ -9,18 +9,21 @@ | |||||||
|     <h1> |     <h1> | ||||||
|       Foreslå en vin! |       Foreslå en vin! | ||||||
|     </h1> |     </h1> | ||||||
|  |  | ||||||
|     <section class="search-container"> |     <section class="search-container"> | ||||||
|       <section class="search-section"> |       <section class="search-section"> | ||||||
|         <input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷" class="search-input-field"> |         <input | ||||||
|         <button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button> |           type="text" | ||||||
|       </section> |           v-model="searchString" | ||||||
|       <section v-for="(wine, index) in this.wines" :key="index" class="single-result"> |           @keyup.enter="searchWines()" | ||||||
|         <img |           placeholder="Søk etter en vin du liker her!🍷" | ||||||
|           v-if="wine.image" |           class="search-input-field" | ||||||
|           :src="wine.image" |  | ||||||
|           class="wine-image" |  | ||||||
|           :class="{ 'fullscreen': fullscreen }" |  | ||||||
|         /> |         /> | ||||||
|  |         <button :disabled="!searchString" @click="searchWines()" class="vin-button">Søk</button> | ||||||
|  |       </section> | ||||||
|  |  | ||||||
|  |       <section v-for="(wine, index) in wines" :key="index" class="single-result"> | ||||||
|  |         <img v-if="wine.image" :src="wine.image" class="wine-image" :class="{ fullscreen: fullscreen }" /> | ||||||
|         <img v-else class="wine-placeholder" alt="Wine image" /> |         <img v-else class="wine-placeholder" alt="Wine image" /> | ||||||
|         <section class="wine-info"> |         <section class="wine-info"> | ||||||
|           <h2 v-if="wine.name">{{ wine.name }}</h2> |           <h2 v-if="wine.name">{{ wine.name }}</h2> | ||||||
| @@ -29,37 +32,38 @@ | |||||||
|             <span v-if="wine.rating">{{ wine.rating }}%</span> |             <span v-if="wine.rating">{{ wine.rating }}%</span> | ||||||
|             <span v-if="wine.price">{{ wine.price }} NOK</span> |             <span v-if="wine.price">{{ wine.price }} NOK</span> | ||||||
|             <span v-if="wine.country">{{ wine.country }}</span> |             <span v-if="wine.country">{{ wine.country }}</span> | ||||||
|  |             <span v-if="wine.year">{{ wine.year }}</span> | ||||||
|           </div> |           </div> | ||||||
|         </section> |         </section> | ||||||
|           <button class="vin-button" @click="request(wine)">Foreslå denne</button> |         <button class="vin-button" @click="requestWine(wine)">Foreslå denne</button> | ||||||
|           <a |         <a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="wine-link">Les mer</a> | ||||||
|           v-if="wine.vivinoLink" |  | ||||||
|           :href="wine.vivinoLink" |  | ||||||
|           class="wine-link" |  | ||||||
|         >Les mer</a> |  | ||||||
|       </section> |       </section> | ||||||
|       <p v-if="this.wines && this.wines.length == 0"> |       <p v-if="loading == false && wines && wines.length == 0"> | ||||||
|         Fant ingen viner med det navnet! |         Fant ingen viner med det navnet! | ||||||
|       </p> |       </p> | ||||||
|  |       <p v-else-if="loading">Loading...</p> | ||||||
|     </section> |     </section> | ||||||
|   </section> |   </section> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { searchForWine, requestNewWine } from "@/api"; | import { searchForWine } from "@/api"; | ||||||
| import Wine from "@/ui/Wine"; | import Wine from "@/ui/Wine"; | ||||||
| import Modal from "@/ui/Modal"; | import Modal from "@/ui/Modal"; | ||||||
|  | import RequestedWineCard from "@/ui/RequestedWineCard"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     Wine, |     Wine, | ||||||
|     Modal |     Modal, | ||||||
|  |     RequestedWineCard | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       searchString: undefined, |       searchString: undefined, | ||||||
|       wines: undefined, |       wines: undefined, | ||||||
|       showModal: false, |       showModal: false, | ||||||
|  |       loading: false, | ||||||
|       modalButtons: [ |       modalButtons: [ | ||||||
|         { |         { | ||||||
|           text: "Legg til flere viner", |           text: "Legg til flere viner", | ||||||
| @@ -70,36 +74,59 @@ export default { | |||||||
|           action: "move" |           action: "move" | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     fetchWineFromVin(){ |     fetchWinesByQuery(query) { | ||||||
|  |       let url = new URL("/api/vinmonopolet/wine/search", window.location); | ||||||
|  |       url.searchParams.set("name", query); | ||||||
|  |  | ||||||
|  |       this.wines = []; | ||||||
|  |       this.loading = true; | ||||||
|  |  | ||||||
|  |       return fetch(url.href) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.wines = response.wines)) | ||||||
|  |         .finally(wines => (this.loading = false)); | ||||||
|  |     }, | ||||||
|  |     searchWines() { | ||||||
|       if (this.searchString) { |       if (this.searchString) { | ||||||
|         this.wines = [] |  | ||||||
|         let localSearchString = this.searchString.replace(/ /g, "_"); |         let localSearchString = this.searchString.replace(/ /g, "_"); | ||||||
|         searchForWine(localSearchString) |         this.fetchWinesByQuery(localSearchString); | ||||||
|           .then(res => this.wines = res) |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     request(wine){ |     requestWine(wine) { | ||||||
|       requestNewWine(wine) |       const options = { | ||||||
|         .then(resp => { |         method: "POST", | ||||||
|           if(resp.success) { |         headers: { "Content-Type": "application/json" }, | ||||||
|             this.showModal = true |         body: JSON.stringify({ wine: wine }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return fetch("/api/request", options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.showModal = true; | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: `Vinen ${wine.name} har blitt foreslått!` | ||||||
|  |             }); | ||||||
|           } else { |           } else { | ||||||
|             alert("Obs, her oppsto det en feil! Feilen er logget."); |             this.$toast.error({ | ||||||
|  |               title: "Obs, her oppsto det en feil! Feilen er logget.", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|           } |           } | ||||||
|         }) |         }); | ||||||
|     }, |     }, | ||||||
|     emitFromModalButton(action) { |     emitFromModalButton(action) { | ||||||
|       if (action == "stay") { |       if (action == "stay") { | ||||||
|         this.showModal = false |         this.showModal = false; | ||||||
|       } else { |       } else { | ||||||
|         this.$router.push("/requested-wines"); |         this.$router.push("/requested-wines"); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |  | ||||||
|   } |   } | ||||||
|  | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -107,7 +134,6 @@ export default { | |||||||
| @import "@/styles/global"; | @import "@/styles/global"; | ||||||
| @import "@/styles/variables"; | @import "@/styles/variables"; | ||||||
|  |  | ||||||
|  |  | ||||||
| h1 { | h1 { | ||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
| @@ -126,14 +152,13 @@ input[type="text"] { | |||||||
|   max-width: 90%; |   max-width: 90%; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| .search-container { | .search-container { | ||||||
|   margin: 1rem; |   margin: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .search-section { | .search-section { | ||||||
|   display: grid; |   display: grid; | ||||||
|   grid: 1fr / 1fr .2fr; |   grid: 1fr / 1fr 0.2fr; | ||||||
|  |  | ||||||
|   @include mobile { |   @include mobile { | ||||||
|     .vin-button { |     .vin-button { | ||||||
| @@ -148,7 +173,7 @@ input[type="text"] { | |||||||
| .single-result { | .single-result { | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
|   display: grid; |   display: grid; | ||||||
|   grid: 1fr / .5fr 2fr .5fr .5fr; |   grid: 1fr / 0.5fr 2fr 0.5fr 0.5fr; | ||||||
|   grid-template-areas: "picture details button-left button-right"; |   grid-template-areas: "picture details button-left button-right"; | ||||||
|   justify-items: center; |   justify-items: center; | ||||||
|   align-items: center; |   align-items: center; | ||||||
| @@ -158,15 +183,15 @@ input[type="text"] { | |||||||
|   box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2); |   box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2); | ||||||
|  |  | ||||||
|   @include mobile { |   @include mobile { | ||||||
|  |     grid: 1fr 0.5fr / 0.5fr 1fr; | ||||||
|     grid: 1fr .5fr / .5fr 1fr; |     grid-template-areas: | ||||||
|     grid-template-areas: "picture details" |       "picture details" | ||||||
|       "button-left button-right"; |       "button-left button-right"; | ||||||
|     grid-gap: .5em; |     grid-gap: 0.5em; | ||||||
|  |  | ||||||
|     .vin-button { |     .vin-button { | ||||||
|       grid-area: button-right; |       grid-area: button-right; | ||||||
|       padding: .5em; |       padding: 0.5em; | ||||||
|       font-size: 1em; |       font-size: 1em; | ||||||
|       line-height: 1em; |       line-height: 1em; | ||||||
|       height: 2em; |       height: 2em; | ||||||
| @@ -181,12 +206,10 @@ input[type="text"] { | |||||||
|       max-width: 80%; |       max-width: 80%; | ||||||
|       white-space: nowrap; |       white-space: nowrap; | ||||||
|       overflow: hidden; |       overflow: hidden; | ||||||
|       text-overflow: ellipsis |       text-overflow: ellipsis; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   .wine-image { |   .wine-image { | ||||||
|     height: 100px; |     height: 100px; | ||||||
|     grid-area: picture; |     grid-area: picture; | ||||||
| @@ -238,6 +261,4 @@ input[type="text"] { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
		Reference in New Issue
	
	Block a user