Merge branch 'master' of github.com:KevinMidboe/vinlottis
This commit is contained in:
		| @@ -33,7 +33,7 @@ steps: | |||||||
|       - drone-test |       - drone-test | ||||||
|     status: success |     status: success | ||||||
|   settings: |   settings: | ||||||
|     host: 10.0.0.52 |     host: vinlottis.schleppe | ||||||
|     username: root |     username: root | ||||||
|     key: |     key: | ||||||
|       from_secret: ssh_key |       from_secret: ssh_key | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  |   "printWidth": 100, | ||||||
|  |   "quoteProps": "consistent", | ||||||
|  |   "semi": true, | ||||||
|  |   "singleQuote": false, | ||||||
|  |   "trailingComma": "es5", | ||||||
|  |   "useTabs": true | ||||||
|  | } | ||||||
| @@ -11,7 +11,15 @@ function searchWines(req, res) { | |||||||
|       page: page, |       page: page, | ||||||
|       success: true |       success: true | ||||||
|     }) |     }) | ||||||
|   ); |   ) | ||||||
|  |   .catch(error => { | ||||||
|  |     const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |     return res.status(statusCode || 500).send({ | ||||||
|  |       message: message || `Unexpected error occured trying to search for wine: ${name} at page: ${page}`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function wineByEAN(req, res) { | function wineByEAN(req, res) { | ||||||
| @@ -28,12 +36,20 @@ function wineByEAN(req, res) { | |||||||
| function wineById(req, res) { | function wineById(req, res) { | ||||||
|   const { id } = req.params; |   const { id } = req.params; | ||||||
|  |  | ||||||
|   return vinmonopoletRepository.wineById(id).then(wines => |   return vinmonopoletRepository.wineById(id).then(wine => | ||||||
|     res.json({ |     res.json({ | ||||||
|       wine: wines[0], |       wine: wine, | ||||||
|       success: true |       success: true | ||||||
|     }) |     }) | ||||||
|   ); |   ) | ||||||
|  |   .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || `Unexpected error occured trying to fetch wine with id: ${id}`, | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function allStores(req, res) { | function allStores(req, res) { | ||||||
|   | |||||||
| @@ -33,7 +33,6 @@ const addWinnerWithWine = async (winner, wine) => { | |||||||
|     wine: savedWine, |     wine: savedWine, | ||||||
|     color: winner.color |     color: winner.color | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   if (exisitingWinner == undefined) { |   if (exisitingWinner == undefined) { | ||||||
|     const newWinner = new Winner({ |     const newWinner = new Winner({ | ||||||
|       name: winner.name, |       name: winner.name, | ||||||
|   | |||||||
| @@ -5,10 +5,13 @@ const Attendee = require(path.join(__dirname, "/schemas/Attendee")); | |||||||
| const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); | const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); | ||||||
| const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); | const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); | ||||||
| const Lottery = require(path.join(__dirname, "/schemas/Purchase")); | const Lottery = require(path.join(__dirname, "/schemas/Purchase")); | ||||||
|  | const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); | ||||||
|  |  | ||||||
| const Message = require(path.join(__dirname, "/message")); | const Message = require(path.join(__dirname, "/message")); | ||||||
| const historyRepository = require(path.join(__dirname, "/history")); | const historyRepository = require(path.join(__dirname, "/history")); | ||||||
| const wineRepository = require(path.join(__dirname, "/wine")); | const wineRepository = require(path.join(__dirname, "/wine")); | ||||||
|  | const winnerRepository = require(path.join(__dirname, "/winner")); | ||||||
|  | const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine")); | ||||||
|  |  | ||||||
| const { | const { | ||||||
|   WinnerNotFound, |   WinnerNotFound, | ||||||
| @@ -17,11 +20,36 @@ const { | |||||||
|   LotteryByDateNotFound |   LotteryByDateNotFound | ||||||
| } = require(path.join(__dirname, "/vinlottisErrors")); | } = require(path.join(__dirname, "/vinlottisErrors")); | ||||||
|  |  | ||||||
|  | const moveUnfoundPrelotteryWineToWines = async (error, tempWine) => { | ||||||
|  |   if(!(error instanceof WineNotFound)) { | ||||||
|  |     throw error | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if(!tempWine.winner) { | ||||||
|  |     throw new WinnerNotFound() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const prelotteryWine = await prelotteryWineRepository.wineById(tempWine._id); | ||||||
|  |   const winner = await winnerRepository.winnerById(tempWine.winner.id, true); | ||||||
|  |  | ||||||
|  |   return wineRepository | ||||||
|  |     .addWine(prelotteryWine) | ||||||
|  |     .then(_ => prelotteryWineRepository.addWinnerToWine(prelotteryWine, winner)) // prelotteryWine.deleteById | ||||||
|  |     .then(_ => historyRepository.addWinnerWithWine(winner, prelotteryWine)) | ||||||
|  |     .then(_ => winnerRepository.setWinnerChosenById(winner.id)) | ||||||
|  | } | ||||||
|  |  | ||||||
| const archive = (date, raffles, stolen, wines) => { | const archive = (date, raffles, stolen, wines) => { | ||||||
|   const { blue, red, yellow, green } = raffles; |   const { blue, red, yellow, green } = raffles; | ||||||
|   const bought = blue + red + yellow + green; |   const bought = blue + red + yellow + green; | ||||||
|  |  | ||||||
|   return Promise.all(wines.map(wine => wineRepository.findWine(wine))).then(resolvedWines => { |   return Promise.all( | ||||||
|  |     wines.map(wine => wineRepository | ||||||
|  |       .findWine(wine) | ||||||
|  |       .catch(error => moveUnfoundPrelotteryWineToWines(error, wine) | ||||||
|  |           .then(_ => wineRepository.findWine(wine)) | ||||||
|  |       )) | ||||||
|  |   ).then(resolvedWines => { | ||||||
|     const lottery = new Lottery({ |     const lottery = new Lottery({ | ||||||
|       date, |       date, | ||||||
|       blue, |       blue, | ||||||
|   | |||||||
| @@ -1,35 +0,0 @@ | |||||||
| const path = require("path"); |  | ||||||
| const Highscore = require(path.join(__dirname, "/schemas/Highscore")); |  | ||||||
|  |  | ||||||
| async function findSavePerson(foundWinner, wonWine, date) { |  | ||||||
|   let person = await Highscore.findOne({ |  | ||||||
|     name: foundWinner.name |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (person == undefined) { |  | ||||||
|     let newPerson = new Highscore({ |  | ||||||
|       name: foundWinner.name, |  | ||||||
|       wins: [ |  | ||||||
|         { |  | ||||||
|           color: foundWinner.color, |  | ||||||
|           date: date, |  | ||||||
|           wine: wonWine |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     await newPerson.save(); |  | ||||||
|   } else { |  | ||||||
|     person.wins.push({ |  | ||||||
|       color: foundWinner.color, |  | ||||||
|       date: date, |  | ||||||
|       wine: wonWine |  | ||||||
|     }); |  | ||||||
|     person.markModified("wins"); |  | ||||||
|     await person.save(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return person; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports.findSavePerson = findSavePerson; |  | ||||||
| @@ -23,7 +23,7 @@ const addWines = wines => { | |||||||
|       country: wine.country, |       country: wine.country, | ||||||
|       id: wine.id |       id: wine.id | ||||||
|     }); |     }); | ||||||
|  |     console.log(newPrelotteryWine) | ||||||
|     return newPrelotteryWine.save(); |     return newPrelotteryWine.save(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -87,8 +87,14 @@ router.get("/chat/history", chatController.getAllHistory); | |||||||
| router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory); | router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory); | ||||||
|  |  | ||||||
| router.post("/login", userController.login); | router.post("/login", userController.login); | ||||||
| router.post("/register", mustBeAuthenticated, userController.register); |  | ||||||
| router.get("/logout", userController.logout); | router.get("/logout", userController.logout); | ||||||
|  | if(process.env !== "production") { | ||||||
|  |     // We don't want to hide registering behind a | ||||||
|  |     // authentication-wall if we are in dev | ||||||
|  |     router.post("/register", userController.register); | ||||||
|  | } else { | ||||||
|  |     router.post("/register", mustBeAuthenticated, userController.register); | ||||||
|  | } | ||||||
|  |  | ||||||
| // router.get("/", documentation.apiInfo); | // router.get("/", documentation.apiInfo); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| 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 config = require(path.join(__dirname + "/../config/env/lottery.config")); | ||||||
|  | const vinmonopoletCache = require(path.join(__dirname, "vinmonopoletCache")); | ||||||
|  |  | ||||||
| const convertToOurWineObject = wine => { | const convertToOurWineObject = wine => { | ||||||
|   if (wine.basic.ageLimit === "18") { |   if (wine.basic.ageLimit === "18") { | ||||||
| @@ -18,6 +19,20 @@ const convertToOurWineObject = wine => { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const convertVinmonopoletProductResponseToWineObject = wine => { | ||||||
|  |   return { | ||||||
|  |     name: wine.name, | ||||||
|  |     vivinoLink: "https://www.vinmonopolet.no" + wine.url, | ||||||
|  |     rating: null, | ||||||
|  |     occurences: 0, | ||||||
|  |     id: wine.code, | ||||||
|  |     year: wine.year, | ||||||
|  |     image: wine.images[1].url, | ||||||
|  |     price: wine.price.value, | ||||||
|  |     country: wine.main_country.name | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| const convertToOurStoreObject = store => { | const convertToOurStoreObject = store => { | ||||||
|   return { |   return { | ||||||
|     id: store.storeId, |     id: store.storeId, | ||||||
| @@ -26,37 +41,32 @@ const convertToOurStoreObject = store => { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const searchWinesByName = async (name, page = 1) => { | const searchWinesByName = (name, page = 1) => { | ||||||
|   const pageSize = 15; |   const pageSize = 25; | ||||||
|   let url = new URL( |  | ||||||
|     `https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=gato&maxResults=15` |  | ||||||
|   ); |  | ||||||
|   url.searchParams.set("maxResults", pageSize); |  | ||||||
|   url.searchParams.set("start", pageSize * (page - 1)); |  | ||||||
|   url.searchParams.set("productShortNameContains", name); |  | ||||||
|  |  | ||||||
|   const vinmonopoletResponse = await fetch(url, { |   return vinmonopoletCache.wineByQueryName(name, page, pageSize) | ||||||
|     headers: { |     .catch(_ => { | ||||||
|       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken |       console.log(`No wines matching query: ${name} at page ${page} found in elastic index, searching vinmonopolet..`) | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .catch(err => console.error(err)); |  | ||||||
|  |  | ||||||
|   if (vinmonopoletResponse.errors != null) { |       const url = `https://www.vinmonopolet.no/api/search?q=${name}:relevance:visibleInSearch:true&searchType=product&pageSize=${pageSize}¤tPage=${page-1}` | ||||||
|     return vinmonopoletResponse.errors.map(error => { |       const options = { | ||||||
|       if (error.type == "UnknownProductError") { |         headers: { "Content-Type": 'application/json' } | ||||||
|         return res.status(404).json({ |       }; | ||||||
|           message: error.message |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         return next(); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean); |  | ||||||
|  |  | ||||||
|   return winesConverted; |       return fetch(url, options) | ||||||
|  |         .then(resp => { | ||||||
|  |           if (resp.ok == false) { | ||||||
|  |             return Promise.reject({ | ||||||
|  |               statusCode: 404, | ||||||
|  |               message: `No wines matching query ${name} at page ${page} found in local cache or at vinmonopolet.`, | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           return resp.json() | ||||||
|  |             .then(response => response?.productSearchResult?.products) | ||||||
|  |         }) | ||||||
|  |     }) | ||||||
|  |     .then(wines => wines.map(convertVinmonopoletProductResponseToWineObject)) | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const wineByEAN = ean => { | const wineByEAN = ean => { | ||||||
| @@ -67,16 +77,30 @@ const wineByEAN = ean => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const wineById = id => { | const wineById = id => { | ||||||
|   const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`; |   return vinmonopoletCache.wineById(id) | ||||||
|   const options = { |     .catch(_ => { | ||||||
|     headers: { |       console.log(`Wine id: ${id} not found in elastic index, searching vinmonopolet..`) | ||||||
|       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return fetch(url, options) |       const url = `https://www.vinmonopolet.no/api/products/${id}?fields=FULL` | ||||||
|     .then(resp => resp.json()) |       const options = { | ||||||
|     .then(response => response.map(convertToOurWineObject)); |         headers: { | ||||||
|  |           "Content-Type": 'application/json' | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return fetch(url, options) | ||||||
|  |         .then(resp => { | ||||||
|  |           if (resp.ok == false) { | ||||||
|  |             return Promise.reject({ | ||||||
|  |               statusCode: 404, | ||||||
|  |               message: `Wine with id ${id} not found in local cache or at vinmonopolet.`, | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           return resp.json() | ||||||
|  |         }) | ||||||
|  |     }) | ||||||
|  |     .then(wine => convertVinmonopoletProductResponseToWineObject(wine)) | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const allStores = () => { | const allStores = () => { | ||||||
|   | |||||||
							
								
								
									
										98
									
								
								api/vinmonopoletCache.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								api/vinmonopoletCache.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | const fetch = require("node-fetch"); | ||||||
|  |  | ||||||
|  | const ELASTIC_URL = 'http://localhost:9200'; | ||||||
|  | const INDEX_URL = `${ELASTIC_URL}/wines*`; | ||||||
|  |  | ||||||
|  | const verifyAndUnpackElasticSearchResult = response => { | ||||||
|  |   const searchHits = response?.hits?.hits; | ||||||
|  |  | ||||||
|  |   if (searchHits == null || searchHits.length == 0) { | ||||||
|  |     return Promise.reject({ | ||||||
|  |       statusCode: 404, | ||||||
|  |       message: `Nothing found in vinmonopolet cache matching this.`, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return searchHits; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const getWineObjectFromSearchHit = hit => { | ||||||
|  |   const { wine } = hit?._source; | ||||||
|  |  | ||||||
|  |   if (wine == null) { | ||||||
|  |     return Promise.reject({ | ||||||
|  |       statusCode: 500, | ||||||
|  |       message: `Found response, but it's missing a wine object. Unable to convert!`, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return wine; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const wineById = id => { | ||||||
|  | 	const url = `${INDEX_URL}/_search` | ||||||
|  |   const options = { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       "size": 1, | ||||||
|  |       "query": { | ||||||
|  |         "match": { | ||||||
|  |           "wine.code": id | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "_source": { | ||||||
|  |         "includes": "wine" | ||||||
|  |       }, | ||||||
|  |       "sort": [ | ||||||
|  |         { | ||||||
|  |           "@timestamp": "desc" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fetch(url, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(verifyAndUnpackElasticSearchResult) | ||||||
|  |     .then(searchHits => getWineObjectFromSearchHit(searchHits[0])) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const wineByQueryName = (name, page=1, size=25) => { | ||||||
|  |   const url = `${INDEX_URL}/_search` | ||||||
|  |   const options = { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { 'Content-Type': 'application/json', }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       "from": page - 1, | ||||||
|  |       "size": size, | ||||||
|  |       "query": { | ||||||
|  |         "multi_match" : { | ||||||
|  |             "query" : name, | ||||||
|  |             "fields": ["wine.name"], | ||||||
|  |             "fuzziness": 2 | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "sort": [ | ||||||
|  |         { | ||||||
|  |           "_score": { | ||||||
|  |             "order": "desc" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ],  | ||||||
|  |       "_source": { | ||||||
|  |         "includes": "wine" | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(verifyAndUnpackElasticSearchResult) | ||||||
|  |     .then(searchHits => Promise.all(searchHits.map(getWineObjectFromSearchHit))) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   wineById, | ||||||
|  |   wineByQueryName | ||||||
|  | } | ||||||
| @@ -10,17 +10,19 @@ const redactWinnerInfoMapper = winner => { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const addWinner = winner => { | ||||||
|  |   let newWinner = new VirtualWinner({ | ||||||
|  |     name: winner.name, | ||||||
|  |     color: winner.color, | ||||||
|  |     timestamp_drawn: new Date().getTime() | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   return newWinner.save() | ||||||
|  | } | ||||||
|  |  | ||||||
| const addWinners = winners => { | const addWinners = winners => { | ||||||
|   return Promise.all( |   return Promise.all( | ||||||
|     winners.map(winner => { |     winners.map(winner => addWinner(winner)) | ||||||
|       let newWinnerElement = new VirtualWinner({ |  | ||||||
|         name: winner.name, |  | ||||||
|         color: winner.color, |  | ||||||
|         timestamp_drawn: new Date().getTime() |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       return newWinnerElement.save(); |  | ||||||
|     }) |  | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -49,6 +51,14 @@ const winnerById = (id, isAdmin = false) => { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const setWinnerChosenById = (id) => { | ||||||
|  |   return VirtualWinner.findOne({id: id}).then(winner => { | ||||||
|  |     winner.prize_selected = true | ||||||
|  |     winner.markModified("wins") | ||||||
|  |     return winner.save() | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
| const updateWinnerById = (id, updateModel) => { | const updateWinnerById = (id, updateModel) => { | ||||||
|   return VirtualWinner.findOne({ id: id }).then(winner => { |   return VirtualWinner.findOne({ id: id }).then(winner => { | ||||||
|     if (winner == null) { |     if (winner == null) { | ||||||
| @@ -86,10 +96,12 @@ const deleteWinners = () => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|  |   addWinner, | ||||||
|   addWinners, |   addWinners, | ||||||
|   allWinners, |   allWinners, | ||||||
|   winnerById, |   winnerById, | ||||||
|   updateWinnerById, |   updateWinnerById, | ||||||
|   deleteWinnerById, |   deleteWinnerById, | ||||||
|   deleteWinners |   deleteWinners, | ||||||
|  |   setWinnerChosenById | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								config/env/lottery.config.example.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								config/env/lottery.config.example.js
									
									
									
									
										vendored
									
									
								
							| @@ -8,5 +8,6 @@ module.exports = { | |||||||
|   gatewayToken: undefined, |   gatewayToken: undefined, | ||||||
|   vinmonopoletToken: undefined, |   vinmonopoletToken: undefined, | ||||||
|   googleanalytics_trackingId: undefined, |   googleanalytics_trackingId: undefined, | ||||||
|   googleanalytics_cookieLifetime: 60 * 60 * 24 * 14 |   googleanalytics_cookieLifetime: 60 * 60 * 24 * 14, | ||||||
|  |   sites: [], | ||||||
| }; | }; | ||||||
| @@ -11,15 +11,15 @@ const webpackConfig = function(isDev) { | |||||||
|     resolve: { |     resolve: { | ||||||
|       extensions: [".js", ".vue"], |       extensions: [".js", ".vue"], | ||||||
|       alias: { |       alias: { | ||||||
|         vue$: "vue/dist/vue.min.js", |         "vue$": "vue/dist/vue.min.js", | ||||||
|         "@": helpers.root("frontend") |         "@": helpers.root("frontend"), | ||||||
|       } |       }, | ||||||
|     }, |     }, | ||||||
|     entry: { |     entry: { | ||||||
|       vinlottis: helpers.root("frontend", "vinlottis-init") |       vinlottis: helpers.root("frontend", "vinlottis-init"), | ||||||
|     }, |     }, | ||||||
|     externals: { |     externals: { | ||||||
|         moment: 'moment' // comes with chart.js |       moment: "moment", // comes with chart.js | ||||||
|     }, |     }, | ||||||
|     module: { |     module: { | ||||||
|       rules: [ |       rules: [ | ||||||
| @@ -31,45 +31,45 @@ const webpackConfig = function(isDev) { | |||||||
|               options: { |               options: { | ||||||
|                 loaders: { |                 loaders: { | ||||||
|                   scss: "vue-style-loader!css-loader!sass-loader", |                   scss: "vue-style-loader!css-loader!sass-loader", | ||||||
|                   sass: "vue-style-loader!css-loader!sass-loader?indentedSyntax" |                   sass: "vue-style-loader!css-loader!sass-loader?indentedSyntax", | ||||||
|                 } |                 }, | ||||||
|               } |               }, | ||||||
|             } |             }, | ||||||
|           ] |           ], | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           test: /\.js$/, |           test: /\.js$/, | ||||||
|           use: [ "babel-loader" ], |           use: ["babel-loader"], | ||||||
|           include: [helpers.root("frontend")] |           include: [helpers.root("frontend")], | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           test: /\.css$/, |           test: /\.css$/, | ||||||
|           use: [ |           use: [ | ||||||
|             MiniCSSExtractPlugin.loader, |             MiniCSSExtractPlugin.loader, | ||||||
|             { loader: "css-loader", options: { sourceMap: isDev } } |             { loader: "css-loader", options: { sourceMap: isDev } }, | ||||||
|           ] |           ], | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           test: /\.scss$/, |           test: /\.scss$/, | ||||||
|           use: [ |           use: [ | ||||||
|             MiniCSSExtractPlugin.loader, |             MiniCSSExtractPlugin.loader, | ||||||
|             { loader: "css-loader", options: { sourceMap: isDev } }, |             { loader: "css-loader", options: { sourceMap: isDev } }, | ||||||
|             { loader: "sass-loader", options: { sourceMap: isDev } } |             { loader: "sass-loader", options: { sourceMap: isDev } }, | ||||||
|           ] |           ], | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           test: /\.woff(2)?(\?[a-z0-9]+)?$/, |           test: /\.woff(2)?(\?[a-z0-9]+)?$/, | ||||||
|           loader: "url-loader", |           loader: "url-loader", | ||||||
|           options: { |           options: { | ||||||
|             limit: 10000, |             limit: 10000, | ||||||
|             mimetype: "application/font-woff" |             mimetype: "application/font-woff", | ||||||
|           } |           }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/, |           test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/, | ||||||
|           loader: "file-loader" |           loader: "file-loader", | ||||||
|         } |         }, | ||||||
|       ] |       ], | ||||||
|     }, |     }, | ||||||
|     plugins: [ |     plugins: [ | ||||||
|       new VueLoaderPlugin(), |       new VueLoaderPlugin(), | ||||||
| @@ -83,9 +83,10 @@ const webpackConfig = function(isDev) { | |||||||
|         __HOURS__: env.hours, |         __HOURS__: env.hours, | ||||||
|         __PUSHENABLED__: JSON.stringify(require("./defaults/push") != false), |         __PUSHENABLED__: JSON.stringify(require("./defaults/push") != false), | ||||||
|         __GA_TRACKINGID__: JSON.stringify(env.googleanalytics_trackingId), |         __GA_TRACKINGID__: JSON.stringify(env.googleanalytics_trackingId), | ||||||
|         __GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime |         __GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime, | ||||||
|       }) |         __sites__: JSON.stringify(env.sites), | ||||||
|     ] |       }), | ||||||
|  |     ], | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,51 +15,52 @@ let webpackConfig = merge(commonConfig(true), { | |||||||
|   output: { |   output: { | ||||||
|     path: helpers.root("dist"), |     path: helpers.root("dist"), | ||||||
|     publicPath: "/", |     publicPath: "/", | ||||||
|     filename: "js/[name].bundle.js" |     filename: "js/[name].bundle.js", | ||||||
|   }, |   }, | ||||||
|   optimization: { |   optimization: { | ||||||
|     concatenateModules: true, |     concatenateModules: true, | ||||||
|     splitChunks: { |     splitChunks: { | ||||||
|       chunks: "initial" |       chunks: "initial", | ||||||
|     } |     }, | ||||||
|   }, |   }, | ||||||
|   plugins: [ |   plugins: [ | ||||||
|     new webpack.EnvironmentPlugin(environment), |     new webpack.EnvironmentPlugin(environment), | ||||||
|     new FriendlyErrorsPlugin(), |     new FriendlyErrorsPlugin(), | ||||||
|     new MiniCSSExtractPlugin({ |     new MiniCSSExtractPlugin({ | ||||||
|       filename: "css/[name].css" |       filename: "css/[name].css", | ||||||
|     }) |     }), | ||||||
|   ], |   ], | ||||||
|   devServer: { |   devServer: { | ||||||
|     compress: true, |     compress: true, | ||||||
|     historyApiFallback: true, |     historyApiFallback: true, | ||||||
|     host: "0.0.0.0", |     host: "0.0.0.0", | ||||||
|  |     disableHostCheck: true, | ||||||
|     hot: true, |     hot: true, | ||||||
|     overlay: true, |     overlay: true, | ||||||
|     stats: { |     stats: { | ||||||
|       normal: true |       normal: true, | ||||||
|     }, |     }, | ||||||
|     proxy: { |     proxy: { | ||||||
|       "/api": { |       "/api": { | ||||||
|         target: "http://localhost:30030", |         target: "http://localhost:30030", | ||||||
|         changeOrigin: true |         changeOrigin: true, | ||||||
|       }, |       }, | ||||||
|       "/socket.io": { |       "/socket.io": { | ||||||
|         target: "ws://localhost:30030", |         target: "ws://localhost:30030", | ||||||
|         changeOrigin: false, |         changeOrigin: false, | ||||||
|         ws: true |         ws: true, | ||||||
|       } |       }, | ||||||
|     }, |     }, | ||||||
|     writeToDisk: false |     writeToDisk: false, | ||||||
|   } |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| webpackConfig = merge(webpackConfig, { | webpackConfig = merge(webpackConfig, { | ||||||
|   plugins: [ |   plugins: [ | ||||||
|     new HtmlWebpackPlugin({ |     new HtmlWebpackPlugin({ | ||||||
|       template: "frontend/templates/Index.html" |       template: "frontend/templates/Index.html", | ||||||
|     }) |     }), | ||||||
|   ] |   ], | ||||||
| }); | }); | ||||||
|  |  | ||||||
| module.exports = webpackConfig; | module.exports = webpackConfig; | ||||||
|   | |||||||
							
								
								
									
										78
									
								
								db/seedSingleDay.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								db/seedSingleDay.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
|  | const session = require("express-session"); | ||||||
|  | const mongoose = require("mongoose"); | ||||||
|  | const MongoStore = require("connect-mongo")(session); | ||||||
|  | mongoose.promise = global.Promise; | ||||||
|  | mongoose | ||||||
|  |   .connect("mongodb://localhost/vinlottis", { | ||||||
|  |     useCreateIndex: true, | ||||||
|  |     useNewUrlParser: true, | ||||||
|  |     useUnifiedTopology: true, | ||||||
|  |     serverSelectionTimeoutMS: 10000 // initial connection timeout | ||||||
|  |   }) | ||||||
|  |   .then(_ => console.log("Mongodb connection established!")) | ||||||
|  |   .catch(err => { | ||||||
|  |     console.log(err); | ||||||
|  |     console.error("ERROR! Mongodb required to run."); | ||||||
|  |     process.exit(1); | ||||||
|  |   }); | ||||||
|  | mongoose.set("debug", false); | ||||||
|  |  | ||||||
|  | const path = require("path") | ||||||
|  | const prelotteryWineRepository = require(path.join(__dirname, "../api/prelotteryWine")); | ||||||
|  | const attendeeRepository = require(path.join(__dirname, "../api/attendee")); | ||||||
|  |  | ||||||
|  | async function add() { | ||||||
|  |     const wines = [ | ||||||
|  |         { | ||||||
|  |             vivinoLink: 'https://www.vinmonopolet.no/Land/Frankrike/Devevey-Bourgogne-Hautes-C%C3%B4tes-de-Beaune-Rouge-2018/p/12351301', | ||||||
|  |             name: 'Devevey Bourgogne Hautes-Côtes de Beaune Rouge 2018', | ||||||
|  |             rating: 3, | ||||||
|  |             id: '12351301', | ||||||
|  |             year: 2018, | ||||||
|  |             image: "https://bilder.vinmonopolet.no/cache/300x300-0/12351301-1.jpg", | ||||||
|  |             price: '370', | ||||||
|  |             country: "Frankrike" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             vivinoLink: 'https://www.vinmonopolet.no/Land/Frankrike/Devevey-Rully-La-Chaume-Rouge-2018/p/12351101', | ||||||
|  |             name: 'Devevey Rully La Chaume Rouge 2018', | ||||||
|  |             rating: 4, | ||||||
|  |             id: '12351101', | ||||||
|  |             year: 2018, | ||||||
|  |             image: 'https://bilder.vinmonopolet.no/cache/300x300-0/12351101-1.jpg', | ||||||
|  |             price: '372', | ||||||
|  |             country: 'Frankrike' | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     const attendees = [ | ||||||
|  |         { | ||||||
|  |             name: "Kasper Rynning-Tønnesen", | ||||||
|  |             red: 0, | ||||||
|  |             blue: 10, | ||||||
|  |             green: 0, | ||||||
|  |             yellow: 0, | ||||||
|  |             phoneNumber: 97777777, | ||||||
|  |             winner: false | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "Kevin Midbøe", | ||||||
|  |             red: 3, | ||||||
|  |             blue: 3, | ||||||
|  |             green: 3, | ||||||
|  |             yellow: 3, | ||||||
|  |             phoneNumber: 95012321, | ||||||
|  |             winner: false | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     await prelotteryWineRepository.addWines(wines) | ||||||
|  |     await Promise.all(attendees.map(attendee => attendeeRepository.addAttendee(attendee))) | ||||||
|  |  | ||||||
|  |     console.log("Added some wines, and 2 attendees to database.") | ||||||
|  |     process.exit(1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | add() | ||||||
| @@ -25,33 +25,33 @@ export default { | |||||||
|       routes: [ |       routes: [ | ||||||
|         { |         { | ||||||
|           name: "Virtuelt lotteri", |           name: "Virtuelt lotteri", | ||||||
|           route: "/lottery" |           route: "/lottery", | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: "Dagens viner", |           name: "Dagens viner", | ||||||
|           route: "/dagens/" |           route: "/dagens/", | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: "Highscore", |           name: "Highscore", | ||||||
|           route: "/highscore" |           route: "/highscore", | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: "Historie", |           name: "Historie", | ||||||
|           route: "/history/" |           route: "/history/", | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: "Foreslå vin", |           name: "Foreslå vin", | ||||||
|           route: "/request" |           route: "/request", | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: "Foreslåtte viner", |           name: "Foreslåtte viner", | ||||||
|           route: "/requested-wines" |           route: "/requested-wines", | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: "Login", |           name: "Login", | ||||||
|           route: "/login" |           route: "/login", | ||||||
|         } |         }, | ||||||
|       ] |       ], | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
| @@ -73,7 +73,7 @@ export default { | |||||||
|     closeToast: function() { |     closeToast: function() { | ||||||
|       this.showToast = false; |       this.showToast = false; | ||||||
|     }, |     }, | ||||||
|   } |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										208
									
								
								frontend/components/AccessCodePage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								frontend/components/AccessCodePage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div class="floating-video"> | ||||||
|  |       <video autoplay loop muted playsinline id="office-party" ref="video"> | ||||||
|  |         <source src="/public/assets/videos/office-party.mp4" type="video/mp4" /> | ||||||
|  |       </video> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="container"> | ||||||
|  |       <div class="container--code label-div row"> | ||||||
|  |         <label>Din vinlottis kode:</label> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="codeinput-container"> | ||||||
|  |         <input v-model="code" placeholder="KODE" @keyup.enter="submit" /> | ||||||
|  |         <button class="vin-button" @click="submit">ENTER</button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <button class="mute-button" @click="toggleMute"> | ||||||
|  |         {{ muted ? "🔇" : "🔈" }} | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <Footer></Footer> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import Footer from "@/ui/FooterUnbranded"; | ||||||
|  | import { createCookie } from "@/utils"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { Footer }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       muted: true, | ||||||
|  |       code: undefined, | ||||||
|  |       // volume: 50, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     const site = __sites__.find(site => site.code == this.code); | ||||||
|  |   }, | ||||||
|  |   // watch: { | ||||||
|  |   //   volume(newValue) { | ||||||
|  |   //     this.$refs.video.volume = newValue / 100; | ||||||
|  |   //   }, | ||||||
|  |   // }, | ||||||
|  |   methods: { | ||||||
|  |     toggleMute() { | ||||||
|  |       const { video } = this.$refs; | ||||||
|  |       this.muted = !this.muted; | ||||||
|  |       video.muted = this.muted; | ||||||
|  |     }, | ||||||
|  |     togglePlayback() { | ||||||
|  |       const { video } = this.$refs; | ||||||
|  |       video.paused ? video.play() : video.pause(); | ||||||
|  |     }, | ||||||
|  |     submit() { | ||||||
|  |       const site = __sites__.find(site => site.code == this.code); | ||||||
|  |  | ||||||
|  |       if (site) { | ||||||
|  |         createCookie("accesscode", site.code, 14); | ||||||
|  |         window.location.href = `${window.location.protocol}//${site.domain}`; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "@/styles/media-queries"; | ||||||
|  |  | ||||||
|  | .floating-video { | ||||||
|  |   position: absolute; | ||||||
|  |   height: 100vh; | ||||||
|  |   width: 100vw; | ||||||
|  |   overflow-x: hidden; | ||||||
|  |   display: grid; | ||||||
|  |   place-items: center; | ||||||
|  |   background-color: var(--primary); | ||||||
|  |   z-index: -1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mute-button { | ||||||
|  |   z-index: 10; | ||||||
|  |   -webkit-appearance: unset; | ||||||
|  |   border: none; | ||||||
|  |   background-color: transparent; | ||||||
|  |   font-size: 1.5rem; | ||||||
|  |   position: absolute; | ||||||
|  |   right: 1rem; | ||||||
|  |   bottom: calc(75px + 1rem); | ||||||
|  |   cursor: pointer; | ||||||
|  |  | ||||||
|  |   input[type="range"] { | ||||||
|  |     transform: rotate(90deg); | ||||||
|  |     background-color: red; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | video { | ||||||
|  |   position: absolute; | ||||||
|  |   display: block; | ||||||
|  |   // left: 0; | ||||||
|  |   height: 100%; | ||||||
|  |  | ||||||
|  |   // -o-filter: blur(1px); | ||||||
|  |   filter: blur(5px); | ||||||
|  |   object-fit: cover; | ||||||
|  |   transform: scale(1.02); | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     top: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .codeinput-container { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     width: 80%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   input { | ||||||
|  |     max-width: 24rem; | ||||||
|  |     width: 100%; | ||||||
|  |     padding: 0.5rem; | ||||||
|  |  | ||||||
|  |     font-size: 4rem; | ||||||
|  |     text-align: center; | ||||||
|  |     z-index: 2; | ||||||
|  |     background-color: white; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       font-size: 3rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   button { | ||||||
|  |     height: 100%; | ||||||
|  |     max-height: unset; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .container { | ||||||
|  |   width: 100%; | ||||||
|  |   height: calc(100vh - 80px); | ||||||
|  |   margin: auto; | ||||||
|  |   display: flex; | ||||||
|  |  | ||||||
|  |   align-items: center; | ||||||
|  |   flex-direction: column; | ||||||
|  |   justify-content: flex-end; | ||||||
|  |   justify-content: center; | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     justify-content: center; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   h1 { | ||||||
|  |     position: relative; | ||||||
|  |     // text-align: center; | ||||||
|  |     font-weight: 600; | ||||||
|  |     // color: white; | ||||||
|  |  | ||||||
|  |     @include desktop { | ||||||
|  |       font-size: 3rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &--code { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |  | ||||||
|  |     label { | ||||||
|  |       color: rgba(255, 255, 255, 0.82); | ||||||
|  |       font-size: 1.5rem; | ||||||
|  |       font-weight: 500; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include desktop { | ||||||
|  |       width: 40%; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .input-line { | ||||||
|  |   margin: auto; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-around; | ||||||
|  |   align-items: center; | ||||||
|  |   margin-top: 2.4rem; | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     margin-top: 1.2rem; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .button-container { | ||||||
|  |   margin-top: 4rem; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -1,9 +1,17 @@ | |||||||
| <template> | <template> | ||||||
|   <main class="container"> |   <main class="container"> | ||||||
|     <h1>Alle foreslåtte viner</h1> |     <div class="header"> | ||||||
|  |       <h1>Alle foreslåtte viner</h1> | ||||||
|  |       <router-link class="vin-button" to="/anbefal"> | ||||||
|  |         Anbefal ny vin | ||||||
|  |         <i class="icon icon--arrow-right"></i> | ||||||
|  |       </router-link> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|     <section class="wines-container"> |     <section class="wines-container"> | ||||||
|       <p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p> |       <p v-if="wines == undefined || wines.length == 0"> | ||||||
|  |         Ingen har foreslått noe enda! | ||||||
|  |       </p> | ||||||
|  |  | ||||||
|       <RequestedWineCard |       <RequestedWineCard | ||||||
|         v-for="requestedWine in wines" |         v-for="requestedWine in wines" | ||||||
| @@ -20,12 +28,12 @@ | |||||||
| import RequestedWineCard from "@/ui/RequestedWineCard"; | import RequestedWineCard from "@/ui/RequestedWineCard"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     RequestedWineCard |     RequestedWineCard, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       wines: undefined, |       wines: undefined, | ||||||
|       isAdmin: false |       isAdmin: false, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
| @@ -33,18 +41,18 @@ export default { | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     filterOutDeletedWine(wine) { |     filterOutDeletedWine(wine) { | ||||||
|       this.wines = this.wines.filter(item => item.wine._id !== wine._id); |       this.wines = this.wines.filter((item) => item.wine._id !== wine._id); | ||||||
|     }, |     }, | ||||||
|     fetchRequestedWines() { |     fetchRequestedWines() { | ||||||
|       return fetch("/api/requests") |       return fetch("/api/requests") | ||||||
|         .then(resp => { |         .then((resp) => { | ||||||
|           this.isAdmin = resp.headers.get("vinlottis-admin") == "true"; |           this.isAdmin = resp.headers.get("vinlottis-admin") == "true"; | ||||||
|           return resp; |           return resp; | ||||||
|         }) |         }) | ||||||
|         .then(resp => resp.json()) |         .then((resp) => resp.json()) | ||||||
|         .then(response => (this.wines = response.wines)); |         .then((response) => (this.wines = response.wines)); | ||||||
|     } |     }, | ||||||
|   } |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| @@ -65,4 +73,30 @@ h1 { | |||||||
|   color: $matte-text-color; |   color: $matte-text-color; | ||||||
|   font-weight: normal; |   font-weight: normal; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .header { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     flex-direction: column; | ||||||
|  |  | ||||||
|  |     a { | ||||||
|  |       align-self: flex-end; | ||||||
|  |       margin-bottom: 4rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | a.vin-button { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   height: calc(4rem - 20px); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | a .icon { | ||||||
|  |   margin-left: 1rem; | ||||||
|  |   vertical-align: middle; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ | |||||||
|             <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> |             <span v-if="wine.year && wine.year !== '0000'">{{ wine.year }}</span> | ||||||
|           </div> |           </div> | ||||||
|         </section> |         </section> | ||||||
|         <button class="vin-button" @click="requestWine(wine)">Foreslå denne</button> |         <button class="vin-button" @click="requestWine(wine)">Foreslå denne</button> | ||||||
|   | |||||||
| @@ -94,7 +94,11 @@ export default { | |||||||
|       return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null; |       return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null; | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   mounted() { | ||||||
|  |     setTimeout(() => { | ||||||
|  |       document.getElementsByClassName("participate-button")[0].classList.add("pulse"); | ||||||
|  |     }, 1800); | ||||||
|  |  | ||||||
|     this.$on("push-allowed", () => { |     this.$on("push-allowed", () => { | ||||||
|       this.pushAllowed = true; |       this.pushAllowed = true; | ||||||
|     }); |     }); | ||||||
| @@ -121,8 +125,9 @@ export default { | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <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/animations.scss"; | ||||||
|  |  | ||||||
| .top-container { | .top-container { | ||||||
|   height: 30em; |   height: 30em; | ||||||
| @@ -173,18 +178,20 @@ export default { | |||||||
|     border: 4px solid black; |     border: 4px solid black; | ||||||
|     padding: 0 1em 0 1em; |     padding: 0 1em 0 1em; | ||||||
|     display: flex; |     display: flex; | ||||||
|     width: 12.5em; |     width: 17.5em; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     text-decoration: none; |     text-decoration: none; | ||||||
|     color: black; |     color: black; | ||||||
|  |  | ||||||
|     i { |     i { | ||||||
|       color: $link-color; |       color: $link-color; | ||||||
|  |       font-size: 1.2rem; | ||||||
|       margin-left: 5px; |       margin-left: 5px; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     p { |     p { | ||||||
|       font-size: 16px; |       font-size: 1.4rem; | ||||||
|  |       margin: 1rem; | ||||||
|       margin-left: 15px; |       margin-left: 15px; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -53,15 +53,15 @@ | |||||||
|  |  | ||||||
|           <template v-slot:bottom> |           <template v-slot:bottom> | ||||||
|             <div class="button-container row small"> |             <div class="button-container row small"> | ||||||
|               <button v-if="editingWine == wine && wine._id" class="vin-button warning" @click="updateWine(wine)"> |               <button v-if="editingWine == wine && wine._id" class="vin-button small warning" @click="updateWine(wine)"> | ||||||
|                 Oppdater vin |                 Oppdater vin | ||||||
|               </button> |               </button> | ||||||
|  |  | ||||||
|               <button class="vin-button" @click="editingWine = editingWine == wine ? false : wine"> |               <button class="vin-button small" @click="editingWine = editingWine == wine ? false : wine"> | ||||||
|                 {{ editingWine == wine ? "Lukk" : "Rediger" }} |                 {{ editingWine == wine ? "Lukk" : "Rediger" }} | ||||||
|               </button> |               </button> | ||||||
|  |  | ||||||
|               <button class="danger vin-button" @click="deleteWine(wine)"> |               <button class="danger vin-button small" @click="deleteWine(wine)"> | ||||||
|                 Slett |                 Slett | ||||||
|               </button> |               </button> | ||||||
|             </div> |             </div> | ||||||
|   | |||||||
| @@ -1,132 +1,172 @@ | |||||||
| const VinlottisPage = () => import( | const VinlottisPage = () => | ||||||
|   /* webpackChunkName: "landing-page" */ |   import( | ||||||
|   "@/components/VinlottisPage"); |     /* webpackChunkName: "landing-page" */ | ||||||
| const VirtualLotteryPage = () => import( |     "@/components/VinlottisPage" | ||||||
|   /* webpackChunkName: "landing-page" */ |   ); | ||||||
|   "@/components/VirtualLotteryPage"); | const VirtualLotteryPage = () => | ||||||
| const GeneratePage = () => import( |   import( | ||||||
|   /* webpackChunkName: "landing-page" */ |     /* webpackChunkName: "landing-page" */ | ||||||
|   "@/components/GeneratePage"); |     "@/components/VirtualLotteryPage" | ||||||
|  |   ); | ||||||
|  | const GeneratePage = () => | ||||||
|  |   import( | ||||||
|  |     /* webpackChunkName: "landing-page" */ | ||||||
|  |     "@/components/GeneratePage" | ||||||
|  |   ); | ||||||
|  |  | ||||||
| const TodaysPage = () => import( | const TodaysPage = () => | ||||||
|   /* webpackChunkName: "sub-pages" */ |   import( | ||||||
|   "@/components/TodaysPage"); |     /* webpackChunkName: "sub-pages" */ | ||||||
| const AllWinesPage = () => import( |     "@/components/TodaysPage" | ||||||
|   /* webpackChunkName: "sub-pages" */ |   ); | ||||||
|   "@/components/AllWinesPage"); | const AllWinesPage = () => | ||||||
| const HistoryPage = () => import( |   import( | ||||||
|   /* webpackChunkName: "sub-pages" */ |     /* webpackChunkName: "sub-pages" */ | ||||||
|   "@/components/HistoryPage"); |     "@/components/AllWinesPage" | ||||||
| const WinnerPage = () => import( |   ); | ||||||
|   /* webpackChunkName: "sub-pages" */ | const HistoryPage = () => | ||||||
|   "@/components/WinnerPage"); |   import( | ||||||
| const SalgsbetingelserPage = () => import( |     /* webpackChunkName: "sub-pages" */ | ||||||
|   /* webpackChunkName: "sub-pages" */ |     "@/components/HistoryPage" | ||||||
|   "@/components/SalgsbetingelserPage"); |   ); | ||||||
|  | const WinnerPage = () => | ||||||
|  |   import( | ||||||
|  |     /* webpackChunkName: "sub-pages" */ | ||||||
|  |     "@/components/WinnerPage" | ||||||
|  |   ); | ||||||
|  | const SalgsbetingelserPage = () => | ||||||
|  |   import( | ||||||
|  |     /* webpackChunkName: "sub-pages" */ | ||||||
|  |     "@/components/SalgsbetingelserPage" | ||||||
|  |   ); | ||||||
|  |  | ||||||
| const LoginPage = () => import( | const LoginPage = () => | ||||||
|   /* webpackChunkName: "user" */ |   import( | ||||||
|   "@/components/LoginPage"); |     /* webpackChunkName: "user" */ | ||||||
| const CreatePage = () => import( |     "@/components/LoginPage" | ||||||
|   /* webpackChunkName: "user" */ |   ); | ||||||
|   "@/components/CreatePage"); | const CreatePage = () => | ||||||
| const AdminPage = () => import( |   import( | ||||||
|   /* webpackChunkName: "admin" */ |     /* webpackChunkName: "user" */ | ||||||
|   "@/components/AdminPage"); |     "@/components/CreatePage" | ||||||
|  |   ); | ||||||
|  | const AdminPage = () => | ||||||
|  |   import( | ||||||
|  |     /* webpackChunkName: "admin" */ | ||||||
|  |     "@/components/AdminPage" | ||||||
|  |   ); | ||||||
|  |  | ||||||
| const PersonalHighscorePage = () => import( | const PersonalHighscorePage = () => | ||||||
|   /* webpackChunkName: "highscore" */ |   import( | ||||||
|   "@/components/PersonalHighscorePage"); |     /* webpackChunkName: "highscore" */ | ||||||
| const HighscorePage = () => import( |     "@/components/PersonalHighscorePage" | ||||||
|   /* webpackChunkName: "highscore" */ |   ); | ||||||
|   "@/components/HighscorePage"); | const HighscorePage = () => | ||||||
|  |   import( | ||||||
|  |     /* webpackChunkName: "highscore" */ | ||||||
|  |     "@/components/HighscorePage" | ||||||
|  |   ); | ||||||
|  |  | ||||||
| const RequestWine = () => import( | const RequestWine = () => | ||||||
|   /* webpackChunkName: "request" */ |   import( | ||||||
|   "@/components/RequestWine"); |     /* webpackChunkName: "request" */ | ||||||
| const AllRequestedWines = () => import( |     "@/components/RequestWine" | ||||||
|   /* webpackChunkName: "request" */ |   ); | ||||||
|   "@/components/AllRequestedWines"); | const AllRequestedWines = () => | ||||||
|  |   import( | ||||||
|  |     /* webpackChunkName: "request" */ | ||||||
|  |     "@/components/AllRequestedWines" | ||||||
|  |   ); | ||||||
|  |  | ||||||
| const routes = [ | const routes = [ | ||||||
|   { |   { | ||||||
|     path: "*", |     path: "*", | ||||||
|     name: "Hjem", |     name: "Hjem", | ||||||
|     component: VinlottisPage |     component: VinlottisPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/lottery", |     path: "/lottery", | ||||||
|     name: "Lotteri", |     name: "Lotteri", | ||||||
|     component: VirtualLotteryPage |     component: VirtualLotteryPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/dagens", |     path: "/dagens", | ||||||
|     name: "Dagens vin", |     name: "Dagens vin", | ||||||
|     component: TodaysPage |     component: TodaysPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/viner", |     path: "/viner", | ||||||
|     name: "All viner", |     name: "All viner", | ||||||
|     component: AllWinesPage |     component: AllWinesPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/login", |     path: "/login", | ||||||
|     name: "Login", |     name: "Login", | ||||||
|     component: LoginPage |     component: LoginPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/create", |     path: "/create", | ||||||
|     name: "Registrer", |     name: "Registrer", | ||||||
|     component: CreatePage |     component: CreatePage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/admin", |     path: "/admin", | ||||||
|     name: "Admin side", |     name: "Admin side", | ||||||
|     component: AdminPage |     component: AdminPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/generate/", |     path: "/generate/", | ||||||
|     component: GeneratePage |     component: GeneratePage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/winner/:id", |     path: "/winner/:id", | ||||||
|     component: WinnerPage |     component: WinnerPage, | ||||||
|   }, |   }, | ||||||
|     { |   { | ||||||
|     path: "/history/:date", |     path: "/history/:date", | ||||||
|     name: "Historie for dato", |     name: "Historie for dato", | ||||||
|     component: HistoryPage |     component: HistoryPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/history", |     path: "/history", | ||||||
|     name: "Historie", |     name: "Historie", | ||||||
|     component: HistoryPage |     component: HistoryPage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/highscore/:name", |     path: "/highscore/:name", | ||||||
|     name: "Personlig topplisten", |     name: "Personlig topplisten", | ||||||
|     component: PersonalHighscorePage |     component: PersonalHighscorePage, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/highscore", |     path: "/highscore", | ||||||
|     name: "Topplisten", |     name: "Topplisten", | ||||||
|     component: HighscorePage |     component: HighscorePage, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/anbefal", | ||||||
|  |     name: "Anbefal ny vin", | ||||||
|  |     component: RequestWine, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/request", |     path: "/request", | ||||||
|     name: "Etterspør vin", |     name: "Etterspør vin", | ||||||
|     component: RequestWine |     component: RequestWine, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     path: "/anbefalte", | ||||||
|  |     name: "Anbefalte viner", | ||||||
|  |     component: AllRequestedWines, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/requested-wines", |     path: "/requested-wines", | ||||||
|     name: "Etterspurte vin", |     name: "Etterspurte vin", | ||||||
|     component: AllRequestedWines |     component: AllRequestedWines, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     path: "/salgsbetingelser", |     path: "/salgsbetingelser", | ||||||
|     name: "Salgsbetingelser", |     name: "Salgsbetingelser", | ||||||
|     component: SalgsbetingelserPage |     component: SalgsbetingelserPage, | ||||||
|   } |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| export { routes }; | export { routes }; | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								frontend/styles/animations.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/styles/animations.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | .pulse { | ||||||
|  |   box-shadow: 0 0 0 0 rgba(0, 0, 0, 1); | ||||||
|  |   transform: scale(1); | ||||||
|  |   animation: pulse 2s infinite; | ||||||
|  |  | ||||||
|  |   @keyframes pulse { | ||||||
|  |     0% { | ||||||
|  |       transform: scale(1); | ||||||
|  |       box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.5); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     70% { | ||||||
|  |       transform: scale(1.02); | ||||||
|  |       box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     100% { | ||||||
|  |       transform: scale(1); | ||||||
|  |       box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,6 +3,7 @@ | |||||||
|   <head> |   <head> | ||||||
|     <meta charset="UTF-8" /> |     <meta charset="UTF-8" /> | ||||||
|     <title>Vinlottis</title> |     <title>Vinlottis</title> | ||||||
|  |     <meta name="robots" content="noindex"> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|     <meta |     <meta | ||||||
|       name="description" |       name="description" | ||||||
|   | |||||||
| @@ -20,9 +20,13 @@ | |||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div v-if="admin" class="attendee-admin"> |       <div v-if="admin" class="attendee-admin"> | ||||||
|         <button class="vin-button small" @click="editingAttendee = editingAttendee == attendee ? false : attendee"> |         <button class="vin-button edit small" @click="editingAttendee = editingAttendee == attendee ? false : attendee"> | ||||||
|           {{ editingAttendee == attendee ? "Lukk" : "Rediger" }} |           {{ editingAttendee == attendee ? "Lukk" : "Rediger" }} | ||||||
|         </button> |         </button> | ||||||
|  |  | ||||||
|  |         <button class="vin-button small danger" @click="deleteAttendee(attendee)"> | ||||||
|  |           Slett deltaker | ||||||
|  |         </button> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div v-if="editingAttendee == attendee" class="attendee-edit"> |       <div v-if="editingAttendee == attendee" class="attendee-edit"> | ||||||
|   | |||||||
| @@ -5,15 +5,27 @@ | |||||||
|       <img src="/public/assets/images/knowit.svg" alt="knowit logo" /> |       <img src="/public/assets/images/knowit.svg" alt="knowit logo" /> | ||||||
|     </router-link> |     </router-link> | ||||||
|  |  | ||||||
|     <a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'"> |     <a | ||||||
|  |       class="menu-toggle-container" | ||||||
|  |       aria-label="show-menu" | ||||||
|  |       @click="toggleMenu" | ||||||
|  |       :class="isOpen ? 'open' : 'collapsed'" | ||||||
|  |     > | ||||||
|       <span class="menu-toggle"></span> |       <span class="menu-toggle"></span> | ||||||
|       <span class="menu-toggle"></span> |       <span class="menu-toggle"></span> | ||||||
|       <span class="menu-toggle"></span> |       <span class="menu-toggle"></span> | ||||||
|     </a> |     </a> | ||||||
|  |  | ||||||
|     <nav class="menu" :class="isOpen ? 'open' : 'collapsed'"> |     <nav class="menu" :class="isOpen ? 'open' : 'collapsed'"> | ||||||
|       <router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link"> |       <router-link | ||||||
|         <a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a> |         v-for="(route, index) in routes" | ||||||
|  |         :key="index" | ||||||
|  |         :to="route.route" | ||||||
|  |         class="menu-item-link" | ||||||
|  |       > | ||||||
|  |         <a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ | ||||||
|  |           route.name | ||||||
|  |         }}</a> | ||||||
|         <i class="icon icon--arrow-right"></i> |         <i class="icon icon--arrow-right"></i> | ||||||
|       </router-link> |       </router-link> | ||||||
|     </nav> |     </nav> | ||||||
| @@ -42,14 +54,14 @@ export default { | |||||||
|       minutes: 0, |       minutes: 0, | ||||||
|       seconds: 0, |       seconds: 0, | ||||||
|       distance: 0, |       distance: 0, | ||||||
|       interval: null |       interval: null, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|     routes: { |     routes: { | ||||||
|       required: true, |       required: true, | ||||||
|       type: Array |       type: Array, | ||||||
|     } |     }, | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.initialize(), this.countdown(); |     this.initialize(), this.countdown(); | ||||||
| @@ -66,7 +78,7 @@ export default { | |||||||
|         return true; |         return true; | ||||||
|       } |       } | ||||||
|       return false; |       return false; | ||||||
|     } |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     toggleMenu() { |     toggleMenu() { | ||||||
| @@ -118,8 +130,8 @@ export default { | |||||||
|         this.initialize(); |         this.initialize(); | ||||||
|       } |       } | ||||||
|       this.interval = setTimeout(this.countdown, 500); |       this.interval = setTimeout(this.countdown, 500); | ||||||
|     } |     }, | ||||||
|   } |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|       <li> |       <li> | ||||||
|         <a href="https://github.com/KevinMidboe/vinlottis" class="github"> |         <a href="https://github.com/KevinMidboe/vinlottis" class="github"> | ||||||
|           <span>Utforsk koden på github</span> |           <span>Utforsk koden på github</span> | ||||||
|           <img src="/public/assets/images/logo-github.png" alt="github logo"> |           <img src="/public/assets/images/logo-github.png" alt="github logo" /> | ||||||
|         </a> |         </a> | ||||||
|       </li> |       </li> | ||||||
|  |  | ||||||
| @@ -16,15 +16,15 @@ | |||||||
|     </ul> |     </ul> | ||||||
|  |  | ||||||
|     <router-link to="/" class="company-logo"> |     <router-link to="/" class="company-logo"> | ||||||
|       <img src="/public/assets/images/knowit.svg" alt="knowit logo"> |       <img src="/public/assets/images/knowit.svg" alt="knowit logo" /> | ||||||
|     </router-link> |     </router-link> | ||||||
|   </footer> |   </footer> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| export default { | export default { | ||||||
|   name: 'WineFooter' |   name: "WineFooter", | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -57,7 +57,6 @@ footer { | |||||||
|     display: flex; |     display: flex; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|  |  | ||||||
|  |  | ||||||
|     img { |     img { | ||||||
|       margin-left: 0.5rem; |       margin-left: 0.5rem; | ||||||
|       height: 30px; |       height: 30px; | ||||||
| @@ -74,7 +73,7 @@ footer { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .company-logo{ |   .company-logo { | ||||||
|     margin-right: 5em; |     margin-right: 5em; | ||||||
|  |  | ||||||
|     img { |     img { | ||||||
| @@ -93,5 +92,4 @@ footer { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
							
								
								
									
										103
									
								
								frontend/ui/FooterUnbranded.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								frontend/ui/FooterUnbranded.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | <template> | ||||||
|  |   <footer> | ||||||
|  |     <a href="https://github.com/KevinMidboe/vinlottis" class="github"> | ||||||
|  |       <span>Utforsk koden på github</span> | ||||||
|  |       <img src="/public/assets/images/logo-github.png" alt="github logo" /> | ||||||
|  |     </a> | ||||||
|  |  | ||||||
|  |     <a href="mailto:questions@vinlottis.no" class="mail"> | ||||||
|  |       <span class="vin-link">questions@vinlottis.no</span> | ||||||
|  |     </a> | ||||||
|  |   </footer> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "WineFooter", | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "@/styles/variables.scss"; | ||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
|  | footer { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 75px; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|  |   background: #f4f4f4; | ||||||
|  |   position: fixed; | ||||||
|  |   bottom: 0; | ||||||
|  |  | ||||||
|  |   > *:first-of-type { | ||||||
|  |     margin-left: 0.5rem; | ||||||
|  |   } | ||||||
|  |   > *:last-of-type { | ||||||
|  |     margin-right: 0.5rem; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     > *:first-of-type { | ||||||
|  |       margin-left: 4rem; | ||||||
|  |     } | ||||||
|  |     > *:last-of-type { | ||||||
|  |       margin-right: 4rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ul { | ||||||
|  |     list-style-type: none; | ||||||
|  |     padding: 0; | ||||||
|  |     margin-left: 5rem; | ||||||
|  |  | ||||||
|  |     li:not(:first-of-type) { | ||||||
|  |       margin-top: 0.75rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   a { | ||||||
|  |     color: $matte-text-color; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .github { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |  | ||||||
|  |     img { | ||||||
|  |       margin-left: 0.5rem; | ||||||
|  |       height: 30px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .mail { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |  | ||||||
|  |     img { | ||||||
|  |       margin-left: 0.5rem; | ||||||
|  |       height: 23px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .company-logo { | ||||||
|  |     margin-right: 5em; | ||||||
|  |  | ||||||
|  |     img { | ||||||
|  |       width: 100px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     $margin: 1rem; | ||||||
|  |     ul { | ||||||
|  |       margin-left: $margin; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .company-logo { | ||||||
|  |       margin-right: $margin; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -19,4 +19,31 @@ function daysAgo(date) { | |||||||
|   return Math.round(Math.abs((new Date() - new Date(date)) / day)); |   return Math.round(Math.abs((new Date() - new Date(date)) / day)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function createCookie(name, value, days) { | ||||||
|  |   if (days) { | ||||||
|  |     var date = new Date(); | ||||||
|  |     date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); | ||||||
|  |     var expires = "; expires=" + date.toGMTString(); | ||||||
|  |   } else var expires = ""; | ||||||
|  |  | ||||||
|  |   const domain = `${window.location.hostname}`; | ||||||
|  |   console.log("cookie:", `${name}=${value + expires}; path=/; domain=${domain}`); | ||||||
|  |   document.cookie = `${name}=${value + expires}; path=/; domain=${domain}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function readCookie(name) { | ||||||
|  |   var nameEQ = name + "="; | ||||||
|  |   var ca = document.cookie.split(";"); | ||||||
|  |   for (var i = 0; i < ca.length; i++) { | ||||||
|  |     var c = ca[i]; | ||||||
|  |     while (c.charAt(0) == " ") c = c.substring(1, c.length); | ||||||
|  |     if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); | ||||||
|  |   } | ||||||
|  |   return null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function eraseCookie(name) { | ||||||
|  |   createCookie(name, "", -1); | ||||||
|  | } | ||||||
|  |  | ||||||
| export { dateString, humanReadableDate, daysAgo }; | export { dateString, humanReadableDate, daysAgo }; | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ import Vue from "vue"; | |||||||
| import VueRouter from "vue-router"; | import VueRouter from "vue-router"; | ||||||
| import { routes } from "@/router.js"; | import { routes } from "@/router.js"; | ||||||
| import Vinlottis from "@/Vinlottis"; | import Vinlottis from "@/Vinlottis"; | ||||||
|  | import AccessCodePage from "@/components/AccessCodePage"; | ||||||
|  | import { readCookie } from "@/utils"; | ||||||
|  |  | ||||||
| import Toast from "@/plugins/Toast"; | import Toast from "@/plugins/Toast"; | ||||||
|  |  | ||||||
| @@ -21,7 +23,7 @@ if (ENV !== "development") { | |||||||
|     beforeSend: event => { |     beforeSend: event => { | ||||||
|       console.error(event); |       console.error(event); | ||||||
|       return event; |       return event; | ||||||
|     } |     }, | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -38,7 +40,7 @@ ga.l = 1 * new Date(); | |||||||
| ga("create", __GA_TRACKINGID__, { | ga("create", __GA_TRACKINGID__, { | ||||||
|   allowAnchor: false, |   allowAnchor: false, | ||||||
|   cookieExpires: __GA_COOKIELIFETIME__, // Time in seconds |   cookieExpires: __GA_COOKIELIFETIME__, // Time in seconds | ||||||
|   cookieFlags: "SameSite=Strict; Secure" |   cookieFlags: "SameSite=Strict; Secure", | ||||||
| }); | }); | ||||||
| ga("set", "anonymizeIp", true); // Enable IP Anonymization/IP masking | ga("set", "anonymizeIp", true); // Enable IP Anonymization/IP masking | ||||||
| ga("send", "pageview"); | ga("send", "pageview"); | ||||||
| @@ -46,13 +48,25 @@ ga("send", "pageview"); | |||||||
| if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true; | if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true; | ||||||
|  |  | ||||||
| const router = new VueRouter({ | const router = new VueRouter({ | ||||||
|   routes: routes |   routes: routes, | ||||||
|  |   mode: "history", | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | function redirectIfHasAccessCodeAndOnIncorrectDomain(accessCode) { | ||||||
|  |   const site = __sites__.find(site => site.code == accessCode); | ||||||
|  |   if (accessCode && site && !!!site.domain.includes(window.location.hostname)) { | ||||||
|  |     window.location.href = `${window.location.protocol}//${site.domain}`; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const accessCode = readCookie("accesscode"); | ||||||
|  | redirectIfHasAccessCodeAndOnIncorrectDomain(1); | ||||||
|  | const component = accessCode ? Vinlottis : AccessCodePage; | ||||||
|  |  | ||||||
| new Vue({ | new Vue({ | ||||||
|   el: "#app", |   el: "#app", | ||||||
|   router, |   router, | ||||||
|   components: { Vinlottis }, |   components: { component }, | ||||||
|   template: "<Vinlottis/>", |   template: "<Vinlottis />", | ||||||
|   render: h => h(Vinlottis) |   render: h => h(component), | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								nodemon.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								nodemon.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | { | ||||||
|  | 	"restartable": "rs", | ||||||
|  | 	"ignore": [".git", "node_modules/**/node_modules"], | ||||||
|  | 	"verbose": true, | ||||||
|  | 	"execMap": { | ||||||
|  | 		"js": "node --harmony" | ||||||
|  | 	}, | ||||||
|  | 	"watch": ["./config", "./api"], | ||||||
|  | 	"ext": "js" | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								package.json
									
									
									
									
									
								
							| @@ -8,34 +8,35 @@ | |||||||
|     "build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress", |     "build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress", | ||||||
|     "watch": "yarn webpack serve --mode development --env development", |     "watch": "yarn webpack serve --mode development --env development", | ||||||
|     "start": "node server.js", |     "start": "node server.js", | ||||||
|     "dev": "cross-env NODE_ENV=development node server.js", |     "dev": "cross-env NODE_ENV=development nodemon --exec node server.js", | ||||||
|     "test": "echo \"Error: no test specified\" && exit 1" |     "test": "echo \"Error: no test specified\" && exit 1", | ||||||
|  |     "seed:single": "node db/seedSingleDay.js" | ||||||
|   }, |   }, | ||||||
|   "author": "", |   "author": "Kevin Midbøe & Kasper Rynning-Tønnesen", | ||||||
|   "license": "ISC", |   "license": "ISC", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@sentry/browser": "^5.28.0", |     "@sentry/browser": "^6.2.0", | ||||||
|     "@sentry/integrations": "^5.28.0", |     "@sentry/integrations": "^6.2.0", | ||||||
|     "@zxing/library": "^0.18.3", |     "@zxing/library": "^0.18.3", | ||||||
|     "canvas-confetti": "^1.2.0", |     "canvas-confetti": "^1.2.0", | ||||||
|     "cross-env": "^7.0.3", |  | ||||||
|     "chart.js": "^2.9.3", |     "chart.js": "^2.9.3", | ||||||
|     "connect-mongo": "^3.2.0", |     "connect-mongo": "^3.2.0", | ||||||
|  |     "cross-env": "^7.0.3", | ||||||
|     "express": "^4.17.1", |     "express": "^4.17.1", | ||||||
|     "express-session": "^1.17.0", |     "express-session": "^1.17.0", | ||||||
|     "moment": "^2.24.0", |     "moment": "^2.24.0", | ||||||
|     "mongoose": "^5.11.4", |     "mongoose": "^5.11.4", | ||||||
|     "node-fetch": "^2.6.0", |     "node-fetch": "^2.6.0", | ||||||
|     "node-sass": "^5.0.0", |     "node-schedule": "^2.0.0", | ||||||
|     "node-schedule": "^1.3.2", |  | ||||||
|     "passport": "^0.4.1", |     "passport": "^0.4.1", | ||||||
|     "passport-local": "^1.0.0", |     "passport-local": "^1.0.0", | ||||||
|     "passport-local-mongoose": "^6.0.1", |     "passport-local-mongoose": "^6.0.1", | ||||||
|     "qrcode": "^1.4.4", |     "qrcode": "^1.4.4", | ||||||
|  |     "sass": "^1.32.12", | ||||||
|     "socket.io": "^3.0.3", |     "socket.io": "^3.0.3", | ||||||
|     "socket.io-client": "^3.0.3", |     "socket.io-client": "^3.0.3", | ||||||
|     "vue": "~2.6", |     "vue": "~2.6", | ||||||
|     "vue-router": "~3.4.9", |     "vue-router": "~3.5.1", | ||||||
|     "vuex": "^3.6.0", |     "vuex": "^3.6.0", | ||||||
|     "web-push": "^3.4.3" |     "web-push": "^3.4.3" | ||||||
|   }, |   }, | ||||||
| @@ -44,24 +45,25 @@ | |||||||
|     "@babel/preset-env": "~7.12", |     "@babel/preset-env": "~7.12", | ||||||
|     "babel-loader": "~8.2.2", |     "babel-loader": "~8.2.2", | ||||||
|     "clean-webpack-plugin": "^3.0.0", |     "clean-webpack-plugin": "^3.0.0", | ||||||
|     "core-js": "3.8.1", |     "core-js": "3.9.0", | ||||||
|     "css-loader": "^5.0.1", |     "css-loader": "^5.0.1", | ||||||
|     "file-loader": "^6.2.0", |     "file-loader": "^6.2.0", | ||||||
|     "friendly-errors-webpack-plugin": "~1.7", |     "friendly-errors-webpack-plugin": "~1.7", | ||||||
|     "google-maps-api-loader": "^1.1.1", |     "google-maps-api-loader": "^1.1.1", | ||||||
|     "html-webpack-plugin": "5.0.0-alpha.15", |     "html-webpack-plugin": "5.2.0", | ||||||
|     "mini-css-extract-plugin": "~1.3.2", |     "mini-css-extract-plugin": "~1.3.2", | ||||||
|  |     "nodemon": "2.0.7", | ||||||
|     "optimize-css-assets-webpack-plugin": "~5.0.4", |     "optimize-css-assets-webpack-plugin": "~5.0.4", | ||||||
|     "redis": "^3.0.2", |     "redis": "^3.0.2", | ||||||
|     "sass-loader": "~10.1.0", |     "sass-loader": "~11.0.1", | ||||||
|     "url-loader": "^4.1.1", |     "url-loader": "^4.1.1", | ||||||
|     "vue-loader": "~15.9.5", |     "vue-loader": "~15.9.5", | ||||||
|     "vue-style-loader": "~4.1", |     "vue-style-loader": "~4.1", | ||||||
|     "vue-template-compiler": "^2.6.12", |     "vue-template-compiler": "^2.6.12", | ||||||
|     "webpack": "~5.10.0", |     "webpack": "~5.23.0", | ||||||
|     "webpack-bundle-analyzer": "^4.2.0", |     "webpack-bundle-analyzer": "~4.4.0", | ||||||
|     "webpack-cli": "~4.2.0", |     "webpack-cli": "~4.5.0", | ||||||
|     "webpack-dev-server": "~3.11", |     "webpack-dev-server": "~3.11", | ||||||
|     "webpack-merge": "~5.4" |     "webpack-merge": "~5.7.3" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								public/assets/videos/office-party.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/assets/videos/office-party.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -95,3 +95,4 @@ app.use("/subscription", subscriptionApi); | |||||||
| app.use("/", (req, res) => res.sendFile(path.join(__dirname + "/public/dist/index.html"))); | app.use("/", (req, res) => res.sendFile(path.join(__dirname + "/public/dist/index.html"))); | ||||||
|  |  | ||||||
| server.listen(30030); | server.listen(30030); | ||||||
|  | console.log("Server listening on :30030") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user