Pulled feature branch up-stream.
This commit is contained in:
		| @@ -9,10 +9,17 @@ platform: | |||||||
|  |  | ||||||
| steps: | steps: | ||||||
| - name: frontend_install | - name: frontend_install | ||||||
|   image: node:13.6.0 |   image: node:14 | ||||||
|   commands: |   commands: | ||||||
|     - node -v |     - node -v | ||||||
|     - yarn --version |     - yarn --version | ||||||
|  | - name: backend_build | ||||||
|  |   image: node:14 | ||||||
|  |   commands: | ||||||
|  |     - node -v | ||||||
|  |     - yarn --version | ||||||
|  |     - yarn | ||||||
|  |     - yarn build | ||||||
| - name: deploy | - name: deploy | ||||||
|   image: appleboy/drone-ssh |   image: appleboy/drone-ssh | ||||||
|   pull: true |   pull: true | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								api/request.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								api/request.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | const express = require("express"); | ||||||
|  | const path = require("path"); | ||||||
|  | const RequestedWine = require(path.join( | ||||||
|  |   __dirname + "/../schemas/RequestedWine" | ||||||
|  | )); | ||||||
|  | const Wine = require(path.join( | ||||||
|  |   __dirname + "/../schemas/Wine" | ||||||
|  | )); | ||||||
|  |  | ||||||
|  | const deleteRequestedWineById = async (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |   if(id == null){ | ||||||
|  |     return res.json({ | ||||||
|  |       message: "Id er ikke definert", | ||||||
|  |       success: false | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   await RequestedWine.deleteOne({wineId: id}) | ||||||
|  |   return res.json({ | ||||||
|  |     message: `Slettet vin med id: ${id}`, | ||||||
|  |     success: true | ||||||
|  |   });  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const getAllRequestedWines = async (req, res) => { | ||||||
|  |   const allWines = await RequestedWine.find({}).populate("wine"); | ||||||
|  |    | ||||||
|  |   return res.json(allWines); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const requestNewWine = async (req, res) => { | ||||||
|  |   const {wine} = req.body | ||||||
|  |    | ||||||
|  |   let thisWineIsLOKO = await Wine.findOne({id: wine.id}) | ||||||
|  |    | ||||||
|  |   if(thisWineIsLOKO == undefined){ | ||||||
|  |     thisWineIsLOKO = new Wine({ | ||||||
|  |       name: wine.name, | ||||||
|  |       vivinoLink: wine.vivinoLink, | ||||||
|  |       rating: null, | ||||||
|  |       occurences: null, | ||||||
|  |       image: wine.image, | ||||||
|  |       id: wine.id | ||||||
|  |     }); | ||||||
|  |     await thisWineIsLOKO.save() | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   let requestedWine = await RequestedWine.findOne({ "wineId": wine.id}) | ||||||
|  |    | ||||||
|  |   if(requestedWine == undefined){ | ||||||
|  |     requestedWine = new RequestedWine({ | ||||||
|  |       count: 1, | ||||||
|  |       wineId: wine.id, | ||||||
|  |       wine: thisWineIsLOKO | ||||||
|  |     }) | ||||||
|  |   } else { | ||||||
|  |     requestedWine.count += 1; | ||||||
|  |   } | ||||||
|  |   await requestedWine.save() | ||||||
|  |    | ||||||
|  |   return res.send(requestedWine); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   requestNewWine, | ||||||
|  |   getAllRequestedWines, | ||||||
|  |   deleteRequestedWineById | ||||||
|  | }; | ||||||
| @@ -1,6 +1,4 @@ | |||||||
| const express = require("express"); |  | ||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const router = express.Router(); |  | ||||||
| const mongoose = require("mongoose"); | const mongoose = require("mongoose"); | ||||||
| mongoose.connect("mongodb://localhost:27017/vinlottis", { | mongoose.connect("mongodb://localhost:27017/vinlottis", { | ||||||
|   useNewUrlParser: true |   useNewUrlParser: true | ||||||
|   | |||||||
| @@ -3,9 +3,11 @@ const path = require("path"); | |||||||
|  |  | ||||||
| // Middleware | // Middleware | ||||||
| const mustBeAuthenticated = require(__dirname + "/../middleware/mustBeAuthenticated"); | const mustBeAuthenticated = require(__dirname + "/../middleware/mustBeAuthenticated"); | ||||||
|  | const setAdminHeaderIfAuthenticated = require(__dirname + "/../middleware/setAdminHeaderIfAuthenticated"); | ||||||
|  |  | ||||||
| const update = require(path.join(__dirname + "/update")); | const update = require(path.join(__dirname + "/update")); | ||||||
| const retrieve = require(path.join(__dirname + "/retrieve")); | const retrieve = require(path.join(__dirname + "/retrieve")); | ||||||
|  | const request = require(path.join(__dirname + "/request")); | ||||||
| const subscriptionApi = require(path.join(__dirname + "/subscriptions")); | const subscriptionApi = require(path.join(__dirname + "/subscriptions")); | ||||||
| const loginApi = require(path.join(__dirname + "/login")); | const loginApi = require(path.join(__dirname + "/login")); | ||||||
| const wineinfo = require(path.join(__dirname + "/wineinfo")); | const wineinfo = require(path.join(__dirname + "/wineinfo")); | ||||||
| @@ -18,6 +20,12 @@ const lottery = require(path.join(__dirname + "/lottery")); | |||||||
|  |  | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
|  |  | ||||||
|  | router.get("/wineinfo/search", wineinfo.wineSearch); | ||||||
|  |  | ||||||
|  | router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines); | ||||||
|  | router.post("/request/new-wine", request.requestNewWine); | ||||||
|  | router.delete("/request/:id", request.deleteRequestedWineById); | ||||||
|  |  | ||||||
| router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); | router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); | ||||||
| router.get("/wineinfo/:ean", wineinfo.byEAN); | router.get("/wineinfo/:ean", wineinfo.byEAN); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,51 @@ | |||||||
| const fetch = require('node-fetch') | const fetch = require('node-fetch') | ||||||
| const path = require('path') | const path = require('path') | ||||||
|  | const config = require(path.join(__dirname + "/../config/env/lottery.config")); | ||||||
|  |  | ||||||
|  | const convertToOurWineObject = wine => { | ||||||
|  |   if(wine.basic.ageLimit === "18"){ | ||||||
|  |     return { | ||||||
|  |       name: wine.basic.productShortName, | ||||||
|  |       vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId, | ||||||
|  |       rating: wine.basic.alcoholContent, | ||||||
|  |       occurences: 0, | ||||||
|  |       id: wine.basic.productId, | ||||||
|  |       image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`, | ||||||
|  |       price: wine.prices[0].salesPrice.toString(), | ||||||
|  |       country: wine.origins.origin.country | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const wineSearch = async (req, res) => { | ||||||
|  |   const {query} = req.query | ||||||
|  |   let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`) | ||||||
|  |   url.searchParams.set('productShortNameContains', query) | ||||||
|  |    | ||||||
|  |   const vinmonopoletResponse = await fetch(url, { | ||||||
|  |     headers: { | ||||||
|  |       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .catch(err => console.error(err)) | ||||||
|  |    | ||||||
|  |    | ||||||
|  |   if (vinmonopoletResponse.errors != null) { | ||||||
|  |     return vinmonopoletResponse.errors.map(error => { | ||||||
|  |       if (error.type == "UnknownProductError") { | ||||||
|  |         return res.status(404).json({ | ||||||
|  |           message: error.message | ||||||
|  |         }) | ||||||
|  |       } else { | ||||||
|  |         return next() | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |   const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean) | ||||||
|  |  | ||||||
|  |   return res.send(winesConverted); | ||||||
|  | } | ||||||
|  |  | ||||||
| const byEAN = async (req, res) => { | const byEAN = async (req, res) => { | ||||||
|   const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean) |   const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean) | ||||||
| @@ -21,5 +67,6 @@ const byEAN = async (req, res) => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   byEAN |   byEAN, | ||||||
|  |   wineSearch | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								config/env/lottery.config.example.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								config/env/lottery.config.example.js
									
									
									
									
										vendored
									
									
								
							| @@ -6,5 +6,6 @@ module.exports = { | |||||||
|   date: 5, |   date: 5, | ||||||
|   hours: 15, |   hours: 15, | ||||||
|   apiUrl: undefined, |   apiUrl: undefined, | ||||||
|   gatewayToken: undefined |   gatewayToken: undefined, | ||||||
| }; |   vinmonopoletToken: undefined | ||||||
|  | }; | ||||||
							
								
								
									
										6
									
								
								middleware/setAdminHeaderIfAuthenticated.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								middleware/setAdminHeaderIfAuthenticated.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | const setAdminHeaderIfAuthenticated = (req, res, next) => { | ||||||
|  |   res.set("Vinlottis-Admin", req.isAuthenticated()); | ||||||
|  |   return next(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = setAdminHeaderIfAuthenticated; | ||||||
| @@ -6,7 +6,7 @@ | |||||||
|   "scripts": { |   "scripts": { | ||||||
|     "test": "echo \"Error: no test specified\" && exit 1", |     "test": "echo \"Error: no test specified\" && exit 1", | ||||||
|     "start": "node server.js", |     "start": "node server.js", | ||||||
|     "dev": "cross-env NODE_ENV=development webpack-dev-server --progress", |     "dev": "cross-env NODE_ENV=development webpack-dev-server", | ||||||
|     "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" |     "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" | ||||||
|   }, |   }, | ||||||
|   "author": "", |   "author": "", | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								schemas/RequestedWine.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								schemas/RequestedWine.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | const mongoose = require("mongoose"); | ||||||
|  | const Schema = mongoose.Schema; | ||||||
|  |  | ||||||
|  | const RequestedWine = new Schema({ | ||||||
|  |   count: Number, | ||||||
|  |   wineId: String, | ||||||
|  |   wine: { | ||||||
|  |     type: Schema.Types.ObjectId, | ||||||
|  |     ref: "Wine" | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | module.exports = mongoose.model("RequestedWine", RequestedWine); | ||||||
| @@ -9,7 +9,6 @@ const User = require(path.join(__dirname + "/schemas/User")); | |||||||
| const apiRouter = require(path.join(__dirname + "/api/router.js")); | const apiRouter = require(path.join(__dirname + "/api/router.js")); | ||||||
|  |  | ||||||
| const loginApi = require(path.join(__dirname + "/api/login")); | const loginApi = require(path.join(__dirname + "/api/login")); | ||||||
| const virtualApi = require(path.join(__dirname + "/api/virtualLottery")); |  | ||||||
| const subscriptionApi = require(path.join(__dirname + "/api/subscriptions")); | const subscriptionApi = require(path.join(__dirname + "/api/subscriptions")); | ||||||
|  |  | ||||||
| //This is required for the chat to work | //This is required for the chat to work | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="app-container"> |   <div class="app-container"> | ||||||
|     <banner /> |     <banner :routes="routes"/> | ||||||
|     <router-view /> |     <router-view /> | ||||||
|     <UpdateToast |     <UpdateToast | ||||||
|       v-if="showToast" |       v-if="showToast" | ||||||
| @@ -24,7 +24,29 @@ export default { | |||||||
|     return { |     return { | ||||||
|       showToast: false, |       showToast: false, | ||||||
|       toastText: null, |       toastText: null, | ||||||
|       refreshToast: false |       refreshToast: false, | ||||||
|  |       routes: [ | ||||||
|  |         { | ||||||
|  |           name: "Dagens viner", | ||||||
|  |           route: "/dagens/" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Historie", | ||||||
|  |           route: "/history/" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Lotteriet", | ||||||
|  |           route: "/lottery/game/" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Foreslå vin", | ||||||
|  |           route: "/request" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Foreslåtte viner", | ||||||
|  |           route: "/requested-wines" | ||||||
|  |         }, | ||||||
|  |       ] | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
| @@ -45,7 +67,7 @@ export default { | |||||||
|   methods: { |   methods: { | ||||||
|     closeToast: function() { |     closeToast: function() { | ||||||
|       this.showToast = false; |       this.showToast = false; | ||||||
|     } |     }, | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								src/api.js
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								src/api.js
									
									
									
									
									
								
							| @@ -1,3 +1,5 @@ | |||||||
|  | import fetch from "node-fetch"; | ||||||
|  |  | ||||||
| const BASE_URL = __APIURL__ || window.location.origin; | const BASE_URL = __APIURL__ || window.location.origin; | ||||||
|  |  | ||||||
| const statistics = () => { | const statistics = () => { | ||||||
| @@ -24,6 +26,16 @@ const overallWineStatistics = () => { | |||||||
|   return fetch(url.href).then(resp => resp.json()); |   return fetch(url.href).then(resp => resp.json()); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const allRequestedWines = () => { | ||||||
|  |   const url = new URL("/api/request/all", BASE_URL); | ||||||
|  |  | ||||||
|  |   return fetch(url.href) | ||||||
|  |     .then(resp => { | ||||||
|  |       const isAdmin = resp.headers.get("vinlottis-admin") || false; | ||||||
|  |       return Promise.all([resp.json(), Boolean(isAdmin)]); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
| const chartWinsByColor = () => { | const chartWinsByColor = () => { | ||||||
|   const url = new URL("/api/purchase/statistics/color", BASE_URL); |   const url = new URL("/api/purchase/statistics/color", BASE_URL); | ||||||
|  |  | ||||||
| @@ -108,6 +120,21 @@ const winners = () => { | |||||||
|   return fetch(url.href).then(resp => resp.json()); |   return fetch(url.href).then(resp => resp.json()); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const deleteRequestedWine = wineToBeDeleted => { | ||||||
|  |  | ||||||
|  |   const url = new URL("api/request/"+ wineToBeDeleted._id, BASE_URL); | ||||||
|  |  | ||||||
|  |   const options = { | ||||||
|  |     headers: { | ||||||
|  |       "Content-Type": "application/json" | ||||||
|  |     }, | ||||||
|  |     method: "DELETE", | ||||||
|  |     body: JSON.stringify(wineToBeDeleted) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options).then(resp => resp.json()) | ||||||
|  | } | ||||||
|  |  | ||||||
| const deleteWinners = () => { | const deleteWinners = () => { | ||||||
|   const url = new URL("/api/virtual/winner/all", BASE_URL); |   const url = new URL("/api/virtual/winner/all", BASE_URL); | ||||||
|  |  | ||||||
| @@ -140,6 +167,23 @@ const attendees = () => { | |||||||
|   return fetch(url.href).then(resp => resp.json()); |   return fetch(url.href).then(resp => resp.json()); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const requestNewWine = (wine) => { | ||||||
|  |   const options = { | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       wine: wine | ||||||
|  |     }), | ||||||
|  |      headers: { | ||||||
|  |       'Accept': 'application/json', | ||||||
|  |       'Content-Type': 'application/json' | ||||||
|  |     }, | ||||||
|  |     method: "post" | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const url = new URL("/api/request/new-wine", BASE_URL) | ||||||
|  |  | ||||||
|  |   return fetch(url.href, options).then(resp => resp.json()) | ||||||
|  | } | ||||||
|  |  | ||||||
| const logWines = wines => { | const logWines = wines => { | ||||||
|   const url = new URL("/api/log/wines", BASE_URL); |   const url = new URL("/api/log/wines", BASE_URL); | ||||||
|  |  | ||||||
| @@ -174,6 +218,21 @@ const barcodeToVinmonopolet = id => { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const searchForWine = searchString => { | ||||||
|  |   const url = new URL("/api/wineinfo/search?query=" + searchString, BASE_URL); | ||||||
|  |  | ||||||
|  |   return fetch(url.href).then(async resp => { | ||||||
|  |     if (!resp.ok) { | ||||||
|  |       if (resp.status == 404) { | ||||||
|  |         throw await resp.json(); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       return resp.json(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
| const handleErrors = async resp => { | const handleErrors = async resp => { | ||||||
|   if ([400, 409].includes(resp.status)) { |   if ([400, 409].includes(resp.status)) { | ||||||
|     throw await resp.json(); |     throw await resp.json(); | ||||||
| @@ -287,6 +346,9 @@ export { | |||||||
|   logWines, |   logWines, | ||||||
|   wineSchema, |   wineSchema, | ||||||
|   barcodeToVinmonopolet, |   barcodeToVinmonopolet, | ||||||
|  |   searchForWine, | ||||||
|  |   requestNewWine, | ||||||
|  |   allRequestedWines, | ||||||
|   login, |   login, | ||||||
|   register, |   register, | ||||||
|   addAttendee, |   addAttendee, | ||||||
| @@ -297,6 +359,7 @@ export { | |||||||
|   winnersSecure, |   winnersSecure, | ||||||
|   deleteWinners, |   deleteWinners, | ||||||
|   deleteAttendees, |   deleteAttendees, | ||||||
|  |   deleteRequestedWine, | ||||||
|   getChatHistory, |   getChatHistory, | ||||||
|   finishedDraw, |   finishedDraw, | ||||||
|   getAmIWinner, |   getAmIWinner, | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								src/components/AllRequestedWines.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/AllRequestedWines.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | <template> | ||||||
|  |   <main> | ||||||
|  |     <h1> | ||||||
|  |       Alle foreslåtte viner | ||||||
|  |     </h1> | ||||||
|  |     <section class="requested-wines-container"> | ||||||
|  |       <p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p> | ||||||
|  |       <RequestedWineCard v-for="requestedEl in wines" :key="requestedEl.id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/> | ||||||
|  |     </section> | ||||||
|  |   </main> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { allRequestedWines } from "@/api"; | ||||||
|  | import RequestedWineCard from "@/ui/RequestedWineCard"; | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     RequestedWineCard | ||||||
|  |   }, | ||||||
|  |   data(){ | ||||||
|  |     return{ | ||||||
|  |       wines: undefined, | ||||||
|  |       canRequest: true, | ||||||
|  |       isAdmin: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     filterOutDeletedWine(wine){ | ||||||
|  |       this.wines = this.wines.filter(item => item.wine._id !== wine._id) | ||||||
|  |     }, | ||||||
|  |     async refreshData(){ | ||||||
|  |       [this.wines, this.isAdmin] = await allRequestedWines() || [[], false] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.refreshData() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | h1{ | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .requested-wines-container{ | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-around; | ||||||
|  |   flex-flow: row wrap; | ||||||
|  |   align-items: stretch | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										195
									
								
								src/components/RequestWine.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								src/components/RequestWine.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | |||||||
|  | <template> | ||||||
|  |   <main> | ||||||
|  |     <h1> | ||||||
|  |       Foreslå en vin! | ||||||
|  |     </h1> | ||||||
|  |     <Modal  | ||||||
|  |       v-if="showModal"  | ||||||
|  |       modalText="Ønsket ditt har blitt lagt til"  | ||||||
|  |       :buttons="modalButtons" | ||||||
|  |       @click="emitFromModalButton" | ||||||
|  |     ></Modal> | ||||||
|  |     <section> | ||||||
|  |       <section class="search-section"> | ||||||
|  |         <input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷"> | ||||||
|  |         <button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button> | ||||||
|  |       </section> | ||||||
|  |       <section v-for="(wine, index) in this.wines" :key="index" class="search-results-container"> | ||||||
|  |         <img | ||||||
|  |           v-if="wine.image" | ||||||
|  |           :src="wine.image" | ||||||
|  |           class="wine-image" | ||||||
|  |           :class="{ 'fullscreen': fullscreen }" | ||||||
|  |         /> | ||||||
|  |         <img v-else class="wine-placeholder" alt="Wine image" /> | ||||||
|  |         <section class="wine-info"> | ||||||
|  |           <h2 v-if="wine.name">{{ wine.name }}</h2> | ||||||
|  |           <h2 v-else>(no name)</h2> | ||||||
|  |           <div class="__details"> | ||||||
|  |             <span v-if="wine.rating">{{ wine.rating }}%</span> | ||||||
|  |             <span v-if="wine.price">{{ wine.price }} NOK</span> | ||||||
|  |             <span v-if="wine.country">{{ wine.country }}</span> | ||||||
|  |           </div> | ||||||
|  |         </section> | ||||||
|  |         <section class="buttons"> | ||||||
|  |           <button class="vin-button" @click="request(wine)">Foreslå denne</button> | ||||||
|  |           <a | ||||||
|  |           v-if="wine.vivinoLink" | ||||||
|  |           :href="wine.vivinoLink" | ||||||
|  |           class="wine-link" | ||||||
|  |         >Les mer på polet</a> | ||||||
|  |         </section> | ||||||
|  |       </section> | ||||||
|  |       <p v-if="this.wines && this.wines.length == 0"> | ||||||
|  |         Fant ingen viner med det navnet! | ||||||
|  |       </p> | ||||||
|  |     </section> | ||||||
|  |   </main> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { searchForWine, requestNewWine } from "@/api"; | ||||||
|  | import Wine from "@/ui/Wine"; | ||||||
|  | import Modal from "@/ui/Modal"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     Wine, | ||||||
|  |     Modal | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       searchString: undefined, | ||||||
|  |       wines: undefined, | ||||||
|  |       showModal: false, | ||||||
|  |       modalButtons: [ | ||||||
|  |         { | ||||||
|  |           text: "Legg til flere viner", | ||||||
|  |           action: "stay" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           text: "Se alle viner", | ||||||
|  |           action: "move" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     fetchWineFromVin(){ | ||||||
|  |       if(this.searchString){ | ||||||
|  |         this.wines = [] | ||||||
|  |         let localSearchString = this.searchString.replace(/ /g,"_"); | ||||||
|  |         searchForWine(localSearchString) | ||||||
|  |           .then(res => this.wines = res) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     request(wine){ | ||||||
|  |       requestNewWine(wine) | ||||||
|  |         .then(() => this.showModal = true) | ||||||
|  |     }, | ||||||
|  |     emitFromModalButton(action){ | ||||||
|  |       if(action == "stay"){ | ||||||
|  |         this.showModal = false | ||||||
|  |       } else { | ||||||
|  |         this.$router.push("/requested-wines"); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "./src/styles/media-queries"; | ||||||
|  | @import "./src/styles/global"; | ||||||
|  | @import "./src/styles/variables"; | ||||||
|  |  | ||||||
|  | main{ | ||||||
|  |   margin: auto; | ||||||
|  |   width: 80%; | ||||||
|  |   text-align: center; | ||||||
|  |   z-index: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input[type="text"] { | ||||||
|  |   width: 100%; | ||||||
|  |   color: black; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   padding: 0.5rem 1rem; | ||||||
|  |   border: 1px solid black; | ||||||
|  |   max-width: 80%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .search-section{ | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-around; | ||||||
|  |   flex-flow: row; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .search-results-container{ | ||||||
|  |   display: flex; | ||||||
|  |   padding: 3px; | ||||||
|  |   border-radius: 1px; | ||||||
|  |   box-shadow: 0px 0px 0px 1px rgba(0,0,0,0.3);    | ||||||
|  |   margin: 1rem 0; | ||||||
|  |   justify-content: space-around; | ||||||
|  |   flex-flow: row wrap; | ||||||
|  |   align-items: stretch; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   .wine-image { | ||||||
|  |     height: 100px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .wine-placeholder { | ||||||
|  |     height: 100px; | ||||||
|  |     width: 70px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .wine-info{ | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     .__details{ | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   .wine-link { | ||||||
|  |     color: #333333; | ||||||
|  |     font-family: Arial; | ||||||
|  |     text-decoration: none; | ||||||
|  |     font-weight: bold; | ||||||
|  |     border-bottom: 1px solid #ff5fff; | ||||||
|  |     width: fit-content; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .buttons{ | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     order: 2; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     width: 40%; | ||||||
|  |     margin-right: 1rem; | ||||||
|  |   } | ||||||
|  |   @include mobile { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     .wine-image { | ||||||
|  |       height: 100px; | ||||||
|  |       width: 50px; | ||||||
|  |       align-self: center; | ||||||
|  |     } | ||||||
|  |     .buttons{ | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       align-self: center; | ||||||
|  |       margin: 1em; | ||||||
|  |       .wine-link{ | ||||||
|  |         margin-top: 1em; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | </style> | ||||||
| @@ -13,6 +13,9 @@ import LotteryPage from "@/components/LotteryPage"; | |||||||
| import HistoryPage from "@/components/HistoryPage"; | import HistoryPage from "@/components/HistoryPage"; | ||||||
| import HighscorePage from "@/components/HighscorePage"; | import HighscorePage from "@/components/HighscorePage"; | ||||||
|  |  | ||||||
|  | import RequestWine from "@/components/RequestWine"; | ||||||
|  | import AllRequestedWines from "@/components/AllRequestedWines"; | ||||||
|  |  | ||||||
| const routes = [ | const routes = [ | ||||||
|   { |   { | ||||||
|     path: "*", |     path: "*", | ||||||
| @@ -57,6 +60,14 @@ const routes = [ | |||||||
|   { |   { | ||||||
|     path: "/highscore", |     path: "/highscore", | ||||||
|     component: HighscorePage |     component: HighscorePage | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/request", | ||||||
|  |     component: RequestWine | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/requested-wines", | ||||||
|  |     component: AllRequestedWines | ||||||
|   } |   } | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										147
									
								
								src/styles/banner.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/styles/banner.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | @import "./media-queries.scss"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // https://codepen.io/erikterwan/pen/EVzeRP | ||||||
|  | @include mobile{ | ||||||
|  |   #menuToggle | ||||||
|  |   { | ||||||
|  |     display: block; | ||||||
|  |     position: relative; | ||||||
|  |     margin: 7px; | ||||||
|  |      | ||||||
|  |     z-index: 1; | ||||||
|  |      | ||||||
|  |     -webkit-user-select: none; | ||||||
|  |     user-select: none; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #menuToggle a | ||||||
|  |   { | ||||||
|  |     text-decoration: none; | ||||||
|  |     color: #333333; | ||||||
|  |      | ||||||
|  |     transition: color 0.3s ease; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   #menuToggle input | ||||||
|  |   { | ||||||
|  |     display: block; | ||||||
|  |     width: 40px; | ||||||
|  |     height: 32px; | ||||||
|  |     position: absolute; | ||||||
|  |     top: -7px; | ||||||
|  |     left: -5px; | ||||||
|  |      | ||||||
|  |     cursor: pointer; | ||||||
|  |      | ||||||
|  |     opacity: 0; /* hide this */ | ||||||
|  |     z-index: 2; /* and place it over the hamburger */ | ||||||
|  |      | ||||||
|  |     -webkit-touch-callout: none; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |   * Just a quick hamburger | ||||||
|  |   */ | ||||||
|  |   #menuToggle span | ||||||
|  |   { | ||||||
|  |     display: block; | ||||||
|  |     width: 33px; | ||||||
|  |     height: 4px; | ||||||
|  |     margin-bottom: 5px; | ||||||
|  |     position: relative; | ||||||
|  |      | ||||||
|  |     background: #333333; | ||||||
|  |     border-radius: 3px; | ||||||
|  |      | ||||||
|  |     z-index: 1; | ||||||
|  |      | ||||||
|  |     transform-origin: 4px 0px; | ||||||
|  |      | ||||||
|  |     transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0), | ||||||
|  |                 background 0.5s cubic-bezier(0.77,0.2,0.05,1.0), | ||||||
|  |                 opacity 0.55s ease; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #menuToggle span:first-child | ||||||
|  |   { | ||||||
|  |     transform-origin: 0% 0%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #menuToggle span:nth-last-child(2) | ||||||
|  |   { | ||||||
|  |     transform-origin: 0% 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /*  | ||||||
|  |   * Transform all the slices of hamburger | ||||||
|  |   * into a crossmark. | ||||||
|  |   */ | ||||||
|  |   #menuToggle input:checked ~ span | ||||||
|  |   { | ||||||
|  |     opacity: 1; | ||||||
|  |     transform: rotate(45deg) translate(-2px, -1px); | ||||||
|  |     background: #232323; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |   * But let's hide the middle one. | ||||||
|  |   */ | ||||||
|  |   #menuToggle input:checked ~ span:nth-last-child(3) | ||||||
|  |   { | ||||||
|  |     opacity: 0; | ||||||
|  |     transform: rotate(0deg) scale(0.2, 0.2); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |   * Ohyeah and the last one should go the other direction | ||||||
|  |   */ | ||||||
|  |   #menuToggle input:checked ~ span:nth-last-child(2) | ||||||
|  |   { | ||||||
|  |     transform: rotate(-45deg) translate(0, -1px); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |   * Make this absolute positioned | ||||||
|  |   * at the top left of the screen | ||||||
|  |   */ | ||||||
|  |   #menu | ||||||
|  |   { | ||||||
|  |     position: absolute; | ||||||
|  |     width: 100vw; | ||||||
|  |     margin: -100px 0 0 -50px; | ||||||
|  |     padding-bottom: 10px; | ||||||
|  |     padding-top: 125px; | ||||||
|  |      | ||||||
|  |     background-color: $primary; | ||||||
|  |     list-style-type: none; | ||||||
|  |     -webkit-font-smoothing: antialiased; | ||||||
|  |     /* to stop flickering of text in safari */ | ||||||
|  |      | ||||||
|  |     transform-origin: 0% 0%; | ||||||
|  |     transform: translate(-100%, 0); | ||||||
|  |      | ||||||
|  |     transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #menu li | ||||||
|  |   { | ||||||
|  |     padding: 10px 0; | ||||||
|  |     font-size: 22px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |   * And let's slide it in from the left | ||||||
|  |   */ | ||||||
|  |   #menuToggle input:checked ~ ul | ||||||
|  |   { | ||||||
|  |     transform: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @include desktop{ | ||||||
|  |   #menuToggle{ | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -144,13 +144,17 @@ textarea { | |||||||
|       0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07); |       0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:hover { |   &:hover:not(:disabled) { | ||||||
|     transform: scale(1.02) translateZ(0); |     transform: scale(1.02) translateZ(0); | ||||||
|  |  | ||||||
|     &::after { |     &::after { | ||||||
|       opacity: 1; |       opacity: 1; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |   &:disabled{ | ||||||
|  |     opacity: 0.25; | ||||||
|  |     cursor: not-allowed; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,18 +1,36 @@ | |||||||
| <template> | <template> | ||||||
|   <router-link to="/" class="link"> |   <div class="top-banner"> | ||||||
|     <div class="top-banner"> |     <!-- Mobile --> | ||||||
|       <img src="/public/assets/images/knowit.svg" alt="knowit logo" /> |     <div id="menuToggle" > | ||||||
|       <div class="clock"> |       <input type="checkbox" /> | ||||||
|         <h2 v-if="!fiveMinutesLeft || !tenMinutesOver"> |       <span></span> | ||||||
|           <span v-if="days > 0">{{ pad(days) }}:</span> |       <span></span> | ||||||
|           <span>{{ pad(hours) }}</span>: |       <span></span> | ||||||
|           <span>{{ pad(minutes) }}</span>: |       <ul id="menu"> | ||||||
|           <span>{{ pad(seconds) }}</span> |         <router-link v-for="(route, index) in routes" :key="index" :to="route.route"> | ||||||
|         </h2> |           <li>{{route.name}}</li> | ||||||
|         <h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2> |         </router-link> | ||||||
|       </div> |       </ul> | ||||||
|     </div> |     </div> | ||||||
|   </router-link> |      | ||||||
|  |     <router-link to="/"> | ||||||
|  |       <img src="/public/assets/images/knowit.svg" alt="knowit logo" /> | ||||||
|  |     </router-link> | ||||||
|  |     <div v-for="(route, index) in routes" :key="index" class="desktop"> | ||||||
|  |       <router-link :to="route.route" class="routes"> | ||||||
|  |         {{route.name}} | ||||||
|  |       </router-link> | ||||||
|  |     </div> | ||||||
|  |     <div class="clock"> | ||||||
|  |       <h2 v-if="!fiveMinutesLeft || !tenMinutesOver"> | ||||||
|  |         <span v-if="days > 0">{{ pad(days) }}:</span> | ||||||
|  |         <span>{{ pad(hours) }}</span>: | ||||||
|  |         <span>{{ pad(minutes) }}</span>: | ||||||
|  |         <span>{{ pad(seconds) }}</span> | ||||||
|  |       </h2> | ||||||
|  |       <h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| @@ -25,12 +43,15 @@ export default { | |||||||
|       minutes: 0, |       minutes: 0, | ||||||
|       seconds: 0, |       seconds: 0, | ||||||
|       distance: 0, |       distance: 0, | ||||||
|       enabled: false, |       interval: null, | ||||||
|       code: "38384040373937396665", |  | ||||||
|       codeDone: "", |  | ||||||
|       interval: null |  | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |   props: { | ||||||
|  |     routes: { | ||||||
|  |       required: true, | ||||||
|  |       type: Array | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.initialize(), this.countdown(); |     this.initialize(), this.countdown(); | ||||||
|   }, |   }, | ||||||
| @@ -55,19 +76,6 @@ export default { | |||||||
|       } |       } | ||||||
|       return num; |       return num; | ||||||
|     }, |     }, | ||||||
|     listenerFunction: function(event) { |  | ||||||
|       this.codeDone += event.keyCode; |  | ||||||
|       if (this.code.substring(0, this.codeDone.length) == this.codeDone) { |  | ||||||
|         if (this.code == this.codeDone && !this.enabled) { |  | ||||||
|           this.enabled = true; |  | ||||||
|           this.initialize(); |  | ||||||
|           this.countdown(); |  | ||||||
|           this.codeDone = ""; |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         this.codeDone = ""; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     initialize: function() { |     initialize: function() { | ||||||
|       let d = new Date(); |       let d = new Date(); | ||||||
|       let dayOfLottery = __DATE__; |       let dayOfLottery = __DATE__; | ||||||
| @@ -115,7 +123,7 @@ export default { | |||||||
|         this.initialize(); |         this.initialize(); | ||||||
|       } |       } | ||||||
|       this.interval = setTimeout(this.countdown, 500); |       this.interval = setTimeout(this.countdown, 500); | ||||||
|     } |     }, | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| @@ -123,41 +131,56 @@ export default { | |||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/media-queries.scss"; | @import "../styles/media-queries.scss"; | ||||||
| @import "../styles/variables.scss"; | @import "../styles/variables.scss"; | ||||||
|  | @import "../styles/banner.scss"; | ||||||
|  |  | ||||||
| .link { | @include mobile { | ||||||
|   text-decoration: none; |   .desktop { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @include desktop { | ||||||
|  |    | ||||||
|  |   .top-banner{ | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     align-items: center; | ||||||
|  |     width: calc(100% - 20px); | ||||||
|  |      | ||||||
|  |     .routes { | ||||||
|  |       text-decoration: none; | ||||||
|  |       color: #333333; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .top-banner { | .top-banner { | ||||||
|   text-align: center; |  | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: row; |   flex-direction: row; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   width: calc(100% - 80px); |   width: calc(100% - 20px); | ||||||
|   margin-top: 0px; |   padding: 5px 10px; | ||||||
|   padding: 0px 40px; |  | ||||||
|   background-color: $primary; |   background-color: $primary; | ||||||
|   -webkit-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65); |   -webkit-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65); | ||||||
|   -moz-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65); |   -moz-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65); | ||||||
|   box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65); |   box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65); | ||||||
|  |  | ||||||
|   @include mobile { |   .clock { | ||||||
|     padding: 0px 40px; |     text-decoration: none; | ||||||
|  |     color: #333333; | ||||||
|     > img { |     display: flex; | ||||||
|       height: 23px; |     font-family: Arial; | ||||||
|  |     margin-right: 2rem; | ||||||
|  |     @include mobile { | ||||||
|  |       font-size: 0.8em; | ||||||
|  |       margin-right: 1rem; | ||||||
|  |     } | ||||||
|  |     h2 { | ||||||
|  |       display: flex; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .clock { |  | ||||||
|   text-decoration: none; |  | ||||||
|   color: #333333; |  | ||||||
|   display: flex; |  | ||||||
|   font-family: Arial; |  | ||||||
|   h2 { |  | ||||||
|     display: flex; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										101
									
								
								src/ui/Modal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/ui/Modal.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | <template> | ||||||
|  |   <transition name="modal-fade"> | ||||||
|  |     <main class="modal-backdrop"> | ||||||
|  |       <section class="modal"> | ||||||
|  |         <header class="modal-header" v-if="headerText"> | ||||||
|  |           {{headerText}} | ||||||
|  |         </header> | ||||||
|  |         <section class="modal-body"> | ||||||
|  |           <p> | ||||||
|  |             {{modalText}} | ||||||
|  |           </p> | ||||||
|  |           <section class="button-container"> | ||||||
|  |             <button v-for="(button, index) in buttons" :key="index" @click="modalButtonClicked(button.action)" class="vin-button"> | ||||||
|  |               {{button.text}} | ||||||
|  |             </button> | ||||||
|  |           </section> | ||||||
|  |         </section> | ||||||
|  |       </section> | ||||||
|  |     </main> | ||||||
|  |   </transition> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     headerText: { | ||||||
|  |       type: String, | ||||||
|  |       required: false | ||||||
|  |     }, | ||||||
|  |     modalText: { | ||||||
|  |       type: String, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     buttons: { | ||||||
|  |       type: Array, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods:{ | ||||||
|  |     modalButtonClicked(action){ | ||||||
|  |       this.$emit('click', action) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "../styles/global.scss"; | ||||||
|  |  | ||||||
|  | .modal-fade-enter, | ||||||
|  | .modal-fade-leave-active { | ||||||
|  |   opacity: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .modal-fade-enter-active, | ||||||
|  | .modal-fade-leave-active { | ||||||
|  |   transition: opacity .5s ease | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .modal-backdrop { | ||||||
|  |   position: fixed; | ||||||
|  |   top: 0; | ||||||
|  |   bottom: 0; | ||||||
|  |   left: 0; | ||||||
|  |   right: 0; | ||||||
|  |   background-color: rgba(0, 0, 0, 0.3); | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   z-index: 1; | ||||||
|  |   width: 100vw; | ||||||
|  |   height: 100vh; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .modal { | ||||||
|  |   background: #FFFFFF; | ||||||
|  |   -webkit-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65); | ||||||
|  |   -moz-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65); | ||||||
|  |   box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65); | ||||||
|  |   overflow-x: auto; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .modal-header { | ||||||
|  |   padding: 15px; | ||||||
|  |   display: flex; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .modal-header { | ||||||
|  |   border-bottom: 1px solid #eeeeee; | ||||||
|  |   color: #4AAE9B; | ||||||
|  |   justify-content: space-between; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .modal-body { | ||||||
|  |   position: relative; | ||||||
|  |   padding: 20px 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | </style> | ||||||
							
								
								
									
										80
									
								
								src/ui/RequestedWineCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/ui/RequestedWineCard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="requested-wine"> | ||||||
|  |     <img | ||||||
|  |       v-if="wine.image" | ||||||
|  |       :src="wine.image" | ||||||
|  |       class="wine-image" | ||||||
|  |       :class="{ 'fullscreen': fullscreen }" | ||||||
|  |     /> | ||||||
|  |     <img v-else class="wine-placeholder" alt="Wine image" /> | ||||||
|  |     <section class="wine-info"> | ||||||
|  |       <h3 v-if="wine.name">{{ wine.name }}</h3> | ||||||
|  |       <h3 v-else>(no name)</h3> | ||||||
|  |       <p>Antall ganger denne har blitt foreslått: {{requestedElement.count}}</p> | ||||||
|  |       <section class="buttons"> | ||||||
|  |           <button class="vin-button" @click="request(wine)" v-if="!locallyRequested">Foreslå denne</button> | ||||||
|  |           <a | ||||||
|  |           v-if="wine.vivinoLink" | ||||||
|  |           :href="wine.vivinoLink" | ||||||
|  |           class="wine-link" | ||||||
|  |         >Les mer på polet</a> | ||||||
|  |         </section> | ||||||
|  |         <button @click="deleteWine(wine)" v-if="showDeleteButton == true"> | ||||||
|  |           Slett vinen | ||||||
|  |         </button> | ||||||
|  |       </section> | ||||||
|  |     </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { deleteRequestedWine, requestNewWine } from "@/api"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   data(){ | ||||||
|  |     return { | ||||||
|  |       wine: this.requestedElement.wine, | ||||||
|  |       locallyRequested: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     requestedElement: { | ||||||
|  |       required: true, | ||||||
|  |       type: Object | ||||||
|  |     }, | ||||||
|  |     showDeleteButton: { | ||||||
|  |       required: false, | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     request(wine){ | ||||||
|  |       this.locallyRequested = true | ||||||
|  |       this.requestedElement.count = this.requestedElement.count +1 | ||||||
|  |       requestNewWine(wine) | ||||||
|  |     }, | ||||||
|  |     async deleteWine(wine) { | ||||||
|  |       if (window.confirm("Er du sikker på at du vil slette vinen?")) { | ||||||
|  |         let response = await deleteRequestedWine(wine); | ||||||
|  |         if (response['success'] == true) { | ||||||
|  |           this.$emit('wineDeleted', wine); | ||||||
|  |         } else { | ||||||
|  |           alert("Klarte ikke slette vinen"); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped>  | ||||||
|  |  | ||||||
|  | .requested-wine{ | ||||||
|  |   padding: 20px; | ||||||
|  |   border-radius: 1px; | ||||||
|  |   margin: 1rem 0; | ||||||
|  |   -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.65); | ||||||
|  |   -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.65); | ||||||
|  |   box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.65); | ||||||
|  | } | ||||||
|  | </style> | ||||||
		Reference in New Issue
	
	Block a user