Merge pull request #75 from KevinMidboe/feat/controllers
Feat/controllers - refactor entire backend and new admin interface
This commit is contained in:
		
							
								
								
									
										81
									
								
								api/attendee.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								api/attendee.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | const path = require("path"); | ||||||
|  |  | ||||||
|  | const Attendee = require(path.join(__dirname, "/schemas/Attendee")); | ||||||
|  | const { UserNotFound } = require(path.join(__dirname, "/vinlottisErrors")); | ||||||
|  |  | ||||||
|  | const redactAttendeeInfoMapper = attendee => { | ||||||
|  |   return { | ||||||
|  |     name: attendee.name, | ||||||
|  |     raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, | ||||||
|  |     red: attendee.red, | ||||||
|  |     blue: attendee.blue, | ||||||
|  |     green: attendee.green, | ||||||
|  |     yellow: attendee.yellow | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const allAttendees = (isAdmin = false) => { | ||||||
|  |   if (!isAdmin) { | ||||||
|  |     return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper)); | ||||||
|  |   } else { | ||||||
|  |     return Attendee.find(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addAttendee = attendee => { | ||||||
|  |   const { name, red, blue, green, yellow, phoneNumber } = attendee; | ||||||
|  |  | ||||||
|  |   let newAttendee = new Attendee({ | ||||||
|  |     name, | ||||||
|  |     red, | ||||||
|  |     blue, | ||||||
|  |     green, | ||||||
|  |     yellow, | ||||||
|  |     phoneNumber, | ||||||
|  |     winner: false | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return newAttendee.save().then(_ => newAttendee); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateAttendeeById = (id, updateModel) => { | ||||||
|  |   return Attendee.findOne({ _id: id }).then(attendee => { | ||||||
|  |     if (attendee == null) { | ||||||
|  |       throw new UserNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const updatedAttendee = { | ||||||
|  |       name: updateModel.name != null ? updateModel.name : attendee.name, | ||||||
|  |       green: updateModel.green != null ? updateModel.green : attendee.green, | ||||||
|  |       red: updateModel.red != null ? updateModel.red : attendee.red, | ||||||
|  |       blue: updateModel.blue != null ? updateModel.blue : attendee.blue, | ||||||
|  |       yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow, | ||||||
|  |       phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber, | ||||||
|  |       winner: updateModel.winner != null ? updateModel.winner : attendee.winner | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteAttendeeById = id => { | ||||||
|  |   return Attendee.findOne({ _id: id }).then(attendee => { | ||||||
|  |     if (attendee == null) { | ||||||
|  |       throw new UserNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return Attendee.deleteOne({ _id: id }).then(_ => attendee); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteAttendees = () => { | ||||||
|  |   return Attendee.deleteMany(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   allAttendees, | ||||||
|  |   addAttendee, | ||||||
|  |   updateAttendeeById, | ||||||
|  |   deleteAttendeeById, | ||||||
|  |   deleteAttendees | ||||||
|  | }; | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const { history, clearHistory } = require(path.join(__dirname + "/../api/redis")); | const { history, clearHistory } = require(path.join(__dirname + "/../redis")); | ||||||
|  | console.log("loading chat"); | ||||||
| 
 | 
 | ||||||
| const getAllHistory = (req, res) => { | const getAllHistory = (req, res) => { | ||||||
|   let { page, limit } = req.query; |   let { page, limit } = req.query; | ||||||
| @@ -8,19 +9,23 @@ const getAllHistory = (req, res) => { | |||||||
| 
 | 
 | ||||||
|   return history(page, limit) |   return history(page, limit) | ||||||
|     .then(messages => res.json(messages)) |     .then(messages => res.json(messages)) | ||||||
|     .catch(error =>  res.status(500).json({ |     .catch(error => | ||||||
|       message: error.message, |       res.status(500).json({ | ||||||
|       success: false |         message: error.message, | ||||||
|     })); |         success: false | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const deleteHistory = (req, res) => { | const deleteHistory = (req, res) => { | ||||||
|   return clearHistory() |   return clearHistory() | ||||||
|     .then(message => res.json(message)) |     .then(message => res.json(message)) | ||||||
|     .catch(error => res.status(500).json({ |     .catch(error => | ||||||
|       message: error.message, |       res.status(500).json({ | ||||||
|       success: false |         message: error.message, | ||||||
|     })); |         success: false | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| module.exports = { | module.exports = { | ||||||
							
								
								
									
										261
									
								
								api/controllers/historyController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								api/controllers/historyController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,261 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const historyRepository = require(path.join(__dirname, "../history")); | ||||||
|  |  | ||||||
|  | const sortOptions = ["desc", "asc"]; | ||||||
|  | const includeWinesOptions = ["true", "false"]; | ||||||
|  |  | ||||||
|  | const all = (req, res) => { | ||||||
|  |   const { sort, includeWines } = req.query; | ||||||
|  |  | ||||||
|  |   if (sort !== undefined && !sortOptions.includes(sort)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `Sort option must be: '${sortOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return historyRepository | ||||||
|  |     .all(includeWines == "true") | ||||||
|  |     .then(winners => | ||||||
|  |       res.send({ | ||||||
|  |         winners: sort !== "asc" ? winners : winners.reverse(), | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winners." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const byDate = (req, res) => { | ||||||
|  |   let { date } = req.params; | ||||||
|  |  | ||||||
|  |   const regexDate = new RegExp("^\\d{4}-\\d{2}-\\d{2}$"); | ||||||
|  |   if (!isNaN(date)) { | ||||||
|  |     date = new Date(new Date(parseInt(date * 1000)).setHours(0, 0, 0, 0)); | ||||||
|  |   } else if (regexDate.test(date)) { | ||||||
|  |     date = new Date(date); | ||||||
|  |   } else if (date !== undefined) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Invalid date parameter, allowed epoch seconds or YYYY-MM-DD.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return historyRepository | ||||||
|  |     .byDate(date) | ||||||
|  |     .then(winners => | ||||||
|  |       res.send({ | ||||||
|  |         date: date, | ||||||
|  |         winners: winners, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winner by date." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const groupByDate = (req, res) => { | ||||||
|  |   const { sort, includeWines } = req.query; | ||||||
|  |  | ||||||
|  |   if (sort !== undefined && !sortOptions.includes(sort)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `Sort option must be: '${sortOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return historyRepository | ||||||
|  |     .groupByDate(includeWines == "true", sort) | ||||||
|  |     .then(lotteries => | ||||||
|  |       res.send({ | ||||||
|  |         lotteries: lotteries, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winner by date." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const latest = (req, res) => { | ||||||
|  |   return historyRepository | ||||||
|  |     .latest() | ||||||
|  |     .then(winners => | ||||||
|  |       res.send({ | ||||||
|  |         ...winners, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winner by date." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const byName = (req, res) => { | ||||||
|  |   const { name } = req.params; | ||||||
|  |   const { sort } = req.query; | ||||||
|  |  | ||||||
|  |   if (sort !== undefined && !sortOptions.includes(sort)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `Sort option must be: '${sortOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return historyRepository | ||||||
|  |     .byName(name, sort) | ||||||
|  |     .then(winner => | ||||||
|  |       res.send({ | ||||||
|  |         winner: winner, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winner by name." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const search = (req, res) => { | ||||||
|  |   const { name, sort } = req.query; | ||||||
|  |  | ||||||
|  |   if (sort !== undefined && !sortOptions.includes(sort)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `Sort option must be: '${sortOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return historyRepository | ||||||
|  |     .search(name, sort) | ||||||
|  |     .then(winners => | ||||||
|  |       res.send({ | ||||||
|  |         winners: winners || [], | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winner by name." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const groupByColor = (req, res) => { | ||||||
|  |   const { includeWines } = req.query; | ||||||
|  |  | ||||||
|  |   if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return historyRepository | ||||||
|  |     .groupByColor(includeWines == "true") | ||||||
|  |     .then(colors => | ||||||
|  |       res.send({ | ||||||
|  |         colors: colors, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winners by color." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const orderByWins = (req, res) => { | ||||||
|  |   let { includeWines, limit } = req.query; | ||||||
|  |  | ||||||
|  |   if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (limit && isNaN(limit)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "If limit query parameter is provided it must be a number", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } else if (!!!isNaN(limit)) { | ||||||
|  |     limit = Number(limit); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return historyRepository | ||||||
|  |     .orderByWins(includeWines == "true", limit) | ||||||
|  |     .then(winners => | ||||||
|  |       res.send({ | ||||||
|  |         winners: winners, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch winners by color." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   all, | ||||||
|  |   byDate, | ||||||
|  |   groupByDate, | ||||||
|  |   latest, | ||||||
|  |   byName, | ||||||
|  |   search, | ||||||
|  |   groupByColor, | ||||||
|  |   orderByWins | ||||||
|  | }; | ||||||
							
								
								
									
										135
									
								
								api/controllers/lotteryAttendeeController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								api/controllers/lotteryAttendeeController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const attendeeRepository = require(path.join(__dirname, "../attendee")); | ||||||
|  |  | ||||||
|  | const allAttendees = (req, res) => { | ||||||
|  |   const isAdmin = req.isAuthenticated(); | ||||||
|  |  | ||||||
|  |   return attendeeRepository | ||||||
|  |     .allAttendees(isAdmin) | ||||||
|  |     .then(attendees => | ||||||
|  |       res.send({ | ||||||
|  |         attendees: attendees, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch lottery attendees." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addAttendee = (req, res) => { | ||||||
|  |   const { attendee } = req.body; | ||||||
|  |  | ||||||
|  |   const requiredColors = [attendee["red"], attendee["blue"], attendee["green"], attendee["yellow"]]; | ||||||
|  |   const correctColorsTypes = requiredColors.filter(color => typeof color === "number"); | ||||||
|  |   if (requiredColors.length !== correctColorsTypes.length) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Incorrect or missing color, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (typeof attendee["name"] !== "string" || typeof attendee["phoneNumber"] !== "number") { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Incorrect or missing attendee keys 'name' or 'phoneNumber'.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return attendeeRepository | ||||||
|  |     .addAttendee(attendee) | ||||||
|  |     .then(savedAttendee => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("new_attendee", {}); | ||||||
|  |       return savedAttendee; | ||||||
|  |     }) | ||||||
|  |     .then(savedAttendee => | ||||||
|  |       res.send({ | ||||||
|  |         attendee: savedAttendee, | ||||||
|  |         message: `Successfully added attendee ${attendee.name} to lottery.`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateAttendeeById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |   const { attendee } = req.body; | ||||||
|  |  | ||||||
|  |   return attendeeRepository | ||||||
|  |     .updateAttendeeById(id, attendee) | ||||||
|  |     .then(updatedAttendee => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |       return updatedAttendee; | ||||||
|  |     }) | ||||||
|  |     .then(attendee => | ||||||
|  |       res.send({ | ||||||
|  |         attendee, | ||||||
|  |         message: `Updated attendee: ${attendee.name}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while deleteing attendee by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteAttendeeById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return attendeeRepository | ||||||
|  |     .deleteAttendeeById(id) | ||||||
|  |     .then(removedAttendee => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |       return removedAttendee; | ||||||
|  |     }) | ||||||
|  |     .then(attendee => | ||||||
|  |       res.send({ | ||||||
|  |         message: `Removed attendee: ${attendee.name}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while deleteing attendee by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteAttendees = (req, res) => { | ||||||
|  |   return attendeeRepository | ||||||
|  |     .deleteAttendees() | ||||||
|  |     .then(removedAttendee => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |     }) | ||||||
|  |     .then(_ => | ||||||
|  |       res.send({ | ||||||
|  |         message: "Removed all attendees", | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   allAttendees, | ||||||
|  |   addAttendee, | ||||||
|  |   updateAttendeeById, | ||||||
|  |   deleteAttendeeById, | ||||||
|  |   deleteAttendees | ||||||
|  | }; | ||||||
							
								
								
									
										192
									
								
								api/controllers/lotteryController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								api/controllers/lotteryController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,192 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const lotteryRepository = require(path.join(__dirname, "../lottery")); | ||||||
|  |  | ||||||
|  | const drawWinner = (req, res) => { | ||||||
|  |   return lotteryRepository | ||||||
|  |     .drawWinner() | ||||||
|  |     .then(({ winner, color, winners }) => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("winner", { | ||||||
|  |         color: color, | ||||||
|  |         name: winner.name, | ||||||
|  |         winner_count: winners.length + 1 | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       return { winner, color, winners }; | ||||||
|  |     }) | ||||||
|  |     .then(({ winner, color, winners }) => | ||||||
|  |       res.send({ | ||||||
|  |         color: color, | ||||||
|  |         winner: winner, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while drawing winner.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const archiveLottery = (req, res) => { | ||||||
|  |   const { lottery } = req.body; | ||||||
|  |   if (lottery == undefined || !lottery instanceof Object) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Missing lottery object.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let { stolen, date, raffles, wines } = lottery; | ||||||
|  |   stolen = stolen !== undefined ? stolen : 0; // default = 0 | ||||||
|  |  | ||||||
|  |   const validDateFormat = new RegExp("d{4}-d{2}-d{2}"); | ||||||
|  |   if (date != undefined && (!validDateFormat.test(date) || isNaN(date))) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Date must be defined as 'yyyy-mm-dd'.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } else if (date != undefined) { | ||||||
|  |     date = Date.parse(date, "yyyy-MM-dd"); | ||||||
|  |   } else { | ||||||
|  |     date = new Date(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return verifyLotteryPayload(raffles, stolen, wines) | ||||||
|  |     .then(_ => lotteryRepository.archive(date, raffles, stolen, wines)) | ||||||
|  |     .then(_ => | ||||||
|  |       res.send({ | ||||||
|  |         message: "Successfully archive lottery", | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while submitting lottery.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const lotteryByDate = (req, res) => { | ||||||
|  |   const { epoch } = req.params; | ||||||
|  |  | ||||||
|  |   if (!/^\d+$/.test(epoch)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Last parameter must be epoch (in seconds).", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   const date = new Date(Number(epoch) * 1000); | ||||||
|  |  | ||||||
|  |   return lotteryRepository | ||||||
|  |     .lotteryByDate(date) | ||||||
|  |     .then(lottery => | ||||||
|  |       res.send({ | ||||||
|  |         lottery, | ||||||
|  |         message: `Lottery for date: ${dateToDateString(date)}/${epoch}.`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while fetching lottery by date.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const sortOptions = ["desc", "asc"]; | ||||||
|  | const allLotteries = (req, res) => { | ||||||
|  |   let { includeWinners, year, sort } = req.query; | ||||||
|  |  | ||||||
|  |   if (sort !== undefined && !sortOptions.includes(sort)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: `Sort option must be: '${sortOptions.join(", ")}'`, | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } else if (sort === undefined) { | ||||||
|  |     sort = "asc"; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let allLotteriesFunction = lotteryRepository.allLotteries; | ||||||
|  |   if (includeWinners === "true") { | ||||||
|  |     allLotteriesFunction = lotteryRepository.allLotteriesIncludingWinners; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return allLotteriesFunction(sort, year) | ||||||
|  |     .then(lotteries => | ||||||
|  |       res.send({ | ||||||
|  |         lotteries, | ||||||
|  |         message: "All lotteries.", | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while fetching all lotteries.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function verifyLotteryPayload(raffles, stolen, wines) { | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     if (raffles == undefined || !raffles instanceof Array) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Raffles must be array.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const requiredColors = [raffles["red"], raffles["blue"], raffles["green"], raffles["yellow"]]; | ||||||
|  |     const correctColorsTypes = requiredColors.filter(color => typeof color === "number"); | ||||||
|  |     if (requiredColors.length !== correctColorsTypes.length) { | ||||||
|  |       reject({ | ||||||
|  |         message: | ||||||
|  |           "Incorrect or missing raffle colors, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (stolen == undefined || (isNaN(stolen) && stolen >= 0)) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Number of stolen raffles must be positive integer or 0.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (wines == undefined || !wines instanceof Array) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wines must be array.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     resolve(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function dateToDateString(date) { | ||||||
|  |   const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); | ||||||
|  |   const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); | ||||||
|  |   const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); | ||||||
|  |  | ||||||
|  |   return `${ye}-${mo}-${da}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   drawWinner, | ||||||
|  |   archiveLottery, | ||||||
|  |   lotteryByDate, | ||||||
|  |   allLotteries | ||||||
|  | }; | ||||||
							
								
								
									
										207
									
								
								api/controllers/lotteryWineController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								api/controllers/lotteryWineController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine")); | ||||||
|  |  | ||||||
|  | const allWines = (req, res) => { | ||||||
|  |   return prelotteryWineRepository | ||||||
|  |     .allWines() | ||||||
|  |     .then(wines => | ||||||
|  |       res.send({ | ||||||
|  |         wines: wines, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch lottery wines." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addWines = (req, res) => { | ||||||
|  |   let { wines } = req.body; | ||||||
|  |  | ||||||
|  |   if (!(wines instanceof Array)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Wines must be array.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const validateAllWines = wines => | ||||||
|  |     wines.map(wine => { | ||||||
|  |       const requiredAttributes = ["name", "vivinoLink", "image", "id", "price"]; | ||||||
|  |  | ||||||
|  |       return Promise.all( | ||||||
|  |         requiredAttributes.map(attr => { | ||||||
|  |           if (typeof wine[attr] === "undefined" || wine[attr] == "") { | ||||||
|  |             return Promise.reject({ | ||||||
|  |               message: `Incorrect or missing attribute: ${attr}.`, | ||||||
|  |               statusCode: 400, | ||||||
|  |               success: false | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |           return Promise.resolve(); | ||||||
|  |         }) | ||||||
|  |       ).then(_ => Promise.resolve(wine)); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   return Promise.all(validateAllWines(wines)) | ||||||
|  |     .then(wines => prelotteryWineRepository.addWines(wines)) | ||||||
|  |     .then(savedWines => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("new_wine", {}); | ||||||
|  |       return true; | ||||||
|  |     }) | ||||||
|  |     .then(success => | ||||||
|  |       res.send({ | ||||||
|  |         message: `Successfully added wines to lottery.`, | ||||||
|  |         success: success | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured adding wines.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const wineById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return prelotteryWineRepository | ||||||
|  |     .wineById(id) | ||||||
|  |     .then(wine => | ||||||
|  |       res.send({ | ||||||
|  |         wine, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while fetching wine by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateWineById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |   const { wine } = req.body; | ||||||
|  |  | ||||||
|  |   if (id == null || id == "undefined") { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Unable to update without id.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return prelotteryWineRepository | ||||||
|  |     .updateWineById(id, wine) | ||||||
|  |     .then(updatedWine => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |       return updatedWine; | ||||||
|  |     }) | ||||||
|  |     .then(wine => | ||||||
|  |       res.send({ | ||||||
|  |         wine, | ||||||
|  |         message: `Updated wine: ${wine.name}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while deleteing wine by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWineById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return prelotteryWineRepository | ||||||
|  |     .deleteWineById(id) | ||||||
|  |     .then(removedWine => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |       return removedWine; | ||||||
|  |     }) | ||||||
|  |     .then(wine => | ||||||
|  |       res.send({ | ||||||
|  |         message: `Removed wine: ${wine.name}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while deleteing wine by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWines = (req, res) => { | ||||||
|  |   return prelotteryWineRepository | ||||||
|  |     .deleteWines() | ||||||
|  |     .then(_ => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |     }) | ||||||
|  |     .then(_ => | ||||||
|  |       res.send({ | ||||||
|  |         message: "Removed all wines.", | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while deleting wines", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const wineSchema = (req, res) => { | ||||||
|  |   return prelotteryWineRepository | ||||||
|  |     .wineSchema() | ||||||
|  |     .then(schema => | ||||||
|  |       res.send({ | ||||||
|  |         schema: schema, | ||||||
|  |         message: `Wine schema template.`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch wine schema template." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   allWines, | ||||||
|  |   addWines, | ||||||
|  |   wineById, | ||||||
|  |   updateWineById, | ||||||
|  |   deleteWineById, | ||||||
|  |   deleteWines, | ||||||
|  |   wineSchema | ||||||
|  | }; | ||||||
							
								
								
									
										195
									
								
								api/controllers/lotteryWinnerController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								api/controllers/lotteryWinnerController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const winnerRepository = require(path.join(__dirname, "../winner")); | ||||||
|  | const { WinnerNotFound } = require(path.join(__dirname, "../vinlottisErrors")); | ||||||
|  | const prizeDistributionRepository = require(path.join(__dirname, "../prizeDistribution")); | ||||||
|  |  | ||||||
|  | // should not be used, is done through POST /lottery/prize-distribution/prize/:id - claimPrize. | ||||||
|  | const addWinners = (req, res) => { | ||||||
|  |   const { winners } = req.body; | ||||||
|  |  | ||||||
|  |   if (!(winners instanceof Array)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Winners must be array.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const requiredAttributes = ["name", "color", "wine"]; | ||||||
|  |   const validColors = ["red", "blue", "green", "yellow"]; | ||||||
|  |   const validateAllWinners = winners => | ||||||
|  |     winners.map(winner => { | ||||||
|  |       return Promise.all( | ||||||
|  |         requiredAttributes.map(attr => { | ||||||
|  |           if (typeof winner[attr] === "undefined") { | ||||||
|  |             return Promise.reject({ | ||||||
|  |               message: `Incorrect or missing attribute: ${attr}.`, | ||||||
|  |               statusCode: 400 | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           if (!validColors.includes(winner.color)) { | ||||||
|  |             return Promise.reject({ | ||||||
|  |               message: `Missing or incorrect color value, must have one of values: ${validColors.join(", ")}.`, | ||||||
|  |               statusCode: 400 | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           return Promise.resolve(); | ||||||
|  |         }) | ||||||
|  |       ).then(_ => Promise.resolve(winner)); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   return Promise.all(validateAllWinners(winners)) | ||||||
|  |     .then(winners => | ||||||
|  |       winners.map(winner => { | ||||||
|  |         return prizeDistributionRepository.claimPrize(winner, winner.wine); | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .then(winners => | ||||||
|  |       res.send({ | ||||||
|  |         winners: winners, | ||||||
|  |         message: `Successfully added winners to lottery.`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured adding winners.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const allWinners = (req, res) => { | ||||||
|  |   const isAdmin = req.isAuthenticated(); | ||||||
|  |  | ||||||
|  |   return winnerRepository | ||||||
|  |     .allWinners(isAdmin) | ||||||
|  |     .then(winners => | ||||||
|  |       res.send({ | ||||||
|  |         winners: winners, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch lottery winners." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const winnerById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |   const isAdmin = req.isAuthenticated(); | ||||||
|  |  | ||||||
|  |   return winnerRepository | ||||||
|  |     .winnerById(id, isAdmin) | ||||||
|  |     .then(winner => | ||||||
|  |       res.send({ | ||||||
|  |         winner, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured, unable to fetch winner by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateWinnerById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |   const { winner } = req.body; | ||||||
|  |  | ||||||
|  |   if (id == null || id == "undefined") { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "Unable to update without id.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return winnerRepository | ||||||
|  |     .updateWinnerById(id, winner) | ||||||
|  |     .then(winner => | ||||||
|  |       res.send({ | ||||||
|  |         winner, | ||||||
|  |         message: `Updated winner: ${winner.name}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while updating winner by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWinnerById = (req, res) => { | ||||||
|  |   const isAdmin = req.isAuthenticated(); | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return winnerRepository | ||||||
|  |     .deleteWinnerById(id, isAdmin) | ||||||
|  |     .then(removedWinner => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |       return removedWinner; | ||||||
|  |     }) | ||||||
|  |     .then(winner => | ||||||
|  |       res.send({ | ||||||
|  |         message: `Removed winner: ${winner.name}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while deleteing wine by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWinners = (req, res) => { | ||||||
|  |   return winnerRepository | ||||||
|  |     .deleteWinners() | ||||||
|  |     .then(_ => { | ||||||
|  |       var io = req.app.get("socketio"); | ||||||
|  |       io.emit("refresh_data", {}); | ||||||
|  |     }) | ||||||
|  |     .then(_ => | ||||||
|  |       res.send({ | ||||||
|  |         message: "Removed all winners.", | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while deleting wines", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   addWinners, | ||||||
|  |   allWinners, | ||||||
|  |   winnerById, | ||||||
|  |   updateWinnerById, | ||||||
|  |   deleteWinnerById, | ||||||
|  |   deleteWinners | ||||||
|  | }; | ||||||
							
								
								
									
										30
									
								
								api/controllers/messageController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								api/controllers/messageController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const messageRepository = require(path.join(__dirname, "../message")); | ||||||
|  | const winnerRepository = require(path.join(__dirname, "../winner")); | ||||||
|  |  | ||||||
|  | const notifyWinnerById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |   const isAdmin = req.isAuthenticated(); | ||||||
|  |  | ||||||
|  |   return winnerRepository | ||||||
|  |     .winnerById(id, isAdmin) | ||||||
|  |     .then(winner => messageRepository.sendPrizeSelectionLink(winner)) | ||||||
|  |     .then(messageResponse => | ||||||
|  |       res.send({ | ||||||
|  |         messageResponse, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while sending message to winner by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   notifyWinnerById | ||||||
|  | }; | ||||||
							
								
								
									
										104
									
								
								api/controllers/prizeDistributionController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								api/controllers/prizeDistributionController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | const path = require("path"); | ||||||
|  |  | ||||||
|  | const prizeDistribution = require(path.join(__dirname, "../prizeDistribution")); | ||||||
|  | const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine")); | ||||||
|  | const winnerRepository = require(path.join(__dirname, "../winner")); | ||||||
|  | const message = require(path.join(__dirname, "../message")); | ||||||
|  |  | ||||||
|  | const start = async (req, res) => { | ||||||
|  |   const allWinners = await winnerRepository.allWinners(true); | ||||||
|  |   if (allWinners.length === 0) { | ||||||
|  |     return res.status(503).send({ | ||||||
|  |       message: "No winners found to distribute prizes to.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const laterWinners = allWinners.slice(1); | ||||||
|  |  | ||||||
|  |   return prizeDistribution | ||||||
|  |     .notifyNextWinner() | ||||||
|  |     .then(_ => message.sendInitialMessageToWinners(laterWinners)) | ||||||
|  |     .then(_ => | ||||||
|  |       res.send({ | ||||||
|  |         message: `Send link to first winner and notified everyone else.`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while starting prize distribution.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getPrizesForWinnerById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return prizeDistribution | ||||||
|  |     .verifyWinnerNextInLine(id) | ||||||
|  |     .then(winner => { | ||||||
|  |       return prelotteryWineRepository.allWinesWithoutWinner().then(wines => [wines, winner]); | ||||||
|  |     }) | ||||||
|  |     .then(([wines, winner]) => | ||||||
|  |       res.send({ | ||||||
|  |         wines: wines, | ||||||
|  |         winner: winner, | ||||||
|  |         message: "Wines to select from", | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while fetching prizes.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const submitPrizeForWinnerById = async (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |   const { wine } = req.body; | ||||||
|  |  | ||||||
|  |   let prelotteryWine, winner; | ||||||
|  |   try { | ||||||
|  |     prelotteryWine = await prelotteryWineRepository.wineById(wine._id); | ||||||
|  |     winner = await winnerRepository.winnerById(id, true); | ||||||
|  |   } catch (error) { | ||||||
|  |     const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |     return res.status(statusCode || 500).send({ | ||||||
|  |       message: message || "Unexpected error occured while claiming prize.", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return prizeDistribution | ||||||
|  |     .claimPrize(prelotteryWine, winner) | ||||||
|  |     .then(_ => prizeDistribution.notifyNextWinner()) | ||||||
|  |     .then(_ => | ||||||
|  |       res.send({ | ||||||
|  |         message: `${winner.name} successfully claimed prize: ${prelotteryWine.name}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while claiming prize.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   start, | ||||||
|  |   getPrizesForWinnerById, | ||||||
|  |   submitPrizeForWinnerById | ||||||
|  | }; | ||||||
							
								
								
									
										104
									
								
								api/controllers/requestController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								api/controllers/requestController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const requestRepository = require(path.join(__dirname, "../request")); | ||||||
|  |  | ||||||
|  | function addRequest(req, res) { | ||||||
|  |   const { wine } = req.body; | ||||||
|  |  | ||||||
|  |   return verifyWineValues(wine) | ||||||
|  |     .then(_ => requestRepository.addNew(wine)) | ||||||
|  |     .then(wine => | ||||||
|  |       res.json({ | ||||||
|  |         message: "Successfully added new request", | ||||||
|  |         wine: wine, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { message, statusCode } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to add requested wine." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function allRequests(req, res) { | ||||||
|  |   return requestRepository | ||||||
|  |     .getAll() | ||||||
|  |     .then(wines => | ||||||
|  |       res.json({ | ||||||
|  |         wines: wines, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { message, statusCode } = error; | ||||||
|  |       return res.status(statusCode || 500).json({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch all requested wines." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function deleteRequest(req, res) { | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return requestRepository | ||||||
|  |     .deleteById(id) | ||||||
|  |     .then(_ => | ||||||
|  |       res.json({ | ||||||
|  |         message: `Slettet vin med id: ${id}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to delete requested wine." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function verifyWineValues(wine) { | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     if (wine == undefined) { | ||||||
|  |       reject({ | ||||||
|  |         message: "No wine object found in request body.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (wine.id == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value id.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } else if (wine.name == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value name.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } else if (wine.vivinoLink == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value vivinoLink.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } else if (wine.image == null) { | ||||||
|  |       reject({ | ||||||
|  |         message: "Wine object missing value image.", | ||||||
|  |         status: 400 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     resolve(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   addRequest, | ||||||
|  |   allRequests, | ||||||
|  |   deleteRequest | ||||||
|  | }; | ||||||
							
								
								
									
										55
									
								
								api/controllers/userController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								api/controllers/userController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const userRepository = require(path.join(__dirname, "../user")); | ||||||
|  |  | ||||||
|  | function register(req, res, next) { | ||||||
|  |   const { username, password } = req.body; | ||||||
|  |  | ||||||
|  |   return userRepository | ||||||
|  |     .register(username, password) | ||||||
|  |     .then(user => userRepository.login(req, user)) | ||||||
|  |     .then(_ => | ||||||
|  |       res.send({ | ||||||
|  |         messsage: `Bruker registrert. Velkommen ${username}`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unable to sign in with given username and passowrd", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const login = (req, res, next) => { | ||||||
|  |   return userRepository | ||||||
|  |     .authenticate(req) | ||||||
|  |     .then(user => userRepository.login(req, user)) | ||||||
|  |     .then(user => { | ||||||
|  |       res.send({ | ||||||
|  |         message: `Velkommen ${user.username}`, | ||||||
|  |         success: true | ||||||
|  |       }); | ||||||
|  |     }) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unable to sign in with given username and passowrd", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const logout = (req, res) => { | ||||||
|  |   req.logout(); | ||||||
|  |   res.redirect("/"); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   register, | ||||||
|  |   login, | ||||||
|  |   logout | ||||||
|  | }; | ||||||
							
								
								
									
										85
									
								
								api/controllers/vinmonopoletController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								api/controllers/vinmonopoletController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const vinmonopoletRepository = require(path.join(__dirname, "../vinmonopolet")); | ||||||
|  |  | ||||||
|  | function searchWines(req, res) { | ||||||
|  |   const { name, page } = req.query; | ||||||
|  |  | ||||||
|  |   return vinmonopoletRepository.searchWinesByName(name, page).then(wines => | ||||||
|  |     res.json({ | ||||||
|  |       wines: wines, | ||||||
|  |       count: wines.length, | ||||||
|  |       page: page, | ||||||
|  |       success: true | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function wineByEAN(req, res) { | ||||||
|  |   const { ean } = req.params; | ||||||
|  |  | ||||||
|  |   return vinmonopoletRepository.searchByEAN(ean).then(wines => | ||||||
|  |     res.json({ | ||||||
|  |       wines: wines, | ||||||
|  |       success: true | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function wineById(req, res) { | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return vinmonopoletRepository.wineById(id).then(wines => | ||||||
|  |     res.json({ | ||||||
|  |       wine: wines[0], | ||||||
|  |       success: true | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function allStores(req, res) { | ||||||
|  |   return vinmonopoletRepository | ||||||
|  |     .allStores() | ||||||
|  |     .then(stores => | ||||||
|  |       res.send({ | ||||||
|  |         stores, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while fetch all vinmonopolet stores.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function searchStores(req, res) { | ||||||
|  |   const { name } = req.query; | ||||||
|  |  | ||||||
|  |   return vinmonopoletRepository | ||||||
|  |     .searchStoresByName(name) | ||||||
|  |     .then(stores => | ||||||
|  |       res.send({ | ||||||
|  |         stores, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while fetch all vinmonopolet stores.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   searchWines, | ||||||
|  |   wineByEAN, | ||||||
|  |   wineById, | ||||||
|  |   allStores, | ||||||
|  |   searchStores | ||||||
|  | }; | ||||||
							
								
								
									
										60
									
								
								api/controllers/wineController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								api/controllers/wineController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const wineRepository = require(path.join(__dirname, "../wine")); | ||||||
|  |  | ||||||
|  | const allWines = (req, res) => { | ||||||
|  |   // TODO add "includeWinners" | ||||||
|  |   let { limit } = req.query; | ||||||
|  |  | ||||||
|  |   if (limit && isNaN(limit)) { | ||||||
|  |     return res.status(400).send({ | ||||||
|  |       message: "If limit query parameter is provided it must be a number", | ||||||
|  |       success: false | ||||||
|  |     }); | ||||||
|  |   } else if (!!!isNaN(limit)) { | ||||||
|  |     limit = Number(limit); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return wineRepository | ||||||
|  |     .allWines(limit) | ||||||
|  |     .then(wines => | ||||||
|  |       res.send({ | ||||||
|  |         wines: wines, | ||||||
|  |         message: `All wines.`, | ||||||
|  |         success: true | ||||||
|  |       }) | ||||||
|  |     ) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         success: false, | ||||||
|  |         message: message || "Unable to fetch all wines." | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const wineById = (req, res) => { | ||||||
|  |   const { id } = req.params; | ||||||
|  |  | ||||||
|  |   return wineRepository | ||||||
|  |     .wineById(id) | ||||||
|  |     .then(wine => { | ||||||
|  |       res.send({ | ||||||
|  |         wine, | ||||||
|  |         success: true | ||||||
|  |       }); | ||||||
|  |     }) | ||||||
|  |     .catch(error => { | ||||||
|  |       const { statusCode, message } = error; | ||||||
|  |  | ||||||
|  |       return res.status(statusCode || 500).send({ | ||||||
|  |         message: message || "Unexpected error occured while fetching wine by id.", | ||||||
|  |         success: false | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   allWines, | ||||||
|  |   wineById | ||||||
|  | }; | ||||||
							
								
								
									
										349
									
								
								api/history.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								api/history.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,349 @@ | |||||||
|  | const path = require("path"); | ||||||
|  |  | ||||||
|  | const Winner = require(path.join(__dirname, "/schemas/Highscore")); | ||||||
|  | const wineRepository = require(path.join(__dirname, "/wine")); | ||||||
|  |  | ||||||
|  | class HistoryByDateNotFound extends Error { | ||||||
|  |   constructor(message = "History for given date not found.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "HistoryByDateNotFound"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class HistoryForUserNotFound extends Error { | ||||||
|  |   constructor(message = "History for given user not found.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "HistoryForUserNotFound"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // highscore | ||||||
|  | const addWinnerWithWine = async (winner, wine) => { | ||||||
|  |   const exisitingWinner = await Winner.findOne({ | ||||||
|  |     name: winner.name | ||||||
|  |   }); | ||||||
|  |   const savedWine = await wineRepository.addWine(wine); | ||||||
|  |  | ||||||
|  |   const date = new Date(); | ||||||
|  |   date.setHours(5, 0, 0, 0); | ||||||
|  |   const winObject = { | ||||||
|  |     date: date, | ||||||
|  |     wine: savedWine, | ||||||
|  |     color: winner.color | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   if (exisitingWinner == undefined) { | ||||||
|  |     const newWinner = new Winner({ | ||||||
|  |       name: winner.name, | ||||||
|  |       wins: [winObject] | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await newWinner.save(); | ||||||
|  |   } else { | ||||||
|  |     exisitingWinner.wins.push(winObject); | ||||||
|  |     exisitingWinner.markModified("wins"); | ||||||
|  |     await exisitingWinner.save(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return exisitingWinner; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // lottery | ||||||
|  | const all = (includeWines = false) => { | ||||||
|  |   if (includeWines === false) { | ||||||
|  |     return Winner.find().sort("-wins.date"); | ||||||
|  |   } else { | ||||||
|  |     return Winner.find() | ||||||
|  |       .sort("-wins.date") | ||||||
|  |       .populate("wins.wine"); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // lottery | ||||||
|  | const byDate = date => { | ||||||
|  |   const startQueryDate = new Date(date.setHours(0, 0, 0, 0)); | ||||||
|  |   const endQueryDate = new Date(date.setHours(24, 59, 59, 99)); | ||||||
|  |   const query = [ | ||||||
|  |     { | ||||||
|  |       $match: { | ||||||
|  |         "wins.date": { | ||||||
|  |           $gte: startQueryDate, | ||||||
|  |           $lte: endQueryDate | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { $unwind: "$wins" }, | ||||||
|  |     { | ||||||
|  |       $match: { | ||||||
|  |         "wins.date": { | ||||||
|  |           $gte: startQueryDate, | ||||||
|  |           $lte: endQueryDate | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $lookup: { | ||||||
|  |         from: "wines", | ||||||
|  |         localField: "wins.wine", | ||||||
|  |         foreignField: "_id", | ||||||
|  |         as: "wins.wine" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { $unwind: "$wins.wine" }, | ||||||
|  |     { | ||||||
|  |       $project: { | ||||||
|  |         name: "$name", | ||||||
|  |         date: "$wins.date", | ||||||
|  |         color: "$wins.color", | ||||||
|  |         wine: "$wins.wine" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   return Winner.aggregate(query).then(winners => { | ||||||
|  |     if (winners.length == 0) { | ||||||
|  |       throw new HistoryByDateNotFound(); | ||||||
|  |     } | ||||||
|  |     return winners; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // highscore | ||||||
|  | const byName = (name, sort = "desc") => { | ||||||
|  |   return Winner.findOne({ name }, ["name", "wins"]) | ||||||
|  |     .sort("-wins.date") | ||||||
|  |     .populate("wins.wine") | ||||||
|  |     .then(winner => { | ||||||
|  |       if (winner) { | ||||||
|  |         winner.wins = sort !== "asc" ? winner.wins.reverse() : winner.wins; | ||||||
|  |         return winner; | ||||||
|  |       } else { | ||||||
|  |         throw new HistoryForUserNotFound(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // highscore | ||||||
|  | const search = (query, sort = "desc") => { | ||||||
|  |   return Winner.find({ name: { $regex: query, $options: "i" } }, ["name"]).then(winners => { | ||||||
|  |     if (winners) { | ||||||
|  |       winners = sort === "desc" ? winners.reverse() : winners; | ||||||
|  |       return winners; | ||||||
|  |     } else { | ||||||
|  |       throw new HistoryForUserNotFound(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // lottery | ||||||
|  | const latest = () => { | ||||||
|  |   const query = [ | ||||||
|  |     { | ||||||
|  |       $unwind: "$wins" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $lookup: { | ||||||
|  |         from: "wines", | ||||||
|  |         localField: "wins.wine", | ||||||
|  |         foreignField: "_id", | ||||||
|  |         as: "wins.wine" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $group: { | ||||||
|  |         _id: "$wins.date", | ||||||
|  |         winners: { | ||||||
|  |           $push: { | ||||||
|  |             _id: "$_id", | ||||||
|  |             name: "$name", | ||||||
|  |             color: "$wins.color", | ||||||
|  |             wine: "$wins.wine" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $project: { | ||||||
|  |         date: "$_id", | ||||||
|  |         winners: "$winners" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $sort: { | ||||||
|  |         _id: -1 | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $limit: 1 | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   return Winner.aggregate(query).then(winners => winners[0]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // lottery - byDate | ||||||
|  | const groupByDate = (includeWines = false, sort = "asc") => { | ||||||
|  |   const sortDirection = sort == "asc" ? -1 : 1; | ||||||
|  |   const query = [ | ||||||
|  |     { | ||||||
|  |       $unwind: "$wins" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $group: { | ||||||
|  |         _id: "$wins.date", | ||||||
|  |         winners: { | ||||||
|  |           $push: { | ||||||
|  |             _id: "$_id", | ||||||
|  |             name: "$name", | ||||||
|  |             color: "$wins.color", | ||||||
|  |             wine: "$wins.wine" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $project: { | ||||||
|  |         date: "$_id", | ||||||
|  |         winners: "$winners" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $sort: { | ||||||
|  |         date: sortDirection | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   if (includeWines) { | ||||||
|  |     query.splice(1, 0, { | ||||||
|  |       $lookup: { | ||||||
|  |         from: "wines", | ||||||
|  |         localField: "wins.wine", | ||||||
|  |         foreignField: "_id", | ||||||
|  |         as: "wins.wine" | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return Winner.aggregate(query); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // highscore - byColor | ||||||
|  | const groupByColor = (includeWines = false) => { | ||||||
|  |   const query = [ | ||||||
|  |     { | ||||||
|  |       $unwind: "$wins" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $group: { | ||||||
|  |         _id: "$wins.color", | ||||||
|  |         winners: { | ||||||
|  |           $push: { | ||||||
|  |             _id: "$_id", | ||||||
|  |             name: "$name", | ||||||
|  |             date: "$wins.date", | ||||||
|  |             wine: "$wins.wine" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         count: { $sum: 1 } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $project: { | ||||||
|  |         color: "$_id", | ||||||
|  |         count: "$count", | ||||||
|  |         winners: "$winners" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $sort: { | ||||||
|  |         _id: -1 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   if (includeWines) { | ||||||
|  |     query.splice(1, 0, { | ||||||
|  |       $lookup: { | ||||||
|  |         from: "wines", | ||||||
|  |         localField: "wins.wine", | ||||||
|  |         foreignField: "_id", | ||||||
|  |         as: "wins.wine" | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return Winner.aggregate(query); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // highscore - byWineOccurences | ||||||
|  |  | ||||||
|  | // highscore - byWinCount | ||||||
|  | const orderByWins = (includeWines = false, limit = undefined) => { | ||||||
|  |   let query = [ | ||||||
|  |     { | ||||||
|  |       $project: { | ||||||
|  |         name: "$name", | ||||||
|  |         wins: "$wins", | ||||||
|  |         totalWins: { $size: "$wins" } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $sort: { | ||||||
|  |         totalWins: -1, | ||||||
|  |         "wins.date": -1 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   if (includeWines) { | ||||||
|  |     const includeWinesSubQuery = [ | ||||||
|  |       { | ||||||
|  |         $unwind: "$wins" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         $lookup: { | ||||||
|  |           from: "wines", | ||||||
|  |           localField: "wins.wine", | ||||||
|  |           foreignField: "_id", | ||||||
|  |           as: "wins.wine" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         $unwind: "$wins._id" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         $group: { | ||||||
|  |           _id: "$_id", | ||||||
|  |           name: { $first: "$name" }, | ||||||
|  |           totalWins: { $first: "$totalWins" }, | ||||||
|  |           wins: { $push: "$wins" } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     query = includeWinesSubQuery.concat(query); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return Winner.aggregate(query).then(winners => { | ||||||
|  |     if (limit == null) { | ||||||
|  |       return winners; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return winners.slice(0, limit); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   addWinnerWithWine, | ||||||
|  |   all, | ||||||
|  |   byDate, | ||||||
|  |   byName, | ||||||
|  |   search, | ||||||
|  |   latest, | ||||||
|  |   groupByDate, | ||||||
|  |   groupByColor, | ||||||
|  |   orderByWins | ||||||
|  | }; | ||||||
							
								
								
									
										369
									
								
								api/lottery.js
									
									
									
									
									
								
							
							
						
						
									
										369
									
								
								api/lottery.js
									
									
									
									
									
								
							| @@ -1,132 +1,263 @@ | |||||||
| const path = require('path'); | const path = require("path"); | ||||||
|  | const crypto = require("crypto"); | ||||||
|  |  | ||||||
| const Highscore = require(path.join(__dirname, '/schemas/Highscore')); | const Attendee = require(path.join(__dirname, "/schemas/Attendee")); | ||||||
| const Wine = require(path.join(__dirname, '/schemas/Wine')); | const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); | ||||||
|  | const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); | ||||||
|  | const Lottery = require(path.join(__dirname, "/schemas/Purchase")); | ||||||
|  |  | ||||||
| // Utils | const Message = require(path.join(__dirname, "/message")); | ||||||
| const epochToDateString = date => new Date(parseInt(date)).toDateString(); | const historyRepository = require(path.join(__dirname, "/history")); | ||||||
|  | const wineRepository = require(path.join(__dirname, "/wine")); | ||||||
|  |  | ||||||
| const sortNewestFirst = (lotteries) => { | const { | ||||||
|   return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1) |   WinnerNotFound, | ||||||
| } |   NoMoreAttendeesToWin, | ||||||
|  |   CouldNotFindNewWinnerAfterNTries, | ||||||
|  |   LotteryByDateNotFound | ||||||
|  | } = require(path.join(__dirname, "/vinlottisErrors")); | ||||||
|  |  | ||||||
| const groupHighscoreByDate = async (highscore=undefined) => { | const archive = (date, raffles, stolen, wines) => { | ||||||
|   if (highscore == undefined) |   const { blue, red, yellow, green } = raffles; | ||||||
|     highscore = await Highscore.find(); |   const bought = blue + red + yellow + green; | ||||||
|  |  | ||||||
|   const highscoreByDate = []; |   return Promise.all(wines.map(wine => wineRepository.findWine(wine))).then(resolvedWines => { | ||||||
|  |     const lottery = new Lottery({ | ||||||
|   highscore.forEach(person => { |  | ||||||
|     person.wins.map(win => { |  | ||||||
|       const epochDate = new Date(win.date).setHours(0,0,0,0); |  | ||||||
|       const winnerObject = { |  | ||||||
|         name: person.name, |  | ||||||
|         color: win.color, |  | ||||||
|         wine: win.wine, |  | ||||||
|         date: epochDate |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate) |  | ||||||
|       if (existingDateIndex > -1) |  | ||||||
|         highscoreByDate[existingDateIndex].winners.push(winnerObject); |  | ||||||
|       else |  | ||||||
|         highscoreByDate.push({ |  | ||||||
|           date: epochDate, |  | ||||||
|           winners: [winnerObject] |  | ||||||
|         }) |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   return sortNewestFirst(highscoreByDate); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const resolveWineReferences = (highscoreObject, key) => { |  | ||||||
|   const listWithWines = highscoreObject[key] |  | ||||||
|  |  | ||||||
|   return Promise.all(listWithWines.map(element => |  | ||||||
|       Wine.findById(element.wine) |  | ||||||
|         .then(wine => { |  | ||||||
|           element.wine = wine |  | ||||||
|           return element |  | ||||||
|         })) |  | ||||||
|     ) |  | ||||||
|     .then(resolvedListWithWines => { |  | ||||||
|       highscoreObject[key] = resolvedListWithWines; |  | ||||||
|       return highscoreObject |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| // end utils |  | ||||||
|  |  | ||||||
| // Routes |  | ||||||
| const all = (req, res) => { |  | ||||||
|   return Highscore.find() |  | ||||||
|     .then(highscore => groupHighscoreByDate(highscore)) |  | ||||||
|     .then(lotteries => res.send({ |  | ||||||
|       message: "Lotteries by date!", |  | ||||||
|       lotteries |  | ||||||
|     })) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const latest = (req, res) => { |  | ||||||
|   return groupHighscoreByDate() |  | ||||||
|     .then(lotteries => lotteries.shift()) // first element in list |  | ||||||
|     .then(latestLottery => resolveWineReferences(latestLottery, "winners")) |  | ||||||
|     .then(lottery => res.send({ |  | ||||||
|         message: "Latest lottery!", |  | ||||||
|         winners: lottery.winners |  | ||||||
|       }) |  | ||||||
|     ) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const byEpochDate = (req, res) => { |  | ||||||
|   let { date } = req.params; |  | ||||||
|   date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime() |  | ||||||
|   const dateString = epochToDateString(date); |  | ||||||
|  |  | ||||||
|   return groupHighscoreByDate() |  | ||||||
|     .then(lotteries => { |  | ||||||
|       const lottery = lotteries.filter(lottery => lottery.date == date) |  | ||||||
|       if (lottery.length > 0) { |  | ||||||
|         return lottery[0] |  | ||||||
|       } else { |  | ||||||
|         return res.status(404).send({ |  | ||||||
|           message: `No lottery found for date: ${ dateString }` |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|     .then(lottery => resolveWineReferences(lottery, "winners")) |  | ||||||
|     .then(lottery => res.send({ |  | ||||||
|       message: `Lottery for date: ${ dateString}`, |  | ||||||
|       date, |       date, | ||||||
|       winners: lottery.winners |       blue, | ||||||
|     })) |       red, | ||||||
| } |       yellow, | ||||||
|  |       green, | ||||||
|  |       bought, | ||||||
|  |       stolen, | ||||||
|  |       wines: resolvedWines | ||||||
|  |     }); | ||||||
|  |  | ||||||
| const byName = (req, res) => { |     return lottery.save(); | ||||||
|   const { name } = req.params; |   }); | ||||||
|   const regexName = new RegExp(name, "i"); // lowercase regex of the name | }; | ||||||
|  |  | ||||||
|   return Highscore.find({ name }) | const lotteryByDate = date => { | ||||||
|     .then(highscore => { |   const startOfDay = new Date(date.setHours(0, 0, 0, 0)); | ||||||
|       if (highscore.length > 0) { |   const endOfDay = new Date(date.setHours(24, 59, 59, 99)); | ||||||
|         return highscore[0] |  | ||||||
|       } else { |   const query = [ | ||||||
|         return res.status(404).send({ |     { | ||||||
|           message: `Name: ${ name } not found in leaderboards.` |       $match: { | ||||||
|         }) |         date: { | ||||||
|  |           $gte: startOfDay, | ||||||
|  |           $lte: endOfDay | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }) |     }, | ||||||
|     .then(highscore => resolveWineReferences(highscore, "wins")) |     { | ||||||
|     .then(highscore => res.send({ |       $lookup: { | ||||||
|       message: `Lottery winnings for name: ${ name }.`, |         from: "wines", | ||||||
|       name: highscore.name, |         localField: "wines", | ||||||
|       highscore: sortNewestFirst(highscore.wins) |         foreignField: "_id", | ||||||
|     })) |         as: "wines" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   const aggregateLottery = Lottery.aggregate(query); | ||||||
|  |   return aggregateLottery.project("-_id -__v").then(lotteries => { | ||||||
|  |     if (lotteries.length == 0) { | ||||||
|  |       throw new LotteryByDateNotFound(date); | ||||||
|  |     } | ||||||
|  |     return lotteries[0]; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const allLotteries = (sort = "asc", yearFilter = undefined) => { | ||||||
|  |   const sortDirection = sort == "asc" ? 1 : -1; | ||||||
|  |  | ||||||
|  |   let startQueryDate = new Date("1970-01-01"); | ||||||
|  |   let endQueryDate = new Date("2999-01-01"); | ||||||
|  |   if (yearFilter) { | ||||||
|  |     startQueryDate = new Date(`${yearFilter}-01-01`); | ||||||
|  |     endQueryDate = new Date(`${Number(yearFilter) + 1}-01-01`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const query = [ | ||||||
|  |     { | ||||||
|  |       $match: { | ||||||
|  |         date: { | ||||||
|  |           $gte: startQueryDate, | ||||||
|  |           $lte: endQueryDate | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $sort: { | ||||||
|  |         date: sortDirection | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $unset: ["_id", "__v"] | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       $lookup: { | ||||||
|  |         from: "wines", | ||||||
|  |         localField: "wines", | ||||||
|  |         foreignField: "_id", | ||||||
|  |         as: "wines" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   return Lottery.aggregate(query); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const allLotteriesIncludingWinners = async (sort = "asc", yearFilter = undefined) => { | ||||||
|  |   const lotteries = await allLotteries(sort, yearFilter); | ||||||
|  |   const allWinners = await historyRepository.groupByDate(false, sort); | ||||||
|  |  | ||||||
|  |   return lotteries.map(lottery => { | ||||||
|  |     const { winners } = allWinners.pop(); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       wines: lottery.wines, | ||||||
|  |       date: lottery.date, | ||||||
|  |       blue: lottery.blue, | ||||||
|  |       green: lottery.green, | ||||||
|  |       yellow: lottery.yellow, | ||||||
|  |       red: lottery.red, | ||||||
|  |       bought: lottery.bought, | ||||||
|  |       stolen: lottery.stolen, | ||||||
|  |       winners: winners | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const drawWinner = async () => { | ||||||
|  |   let allContestants = await Attendee.find({ winner: false }); | ||||||
|  |  | ||||||
|  |   if (allContestants.length == 0) { | ||||||
|  |     throw new NoMoreAttendeesToWin(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let raffleColors = []; | ||||||
|  |   for (let i = 0; i < allContestants.length; i++) { | ||||||
|  |     let currentContestant = allContestants[i]; | ||||||
|  |     for (let blue = 0; blue < currentContestant.blue; blue++) { | ||||||
|  |       raffleColors.push("blue"); | ||||||
|  |     } | ||||||
|  |     for (let red = 0; red < currentContestant.red; red++) { | ||||||
|  |       raffleColors.push("red"); | ||||||
|  |     } | ||||||
|  |     for (let green = 0; green < currentContestant.green; green++) { | ||||||
|  |       raffleColors.push("green"); | ||||||
|  |     } | ||||||
|  |     for (let yellow = 0; yellow < currentContestant.yellow; yellow++) { | ||||||
|  |       raffleColors.push("yellow"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   raffleColors = shuffle(raffleColors); | ||||||
|  |  | ||||||
|  |   let colorToChooseFrom = raffleColors[Math.floor(Math.random() * raffleColors.length)]; | ||||||
|  |   let findObject = { winner: false }; | ||||||
|  |  | ||||||
|  |   findObject[colorToChooseFrom] = { $gt: 0 }; | ||||||
|  |  | ||||||
|  |   let tries = 0; | ||||||
|  |   const maxTries = 3; | ||||||
|  |   let contestantsToChooseFrom = undefined; | ||||||
|  |   while (contestantsToChooseFrom == undefined && tries < maxTries) { | ||||||
|  |     const hit = await Attendee.find(findObject); | ||||||
|  |     if (hit && hit.length) { | ||||||
|  |       contestantsToChooseFrom = hit; | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     tries++; | ||||||
|  |   } | ||||||
|  |   if (contestantsToChooseFrom == undefined) { | ||||||
|  |     throw new CouldNotFindNewWinnerAfterNTries(maxTries); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let attendeeListDemocratic = []; | ||||||
|  |  | ||||||
|  |   let currentContestant; | ||||||
|  |   for (let i = 0; i < contestantsToChooseFrom.length; i++) { | ||||||
|  |     currentContestant = contestantsToChooseFrom[i]; | ||||||
|  |     for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) { | ||||||
|  |       attendeeListDemocratic.push({ | ||||||
|  |         name: currentContestant.name, | ||||||
|  |         phoneNumber: currentContestant.phoneNumber, | ||||||
|  |         red: currentContestant.red, | ||||||
|  |         blue: currentContestant.blue, | ||||||
|  |         green: currentContestant.green, | ||||||
|  |         yellow: currentContestant.yellow | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   attendeeListDemocratic = shuffle(attendeeListDemocratic); | ||||||
|  |  | ||||||
|  |   let winner = attendeeListDemocratic[Math.floor(Math.random() * attendeeListDemocratic.length)]; | ||||||
|  |  | ||||||
|  |   let newWinnerElement = new VirtualWinner({ | ||||||
|  |     name: winner.name, | ||||||
|  |     phoneNumber: winner.phoneNumber, | ||||||
|  |     color: colorToChooseFrom, | ||||||
|  |     red: winner.red, | ||||||
|  |     blue: winner.blue, | ||||||
|  |     green: winner.green, | ||||||
|  |     yellow: winner.yellow, | ||||||
|  |     id: sha512(winner.phoneNumber, genRandomString(10)), | ||||||
|  |     timestamp_drawn: new Date().getTime() | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   await newWinnerElement.save(); | ||||||
|  |   await Attendee.updateOne({ name: winner.name, phoneNumber: winner.phoneNumber }, { $set: { winner: true } }); | ||||||
|  |  | ||||||
|  |   let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ | ||||||
|  |     timestamp_drawn: 1 | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return { winner, color: colorToChooseFrom, winners }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** - - UTILS - - **/ | ||||||
|  | const genRandomString = function(length) { | ||||||
|  |   return crypto | ||||||
|  |     .randomBytes(Math.ceil(length / 2)) | ||||||
|  |     .toString("hex") /** convert to hexadecimal format */ | ||||||
|  |     .slice(0, length); /** return required number of characters */ | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const sha512 = function(password, salt) { | ||||||
|  |   var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */ | ||||||
|  |   hash.update(password); | ||||||
|  |   var value = hash.digest("hex"); | ||||||
|  |   return value; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function shuffle(array) { | ||||||
|  |   let currentIndex = array.length, | ||||||
|  |     temporaryValue, | ||||||
|  |     randomIndex; | ||||||
|  |  | ||||||
|  |   // While there remain elements to shuffle... | ||||||
|  |   while (0 !== currentIndex) { | ||||||
|  |     // Pick a remaining element... | ||||||
|  |     randomIndex = Math.floor(Math.random() * currentIndex); | ||||||
|  |     currentIndex -= 1; | ||||||
|  |  | ||||||
|  |     // And swap it with the current element. | ||||||
|  |     temporaryValue = array[currentIndex]; | ||||||
|  |     array[currentIndex] = array[randomIndex]; | ||||||
|  |     array[randomIndex] = temporaryValue; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return array; | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   all, |   drawWinner, | ||||||
|   latest, |   archive, | ||||||
|   byEpochDate, |   lotteryByDate, | ||||||
|   byName |   allLotteries, | ||||||
|  |   allLotteriesIncludingWinners | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										123
									
								
								api/message.js
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								api/message.js
									
									
									
									
									
								
							| @@ -2,34 +2,50 @@ const https = require("https"); | |||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const config = require(path.join(__dirname + "/../config/defaults/lottery")); | const config = require(path.join(__dirname + "/../config/defaults/lottery")); | ||||||
|  |  | ||||||
| const dateString = (date) => { | const dateString = date => { | ||||||
|   if (typeof(date) == "string") { |   if (typeof date == "string") { | ||||||
|     date = new Date(date); |     date = new Date(date); | ||||||
|   } |   } | ||||||
|   const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date) |   const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); | ||||||
|   const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date) |   const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); | ||||||
|   const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date) |   const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); | ||||||
|  |  | ||||||
|   return `${da}-${mo}-${ye}` |   return `${da}-${mo}-${ye}`; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | async function sendInitialMessageToWinners(winners) { | ||||||
|  |   const numbers = winners.map(winner => ({ msisdn: `47${winner.phoneNumber}` })); | ||||||
|  |  | ||||||
|  |   const body = { | ||||||
|  |     sender: "Vinlottis", | ||||||
|  |     message: "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!", | ||||||
|  |     recipients: numbers | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return gatewayRequest(body); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function sendWineSelectMessage(winnerObject) { | async function sendPrizeSelectionLink(winner) { | ||||||
|   winnerObject.timestamp_sent = new Date().getTime(); |   winner.timestamp_sent = new Date().getTime(); | ||||||
|   winnerObject.timestamp_limit = new Date().getTime() * 600000; |   winner.timestamp_limit = new Date().getTime() + 1000 * 600; | ||||||
|   await winnerObject.save(); |   await winner.save(); | ||||||
|  |  | ||||||
|   let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin"); |   const { id, name, phoneNumber } = winner; | ||||||
|  |   const url = new URL(`/#/winner/${id}`, "https://lottis.vin"); | ||||||
|  |   const message = `Gratulerer som heldig vinner av vinlotteriet ${name}! Her er linken for \ | ||||||
|  | å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst \ | ||||||
|  | i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.`; | ||||||
|  |  | ||||||
|   return sendMessageToUser( |   return sendMessageToNumber(phoneNumber, message); | ||||||
|     winnerObject.phoneNumber, |  | ||||||
|     `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Her er linken for å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.)` |  | ||||||
|   ) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| async function sendWineConfirmation(winnerObject, wineObject, date) { | async function sendWineConfirmation(winnerObject, wineObject, date) { | ||||||
|   date = dateString(date); |   date = dateString(date); | ||||||
|   return sendMessageToUser(winnerObject.phoneNumber, |   return sendMessageToNumber( | ||||||
|     `Bekreftelse på din vin ${ winnerObject.name }.\nDato vunnet: ${ date }.\nVin valgt: ${ wineObject.name }.\nDu vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`) |     winnerObject.phoneNumber, | ||||||
|  |     `Bekreftelse på din vin ${winnerObject.name}.\nDato vunnet: ${date}.\nVin valgt: ${wineObject.name}.\ | ||||||
|  | \nDu vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!` | ||||||
|  |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function sendLastWinnerMessage(winnerObject, wineObject) { | async function sendLastWinnerMessage(winnerObject, wineObject) { | ||||||
| @@ -38,84 +54,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) { | |||||||
|   winnerObject.timestamp_limit = new Date().getTime(); |   winnerObject.timestamp_limit = new Date().getTime(); | ||||||
|   await winnerObject.save(); |   await winnerObject.save(); | ||||||
|  |  | ||||||
|   return sendMessageToUser( |   return sendMessageToNumber( | ||||||
|     winnerObject.phoneNumber, |     winnerObject.phoneNumber, | ||||||
|     `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!` |     `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, \ | ||||||
|  | du vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!` | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function sendWineSelectMessageTooLate(winnerObject) { | async function sendWineSelectMessageTooLate(winnerObject) { | ||||||
|   return sendMessageToUser( |   return sendMessageToNumber( | ||||||
|     winnerObject.phoneNumber, |     winnerObject.phoneNumber, | ||||||
|     `Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.` |     `Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor \ | ||||||
|  | puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.` | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function sendMessageToUser(phoneNumber, message) { | async function sendMessageToNumber(phoneNumber, message) { | ||||||
|   console.log(`Attempting to send message to ${ phoneNumber }.`) |   console.log(`Attempting to send message to ${phoneNumber}.`); | ||||||
|  |  | ||||||
|   const body = { |   const body = { | ||||||
|     sender: "Vinlottis", |     sender: "Vinlottis", | ||||||
|     message: message, |     message: message, | ||||||
|     recipients: [{ msisdn: `47${ phoneNumber }`}] |     recipients: [{ msisdn: `47${phoneNumber}` }] | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return gatewayRequest(body); |   return gatewayRequest(body); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| async function sendInitialMessageToWinners(winners) { |  | ||||||
|   let numbers = []; |  | ||||||
|   for (let i = 0; i < winners.length; i++) { |  | ||||||
|     numbers.push({ msisdn: `47${winners[i].phoneNumber}` }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const body = { |  | ||||||
|     sender: "Vinlottis", |  | ||||||
|     message: |  | ||||||
|       "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!", |  | ||||||
|     recipients: numbers |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return gatewayRequest(body); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function gatewayRequest(body) { | async function gatewayRequest(body) { | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     const options = { |     const options = { | ||||||
|       hostname: "gatewayapi.com", |       hostname: "gatewayapi.com", | ||||||
|       post: 443, |       post: 443, | ||||||
|       path: `/rest/mtsms?token=${ config.gatewayToken }`, |       path: `/rest/mtsms?token=${config.gatewayToken}`, | ||||||
|       method: "POST", |       method: "POST", | ||||||
|       headers: { |       headers: { | ||||||
|         "Content-Type": "application/json" |         "Content-Type": "application/json" | ||||||
|       } |       } | ||||||
|     } |     }; | ||||||
|  |  | ||||||
|     const req = https.request(options, (res) => { |     const req = https.request(options, res => { | ||||||
|       console.log(`statusCode: ${ res.statusCode }`); |       console.log(`statusCode: ${res.statusCode}`); | ||||||
|       console.log(`statusMessage: ${ res.statusMessage }`); |       console.log(`statusMessage: ${res.statusMessage}`); | ||||||
|  |  | ||||||
|       res.setEncoding('utf8'); |       res.setEncoding("utf8"); | ||||||
|  |  | ||||||
|       if (res.statusCode == 200) { |       if (res.statusCode == 200) { | ||||||
|         res.on("data", (data) => { |         res.on("data", data => { | ||||||
|           console.log("Response from message gateway:", data) |           console.log("Response from message gateway:", data); | ||||||
|  |  | ||||||
|           resolve(JSON.parse(data)) |           resolve(JSON.parse(data)); | ||||||
|         }); |         }); | ||||||
|       } else { |       } else { | ||||||
|         res.on("data", (data) => { |         res.on("data", data => { | ||||||
|           data = JSON.parse(data); |           data = JSON.parse(data); | ||||||
|           return reject('Gateway error: ' + data['message'] || data) |           return reject("Gateway error: " + data["message"] || data); | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     }) |     }); | ||||||
|  |  | ||||||
|     req.on("error", (error) => { |     req.on("error", error => { | ||||||
|       console.error(`Error from sms service: ${ error }`); |       console.error(`Error from sms service: ${error}`); | ||||||
|       reject(`Error from sms service: ${ error }`); |       reject(`Error from sms service: ${error}`); | ||||||
|     }) |     }); | ||||||
|  |  | ||||||
|     req.write(JSON.stringify(body)); |     req.write(JSON.stringify(body)); | ||||||
|     req.end(); |     req.end(); | ||||||
| @@ -123,9 +124,9 @@ async function gatewayRequest(body) { | |||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   sendWineSelectMessage, |   sendInitialMessageToWinners, | ||||||
|  |   sendPrizeSelectionLink, | ||||||
|   sendWineConfirmation, |   sendWineConfirmation, | ||||||
|   sendLastWinnerMessage, |   sendLastWinnerMessage, | ||||||
|   sendWineSelectMessageTooLate, |   sendWineSelectMessageTooLate | ||||||
|   sendInitialMessageToWinners | }; | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								api/middleware/alwaysAuthenticatedWhenLocalhost.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								api/middleware/alwaysAuthenticatedWhenLocalhost.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | const alwaysAuthenticatedWhenLocalhost = (req, res, next) => { | ||||||
|  |   req.isAuthenticated = () => true; | ||||||
|  |   return next(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = alwaysAuthenticatedWhenLocalhost; | ||||||
| @@ -1,10 +1,4 @@ | |||||||
| const mustBeAuthenticated = (req, res, next) => { | const mustBeAuthenticated = (req, res, next) => { | ||||||
|   if (process.env.NODE_ENV == "development") { |  | ||||||
|     console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`) |  | ||||||
|     req.isAuthenticated = () => true; |  | ||||||
|     return next(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (!req.isAuthenticated()) { |   if (!req.isAuthenticated()) { | ||||||
|     return res.status(401).send({ |     return res.status(401).send({ | ||||||
|       success: false, |       success: false, | ||||||
|   | |||||||
							
								
								
									
										103
									
								
								api/prelotteryWine.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								api/prelotteryWine.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | const path = require("path"); | ||||||
|  |  | ||||||
|  | const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); | ||||||
|  | const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); | ||||||
|  |  | ||||||
|  | const allWines = () => { | ||||||
|  |   return PreLotteryWine.find().populate("winner"); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const allWinesWithoutWinner = () => { | ||||||
|  |   return PreLotteryWine.find({ winner: { $exists: false } }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addWines = wines => { | ||||||
|  |   const prelotteryWines = wines.map(wine => { | ||||||
|  |     let newPrelotteryWine = new PreLotteryWine({ | ||||||
|  |       name: wine.name, | ||||||
|  |       vivinoLink: wine.vivinoLink, | ||||||
|  |       rating: wine.rating, | ||||||
|  |       year: wine.year, | ||||||
|  |       image: wine.image, | ||||||
|  |       price: wine.price, | ||||||
|  |       country: wine.country, | ||||||
|  |       id: wine.id | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return newPrelotteryWine.save(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return Promise.all(prelotteryWines); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const wineById = id => { | ||||||
|  |   return PreLotteryWine.findOne({ _id: id }).then(wine => { | ||||||
|  |     if (wine == null) { | ||||||
|  |       throw new WineNotFound(); | ||||||
|  |     } | ||||||
|  |     return wine; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateWineById = (id, updateModel) => { | ||||||
|  |   return PreLotteryWine.findOne({ _id: id }).then(wine => { | ||||||
|  |     if (wine == null) { | ||||||
|  |       throw new WineNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const updatedWine = { | ||||||
|  |       name: updateModel.name != null ? updateModel.name : wine.name, | ||||||
|  |       vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink, | ||||||
|  |       rating: updateModel.rating != null ? updateModel.rating : wine.rating, | ||||||
|  |       year: updateModel.year != null ? updateModel.year : wine.year, | ||||||
|  |       image: updateModel.image != null ? updateModel.image : wine.image, | ||||||
|  |       price: updateModel.price != null ? updateModel.price : wine.price, | ||||||
|  |       country: updateModel.country != null ? updateModel.country : wine.country, | ||||||
|  |       id: updateModel.id != null ? updateModel.id : wine.id | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addWinnerToWine = (wine, winner) => { | ||||||
|  |   wine.winner = winner; | ||||||
|  |   winner.prize_selected = true; | ||||||
|  |   return Promise.all([wine.save(), winner.save()]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWineById = id => { | ||||||
|  |   return PreLotteryWine.findOne({ _id: id }).then(wine => { | ||||||
|  |     if (wine == null) { | ||||||
|  |       throw new WineNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWines = () => { | ||||||
|  |   return PreLotteryWine.deleteMany(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const wineSchema = () => { | ||||||
|  |   let schema = { ...PreLotteryWine.schema.obj }; | ||||||
|  |   let nulledSchema = Object.keys(schema).reduce((accumulator, current) => { | ||||||
|  |     accumulator[current] = ""; | ||||||
|  |     return accumulator; | ||||||
|  |   }, {}); | ||||||
|  |  | ||||||
|  |   return Promise.resolve(nulledSchema); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   allWines, | ||||||
|  |   allWinesWithoutWinner, | ||||||
|  |   addWines, | ||||||
|  |   wineById, | ||||||
|  |   addWinnerToWine, | ||||||
|  |   updateWineById, | ||||||
|  |   deleteWineById, | ||||||
|  |   deleteWines, | ||||||
|  |   wineSchema | ||||||
|  | }; | ||||||
							
								
								
									
										110
									
								
								api/prizeDistribution.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								api/prizeDistribution.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | const path = require("path"); | ||||||
|  |  | ||||||
|  | const Wine = require(path.join(__dirname, "/schemas/Wine")); | ||||||
|  | const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); | ||||||
|  | const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); | ||||||
|  |  | ||||||
|  | const message = require(path.join(__dirname, "/message")); | ||||||
|  | const historyRepository = require(path.join(__dirname, "/history")); | ||||||
|  | const winnerRepository = require(path.join(__dirname, "/winner")); | ||||||
|  | const wineRepository = require(path.join(__dirname, "/wine")); | ||||||
|  | const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine")); | ||||||
|  |  | ||||||
|  | const { WinnerNotFound, WineSelectionWinnerNotNextInLine, WinnersTimelimitExpired } = require(path.join( | ||||||
|  |   __dirname, | ||||||
|  |   "/vinlottisErrors" | ||||||
|  | )); | ||||||
|  |  | ||||||
|  | const verifyWinnerNextInLine = async id => { | ||||||
|  |   let foundWinner = await VirtualWinner.findOne({ id: id }); | ||||||
|  |  | ||||||
|  |   if (!foundWinner) { | ||||||
|  |     throw new WinnerNotFound(); | ||||||
|  |   } else if (foundWinner.timestamp_limit < new Date().getTime()) { | ||||||
|  |     throw new WinnersTimelimitExpired(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     foundWinner.timestamp_limit == undefined || | ||||||
|  |     foundWinner.timestamp_sent == undefined || | ||||||
|  |     foundWinner.prize_selected == true | ||||||
|  |   ) { | ||||||
|  |     throw new WineSelectionWinnerNotNextInLine(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return Promise.resolve(foundWinner); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const claimPrize = (wine, winner) => { | ||||||
|  |   return wineRepository | ||||||
|  |     .addWine(wine) | ||||||
|  |     .then(_ => prelotteryWineRepository.addWinnerToWine(wine, winner)) // prelotteryWine.deleteById | ||||||
|  |     .then(_ => historyRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine | ||||||
|  |     .then(_ => message.sendWineConfirmation(winner, wine)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const notifyNextWinner = async () => { | ||||||
|  |   let nextWinner = undefined; | ||||||
|  |  | ||||||
|  |   const winnersLeft = await VirtualWinner.find({ prize_selected: false }).sort({ timestamp_drawn: 1 }); | ||||||
|  |   const winesLeft = await PreLotteryWine.find({ winner: { $exists: false } }); | ||||||
|  |  | ||||||
|  |   if (winnersLeft.length > 1) { | ||||||
|  |     console.log("multiple winners left, choose next in line"); | ||||||
|  |     nextWinner = winnersLeft[0]; // multiple winners left, choose next in line | ||||||
|  |   } else if (winnersLeft.length == 1 && winesLeft.length > 1) { | ||||||
|  |     console.log("one winner left, but multiple wines"); | ||||||
|  |     nextWinner = winnersLeft[0]; // one winner left, but multiple wines | ||||||
|  |   } else if (winnersLeft.length == 1 && winesLeft.length == 1) { | ||||||
|  |     console.log("one winner and one wine left, choose for user"); | ||||||
|  |     nextWinner = winnersLeft[0]; // one winner and one wine left, choose for user | ||||||
|  |     wine = winesLeft[0]; | ||||||
|  |     return claimPrize(wine, nextWinner); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (nextWinner) { | ||||||
|  |     return message.sendPrizeSelectionLink(nextWinner).then(_ => startTimeout(nextWinner.id)); | ||||||
|  |   } else { | ||||||
|  |     console.info("All winners notified. Could start cleanup here."); | ||||||
|  |     return Promise.resolve({ | ||||||
|  |       message: "All winners notified." | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // these need to be register somewhere to cancel if something | ||||||
|  | // goes wrong and we want to start prize distribution again | ||||||
|  | function startTimeout(id) { | ||||||
|  |   const minute = 60000; | ||||||
|  |   const minutesForTimeout = 10; | ||||||
|  |  | ||||||
|  |   console.log(`Starting timeout for user ${id}.`); | ||||||
|  |   console.log(`Timeout duration: ${minutesForTimeout * minute}`); | ||||||
|  |   setTimeout(async () => { | ||||||
|  |     let virtualWinner = await VirtualWinner.findOne({ id: id, prize_selected: false }); | ||||||
|  |     if (!virtualWinner) { | ||||||
|  |       console.log(`Timeout done for user ${id}, but user has already sent data.`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     console.log(`Timeout done for user ${id}, sending update to user.`); | ||||||
|  |  | ||||||
|  |     message.sendWineSelectMessageTooLate(virtualWinner); | ||||||
|  |  | ||||||
|  |     virtualWinner.timestamp_drawn = new Date().getTime(); | ||||||
|  |     virtualWinner.timestamp_limit = null; | ||||||
|  |     virtualWinner.timestamp_sent = null; | ||||||
|  |     await virtualWinner.save(); | ||||||
|  |  | ||||||
|  |     notifyNextWinner(); | ||||||
|  |   }, minutesForTimeout * minute); | ||||||
|  |  | ||||||
|  |   return Promise.resolve(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   verifyWinnerNextInLine, | ||||||
|  |   claimPrize, | ||||||
|  |   notifyNextWinner | ||||||
|  | }; | ||||||
| @@ -1,41 +1,20 @@ | |||||||
| const express = require("express"); |  | ||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const RequestedWine = require(path.join( | const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine")); | ||||||
|   __dirname, "/schemas/RequestedWine" | const Wine = require(path.join(__dirname, "/schemas/Wine")); | ||||||
| )); |  | ||||||
| const Wine = require(path.join( |  | ||||||
|   __dirname, "/schemas/Wine" |  | ||||||
| )); |  | ||||||
|  |  | ||||||
| const deleteRequestedWineById = async (req, res) => { | class RequestedWineNotFound extends Error { | ||||||
|   const { id } = req.params; |   constructor(message = "Wine with this id was not found.") { | ||||||
|   if(id == null){ |     super(message); | ||||||
|     return res.json({ |     this.name = "RequestedWineNotFound"; | ||||||
|       message: "Id er ikke definert", |     this.statusCode = 404; | ||||||
|       success: false |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   await RequestedWine.deleteOne({wineId: id}) |  | ||||||
|   return res.json({ |  | ||||||
|     message: `Slettet vin med id: ${id}`, |  | ||||||
|     success: true |  | ||||||
|   }); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const getAllRequestedWines = async (req, res) => { | const addNew = async wine => { | ||||||
|   const allWines = await RequestedWine.find({}).populate("wine"); |   let foundWine = await Wine.findOne({ id: wine.id }); | ||||||
|  |  | ||||||
|   return res.json(allWines); |   if (foundWine == undefined) { | ||||||
| } |     foundWine = new Wine({ | ||||||
|  |  | ||||||
| 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, |       name: wine.name, | ||||||
|       vivinoLink: wine.vivinoLink, |       vivinoLink: wine.vivinoLink, | ||||||
|       rating: null, |       rating: null, | ||||||
| @@ -43,27 +22,47 @@ const requestNewWine = async (req, res) => { | |||||||
|       image: wine.image, |       image: wine.image, | ||||||
|       id: wine.id |       id: wine.id | ||||||
|     }); |     }); | ||||||
|     await thisWineIsLOKO.save() |     await foundWine.save(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   let requestedWine = await RequestedWine.findOne({ "wineId": wine.id}) |   let requestedWine = await RequestedWine.findOne({ wineId: wine.id }); | ||||||
|  |  | ||||||
|   if(requestedWine == undefined){ |   if (requestedWine == undefined) { | ||||||
|     requestedWine = new RequestedWine({ |     requestedWine = new RequestedWine({ | ||||||
|       count: 1, |       count: 1, | ||||||
|       wineId: wine.id, |       wineId: wine.id, | ||||||
|       wine: thisWineIsLOKO |       wine: foundWine | ||||||
|     }) |     }); | ||||||
|   } else { |   } else { | ||||||
|     requestedWine.count += 1; |     requestedWine.count += 1; | ||||||
|   } |   } | ||||||
|   await requestedWine.save() |   await requestedWine.save(); | ||||||
|  |  | ||||||
|   return res.send(requestedWine); |   return requestedWine; | ||||||
| } | }; | ||||||
|  |  | ||||||
|  | const getById = id => { | ||||||
|  |   return RequestedWine.findOne({ wineId: id }) | ||||||
|  |     .populate("wine") | ||||||
|  |     .then(wine => { | ||||||
|  |       if (wine == null) { | ||||||
|  |         throw new RequestedWineNotFound(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return wine; | ||||||
|  |     }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteById = id => { | ||||||
|  |   return getById(id).then(requestedWine => RequestedWine.deleteOne({ _id: requestedWine._id })); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getAll = () => { | ||||||
|  |   return RequestedWine.find({}).populate("wine"); | ||||||
|  | }; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   requestNewWine, |   addNew, | ||||||
|   getAllRequestedWines, |   getAll, | ||||||
|   deleteRequestedWineById |   deleteById | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										154
									
								
								api/retrieve.js
									
									
									
									
									
								
							
							
						
						
									
										154
									
								
								api/retrieve.js
									
									
									
									
									
								
							| @@ -1,154 +0,0 @@ | |||||||
| const path = require("path"); |  | ||||||
|  |  | ||||||
| const Purchase = require(path.join(__dirname, "/schemas/Purchase")); |  | ||||||
| const Wine = require(path.join(__dirname, "/schemas/Wine")); |  | ||||||
| const Highscore = require(path.join(__dirname, "/schemas/Highscore")); |  | ||||||
| const PreLotteryWine = require(path.join( |  | ||||||
|   __dirname, "/schemas/PreLotteryWine" |  | ||||||
| )); |  | ||||||
|  |  | ||||||
| const prelotteryWines = async (req, res) => { |  | ||||||
|   let wines = await PreLotteryWine.find(); |  | ||||||
|   return res.json(wines); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const allPurchase = async (req, res) => { |  | ||||||
|   let purchases = await Purchase.find() |  | ||||||
|     .populate("wines") |  | ||||||
|     .sort({ date: 1 }); |  | ||||||
|   return res.json(purchases); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const purchaseByColor = async (req, res) => { |  | ||||||
|   const countColor = await Purchase.find(); |  | ||||||
|   let red = 0; |  | ||||||
|   let blue = 0; |  | ||||||
|   let yellow = 0; |  | ||||||
|   let green = 0; |  | ||||||
|   let stolen = 0; |  | ||||||
|   for (let i = 0; i < countColor.length; i++) { |  | ||||||
|     let element = countColor[i]; |  | ||||||
|     red += element.red; |  | ||||||
|     blue += element.blue; |  | ||||||
|     yellow += element.yellow; |  | ||||||
|     green += element.green; |  | ||||||
|     if (element.stolen != undefined) { |  | ||||||
|       stolen += element.stolen; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const highscore = await Highscore.find(); |  | ||||||
|   let redWin = 0; |  | ||||||
|   let blueWin = 0; |  | ||||||
|   let yellowWin = 0; |  | ||||||
|   let greenWin = 0; |  | ||||||
|   for (let i = 0; i < highscore.length; i++) { |  | ||||||
|     let element = highscore[i]; |  | ||||||
|     for (let y = 0; y < element.wins.length; y++) { |  | ||||||
|       let currentWin = element.wins[y]; |  | ||||||
|       switch (currentWin.color) { |  | ||||||
|         case "blue": |  | ||||||
|           blueWin += 1; |  | ||||||
|           break; |  | ||||||
|         case "red": |  | ||||||
|           redWin += 1; |  | ||||||
|           break; |  | ||||||
|         case "yellow": |  | ||||||
|           yellowWin += 1; |  | ||||||
|           break; |  | ||||||
|         case "green": |  | ||||||
|           greenWin += 1; |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const total = red + yellow + blue + green; |  | ||||||
|  |  | ||||||
|   return res.json({ |  | ||||||
|     red: { |  | ||||||
|       total: red, |  | ||||||
|       win: redWin |  | ||||||
|     }, |  | ||||||
|     blue: { |  | ||||||
|       total: blue, |  | ||||||
|       win: blueWin |  | ||||||
|     }, |  | ||||||
|     green: { |  | ||||||
|       total: green, |  | ||||||
|       win: greenWin |  | ||||||
|     }, |  | ||||||
|     yellow: { |  | ||||||
|       total: yellow, |  | ||||||
|       win: yellowWin |  | ||||||
|     }, |  | ||||||
|     stolen: stolen, |  | ||||||
|     total: total |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const highscore = async (req, res) => { |  | ||||||
|   const highscore = await Highscore.find().populate("wins.wine"); |  | ||||||
|  |  | ||||||
|   return res.json(highscore); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const allWines = async (req, res) => { |  | ||||||
|   const wines = await Wine.find(); |  | ||||||
|  |  | ||||||
|   return res.json(wines); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const allWinesSummary = async (req, res) => { |  | ||||||
|   const highscore = await Highscore.find().populate("wins.wine"); |  | ||||||
|   let wines = {}; |  | ||||||
|  |  | ||||||
|   for (let i = 0; i < highscore.length; i++) { |  | ||||||
|     let person = highscore[i]; |  | ||||||
|     for (let y = 0; y < person.wins.length; y++) { |  | ||||||
|       let wine = person.wins[y].wine; |  | ||||||
|       let date = person.wins[y].date; |  | ||||||
|       let color = person.wins[y].color; |  | ||||||
|  |  | ||||||
|       if (wines[wine._id] == undefined) { |  | ||||||
|         wines[wine._id] = { |  | ||||||
|           name: wine.name, |  | ||||||
|           occurences: wine.occurences, |  | ||||||
|           vivinoLink: wine.vivinoLink, |  | ||||||
|           rating: wine.rating, |  | ||||||
|           image: wine.image, |  | ||||||
|           id: wine.id, |  | ||||||
|           _id: wine._id, |  | ||||||
|           dates: [date], |  | ||||||
|           winners: [person.name], |  | ||||||
|           red: 0, |  | ||||||
|           blue: 0, |  | ||||||
|           green: 0, |  | ||||||
|           yellow: 0 |  | ||||||
|         }; |  | ||||||
|         wines[wine._id][color] += 1; |  | ||||||
|       } else { |  | ||||||
|         wines[wine._id].dates.push(date); |  | ||||||
|         wines[wine._id].winners.push(person.name); |  | ||||||
|         if (wines[wine._id][color] == undefined) { |  | ||||||
|           wines[wine._id][color] = 1; |  | ||||||
|         } else { |  | ||||||
|           wines[wine._id][color] += 1; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   wines = Object.values(wines).reverse() |  | ||||||
|  |  | ||||||
|   return res.json(wines); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|   prelotteryWines, |  | ||||||
|   allPurchase, |  | ||||||
|   purchaseByColor, |  | ||||||
|   highscore, |  | ||||||
|   allWines, |  | ||||||
|   allWinesSummary |  | ||||||
| }; |  | ||||||
							
								
								
									
										130
									
								
								api/router.js
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								api/router.js
									
									
									
									
									
								
							| @@ -4,67 +4,97 @@ const path = require("path"); | |||||||
| const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated")); | const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated")); | ||||||
| const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated")); | const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated")); | ||||||
|  |  | ||||||
| const update = require(path.join(__dirname, "/update")); | const requestController = require(path.join(__dirname, "/controllers/requestController")); | ||||||
| const retrieve = require(path.join(__dirname, "/retrieve")); | const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController")); | ||||||
| const request = require(path.join(__dirname, "/request")); | const chatController = require(path.join(__dirname, "/controllers/chatController")); | ||||||
| const subscriptionApi = require(path.join(__dirname, "/subscriptions")); | const userController = require(path.join(__dirname, "/controllers/userController")); | ||||||
| const userApi = require(path.join(__dirname, "/user")); | const historyController = require(path.join(__dirname, "/controllers/historyController")); | ||||||
| const wineinfo = require(path.join(__dirname, "/wineinfo")); | const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController")); | ||||||
| const virtualApi = require(path.join(__dirname, "/virtualLottery")); | const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController")); | ||||||
| const virtualRegistrationApi = require(path.join( | const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController")); | ||||||
|   __dirname, "/virtualRegistration" | const lotteryController = require(path.join(__dirname, "/controllers/lotteryController")); | ||||||
| )); | const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController")); | ||||||
| const lottery = require(path.join(__dirname, "/lottery")); | const wineController = require(path.join(__dirname, "/controllers/wineController")); | ||||||
| const chatHistoryApi = require(path.join(__dirname, "/chatHistory")); | const messageController = require(path.join(__dirname, "/controllers/messageController")); | ||||||
|  |  | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
|  |  | ||||||
| router.get("/wineinfo/search", wineinfo.wineSearch); | router.get("/vinmonopolet/wine/search", vinmonopoletController.searchWines); | ||||||
|  | router.get("/vinmonopolet/wine/by-ean/:ean", vinmonopoletController.wineByEAN); | ||||||
|  | router.get("/vinmonopolet/wine/by-id/:id", vinmonopoletController.wineById); | ||||||
|  | router.get("/vinmonopolet/stores/", vinmonopoletController.allStores); | ||||||
|  | router.get("/vinmonopolet/stores/search", vinmonopoletController.searchStores); | ||||||
|  |  | ||||||
| router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines); | router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests); | ||||||
| router.post("/request/new-wine", request.requestNewWine); | router.post("/request", requestController.addRequest); | ||||||
| router.delete("/request/:id", request.deleteRequestedWineById); | router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest); | ||||||
|  |  | ||||||
| router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); | router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences | ||||||
| router.get("/wineinfo/:ean", wineinfo.byEAN); | router.get("/wine/:id", wineController.wineById); // sort = by-date, by-name, by-occurences | ||||||
|  | // router.update("/wine/:id", mustBeAuthenticated, wineController.update); | ||||||
|  |  | ||||||
| router.post("/log/wines", mustBeAuthenticated, update.submitWines); | router.get("/history", historyController.all); | ||||||
| router.post("/lottery", update.submitLottery); | router.get("/history/latest", historyController.latest); | ||||||
| router.post("/lottery/wines", update.submitWinesToLottery); | router.get("/history/by-wins/", historyController.orderByWins); | ||||||
| // router.delete("/lottery/wine/:id", update.deleteWineFromLottery); | router.get("/history/by-color/", historyController.groupByColor); | ||||||
| router.post("/lottery/winners", update.submitWinnersToLottery); | router.get("/history/by-date/:date", historyController.byDate); | ||||||
|  | router.get("/history/by-name/:name", historyController.byName); | ||||||
|  | router.get("/history/search/", historyController.search); | ||||||
|  | router.get("/history/by-date/", historyController.groupByDate); | ||||||
|  |  | ||||||
| router.get("/wines/prelottery", retrieve.prelotteryWines); | // router.get("/purchases", purchaseController.lotteryPurchases); | ||||||
| router.get("/purchase/statistics", retrieve.allPurchase); | // // returns list per date and count of each colors that where bought | ||||||
| router.get("/purchase/statistics/color", retrieve.purchaseByColor); | // router.get("/purchases/summary", purchaseController.lotteryPurchases); | ||||||
| router.get("/highscore/statistics", retrieve.highscore) | // // returns total, wins?, stolen | ||||||
| router.get("/wines/statistics", retrieve.allWines); | // router.get("/purchase/:date", purchaseController.lotteryPurchaseByDate); | ||||||
| router.get("/wines/statistics/overall", retrieve.allWinesSummary); |  | ||||||
|  |  | ||||||
| router.get("/lottery/all", lottery.all); | router.get("/lottery/wines", prelotteryWineController.allWines); | ||||||
| router.get("/lottery/latest", lottery.latest); | router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema); | ||||||
| router.get("/lottery/by-date/:date", lottery.byEpochDate); | router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById); | ||||||
| router.get("/lottery/by-name/:name", lottery.byName); | router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines); | ||||||
|  | router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines); | ||||||
|  | router.put("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.updateWineById); | ||||||
|  | router.delete("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.deleteWineById); | ||||||
|  |  | ||||||
| router.delete('/virtual/winner/all', mustBeAuthenticated, virtualApi.deleteWinners); | router.get("/lottery/attendees", setAdminHeaderIfAuthenticated, attendeeController.allAttendees); | ||||||
| router.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees); | router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees); | ||||||
| router.get('/virtual/winner/draw', virtualApi.drawWinner); | router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee); | ||||||
| router.get('/virtual/winner/all', virtualApi.winners); | router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById); | ||||||
| router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure); | router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById); | ||||||
| router.post('/virtual/finish', mustBeAuthenticated, virtualApi.finish); |  | ||||||
| router.get('/virtual/attendee/all', virtualApi.attendees); |  | ||||||
| router.get('/virtual/attendee/all/secure', mustBeAuthenticated, virtualApi.attendeesSecure); |  | ||||||
| router.post('/virtual/attendee/add', mustBeAuthenticated, virtualApi.addAttendee); |  | ||||||
|  |  | ||||||
| router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById); | router.get("/lottery/winners", winnerController.allWinners); | ||||||
| router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById); | router.get("/lottery/winner/:id", winnerController.winnerById); | ||||||
| router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection); | router.post("/lottery/winners", mustBeAuthenticated, winnerController.addWinners); | ||||||
|  | router.delete("/lottery/winners", mustBeAuthenticated, winnerController.deleteWinners); | ||||||
|  | router.put("/lottery/winner/:id", mustBeAuthenticated, winnerController.updateWinnerById); | ||||||
|  | router.delete("/lottery/winner/:id", mustBeAuthenticated, winnerController.deleteWinnerById); | ||||||
|  |  | ||||||
| router.get('/chat/history', chatHistoryApi.getAllHistory) | router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner); | ||||||
| router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory) | router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery); | ||||||
|  | router.get("/lottery/:epoch", lotteryController.lotteryByDate); | ||||||
|  | router.get("/lotteries/", lotteryController.allLotteries); | ||||||
|  |  | ||||||
| router.post('/login', userApi.login); | // router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status); | ||||||
| router.post('/register', mustBeAuthenticated, userApi.register); | router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start); | ||||||
| router.get('/logout', userApi.logout); | // router.post("/lottery/prize-distribution/stop", mustBeAuthenticated, prizeDistributionController.stop); | ||||||
|  | router.get("/lottery/prize-distribution/prizes/:id", prizeDistributionController.getPrizesForWinnerById); | ||||||
|  | router.post("/lottery/prize-distribution/prize/:id", prizeDistributionController.submitPrizeForWinnerById); | ||||||
|  |  | ||||||
|  | router.post("/lottery/messages/winner/:id", mustBeAuthenticated, messageController.notifyWinnerById); | ||||||
|  |  | ||||||
|  | router.get("/chat/history", chatController.getAllHistory); | ||||||
|  | router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory); | ||||||
|  |  | ||||||
|  | router.post("/login", userController.login); | ||||||
|  | router.post("/register", mustBeAuthenticated, userController.register); | ||||||
|  | router.get("/logout", userController.logout); | ||||||
|  |  | ||||||
|  | // router.get("/", documentation.apiInfo); | ||||||
|  |  | ||||||
|  | // router.get("/wine/schema", mustBeAuthenticated, update.schema); | ||||||
|  | // router.get("/purchase/statistics", retrieve.allPurchase); | ||||||
|  | // router.get("/highscore/statistics", retrieve.highscore); | ||||||
|  | // router.get("/wines/statistics", retrieve.allWines); | ||||||
|  | // router.get("/wines/statistics/overall", retrieve.allWinesSummary); | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
|   | |||||||
| @@ -6,9 +6,14 @@ const PreLotteryWine = new Schema({ | |||||||
|   vivinoLink: String, |   vivinoLink: String, | ||||||
|   rating: Number, |   rating: Number, | ||||||
|   id: String, |   id: String, | ||||||
|  |   year: Number, | ||||||
|   image: String, |   image: String, | ||||||
|   price: String, |   price: String, | ||||||
|   country: String |   country: String, | ||||||
|  |   winner: { | ||||||
|  |     type: Schema.Types.ObjectId, | ||||||
|  |     ref: "VirtualWinner" | ||||||
|  |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| module.exports = mongoose.model("PreLotteryWine", PreLotteryWine); | module.exports = mongoose.model("PreLotteryWine", PreLotteryWine); | ||||||
|   | |||||||
| @@ -10,6 +10,10 @@ const VirtualWinner = new Schema({ | |||||||
|   red: Number, |   red: Number, | ||||||
|   yellow: Number, |   yellow: Number, | ||||||
|   id: String, |   id: String, | ||||||
|  |   prize_selected: { | ||||||
|  |     type: Boolean, | ||||||
|  |     default: false | ||||||
|  |   }, | ||||||
|   timestamp_drawn: Number, |   timestamp_drawn: Number, | ||||||
|   timestamp_sent: Number, |   timestamp_sent: Number, | ||||||
|   timestamp_limit: Number |   timestamp_limit: Number | ||||||
|   | |||||||
| @@ -1,15 +1,16 @@ | |||||||
| const mongoose = require("mongoose"); | const mongoose = require("mongoose"); | ||||||
| const Schema = mongoose.Schema; | const Schema = mongoose.Schema; | ||||||
|  |  | ||||||
| const Wine = new Schema({ | const WineSchema = new Schema({ | ||||||
|   name: String, |   name: String, | ||||||
|   vivinoLink: String, |   vivinoLink: String, | ||||||
|   rating: Number, |   rating: Number, | ||||||
|   occurences: Number, |   occurences: Number, | ||||||
|   id: String, |   id: String, | ||||||
|  |   year: Number, | ||||||
|   image: String, |   image: String, | ||||||
|   price: String, |   price: String, | ||||||
|   country: String |   country: String | ||||||
| }); | }); | ||||||
|  |  | ||||||
| module.exports = mongoose.model("Wine", Wine); | module.exports = mongoose.model("Wine", WineSchema); | ||||||
|   | |||||||
							
								
								
									
										142
									
								
								api/update.js
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								api/update.js
									
									
									
									
									
								
							| @@ -1,142 +0,0 @@ | |||||||
| const express = require("express"); |  | ||||||
| const path = require("path"); |  | ||||||
|  |  | ||||||
| const sub = require(path.join(__dirname, "/subscriptions")); |  | ||||||
|  |  | ||||||
| const _wineFunctions = require(path.join(__dirname, "/wine")); |  | ||||||
| const _personFunctions = require(path.join(__dirname, "/person")); |  | ||||||
| const Subscription = require(path.join(__dirname, "/schemas/Subscription")); |  | ||||||
| const Lottery = require(path.join(__dirname, "/schemas/Purchase")); |  | ||||||
| const PreLotteryWine = require(path.join( |  | ||||||
|   __dirname, "/schemas/PreLotteryWine" |  | ||||||
| )); |  | ||||||
|  |  | ||||||
| const submitWines = async (req, res) => { |  | ||||||
|   const wines = req.body; |  | ||||||
|   for (let i = 0; i < wines.length; i++) { |  | ||||||
|     let wine = wines[i]; |  | ||||||
|     let newWonWine = new PreLotteryWine({ |  | ||||||
|       name: wine.name, |  | ||||||
|       vivinoLink: wine.vivinoLink, |  | ||||||
|       rating: wine.rating, |  | ||||||
|       image: wine.image, |  | ||||||
|       price: wine.price, |  | ||||||
|       country: wine.country, |  | ||||||
|       id: wine.id |  | ||||||
|     }); |  | ||||||
|     await newWonWine.save(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let subs = await Subscription.find(); |  | ||||||
|   console.log("Sending new wines w/ push notification to all subscribers.") |  | ||||||
|   for (let i = 0; i < subs.length; i++) { |  | ||||||
|     let subscription = subs[i]; //get subscription from your databse here. |  | ||||||
|  |  | ||||||
|     const message = JSON.stringify({ |  | ||||||
|       message: "Dagens vin er lagt til, se den på lottis.vin/dagens!", |  | ||||||
|       title: "Ny vin!", |  | ||||||
|       link: "/#/dagens" |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       sub.sendNotification(subscription, message); |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error("Error when trying to send push notification to subscriber."); |  | ||||||
|       console.error(error); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return res.send({ |  | ||||||
|     message: "Submitted and notified push subscribers of new wines!", |  | ||||||
|     success: true |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const schema = async (req, res) => { |  | ||||||
|   let schema = { ...PreLotteryWine.schema.obj }; |  | ||||||
|   let nulledSchema = Object.keys(schema).reduce((accumulator, current) => { |  | ||||||
|     accumulator[current] = ""; |  | ||||||
|     return accumulator |  | ||||||
|   }, {}); |  | ||||||
|  |  | ||||||
|   return res.send(nulledSchema); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // TODO IMPLEMENT WITH FRONTEND (unused) |  | ||||||
| const submitWinesToLottery = async (req, res) => { |  | ||||||
|   const { lottery } = req.body; |  | ||||||
|   const { date, wines } = lottery; |  | ||||||
|   const wineObjects = await Promise.all(wines.map(async (wine) => await _wineFunctions.findSaveWine(wine))) |  | ||||||
|  |  | ||||||
|   return Lottery.findOneAndUpdate({ date: date }, { |  | ||||||
|       date: date, |  | ||||||
|       wines: wineObjects |  | ||||||
|     }, { |  | ||||||
|       upsert: true |  | ||||||
|     }).then(_ => res.send(true)) |  | ||||||
|       .catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving wine to lottery.', |  | ||||||
|                                            success: false, |  | ||||||
|                                            exception: err.message })); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  /** |  | ||||||
|   * @apiParam (Request body) {Array} winners List of winners |  | ||||||
|   */ |  | ||||||
| const submitWinnersToLottery = async (req, res) => { |  | ||||||
|   const { lottery } = req.body; |  | ||||||
|   const { winners, date } = lottery; |  | ||||||
|  |  | ||||||
|   for (let i = 0; i < winners.length; i++) { |  | ||||||
|     let currentWinner = winners[i]; |  | ||||||
|     let wonWine = await _wineFunctions.findSaveWine(currentWinner.wine); // TODO rename to findAndSaveWineToLottery |  | ||||||
|     await _personFunctions.findSavePerson(currentWinner, wonWine, date); // TODO rename to findAndSaveWineToPerson |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return res.json(true); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  /** |  | ||||||
|   * @apiParam (Request body) {Date} date Date of lottery |  | ||||||
|   * @apiParam (Request body) {Number} blue Number of blue tickets |  | ||||||
|   * @apiParam (Request body) {Number} red Number of red tickets |  | ||||||
|   * @apiParam (Request body) {Number} green Number of green tickets |  | ||||||
|   * @apiParam (Request body) {Number} yellow Number of yellow tickets |  | ||||||
|   * @apiParam (Request body) {Number} bought Number of tickets bought |  | ||||||
|   * @apiParam (Request body) {Number} stolen Number of tickets stolen |  | ||||||
|   */ |  | ||||||
| const submitLottery = async (req, res) => { |  | ||||||
|   const { lottery } = req.body |  | ||||||
|  |  | ||||||
|   const { date, |  | ||||||
|           blue, |  | ||||||
|           red, |  | ||||||
|           yellow, |  | ||||||
|           green, |  | ||||||
|           bought, |  | ||||||
|           stolen } = lottery; |  | ||||||
|  |  | ||||||
|   return Lottery.findOneAndUpdate({ date: date }, { |  | ||||||
|       date: date, |  | ||||||
|       blue: blue, |  | ||||||
|       yellow: yellow, |  | ||||||
|       red: red, |  | ||||||
|       green: green, |  | ||||||
|       bought: bought, |  | ||||||
|       stolen: stolen |  | ||||||
|     }, { |  | ||||||
|       upsert: true |  | ||||||
|     }).then(_ => res.send(true)) |  | ||||||
|       .catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving lottery.', |  | ||||||
|                                            success: false, |  | ||||||
|                                            exception: err.message })); |  | ||||||
|  |  | ||||||
|   return res.send(true); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|   submitWines, |  | ||||||
|   schema, |  | ||||||
|   submitLottery, |  | ||||||
|   submitWinnersToLottery, |  | ||||||
|   submitWinesToLottery |  | ||||||
| }; |  | ||||||
							
								
								
									
										111
									
								
								api/user.js
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								api/user.js
									
									
									
									
									
								
							| @@ -1,51 +1,90 @@ | |||||||
| const passport = require("passport"); | const passport = require("passport"); | ||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const User = require(path.join(__dirname, "/schemas/User")); | const User = require(path.join(__dirname, "/schemas/User")); | ||||||
| const router = require("express").Router(); |  | ||||||
|  |  | ||||||
| const register = (req, res, next) => { | class UserExistsError extends Error { | ||||||
|   User.register( |   constructor(message = "Username already exists.") { | ||||||
|     new User({ username: req.body.username }), |     super(message); | ||||||
|     req.body.password, |     this.name = "UserExists"; | ||||||
|     function(err) { |     this.statusCode = 409; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class MissingUsernameError extends Error { | ||||||
|  |   constructor(message = "No username given.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "MissingUsernameError"; | ||||||
|  |     this.statusCode = 400; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class MissingPasswordError extends Error { | ||||||
|  |   constructor(message = "No password given.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "MissingPasswordError"; | ||||||
|  |     this.statusCode = 400; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class IncorrectUserCredentialsError extends Error { | ||||||
|  |   constructor(message = "Incorrect username or password") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "IncorrectUserCredentialsError"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function userAuthenticationErrorHandler(err) { | ||||||
|  |   if (err.name == "UserExistsError") { | ||||||
|  |     throw new UserExistsError(err.message); | ||||||
|  |   } else if (err.name == "MissingUsernameError") { | ||||||
|  |     throw new MissingUsernameError(err.message); | ||||||
|  |   } else if (err.name == "MissingPasswordError") { | ||||||
|  |     throw new MissingPasswordError(err.message); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   throw err; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const register = (username, password) => { | ||||||
|  |   return User.register(new User({ username: username }), password).catch(userAuthenticationErrorHandler); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const authenticate = req => { | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     const { username, password } = req.body; | ||||||
|  |  | ||||||
|  |     if (username == undefined) throw new MissingUsernameError(); | ||||||
|  |     if (password == undefined) throw new MissingPasswordError(); | ||||||
|  |  | ||||||
|  |     passport.authenticate("local", function(err, user, info) { | ||||||
|       if (err) { |       if (err) { | ||||||
|         if (err.name == "UserExistsError") |         reject(err); | ||||||
|           res.status(409).send({ success: false, message: err.message }) |  | ||||||
|         else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") |  | ||||||
|           res.status(400).send({ success: false, message: err.message }) |  | ||||||
|         return next(err); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true }) |       if (!user) { | ||||||
|      } |         reject(new IncorrectUserCredentialsError()); | ||||||
|   ); |       } | ||||||
|  |  | ||||||
|  |       resolve(user); | ||||||
|  |     })(req); | ||||||
|  |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const login = (req, res, next) => { | const login = (req, user) => { | ||||||
|   passport.authenticate("local", function(err, user, info) { |   return new Promise((resolve, reject) => { | ||||||
|     if (err) { |     req.logIn(user, err => { | ||||||
|       if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") |       if (err) { | ||||||
|         return res.status(400).send({ message: err.message, success: false }) |         reject(err); | ||||||
|       return next(err); |       } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false }) |       resolve(user); | ||||||
|  |     }); | ||||||
|     req.logIn(user, (err) => { |   }); | ||||||
|       if (err) { return next(err) } |  | ||||||
|  |  | ||||||
|       return res.status(200).send({ message: "Velkommen " + user.username, success: true }) |  | ||||||
|     }) |  | ||||||
|   })(req, res, next); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const logout = (req, res) => { |  | ||||||
|   req.logout(); |  | ||||||
|   res.redirect("/"); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   register, |   register, | ||||||
|   login, |   authenticate, | ||||||
|   logout |   login | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										90
									
								
								api/vinlottisErrors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								api/vinlottisErrors.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | class UserNotFound extends Error { | ||||||
|  |   constructor(message = "User not found.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "UserNotFound"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // TODO log missing user | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class WineNotFound extends Error { | ||||||
|  |   constructor(message = "Wine not found.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "WineNotFound"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // TODO log missing user | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class WinnerNotFound extends Error { | ||||||
|  |   constructor(message = "Winner not found.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "WinnerNotFound"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // TODO log missing user | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class WinnersTimelimitExpired extends Error { | ||||||
|  |   constructor(message = "Timelimit expired, you will need to wait until it's your turn again.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "WinnersTimelimitExpired"; | ||||||
|  |     this.statusCode = 403; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class WineSelectionWinnerNotNextInLine extends Error { | ||||||
|  |   constructor(message = "Not the winner next in line!") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "WineSelectionWinnerNotNextInLine"; | ||||||
|  |     this.statusCode = 403; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // TODO log missing user | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class NoMoreAttendeesToWin extends Error { | ||||||
|  |   constructor(message = "No more attendees left to drawn from.") { | ||||||
|  |     super(message); | ||||||
|  |     this.name = "NoMoreAttendeesToWin"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class CouldNotFindNewWinnerAfterNTries extends Error { | ||||||
|  |   constructor(tries) { | ||||||
|  |     let message = `Could not a new winner after ${tries} tries.`; | ||||||
|  |     super(message); | ||||||
|  |     this.name = "CouldNotFindNewWinnerAfterNTries"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class LotteryByDateNotFound extends Error { | ||||||
|  |   constructor(date) { | ||||||
|  |     const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); | ||||||
|  |     const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); | ||||||
|  |     const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); | ||||||
|  |  | ||||||
|  |     const dateString = `${ye}-${mo}-${da}`; | ||||||
|  |     const dateUnix = date.getTime(); | ||||||
|  |     const message = `Could not find lottery for date: ${dateString}.`; | ||||||
|  |     super(message); | ||||||
|  |     this.name = "LotteryByDateNotFoundError"; | ||||||
|  |     this.statusCode = 404; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   UserNotFound, | ||||||
|  |   WineNotFound, | ||||||
|  |   WinnerNotFound, | ||||||
|  |   WinnersTimelimitExpired, | ||||||
|  |   WineSelectionWinnerNotNextInLine, | ||||||
|  |   NoMoreAttendeesToWin, | ||||||
|  |   CouldNotFindNewWinnerAfterNTries, | ||||||
|  |   LotteryByDateNotFound | ||||||
|  | }; | ||||||
							
								
								
									
										114
									
								
								api/vinmonopolet.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								api/vinmonopolet.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | const fetch = require("node-fetch"); | ||||||
|  | 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, | ||||||
|  |       year: wine.basic.vintage, | ||||||
|  |       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 convertToOurStoreObject = store => { | ||||||
|  |   return { | ||||||
|  |     id: store.storeId, | ||||||
|  |     name: store.storeName, | ||||||
|  |     ...store.address | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const searchWinesByName = async (name, page = 1) => { | ||||||
|  |   const pageSize = 15; | ||||||
|  |   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, { | ||||||
|  |     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 winesConverted; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const wineByEAN = ean => { | ||||||
|  |   const url = `https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/${ean}`; | ||||||
|  |   return fetch(url) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(response => response.map(convertToOurWineObject)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const wineById = id => { | ||||||
|  |   const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`; | ||||||
|  |   const options = { | ||||||
|  |     headers: { | ||||||
|  |       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(response => response.map(convertToOurWineObject)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const allStores = () => { | ||||||
|  |   const url = `https://apis.vinmonopolet.no/stores/v0/details`; | ||||||
|  |   const options = { | ||||||
|  |     headers: { | ||||||
|  |       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(response => response.map(convertToOurStoreObject)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const searchStoresByName = name => { | ||||||
|  |   const url = `https://apis.vinmonopolet.no/stores/v0/details?storeNameContains=${name}`; | ||||||
|  |   const options = { | ||||||
|  |     headers: { | ||||||
|  |       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(response => response.map(convertToOurStoreObject)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   searchWinesByName, | ||||||
|  |   wineByEAN, | ||||||
|  |   wineById, | ||||||
|  |   allStores, | ||||||
|  |   searchStoresByName | ||||||
|  | }; | ||||||
| @@ -1,281 +0,0 @@ | |||||||
| const path = require("path"); |  | ||||||
| const crypto = require("crypto"); |  | ||||||
|  |  | ||||||
| const config = require(path.join(__dirname, "/../config/defaults/lottery")); |  | ||||||
| const Message = require(path.join(__dirname, "/message")); |  | ||||||
| const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration")); |  | ||||||
|  |  | ||||||
| const Attendee = require(path.join(__dirname, "/schemas/Attendee")); |  | ||||||
| const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); |  | ||||||
| const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine")); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| const winners = async (req, res) => { |  | ||||||
|   let winners = await VirtualWinner.find(); |  | ||||||
|   let winnersRedacted = []; |  | ||||||
|   let winner; |  | ||||||
|   for (let i = 0; i < winners.length; i++) { |  | ||||||
|     winner = winners[i]; |  | ||||||
|     winnersRedacted.push({ |  | ||||||
|       name: winner.name, |  | ||||||
|       color: winner.color |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   res.json(winnersRedacted); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const winnersSecure = async (req, res) => { |  | ||||||
|   let winners = await VirtualWinner.find(); |  | ||||||
|  |  | ||||||
|   return res.json(winners); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const deleteWinners = async (req, res) => { |  | ||||||
|   await VirtualWinner.deleteMany(); |  | ||||||
|   var io = req.app.get('socketio'); |  | ||||||
|   io.emit("refresh_data", {}); |  | ||||||
|   return res.json(true); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const attendees = async (req, res) => { |  | ||||||
|   let attendees = await Attendee.find(); |  | ||||||
|   let attendeesRedacted = []; |  | ||||||
|   let attendee; |  | ||||||
|   for (let i = 0; i < attendees.length; i++) { |  | ||||||
|     attendee = attendees[i]; |  | ||||||
|     attendeesRedacted.push({ |  | ||||||
|       name: attendee.name, |  | ||||||
|       raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green, |  | ||||||
|       red: attendee.red, |  | ||||||
|       blue: attendee.blue, |  | ||||||
|       green: attendee.green, |  | ||||||
|       yellow: attendee.yellow |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   return res.json(attendeesRedacted); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const attendeesSecure = async (req, res) => { |  | ||||||
|   let attendees = await Attendee.find(); |  | ||||||
|  |  | ||||||
|   return res.json(attendees); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const addAttendee = async (req, res) => { |  | ||||||
|   const attendee = req.body; |  | ||||||
|   const { red, blue, yellow, green } = attendee; |  | ||||||
|  |  | ||||||
|   let newAttendee = new Attendee({ |  | ||||||
|     name: attendee.name, |  | ||||||
|     red, |  | ||||||
|     blue, |  | ||||||
|     green, |  | ||||||
|     yellow, |  | ||||||
|     phoneNumber: attendee.phoneNumber, |  | ||||||
|     winner: false |  | ||||||
|   }); |  | ||||||
|   await newAttendee.save(); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   var io = req.app.get('socketio'); |  | ||||||
|   io.emit("new_attendee", {}); |  | ||||||
|  |  | ||||||
|   return res.send(true); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const deleteAttendees = async (req, res) => { |  | ||||||
|   await Attendee.deleteMany(); |  | ||||||
|   var io = req.app.get('socketio'); |  | ||||||
|   io.emit("refresh_data", {}); |  | ||||||
|   return res.json(true); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const drawWinner = async (req, res) => { |  | ||||||
|   let allContestants = await Attendee.find({ winner: false }); |  | ||||||
|  |  | ||||||
|   if (allContestants.length == 0) { |  | ||||||
|     return res.json({ |  | ||||||
|       success: false, |  | ||||||
|       message: "No attendees left that have not won." |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   let raffleColors = []; |  | ||||||
|   for (let i = 0; i < allContestants.length; i++) { |  | ||||||
|     let currentContestant = allContestants[i]; |  | ||||||
|     for (let blue = 0; blue < currentContestant.blue; blue++) { |  | ||||||
|       raffleColors.push("blue"); |  | ||||||
|     } |  | ||||||
|     for (let red = 0; red < currentContestant.red; red++) { |  | ||||||
|       raffleColors.push("red"); |  | ||||||
|     } |  | ||||||
|     for (let green = 0; green < currentContestant.green; green++) { |  | ||||||
|       raffleColors.push("green"); |  | ||||||
|     } |  | ||||||
|     for (let yellow = 0; yellow < currentContestant.yellow; yellow++) { |  | ||||||
|       raffleColors.push("yellow"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   raffleColors = shuffle(raffleColors); |  | ||||||
|  |  | ||||||
|   let colorToChooseFrom = |  | ||||||
|     raffleColors[Math.floor(Math.random() * raffleColors.length)]; |  | ||||||
|   let findObject = { winner: false }; |  | ||||||
|  |  | ||||||
|   findObject[colorToChooseFrom] = { $gt: 0 }; |  | ||||||
|  |  | ||||||
|   let tries = 0; |  | ||||||
|   const maxTries = 3; |  | ||||||
|   let contestantsToChooseFrom = undefined; |  | ||||||
|   while (contestantsToChooseFrom == undefined && tries < maxTries) { |  | ||||||
|     const hit = await Attendee.find(findObject); |  | ||||||
|     if (hit && hit.length) { |  | ||||||
|       contestantsToChooseFrom = hit; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     tries++; |  | ||||||
|   } |  | ||||||
|   if (contestantsToChooseFrom == undefined) { |  | ||||||
|     return res.status(404).send({ |  | ||||||
|       success: false, |  | ||||||
|       message: `Klarte ikke trekke en vinner etter ${maxTries} forsøk.` |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let attendeeListDemocratic = []; |  | ||||||
|  |  | ||||||
|   let currentContestant; |  | ||||||
|   for (let i = 0; i < contestantsToChooseFrom.length; i++) { |  | ||||||
|     currentContestant = contestantsToChooseFrom[i]; |  | ||||||
|     for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) { |  | ||||||
|       attendeeListDemocratic.push({ |  | ||||||
|         name: currentContestant.name, |  | ||||||
|         phoneNumber: currentContestant.phoneNumber, |  | ||||||
|         red: currentContestant.red, |  | ||||||
|         blue: currentContestant.blue, |  | ||||||
|         green: currentContestant.green, |  | ||||||
|         yellow: currentContestant.yellow |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   attendeeListDemocratic = shuffle(attendeeListDemocratic); |  | ||||||
|  |  | ||||||
|   let winner = |  | ||||||
|     attendeeListDemocratic[ |  | ||||||
|       Math.floor(Math.random() * attendeeListDemocratic.length) |  | ||||||
|     ]; |  | ||||||
|  |  | ||||||
|   let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ |  | ||||||
|     timestamp_drawn: 1 |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   var io = req.app.get('socketio'); |  | ||||||
|   io.emit("winner", { |  | ||||||
|     color: colorToChooseFrom, |  | ||||||
|     name: winner.name, |  | ||||||
|     winner_count: winners.length + 1 |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   let newWinnerElement = new VirtualWinner({ |  | ||||||
|     name: winner.name, |  | ||||||
|     phoneNumber: winner.phoneNumber, |  | ||||||
|     color: colorToChooseFrom, |  | ||||||
|     red: winner.red, |  | ||||||
|     blue: winner.blue, |  | ||||||
|     green: winner.green, |  | ||||||
|     yellow: winner.yellow, |  | ||||||
|     id: sha512(winner.phoneNumber, genRandomString(10)), |  | ||||||
|     timestamp_drawn: new Date().getTime() |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   await Attendee.update( |  | ||||||
|     { name: winner.name, phoneNumber: winner.phoneNumber }, |  | ||||||
|     { $set: { winner: true } } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   await newWinnerElement.save(); |  | ||||||
|   return res.json({ |  | ||||||
|     success: true, |  | ||||||
|     winner |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const finish = async (req, res) => { |  | ||||||
|   if (!config.gatewayToken) { |  | ||||||
|     return res.json({ |  | ||||||
|       message: "Missing api token for sms gateway.", |  | ||||||
|       success: false |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({ |  | ||||||
|     timestamp_drawn: 1 |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (winners.length == 0) { |  | ||||||
|     return res.json({ |  | ||||||
|       message: "No winners to draw from.", |  | ||||||
|       success: false |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Message.sendInitialMessageToWinners(winners.slice(1)); |  | ||||||
|  |  | ||||||
|   return findAndNotifyNextWinner() |  | ||||||
|     .then(() => res.json({ |  | ||||||
|       success: true, |  | ||||||
|       message: "Sent wine select message to first winner and update message to rest of winners." |  | ||||||
|     })) |  | ||||||
|     .catch(error => res.json({ |  | ||||||
|       message: error["message"] || "Unable to send message to first winner.", |  | ||||||
|       success: false |  | ||||||
|     })) |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const genRandomString = function(length) { |  | ||||||
|   return crypto |  | ||||||
|     .randomBytes(Math.ceil(length / 2)) |  | ||||||
|     .toString("hex") /** convert to hexadecimal format */ |  | ||||||
|     .slice(0, length); /** return required number of characters */ |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const sha512 = function(password, salt) { |  | ||||||
|   var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */ |  | ||||||
|   hash.update(password); |  | ||||||
|   var value = hash.digest("hex"); |  | ||||||
|   return value; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| function shuffle(array) { |  | ||||||
|   let currentIndex = array.length, |  | ||||||
|     temporaryValue, |  | ||||||
|     randomIndex; |  | ||||||
|  |  | ||||||
|   // While there remain elements to shuffle... |  | ||||||
|   while (0 !== currentIndex) { |  | ||||||
|     // Pick a remaining element... |  | ||||||
|     randomIndex = Math.floor(Math.random() * currentIndex); |  | ||||||
|     currentIndex -= 1; |  | ||||||
|  |  | ||||||
|     // And swap it with the current element. |  | ||||||
|     temporaryValue = array[currentIndex]; |  | ||||||
|     array[currentIndex] = array[randomIndex]; |  | ||||||
|     array[randomIndex] = temporaryValue; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return array; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|   deleteWinners, |  | ||||||
|   deleteAttendees, |  | ||||||
|   winners, |  | ||||||
|   winnersSecure, |  | ||||||
|   drawWinner, |  | ||||||
|   finish, |  | ||||||
|   attendees, |  | ||||||
|   attendeesSecure, |  | ||||||
|   addAttendee |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @@ -1,200 +0,0 @@ | |||||||
| const path = require("path"); |  | ||||||
|  |  | ||||||
| const _wineFunctions = require(path.join(__dirname, "/wine")); |  | ||||||
| const _personFunctions = require(path.join(__dirname, "/person")); |  | ||||||
| const Message = require(path.join(__dirname, "/message")); |  | ||||||
| const VirtualWinner = require(path.join( |  | ||||||
|   __dirname, "/schemas/VirtualWinner" |  | ||||||
| )); |  | ||||||
| const PreLotteryWine = require(path.join( |  | ||||||
|   __dirname, "/schemas/PreLotteryWine" |  | ||||||
| )); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| const getWinesToWinnerById = async (req, res) => { |  | ||||||
|   let id = req.params.id; |  | ||||||
|   let foundWinner = await VirtualWinner.findOne({ id: id }); |  | ||||||
|  |  | ||||||
|   if (!foundWinner) { |  | ||||||
|     return res.json({ |  | ||||||
|       success: false, |  | ||||||
|       message: "No winner with this id.", |  | ||||||
|       existing: false, |  | ||||||
|       turn: false |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); |  | ||||||
|   if ( |  | ||||||
|     allWinners[0].id != foundWinner.id || |  | ||||||
|     foundWinner.timestamp_limit == undefined || |  | ||||||
|     foundWinner.timestamp_sent == undefined |  | ||||||
|   ) { |  | ||||||
|     return res.json({ |  | ||||||
|       success: false, |  | ||||||
|       message: "Not the winner next in line!", |  | ||||||
|       existing: true, |  | ||||||
|       turn: false |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return res.json({ |  | ||||||
|     success: true, |  | ||||||
|     existing: true, |  | ||||||
|     turn: true, |  | ||||||
|     name: foundWinner.name, |  | ||||||
|     color: foundWinner.color |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const registerWinnerSelection = async (req, res) => { |  | ||||||
|   let id = req.params.id; |  | ||||||
|   let wineName = req.body.wineName; |  | ||||||
|   let foundWinner = await VirtualWinner.findOne({ id: id }); |  | ||||||
|  |  | ||||||
|   if (!foundWinner) { |  | ||||||
|     return res.json({ |  | ||||||
|       success: false, |  | ||||||
|       message: "No winner with this id." |  | ||||||
|     }) |  | ||||||
|   } else if (foundWinner.timestamp_limit < new Date().getTime()) { |  | ||||||
|     return res.json({ |  | ||||||
|       success: false, |  | ||||||
|       message: "Timelimit expired, you will receive a wine after other users have chosen.", |  | ||||||
|       limit: true |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let date = new Date(); |  | ||||||
|   date.setHours(5, 0, 0, 0); |  | ||||||
|   let prelotteryWine = await PreLotteryWine.findOne({ name: wineName }); |  | ||||||
|  |  | ||||||
|   if (!prelotteryWine) { |  | ||||||
|     return res.json({ |  | ||||||
|       success: false, |  | ||||||
|       message: "No wine with this name.", |  | ||||||
|       wine: false |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let wonWine = await _wineFunctions.findSaveWine(prelotteryWine); |  | ||||||
|   await prelotteryWine.delete(); |  | ||||||
|   await _personFunctions.findSavePerson(foundWinner, wonWine, date); |  | ||||||
|   await Message.sendWineConfirmation(foundWinner, wonWine, date); |  | ||||||
|  |  | ||||||
|   await foundWinner.delete(); |  | ||||||
|   console.info("Saved winners choice."); |  | ||||||
|  |  | ||||||
|   return findAndNotifyNextWinner() |  | ||||||
|     .then(() => res.json({ |  | ||||||
|       message: "Choice saved and next in line notified.", |  | ||||||
|       success: true |  | ||||||
|     })) |  | ||||||
|     .catch(error => res.json({ |  | ||||||
|       message: error["message"] || "Error when notifing next winner.", |  | ||||||
|       success: false |  | ||||||
|     })) |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const chooseLastWineForUser = (winner, preLotteryWine) => { |  | ||||||
|   let date = new Date(); |  | ||||||
|   date.setHours(5, 0, 0, 0); |  | ||||||
|  |  | ||||||
|   return _wineFunctions.findSaveWine(preLotteryWine) |  | ||||||
|     .then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date)) |  | ||||||
|     .then(() => preLotteryWine.delete()) |  | ||||||
|     .then(() => Message.sendLastWinnerMessage(winner, preLotteryWine)) |  | ||||||
|     .then(() => winner.delete()) |  | ||||||
|     .catch(err => { |  | ||||||
|       console.log("Error thrown from chooseLastWineForUser: " + err); |  | ||||||
|       throw err; |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const findAndNotifyNextWinner = async () => { |  | ||||||
|   let nextWinner = undefined; |  | ||||||
|  |  | ||||||
|   let winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 }); |  | ||||||
|   let winesLeft = await PreLotteryWine.find(); |  | ||||||
|  |  | ||||||
|   if (winnersLeft.length > 1) { |  | ||||||
|     console.log("multiple winners left, choose next in line") |  | ||||||
|     nextWinner = winnersLeft[0]; // multiple winners left, choose next in line |  | ||||||
|   } else if (winnersLeft.length == 1 && winesLeft.length > 1) { |  | ||||||
|     console.log("one winner left, but multiple wines") |  | ||||||
|     nextWinner = winnersLeft[0] // one winner left, but multiple wines |  | ||||||
|   } else if (winnersLeft.length == 1 && winesLeft.length == 1) { |  | ||||||
|     console.log("one winner and one wine left, choose for user") |  | ||||||
|     nextWinner = winnersLeft[0] // one winner and one wine left, choose for user |  | ||||||
|     wine = winesLeft[0] |  | ||||||
|     return chooseLastWineForUser(nextWinner, wine); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (nextWinner) { |  | ||||||
|     return Message.sendWineSelectMessage(nextWinner) |  | ||||||
|       .then(messageResponse => startTimeout(nextWinner.id)) |  | ||||||
|   } else { |  | ||||||
|     console.info("All winners notified. Could start cleanup here."); |  | ||||||
|     return Promise.resolve({ |  | ||||||
|       message: "All winners notified." |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const sendNotificationToWinnerById = async (req, res) => { |  | ||||||
|   const { id } = req.params; |  | ||||||
|   let winner = await VirtualWinner.findOne({ id: id }); |  | ||||||
|  |  | ||||||
|   if (!winner) { |  | ||||||
|     return res.json({ |  | ||||||
|       message: "No winner with this id.", |  | ||||||
|       success: false |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return Message.sendWineSelectMessage(winner) |  | ||||||
|     .then(success => res.json({ |  | ||||||
|       success: success, |  | ||||||
|       message: `Message sent to winner ${id} successfully!` |  | ||||||
|     })) |  | ||||||
|     .catch(err => res.json({ |  | ||||||
|       success: false, |  | ||||||
|       message: "Error while trying to send sms.", |  | ||||||
|       error: err |  | ||||||
|     })) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function startTimeout(id) { |  | ||||||
|   const minute = 60000; |  | ||||||
|   const minutesForTimeout = 10; |  | ||||||
|  |  | ||||||
|   console.log(`Starting timeout for user ${id}.`); |  | ||||||
|   console.log(`Timeout duration: ${ minutesForTimeout * minute }`) |  | ||||||
|   setTimeout(async () => { |  | ||||||
|     let virtualWinner = await VirtualWinner.findOne({ id: id }); |  | ||||||
|     if (!virtualWinner) { |  | ||||||
|       console.log(`Timeout done for user ${id}, but user has already sent data.`); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     console.log(`Timeout done for user ${id}, sending update to user.`); |  | ||||||
|  |  | ||||||
|     Message.sendWineSelectMessageTooLate(virtualWinner); |  | ||||||
|  |  | ||||||
|     virtualWinner.timestamp_drawn = new Date().getTime(); |  | ||||||
|     virtualWinner.timestamp_limit = null; |  | ||||||
|     virtualWinner.timestamp_sent = null; |  | ||||||
|     await virtualWinner.save(); |  | ||||||
|  |  | ||||||
|     findAndNotifyNextWinner(); |  | ||||||
|   }, minutesForTimeout * minute); |  | ||||||
|  |  | ||||||
|   return Promise.resolve() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|   getWinesToWinnerById, |  | ||||||
|   registerWinnerSelection, |  | ||||||
|   findAndNotifyNextWinner, |  | ||||||
|  |  | ||||||
|   sendNotificationToWinnerById |  | ||||||
| }; |  | ||||||
							
								
								
									
										72
									
								
								api/wine.js
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								api/wine.js
									
									
									
									
									
								
							| @@ -1,27 +1,63 @@ | |||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const Wine = require(path.join(__dirname, "/schemas/Wine")); | const Wine = require(path.join(__dirname, "/schemas/Wine")); | ||||||
|  |  | ||||||
| async function findSaveWine(prelotteryWine) { | const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); | ||||||
|   let wonWine = await Wine.findOne({ name: prelotteryWine.name }); |  | ||||||
|   if (wonWine == undefined) { | const addWine = async wine => { | ||||||
|     let newWonWine = new Wine({ |   let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }); | ||||||
|       name: prelotteryWine.name, |  | ||||||
|       vivinoLink: prelotteryWine.vivinoLink, |   if (existingWine == undefined) { | ||||||
|       rating: prelotteryWine.rating, |     let newWine = new Wine({ | ||||||
|  |       name: wine.name, | ||||||
|  |       vivinoLink: wine.vivinoLink, | ||||||
|  |       rating: wine.rating, | ||||||
|       occurences: 1, |       occurences: 1, | ||||||
|       image: prelotteryWine.image, |       id: wine.id, | ||||||
|       id: prelotteryWine.id |       year: wine.year, | ||||||
|  |       image: wine.image, | ||||||
|  |       price: wine.price, | ||||||
|  |       country: wine.country | ||||||
|     }); |     }); | ||||||
|     await newWonWine.save(); |     await newWine.save(); | ||||||
|     wonWine = newWonWine; |     return newWine; | ||||||
|   } else { |   } else { | ||||||
|     wonWine.occurences += 1; |     existingWine.occurences += 1; | ||||||
|     wonWine.image = prelotteryWine.image; |     await existingWine.save(); | ||||||
|     wonWine.id = prelotteryWine.id; |     return existingWine; | ||||||
|     await wonWine.save(); |  | ||||||
|   } |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|   return wonWine; | const allWines = (limit = undefined) => { | ||||||
| } |   if (limit) { | ||||||
|  |     return Wine.find().limit(limit); | ||||||
|  |   } else { | ||||||
|  |     return Wine.find(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| module.exports.findSaveWine = findSaveWine; | const wineById = id => { | ||||||
|  |   return Wine.findOne({ _id: id }).then(wine => { | ||||||
|  |     if (wine == null) { | ||||||
|  |       throw new WineNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return wine; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const findWine = wine => { | ||||||
|  |   return Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }).then(wine => { | ||||||
|  |     if (wine == null) { | ||||||
|  |       throw new WineNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return wine; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   addWine, | ||||||
|  |   allWines, | ||||||
|  |   wineById, | ||||||
|  |   findWine | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -1,72 +0,0 @@ | |||||||
| const fetch = require('node-fetch') |  | ||||||
| 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 vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean) |  | ||||||
|     .then(resp => resp.json()) |  | ||||||
|  |  | ||||||
|   if (vinmonopoletResponse.errors != null) { |  | ||||||
|     return vinmonopoletResponse.errors.map(error => { |  | ||||||
|       if (error.type == "UnknownProductError") { |  | ||||||
|         return res.status(404).json({ |  | ||||||
|           message: error.message |  | ||||||
|         }) |  | ||||||
|       } else { |  | ||||||
|         return next() |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return res.send(vinmonopoletResponse); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| module.exports = { |  | ||||||
|   byEAN, |  | ||||||
|   wineSearch |  | ||||||
| }; |  | ||||||
							
								
								
									
										95
									
								
								api/winner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								api/winner.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | const path = require("path"); | ||||||
|  |  | ||||||
|  | const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner")); | ||||||
|  | const { WinnerNotFound } = require(path.join(__dirname, "/vinlottisErrors")); | ||||||
|  |  | ||||||
|  | const redactWinnerInfoMapper = winner => { | ||||||
|  |   return { | ||||||
|  |     name: winner.name, | ||||||
|  |     color: winner.color | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const addWinners = winners => { | ||||||
|  |   return Promise.all( | ||||||
|  |     winners.map(winner => { | ||||||
|  |       let newWinnerElement = new VirtualWinner({ | ||||||
|  |         name: winner.name, | ||||||
|  |         color: winner.color, | ||||||
|  |         timestamp_drawn: new Date().getTime() | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       return newWinnerElement.save(); | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const allWinners = (isAdmin = false) => { | ||||||
|  |   const sortQuery = { timestamp_drawn: 1 }; | ||||||
|  |  | ||||||
|  |   if (!isAdmin) { | ||||||
|  |     return VirtualWinner.find() | ||||||
|  |       .sort(sortQuery) | ||||||
|  |       .then(winners => winners.map(redactWinnerInfoMapper)); | ||||||
|  |   } else { | ||||||
|  |     return VirtualWinner.find().sort(sortQuery); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const winnerById = (id, isAdmin = false) => { | ||||||
|  |   return VirtualWinner.findOne({ id: id }).then(winner => { | ||||||
|  |     if (winner == null) { | ||||||
|  |       throw new WinnerNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!isAdmin) { | ||||||
|  |       return redactWinnerInfoMapper(winner); | ||||||
|  |     } | ||||||
|  |     return winner; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateWinnerById = (id, updateModel) => { | ||||||
|  |   return VirtualWinner.findOne({ id: id }).then(winner => { | ||||||
|  |     if (winner == null) { | ||||||
|  |       throw new WinnerNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const updatedWinner = { | ||||||
|  |       name: updateModel.name != null ? updateModel.name : winner.name, | ||||||
|  |       phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : winner.phoneNumber, | ||||||
|  |       red: updateModel.red != null ? updateModel.red : winner.red, | ||||||
|  |       green: updateModel.green != null ? updateModel.green : winner.green, | ||||||
|  |       blue: updateModel.blue != null ? updateModel.blue : winner.blue, | ||||||
|  |       yellow: updateModel.yellow != null ? updateModel.yellow : winner.yellow, | ||||||
|  |       timestamp_drawn: updateModel.timestamp_drawn != null ? updateModel.timestamp_drawn : winner.timestamp_drawn, | ||||||
|  |       timestamp_limit: updateModel.timestamp_limit != null ? updateModel.timestamp_limit : winner.timestamp_limit, | ||||||
|  |       timestamp_sent: updateModel.timestamp_sent != null ? updateModel.timestamp_sent : winner.timestamp_sent | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return VirtualWinner.updateOne({ id: id }, updatedWinner).then(_ => updatedWinner); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWinnerById = id => { | ||||||
|  |   return VirtualWinner.findOne({ id: id }).then(winner => { | ||||||
|  |     if (winner == null) { | ||||||
|  |       throw new WinnerNotFound(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return VirtualWinner.deleteOne({ id: id }).then(_ => winner); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteWinners = () => { | ||||||
|  |   return VirtualWinner.deleteMany(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   addWinners, | ||||||
|  |   allWinners, | ||||||
|  |   winnerById, | ||||||
|  |   updateWinnerById, | ||||||
|  |   deleteWinnerById, | ||||||
|  |   deleteWinners | ||||||
|  | }; | ||||||
| @@ -26,7 +26,8 @@ const allRequestedWines = () => {; | |||||||
|   return fetch("/api/request/all") |   return fetch("/api/request/all") | ||||||
|     .then(resp => { |     .then(resp => { | ||||||
|       const isAdmin = resp.headers.get("vinlottis-admin") == "true"; |       const isAdmin = resp.headers.get("vinlottis-admin") == "true"; | ||||||
|       return Promise.all([resp.json(), isAdmin]); |       const getWinesFromBody = (resp) => resp.json().then(body => body.wines); | ||||||
|  |       return Promise.all([getWinesFromBody(resp), isAdmin]); | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -109,8 +110,7 @@ const deleteRequestedWine = wineToBeDeleted => { | |||||||
|     headers: { |     headers: { | ||||||
|       "Content-Type": "application/json" |       "Content-Type": "application/json" | ||||||
|     }, |     }, | ||||||
|     method: "DELETE", |     method: "DELETE" | ||||||
|     body: JSON.stringify(wineToBeDeleted) |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return fetch("api/request/" + wineToBeDeleted.id, options) |   return fetch("api/request/" + wineToBeDeleted.id, options) | ||||||
| @@ -148,14 +148,12 @@ const attendees = () => { | |||||||
|  |  | ||||||
| const requestNewWine = (wine) => { | const requestNewWine = (wine) => { | ||||||
|   const options = { |   const options = { | ||||||
|     body: JSON.stringify({ |     method: "POST", | ||||||
|       wine: wine |  | ||||||
|     }), |  | ||||||
|      headers: { |      headers: { | ||||||
|       'Accept': 'application/json', |       'Accept': 'application/json', | ||||||
|       'Content-Type': 'application/json' |       'Content-Type': 'application/json' | ||||||
|     }, |     }, | ||||||
|     method: "post" |     body: JSON.stringify({ wine }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return fetch("/api/request/new-wine", options) |   return fetch("/api/request/new-wine", options) | ||||||
|   | |||||||
| @@ -1,34 +1,72 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <h1>Admin-side</h1> |  | ||||||
|     <Tabs :tabs="tabs" /> |     <Tabs :tabs="tabs" /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import Tabs from "@/ui/Tabs"; | import Tabs from "@/ui/Tabs"; | ||||||
| import RegisterPage from "@/components/RegisterPage"; | import RegisterWinePage from "@/components/admin/RegisterWinePage"; | ||||||
| import VirtualLotteryRegistrationPage from "@/components/VirtualLotteryRegistrationPage"; | import archiveLotteryPage from "@/components/admin/archiveLotteryPage"; | ||||||
|  | import registerAttendeePage from "@/components/admin/registerAttendeePage"; | ||||||
|  | import DrawWinnerPage from "@/components/admin/DrawWinnerPage"; | ||||||
|  | import PushPage from "@/components/admin/PushPage"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     Tabs, |     Tabs | ||||||
|     RegisterPage, |  | ||||||
|     VirtualLotteryRegistrationPage |  | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       tabs: [ |       tabs: [ | ||||||
|         { name: "Registrering", component: RegisterPage }, |         { | ||||||
|         { name: "Virtuelt lotteri", component: VirtualLotteryRegistrationPage } |           name: "Vin", | ||||||
|  |           component: RegisterWinePage, | ||||||
|  |           slug: "vin", | ||||||
|  |           counter: null | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Legg til deltakere", | ||||||
|  |           component: registerAttendeePage, | ||||||
|  |           slug: "attendees", | ||||||
|  |           counter: null | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Trekk vinner", | ||||||
|  |           component: DrawWinnerPage, | ||||||
|  |           slug: "draw", | ||||||
|  |           counter: null | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Arkiver lotteri", | ||||||
|  |           component: archiveLotteryPage, | ||||||
|  |           slug: "reg", | ||||||
|  |           counter: null | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: "Push meldinger", | ||||||
|  |           component: PushPage, | ||||||
|  |           slug: "push" | ||||||
|  |         } | ||||||
|       ] |       ] | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss"> | ||||||
| h1 { | @import "@/styles/media-queries"; | ||||||
|   text-align: center; |  | ||||||
|  | .page-container { | ||||||
|  |   padding: 0 1.5rem 3rem; | ||||||
|  |  | ||||||
|  |   h1 { | ||||||
|  |     text-align: center; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     max-width: 60vw; | ||||||
|  |     margin: 0 auto; | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -2,40 +2,50 @@ | |||||||
|   <main class="container"> |   <main class="container"> | ||||||
|     <h1>Alle foreslåtte viner</h1> |     <h1>Alle foreslåtte viner</h1> | ||||||
|  |  | ||||||
|     <section class="requested-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 v-for="requestedEl in wines" :key="requestedEl.wine._id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/> |       <RequestedWineCard | ||||||
|  |         v-for="requestedWine in wines" | ||||||
|  |         :key="requestedWine.wine._id" | ||||||
|  |         :requestedElement="requestedWine" | ||||||
|  |         @wineDeleted="filterOutDeletedWine" | ||||||
|  |         :showDeleteButton="isAdmin" | ||||||
|  |       /> | ||||||
|     </section> |     </section> | ||||||
|   </main> |   </main> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { allRequestedWines } from "@/api"; |  | ||||||
| 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, | ||||||
|       canRequest: true, |  | ||||||
|       isAdmin: false |       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() { |   mounted() { | ||||||
|     this.refreshData() |     this.fetchRequestedWines(); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     filterOutDeletedWine(wine) { | ||||||
|  |       this.wines = this.wines.filter(item => item.wine._id !== wine._id); | ||||||
|  |     }, | ||||||
|  |     fetchRequestedWines() { | ||||||
|  |       return fetch("/api/requests") | ||||||
|  |         .then(resp => { | ||||||
|  |           this.isAdmin = resp.headers.get("vinlottis-admin") == "true"; | ||||||
|  |           return resp; | ||||||
|  |         }) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.wines = response.wines)); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -55,10 +65,4 @@ h1 { | |||||||
|   color: $matte-text-color; |   color: $matte-text-color; | ||||||
|   font-weight: normal; |   font-weight: normal; | ||||||
| } | } | ||||||
|  | </style> | ||||||
| .requested-wines-container{ |  | ||||||
|   display: grid; |  | ||||||
|   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |  | ||||||
|   grid-gap: 2rem; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|   | |||||||
| @@ -2,10 +2,9 @@ | |||||||
|   <div class="container"> |   <div class="container"> | ||||||
|     <h1 class="">Alle viner</h1> |     <h1 class="">Alle viner</h1> | ||||||
|  |  | ||||||
|     <div id="wines-container"> |     <div class="wines-container"> | ||||||
|       <Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id"> |       <Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id"> | ||||||
|         <div class="winners-container"> |         <div class="winners-container"> | ||||||
|  |  | ||||||
|           <span class="label">Vinnende lodd:</span> |           <span class="label">Vinnende lodd:</span> | ||||||
|           <div class="flex row"> |           <div class="flex row"> | ||||||
|             <span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span> |             <span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span> | ||||||
| @@ -19,43 +18,44 @@ | |||||||
|             <ul class="names"> |             <ul class="names"> | ||||||
|               <li v-for="(winner, index) in wine.winners"> |               <li v-for="(winner, index) in wine.winners"> | ||||||
|                 <router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link> |                 <router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link> | ||||||
|                  -  |                 -  | ||||||
|                 <router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ dateString(wine.dates[index]) }}</router-link> |                 <router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ | ||||||
|  |                   dateString(wine.dates[index]) | ||||||
|  |                 }}</router-link> | ||||||
|               </li> |               </li> | ||||||
|             </ul> |             </ul> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </Wine> |       </Wine> | ||||||
|  |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import Banner from "@/ui/Banner"; |  | ||||||
| import Wine from "@/ui/Wine"; | import Wine from "@/ui/Wine"; | ||||||
| import { overallWineStatistics } from "@/api"; |  | ||||||
| import { dateString } from "@/utils"; | import { dateString } from "@/utils"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { Wine }, | ||||||
|     Banner, |  | ||||||
|     Wine |  | ||||||
|   }, |  | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       wines: [] |       wines: [] | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.overallWineStatistics(); | ||||||
|  |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     winDateUrl(date) { |     winDateUrl(date) { | ||||||
|       const timestamp = new Date(date).getTime(); |       const timestamp = new Date(date).getTime(); | ||||||
|       return `/history/${timestamp}` |       return `/history/${timestamp}`; | ||||||
|  |     }, | ||||||
|  |     overallWineStatistics() { | ||||||
|  |       return fetch("/api/wines") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.wines = response.wines)); | ||||||
|     }, |     }, | ||||||
|     dateString: dateString |     dateString: dateString | ||||||
|   }, |  | ||||||
|   async mounted() { |  | ||||||
|     this.wines = await overallWineStatistics(); |  | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| @@ -84,18 +84,6 @@ h1 { | |||||||
|   font-weight: 600; |   font-weight: 600; | ||||||
| } | } | ||||||
|  |  | ||||||
| #wines-container { |  | ||||||
|   display: grid; |  | ||||||
|   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |  | ||||||
|   grid-gap: 2rem; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   > div { |  | ||||||
|     justify-content: flex-start; |  | ||||||
|     margin-bottom: 2rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .name-wins { | .name-wins { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
|       Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer' |       Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer' | ||||||
|     </p> |     </p> | ||||||
|  |  | ||||||
|     <RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" /> |     <RaffleGenerator @numberOfRaffles="val => (this.numberOfRaffles = val)" /> | ||||||
|  |  | ||||||
|     <Vipps class="vipps" :amount="numberOfRaffles" /> |     <Vipps class="vipps" :amount="numberOfRaffles" /> | ||||||
|     <Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> |     <Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> | ||||||
| @@ -43,16 +43,16 @@ export default { | |||||||
|       this.hardStart = true; |       this.hardStart = true; | ||||||
|     }, |     }, | ||||||
|     track() { |     track() { | ||||||
|       window.ga('send', 'pageview', '/lottery/generate'); |       window.ga("send", "pageview", "/lottery/generate"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/variables.scss"; | @import "@/styles/variables.scss"; | ||||||
| @import "../styles/global.scss"; | @import "@/styles/global.scss"; | ||||||
| @import "../styles/media-queries.scss"; | @import "@/styles/media-queries.scss"; | ||||||
| h1 { | h1 { | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
| } | } | ||||||
| @@ -67,7 +67,9 @@ p { | |||||||
| } | } | ||||||
|  |  | ||||||
| .vipps { | .vipps { | ||||||
|   margin: 5rem auto 2.5rem auto; |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   margin-top: 4rem; | ||||||
|  |  | ||||||
|   @include mobile { |   @include mobile { | ||||||
|     margin-top: 2rem; |     margin-top: 2rem; | ||||||
| @@ -75,7 +77,6 @@ p { | |||||||
| } | } | ||||||
|  |  | ||||||
| .container { | .container { | ||||||
|   margin: auto; |  | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,7 +12,12 @@ | |||||||
|     <p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p> |     <p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p> | ||||||
|  |  | ||||||
|     <ol v-if="highscore.length > 0" class="highscore-list"> |     <ol v-if="highscore.length > 0" class="highscore-list"> | ||||||
|       <li v-for="person in filteredResults" @click="selectWinner(person)" @keydown.enter="selectWinner(person)" tabindex="0"> |       <li | ||||||
|  |         v-for="person in filteredResults" | ||||||
|  |         @click="goToWinner(person)" | ||||||
|  |         @keydown.enter="goToWinner(person)" | ||||||
|  |         tabindex="0" | ||||||
|  |       > | ||||||
|         <b>{{ person.rank }}.</b>   {{ person.name }} - {{ person.wins.length }} |         <b>{{ person.rank }}.</b>   {{ person.name }} - {{ person.wins.length }} | ||||||
|       </li> |       </li> | ||||||
|     </ol> |     </ol> | ||||||
| @@ -24,8 +29,6 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  |  | ||||||
| import { highscoreStatistics } from "@/api"; |  | ||||||
| import { humanReadableDate, daysAgo } from "@/utils"; | import { humanReadableDate, daysAgo } from "@/utils"; | ||||||
| import Wine from "@/ui/Wine"; | import Wine from "@/ui/Wine"; | ||||||
|  |  | ||||||
| @@ -34,18 +37,12 @@ export default { | |||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       highscore: [], |       highscore: [], | ||||||
|       filterInput: '' |       filterInput: "" | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     let response = await highscoreStatistics(); |     const winners = await this.highscoreStatistics(); | ||||||
|     response.sort((a, b) => { |     this.highscore = this.generateScoreBoard(winners); | ||||||
|       return a.wins.length > b.wins.length ? -1 : 1; |  | ||||||
|     }); |  | ||||||
|     response = response.filter( |  | ||||||
|       person => person.name != null && person.name != "" |  | ||||||
|     ); |  | ||||||
|     this.highscore = this.generateScoreBoard(response); |  | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     filteredResults() { |     filteredResults() { | ||||||
| @@ -53,37 +50,42 @@ export default { | |||||||
|       let val = this.filterInput; |       let val = this.filterInput; | ||||||
|  |  | ||||||
|       if (val.length) { |       if (val.length) { | ||||||
|         val = val.toLowerCase() |         val = val.toLowerCase(); | ||||||
|         const nameIncludesString = (person) => person.name.toLowerCase().includes(val); |         const nameIncludesString = person => person.name.toLowerCase().includes(val); | ||||||
|         highscore = highscore.filter(nameIncludesString) |         highscore = highscore.filter(nameIncludesString); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       return highscore |       return highscore; | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     generateScoreBoard(highscore=this.highscore) { |     highscoreStatistics() { | ||||||
|  |       return fetch("/api/history/by-wins") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response.winners); | ||||||
|  |     }, | ||||||
|  |     generateScoreBoard(highscore = this.highscore) { | ||||||
|       let place = 0; |       let place = 0; | ||||||
|       let highestWinCount = -1; |       let highestWinCount = -1; | ||||||
|  |  | ||||||
|       return highscore.map(win => { |       return highscore.map(win => { | ||||||
|         const wins = win.wins.length |         const wins = win.wins.length; | ||||||
|         if (wins != highestWinCount) { |         if (wins != highestWinCount) { | ||||||
|           place += 1 |           place += 1; | ||||||
|           highestWinCount = wins |           highestWinCount = wins; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const placeString = place.toString().padStart(2, "0"); |         const placeString = place.toString().padStart(2, "0"); | ||||||
|         win.rank = placeString; |         win.rank = placeString; | ||||||
|         return win |         return win; | ||||||
|       }) |       }); | ||||||
|     }, |     }, | ||||||
|     resetFilter() { |     resetFilter() { | ||||||
|       this.filterInput = ''; |       this.filterInput = ""; | ||||||
|       document.getElementsByTagName('input')[0].focus(); |       document.getElementsByTagName("input")[0].focus(); | ||||||
|     }, |     }, | ||||||
|     selectWinner(winner) { |     goToWinner(winner) { | ||||||
|       const path = "/highscore/" + encodeURIComponent(winner.name) |       const path = "/highscore/" + encodeURIComponent(winner.name); | ||||||
|       this.$router.push(path); |       this.$router.push(path); | ||||||
|     }, |     }, | ||||||
|     humanReadableDate: humanReadableDate, |     humanReadableDate: humanReadableDate, | ||||||
| @@ -152,7 +154,8 @@ h1 { | |||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |  | ||||||
|     border-bottom: 2px solid transparent; |     border-bottom: 2px solid transparent; | ||||||
|     &:hover, &:focus { |     &:hover, | ||||||
|  |     &:focus { | ||||||
|       border-color: $link-color; |       border-color: $link-color; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -3,42 +3,51 @@ | |||||||
|     <h1>Historie fra tidligere lotteri</h1> |     <h1>Historie fra tidligere lotteri</h1> | ||||||
|  |  | ||||||
|     <div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries"> |     <div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries"> | ||||||
|       <Winners :winners="lottery.winners" :title="`Vinnere fra ${ humanReadableDate(lottery.date) }`" /> |       <Winners :winners="lottery.winners" :title="`Vinnere fra ${humanReadableDate(lottery.date)}`" /> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { historyByDate, historyAll } from '@/api' | import { historyByDate, historyAll } from "@/api"; | ||||||
| import { humanReadableDate } from "@/utils"; | import { humanReadableDate } from "@/utils"; | ||||||
| import Winners from '@/ui/Winners' | import Winners from "@/ui/Winners"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'History page of prev lotteries', |   name: "History page of prev lotteries", | ||||||
|   components: { Winners }, |   components: { Winners }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       lotteries: [], |       lotteries: [] | ||||||
|     } |     }; | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     humanReadableDate: humanReadableDate |  | ||||||
|   }, |   }, | ||||||
|   created() { |   created() { | ||||||
|     const dateFromUrl = this.$route.params.date; |     const dateFromUrl = this.$route.params.date; | ||||||
|  |  | ||||||
|     if (dateFromUrl !== undefined) |     if (dateFromUrl !== undefined) { | ||||||
|       historyByDate(dateFromUrl) |       this.fetchHistoryByDate(dateFromUrl).then(history => (this.lotteries = [history])); | ||||||
|         .then(history => this.lotteries = { "lottery": history }) |     } else { | ||||||
|     else |       this.fetchHistory().then(history => (this.lotteries = history)); | ||||||
|       historyAll() |     } | ||||||
|         .then(history => this.lotteries = history.lotteries) |   }, | ||||||
|  |   methods: { | ||||||
|  |     humanReadableDate: humanReadableDate, | ||||||
|  |     fetchHistory() { | ||||||
|  |       return fetch("/api/history/by-date") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response.lotteries); | ||||||
|  |     }, | ||||||
|  |     fetchHistoryByDate(date) { | ||||||
|  |       return fetch(`/api/history/by-date/${date}`) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| h1 { | h1 { | ||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
|         <input |         <input | ||||||
|           type="text" |           type="text" | ||||||
|           v-model="username" |           v-model="username" | ||||||
|  |           ref="username" | ||||||
|           placeholder="Brukernavn" |           placeholder="Brukernavn" | ||||||
|           autocapitalize="none" |           autocapitalize="none" | ||||||
|           @keyup.enter="submit" |           @keyup.enter="submit" | ||||||
| @@ -34,6 +35,9 @@ export default { | |||||||
|       error: undefined |       error: undefined | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.$refs.username.focus(); | ||||||
|  |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     submit() { |     submit() { | ||||||
|       login(this.username, this.password) |       login(this.username, this.password) | ||||||
|   | |||||||
| @@ -14,20 +14,25 @@ | |||||||
|  |  | ||||||
|         <h4 class="margin-bottom-0">Vinnende farger:</h4> |         <h4 class="margin-bottom-0">Vinnende farger:</h4> | ||||||
|         <div class="raffle-container el-spacing"> |         <div class="raffle-container el-spacing"> | ||||||
|           <div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)" :key="color"> |           <div | ||||||
|  |             class="raffle-element" | ||||||
|  |             :class="color + `-raffle`" | ||||||
|  |             v-for="[color, occurences] in Object.entries(winningColors)" | ||||||
|  |             :key="color" | ||||||
|  |           > | ||||||
|             {{ occurences }} |             {{ occurences }} | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <h4 class="el-spacing">Flasker vunnet:</h4> |         <h4 class="el-spacing">Flasker vunnet:</h4> | ||||||
|  |  | ||||||
|         <div v-for="win in winner.highscore" :key="win._id"> |         <div v-for="win in winner.wins" :key="win._id"> | ||||||
|           <router-link :to="winDateUrl(win.date)" class="days-ago"> |           <router-link :to="winDateUrl(win.date)" class="days-ago"> | ||||||
|             {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden |             {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden | ||||||
|           </router-link> |           </router-link> | ||||||
|            |  | ||||||
|           <div class="won-wine"> |           <div class="won-wine" v-if="win.wine"> | ||||||
|             <img :src="smallerWineImage(win.wine.image)"> |             <img :src="smallerWineImage(win.wine.image)" /> | ||||||
|  |  | ||||||
|             <div class="won-wine-details"> |             <div class="won-wine-details"> | ||||||
|               <h3>{{ win.wine.name }}</h3> |               <h3>{{ win.wine.name }}</h3> | ||||||
| @@ -38,6 +43,11 @@ | |||||||
|  |  | ||||||
|             <div class="raffle-element small" :class="win.color + `-raffle`"></div> |             <div class="raffle-element small" :class="win.color + `-raffle`"></div> | ||||||
|           </div> |           </div> | ||||||
|  |           <div class="won-wine" v-else> | ||||||
|  |             <div class="won-wine-details"> | ||||||
|  |               <h3>Oisann! Klarte ikke finne vin.</h3> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </section> |       </section> | ||||||
|  |  | ||||||
| @@ -49,67 +59,71 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { getWinnerByName } from "@/api"; | import { dateString, humanReadableDate, daysAgo } from "@/utils"; | ||||||
| import { humanReadableDate, daysAgo } from "@/utils"; |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       winner: undefined, |       winner: undefined, | ||||||
|  |       name: undefined, | ||||||
|       error: undefined, |       error: undefined, | ||||||
|       previousRoute: { |       previousRoute: { | ||||||
|         default: true, |         default: true, | ||||||
|         name: "topplisten", |         name: "topplisten", | ||||||
|         path: "/highscore" |         path: "/highscore" | ||||||
|       } |       } | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   beforeRouteEnter(to, from, next) { |   beforeRouteEnter(to, from, next) { | ||||||
|     next(vm => { |     next(vm => { | ||||||
|       if (from.name != null) |       if (from.name != null) vm.previousRoute = from; | ||||||
|         vm.previousRoute = from |     }); | ||||||
|     }) |  | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     numberOfWins() { |     numberOfWins() { | ||||||
|       return this.winner.highscore.length |       return this.winner.wins.length; | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   created() { |   created() { | ||||||
|     const nameFromURL = this.$route.params.name; |     this.name = this.$route.params.name; | ||||||
|     getWinnerByName(nameFromURL) |     this.getWinnerByName(this.name) | ||||||
|       .then(winner => this.setWinner(winner)) |       .then(winner => this.setWinner(winner)) | ||||||
|       .catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`) |       .catch(err => (this.error = `Ingen med navn: "${nameFromURL}" funnet.`)); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     getWinnerByName(name) { | ||||||
|  |       return fetch(`/api/history/by-name/${name}`) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response.winner); | ||||||
|  |     }, | ||||||
|     setWinner(winner) { |     setWinner(winner) { | ||||||
|       this.winner = { |       this.winner = { | ||||||
|         name: winner.name, |         name: winner.name, | ||||||
|         highscore: [], |         highscore: [], | ||||||
|         ...winner |         ...winner | ||||||
|       } |       }; | ||||||
|       this.winningColors = this.findWinningColors() |       this.winningColors = this.findWinningColors(); | ||||||
|     }, |     }, | ||||||
|     smallerWineImage(image) { |     smallerWineImage(image) { | ||||||
|       if (image && image.includes(`515x515`)) |       if (image && image.includes(`515x515`)) return image.replace(`515x515`, `175x175`); | ||||||
|         return image.replace(`515x515`, `175x175`) |       if (image && image.includes(`500x500`)) return image.replace(`500x500`, `175x175`); | ||||||
|       return image |       return image; | ||||||
|     }, |     }, | ||||||
|     findWinningColors() { |     findWinningColors() { | ||||||
|       const colors = this.winner.highscore.map(win => win.color) |       const colors = this.winner.wins.map(win => win.color); | ||||||
|       const colorOccurences = {} |       const colorOccurences = {}; | ||||||
|       colors.forEach(color => { |       colors.forEach(color => { | ||||||
|         if (colorOccurences[color] == undefined) { |         if (colorOccurences[color] == undefined) { | ||||||
|           colorOccurences[color] = 1 |           colorOccurences[color] = 1; | ||||||
|         } else { |         } else { | ||||||
|           colorOccurences[color] += 1 |           colorOccurences[color] += 1; | ||||||
|         } |         } | ||||||
|       }) |       }); | ||||||
|       return colorOccurences |       return colorOccurences; | ||||||
|     }, |     }, | ||||||
|     winDateUrl(date) { |     winDateUrl(date) { | ||||||
|       const timestamp = new Date(date).getTime(); |       const dateParameter = dateString(new Date(date)); | ||||||
|       return `/history/${timestamp}` |       return `/history/${dateParameter}`; | ||||||
|     }, |     }, | ||||||
|     navigateBack() { |     navigateBack() { | ||||||
|       if (this.previousRoute.default) { |       if (this.previousRoute.default) { | ||||||
| @@ -121,7 +135,7 @@ export default { | |||||||
|     humanReadableDate: humanReadableDate, |     humanReadableDate: humanReadableDate, | ||||||
|     daysAgo: daysAgo |     daysAgo: daysAgo | ||||||
|   } |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -142,7 +156,7 @@ $elementSpacing: 3rem; | |||||||
| } | } | ||||||
|  |  | ||||||
| .container { | .container { | ||||||
|   width: 90vw;   |   width: 90vw; | ||||||
|   margin: 3rem auto; |   margin: 3rem auto; | ||||||
|   margin-bottom: 0; |   margin-bottom: 0; | ||||||
|   padding-bottom: 3rem; |   padding-bottom: 3rem; | ||||||
| @@ -233,7 +247,7 @@ h1 { | |||||||
|     @include tablet { |     @include tablet { | ||||||
|       width: calc(100% - 160px - 80px); |       width: calc(100% - 160px - 80px); | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     & > * { |     & > * { | ||||||
|       width: 100%; |       width: 100%; | ||||||
|     } |     } | ||||||
| @@ -259,10 +273,9 @@ h1 { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| .backdrop { | .backdrop { | ||||||
|   $background: rgb(244,244,244); |   $background: rgb(244, 244, 244); | ||||||
|    |  | ||||||
|   --padding: 2rem; |   --padding: 2rem; | ||||||
|   @include desktop { |   @include desktop { | ||||||
|     --padding: 5rem; |     --padding: 5rem; | ||||||
| @@ -270,4 +283,4 @@ h1 { | |||||||
|   background-color: $background; |   background-color: $background; | ||||||
|   padding: var(--padding); |   padding: var(--padding); | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,728 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="page-container"> |  | ||||||
|     <h1>Registrering</h1> |  | ||||||
|     <br /> |  | ||||||
|     <br /> |  | ||||||
|     <div class="notification-element"> |  | ||||||
|       <div class="label-div"> |  | ||||||
|         <label for="notification">Push-melding</label> |  | ||||||
|         <textarea |  | ||||||
|           id="notification" |  | ||||||
|           type="text" |  | ||||||
|           rows="3" |  | ||||||
|           v-model="pushMessage" |  | ||||||
|           placeholder="Push meldingtekst" |  | ||||||
|         /> |  | ||||||
|         <input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" /> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="button-container"> |  | ||||||
|       <button class="vin-button" @click="sendPush">Send push</button> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <hr /> |  | ||||||
|  |  | ||||||
|     <h2 id="addwine-title">Prelottery</h2> |  | ||||||
|  |  | ||||||
|     <ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" /> |  | ||||||
|  |  | ||||||
|     <div class="button-container"> |  | ||||||
|       <button |  | ||||||
|         class="vin-button" |  | ||||||
|         @click="showCamera = !showCamera" |  | ||||||
|       >{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}</button> |  | ||||||
|  |  | ||||||
|       <button class="vin-button" @click="addWine">Legg til en vin manuelt</button> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div v-if="wines.length > 0" class="edit-container"> |  | ||||||
|       <wine v-for="wine in wines" :key="key" :wine="wine"> |  | ||||||
|         <div class="edit"> |  | ||||||
|           <div class="button-container row"> |  | ||||||
|             <button |  | ||||||
|               class="vin-button" |  | ||||||
|               @click="editWine = amIBeingEdited(wine) ? false : wine" |  | ||||||
|             >{{ amIBeingEdited(wine) ? "Lukk" : "Rediger" }}</button> |  | ||||||
|             <button class="red vin-button" @click="deleteWine(wine)">Slett</button> |  | ||||||
|           </div> |  | ||||||
|  |  | ||||||
|           <div v-if="amIBeingEdited(wine)" class="wine-edit"> |  | ||||||
|             <div class="label-div" v-for="key in Object.keys(wine)" :key="key"> |  | ||||||
|               <label>{{ key }}</label> |  | ||||||
|               <input type="text" v-model="wine[key]" :placeholder="key" /> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </wine> |  | ||||||
|     </div> |  | ||||||
|     <div class="button-container" v-if="wines.length > 0"> |  | ||||||
|       <button class="vin-button" @click="sendWines">Send inn viner</button> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <hr /> |  | ||||||
|  |  | ||||||
|     <h2>Lottery</h2> |  | ||||||
|  |  | ||||||
|     <h3>Legg til lodd kjøpt</h3> |  | ||||||
|     <div class="colors"> |  | ||||||
|       <div v-for="color in lotteryColors" :class="color.css + ' colors-box'" :key="color"> |  | ||||||
|         <div class="colors-overlay"> |  | ||||||
|           <p>{{ color.name }} kjøpt</p> |  | ||||||
|           <input v-model="color.value" min="0" :placeholder="0" type="number" /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <div class="label-div"> |  | ||||||
|         <label>Totalt kjøpt for:</label> |  | ||||||
|         <input v-model="payed" placeholder="NOK" type="number" :step="price || 1" min="0" /> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div class="button-container"> |  | ||||||
|       <button class="vin-button" @click="submitLottery">Send inn lotteri</button> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <h3>Vinnere</h3> |  | ||||||
|     <a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a> |  | ||||||
|     <div class="winner-container" v-if="winners.length > 0"> |  | ||||||
|       <wine v-for="winner in winners" :key="winner" :wine="winner.wine"> |  | ||||||
|         <div class="winner-element"> |  | ||||||
|           <div class="color-selector"> |  | ||||||
|             <div class="label-div"> |  | ||||||
|               <label>Farge vunnet</label> |  | ||||||
|             </div> |  | ||||||
|             <button |  | ||||||
|               class="blue" |  | ||||||
|               :class="{ active: winner.color == 'blue' }" |  | ||||||
|               @click="winner.color = 'blue'" |  | ||||||
|             ></button> |  | ||||||
|             <button |  | ||||||
|               class="red" |  | ||||||
|               :class="{ active: winner.color == 'red' }" |  | ||||||
|               @click="winner.color = 'red'" |  | ||||||
|             ></button> |  | ||||||
|             <button |  | ||||||
|               class="green" |  | ||||||
|               :class="{ active: winner.color == 'green' }" |  | ||||||
|               @click="winner.color = 'green'" |  | ||||||
|             ></button> |  | ||||||
|             <button |  | ||||||
|               class="yellow" |  | ||||||
|               :class="{ active: winner.color == 'yellow' }" |  | ||||||
|               @click="winner.color = 'yellow'" |  | ||||||
|             ></button> |  | ||||||
|           </div> |  | ||||||
|           <div class="label-div"> |  | ||||||
|             <label for="winner-name">Navn vinner</label> |  | ||||||
|             <input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="label-div"> |  | ||||||
|           <label for="potential-winner-name">Virtuelle vinnere</label> |  | ||||||
|           <select |  | ||||||
|             id="potential-winner-name" |  | ||||||
|             type="text" |  | ||||||
|             placeholder="Navn" |  | ||||||
|             v-model="winner.potentialWinner" |  | ||||||
|             @change="potentialChange($event, winner)" |  | ||||||
|           > |  | ||||||
|             <option |  | ||||||
|               v-for="fetchedWinner in fetchedWinners" |  | ||||||
|               :value="stringify(fetchedWinner)" |  | ||||||
|             >{{fetchedWinner.name}}</option> |  | ||||||
|           </select> |  | ||||||
|         </div> |  | ||||||
|       </wine> |  | ||||||
|  |  | ||||||
|       <div class="button-container column"> |  | ||||||
|         <button class="vin-button" @click="submitLotteryWinners">Send inn vinnere</button> |  | ||||||
|         <button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" /> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import eventBus from "@/mixins/EventBus"; |  | ||||||
| import { dateString } from '@/utils' |  | ||||||
| import { |  | ||||||
|   prelottery, |  | ||||||
|   sendLotteryWinners, |  | ||||||
|   sendLottery, |  | ||||||
|   logWines, |  | ||||||
|   wineSchema, |  | ||||||
|   winnersSecure, |  | ||||||
|   attendees |  | ||||||
| } from "@/api"; |  | ||||||
| import TextToast from "@/ui/TextToast"; |  | ||||||
| import Wine from "@/ui/Wine"; |  | ||||||
| import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet"; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { TextToast, Wine, ScanToVinmonopolet }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       payed: undefined, |  | ||||||
|       winners: [], |  | ||||||
|       fetchedWinners: [], |  | ||||||
|       wines: [], |  | ||||||
|       pushMessage: "", |  | ||||||
|       pushLink: "/", |  | ||||||
|       toastText: undefined, |  | ||||||
|       showToast: false, |  | ||||||
|       showCamera: false, |  | ||||||
|       editWine: false, |  | ||||||
|       lotteryColors: [ |  | ||||||
|         { value: null, name: "Blå", css: "blue" }, |  | ||||||
|         { value: null, name: "Rød", css: "red" }, |  | ||||||
|         { value: null, name: "Grønn", css: "green" }, |  | ||||||
|         { value: null, name: "Gul", css: "yellow" } |  | ||||||
|       ], |  | ||||||
|       price: __PRICE__ |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     this.fetchAndAddPrelotteryWines().then(this.getWinnerdataFromStorage); |  | ||||||
|  |  | ||||||
|     window.addEventListener("unload", this.setWinnerdataToStorage); |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     this.setWinnerdataToStorage(); |  | ||||||
|     eventBus.$off("tab-change", () => { |  | ||||||
|       this.fetchColorsAndWinners(); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     this.fetchColorsAndWinners(); |  | ||||||
|  |  | ||||||
|     eventBus.$on("tab-change", () => { |  | ||||||
|       this.fetchColorsAndWinners(); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     stringify(json) { |  | ||||||
|       return JSON.stringify(json); |  | ||||||
|     }, |  | ||||||
|     potentialChange(event, winner) { |  | ||||||
|       let data = JSON.parse(event.target.value); |  | ||||||
|       winner.name = data.name; |  | ||||||
|       winner.color = data.color; |  | ||||||
|     }, |  | ||||||
|     async fetchColorsAndWinners() { |  | ||||||
|       let winners = await winnersSecure(); |  | ||||||
|       let _attendees = await attendees(); |  | ||||||
|       let colors = { |  | ||||||
|         red: 0, |  | ||||||
|         blue: 0, |  | ||||||
|         green: 0, |  | ||||||
|         yellow: 0 |  | ||||||
|       }; |  | ||||||
|       this.payed = 0; |  | ||||||
|       for (let i = 0; i < _attendees.length; i++) { |  | ||||||
|         let attendee = _attendees[i]; |  | ||||||
|         colors.red += attendee.red; |  | ||||||
|         colors.blue += attendee.blue; |  | ||||||
|         colors.green += attendee.green; |  | ||||||
|         colors.yellow += attendee.yellow; |  | ||||||
|         this.payed += |  | ||||||
|           (attendee.red + attendee.blue + attendee.green + attendee.yellow) * |  | ||||||
|           10; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       for (let i = 0; i < this.lotteryColors.length; i++) { |  | ||||||
|         let currentColor = this.lotteryColors[i]; |  | ||||||
|         switch (currentColor.css) { |  | ||||||
|           case "red": |  | ||||||
|             currentColor.value = colors.red; |  | ||||||
|             break; |  | ||||||
|           case "blue": |  | ||||||
|             currentColor.value = colors.blue; |  | ||||||
|             break; |  | ||||||
|             a; |  | ||||||
|           case "green": |  | ||||||
|             currentColor.value = colors.green; |  | ||||||
|             break; |  | ||||||
|           case "yellow": |  | ||||||
|             currentColor.value = colors.yellow; |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       this.fetchedWinners = winners; |  | ||||||
|     }, |  | ||||||
|     amIBeingEdited(wine) { |  | ||||||
|       return this.editWine.id == wine.id && this.editWine.name == wine.name; |  | ||||||
|     }, |  | ||||||
|     async fetchAndAddPrelotteryWines() { |  | ||||||
|       const wines = await prelottery(); |  | ||||||
|  |  | ||||||
|       for (let i = 0; i < wines.length; i++) { |  | ||||||
|         let wine = wines[i]; |  | ||||||
|         this.winners.push({ |  | ||||||
|           name: "", |  | ||||||
|           color: "", |  | ||||||
|           potentialWinner: "", |  | ||||||
|           wine: { |  | ||||||
|             name: wine.name, |  | ||||||
|             vivinoLink: wine.vivinoLink, |  | ||||||
|             rating: wine.rating, |  | ||||||
|             image: wine.image, |  | ||||||
|             id: wine.id |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     wineFromVinmonopoletScan(wineResponse) { |  | ||||||
|       if (this.wines.map(wine => wine.name).includes(wineResponse.name)) { |  | ||||||
|         this.toastText = "Vinen er allerede lagt til."; |  | ||||||
|         this.showToast = true; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.toastText = "Fant og la til vin:<br>" + wineResponse.name; |  | ||||||
|       this.showToast = true; |  | ||||||
|  |  | ||||||
|       this.wines.unshift(wineResponse); |  | ||||||
|     }, |  | ||||||
|     sendPush: async function() { |  | ||||||
|       let _response = await fetch("/subscription/send-notification", { |  | ||||||
|         headers: { |  | ||||||
|           "Content-Type": "application/json" |  | ||||||
|           // 'Content-Type': 'application/x-www-form-urlencoded', |  | ||||||
|         }, |  | ||||||
|         method: "POST", |  | ||||||
|         body: JSON.stringify({ message: this.pushMessage, link: this.pushLink }) |  | ||||||
|       }); |  | ||||||
|       let response = await _response.json(); |  | ||||||
|       if (response) { |  | ||||||
|         alert("Sendt!"); |  | ||||||
|       } else { |  | ||||||
|         alert("Noe gikk galt!"); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     addWine: async function(event) { |  | ||||||
|       const wine = await wineSchema(); |  | ||||||
|  |  | ||||||
|       this.editWine = wine; |  | ||||||
|       this.wines.unshift(wine); |  | ||||||
|     }, |  | ||||||
|     deleteWine(deletedWine) { |  | ||||||
|       this.wines = this.wines.filter(wine => wine.name != deletedWine.name); |  | ||||||
|     }, |  | ||||||
|     sendWines: async function() { |  | ||||||
|       let response = await logWines(this.wines); |  | ||||||
|       if (response.success == true) { |  | ||||||
|         alert("Sendt!"); |  | ||||||
|         window.location.reload(); |  | ||||||
|       } else { |  | ||||||
|         alert("Noe gikk galt under innsending"); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     addWinner: function(event) { |  | ||||||
|       this.winners.push({ |  | ||||||
|         name: "", |  | ||||||
|         color: "", |  | ||||||
|         wine: { |  | ||||||
|           name: "", |  | ||||||
|           vivinoLink: "", |  | ||||||
|           rating: "" |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     submitLottery: async function(event) { |  | ||||||
|       const colors = { |  | ||||||
|         red: this.lotteryColors.filter(c => c.css == "red")[0].value, |  | ||||||
|         green: this.lotteryColors.filter(c => c.css == "green")[0].value, |  | ||||||
|         blue: this.lotteryColors.filter(c => c.css == "blue")[0].value, |  | ||||||
|         yellow: this.lotteryColors.filter(c => c.css == "yellow")[0].value |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       let sendObject = { |  | ||||||
|         lottery: { |  | ||||||
|           date: dateString(new Date()), |  | ||||||
|           ...colors |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       if (sendObject.lottery.red == undefined) { |  | ||||||
|         alert("Rød må defineres"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (sendObject.lottery.green == undefined) { |  | ||||||
|         alert("Grønn må defineres"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (sendObject.lottery.yellow == undefined) { |  | ||||||
|         alert("Gul må defineres"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (sendObject.lottery.blue == undefined) { |  | ||||||
|         alert("Blå må defineres"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       sendObject.lottery.bought = |  | ||||||
|         parseInt(colors.blue) + |  | ||||||
|         parseInt(colors.red) + |  | ||||||
|         parseInt(colors.green) + |  | ||||||
|         parseInt(colors.yellow); |  | ||||||
|       const stolen = sendObject.lottery.bought - parseInt(this.payed) / 10; |  | ||||||
|       if (isNaN(stolen) || stolen == undefined) { |  | ||||||
|         alert("Betalt må registreres"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       sendObject.lottery.stolen = stolen; |  | ||||||
|  |  | ||||||
|       let response = await sendLottery(sendObject); |  | ||||||
|       if (response == true) { |  | ||||||
|         alert("Sendt!"); |  | ||||||
|         window.location.reload(); |  | ||||||
|       } else { |  | ||||||
|         alert(response.message || "Noe gikk galt under innsending"); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     submitLotteryWinners: async function(event) { |  | ||||||
|       let sendObject = { |  | ||||||
|         lottery: { |  | ||||||
|           date: dateString(new Date()), |  | ||||||
|           winners: this.winners |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (sendObject.lottery.winners.length == 0) { |  | ||||||
|         alert("Det må være med vinnere"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       for (let i = 0; i < sendObject.lottery.winners.length; i++) { |  | ||||||
|         let currentWinner = sendObject.lottery.winners[i]; |  | ||||||
|  |  | ||||||
|         if (currentWinner.name == undefined || currentWinner.name == "") { |  | ||||||
|           alert("Navn må defineres"); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         if (currentWinner.color == undefined || currentWinner.color == "") { |  | ||||||
|           alert("Farge må defineres"); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let response = await sendLotteryWinners(sendObject); |  | ||||||
|       if (response == true) { |  | ||||||
|         alert("Sendt!"); |  | ||||||
|         window.location.reload(); |  | ||||||
|       } else { |  | ||||||
|         alert(response.message || "Noe gikk galt under innsending"); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     getWinnerdataFromStorage() { |  | ||||||
|       let localWinners = localStorage.getItem("winners"); |  | ||||||
|       if (localWinners && this.winners.length) { |  | ||||||
|         localWinners = JSON.parse(localWinners); |  | ||||||
|  |  | ||||||
|         this.winners = this.winners.map(winner => { |  | ||||||
|           const localWinnerMatch = localWinners.filter( |  | ||||||
|             localWinner => |  | ||||||
|               localWinner.wine.name == winner.wine.name || |  | ||||||
|               localWinner.wine.id == winner.wine.id |  | ||||||
|           ); |  | ||||||
|  |  | ||||||
|           if (localWinnerMatch.length > 0) { |  | ||||||
|             winner.name = localWinnerMatch[0].name || winner.name; |  | ||||||
|             winner.color = localWinnerMatch[0].color || winner.name; |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           return winner; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let localColors = localStorage.getItem("colorValues"); |  | ||||||
|       if (localColors) { |  | ||||||
|         localColors = localColors.split(","); |  | ||||||
|         this.lotteryColors.forEach((color, i) => { |  | ||||||
|           const localColorValue = Number(localColors[i]); |  | ||||||
|           color.value = localColorValue == 0 ? null : localColorValue; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     setWinnerdataToStorage() { |  | ||||||
|       localStorage.setItem("winners", JSON.stringify(this.winners)); |  | ||||||
|       localStorage.setItem( |  | ||||||
|         "colorValues", |  | ||||||
|         this.lotteryColors.map(color => Number(color.value)) |  | ||||||
|       ); |  | ||||||
|       window.removeEventListener("unload", this.setWinnerdataToStorage); |  | ||||||
|     }, |  | ||||||
|     resetWinnerDataInStorage() { |  | ||||||
|       this.winners = []; |  | ||||||
|       this.fetchAndAddPrelotteryWines().then(resp => (this.winners = resp)); |  | ||||||
|       this.lotteryColors.map(color => (color.value = null)); |  | ||||||
|       window.location.reload(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "../styles/global.scss"; |  | ||||||
| @import "../styles/media-queries.scss"; |  | ||||||
| select { |  | ||||||
|   margin: 0 0 auto; |  | ||||||
|   height: 2rem; |  | ||||||
|   min-width: 0; |  | ||||||
|   width: 98%; |  | ||||||
|   padding: 1%; |  | ||||||
| } |  | ||||||
| h1 { |  | ||||||
|   width: 100%; |  | ||||||
|   text-align: center; |  | ||||||
|   font-family: knowit, Arial; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h2 { |  | ||||||
|   width: 100%; |  | ||||||
|   text-align: center; |  | ||||||
|   font-size: 1.6rem; |  | ||||||
|   font-family: knowit, Arial; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .wine-link { |  | ||||||
|   color: #333333; |  | ||||||
|   text-decoration: none; |  | ||||||
|   font-weight: bold; |  | ||||||
|   cursor: pointer; |  | ||||||
|   border-bottom: 1px solid $link-color; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| hr { |  | ||||||
|   width: 90%; |  | ||||||
|   margin: 2rem auto; |  | ||||||
|   color: grey; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .button-container { |  | ||||||
|   margin-top: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .page-container { |  | ||||||
|   padding: 0 1.5rem 3rem; |  | ||||||
|  |  | ||||||
|   @include desktop { |  | ||||||
|     max-width: 60vw; |  | ||||||
|     margin: 0 auto; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .winner-container { |  | ||||||
|   width: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   justify-content: space-between; |  | ||||||
|  |  | ||||||
|   .button-container { |  | ||||||
|     width: 100%; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .edit-container { |  | ||||||
|   margin-top: 2rem; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   flex-direction: row; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|  |  | ||||||
|   > .wine { |  | ||||||
|     margin-right: 1rem; |  | ||||||
|     margin-bottom: 1rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .edit { |  | ||||||
|   width: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .notification-element { |  | ||||||
|   margin-bottom: 2rem; |  | ||||||
| } |  | ||||||
| .winner-element { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|  |  | ||||||
|   > div { |  | ||||||
|     margin-bottom: 1rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     width: 100%; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| .wine-element { |  | ||||||
|   align-items: flex-start; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .generate-link { |  | ||||||
|   color: #333333; |  | ||||||
|   text-decoration: none; |  | ||||||
|   display: block; |  | ||||||
|   text-align: center; |  | ||||||
|   margin-bottom: 0px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .wine-edit { |  | ||||||
|   width: 100%; |  | ||||||
|   margin-top: 1.5rem; |  | ||||||
|  |  | ||||||
|   label { |  | ||||||
|     margin-top: 0.75rem; |  | ||||||
|     margin-bottom: 0; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .color-selector { |  | ||||||
|   margin-bottom: 0.65rem; |  | ||||||
|   margin-right: 1rem; |  | ||||||
|  |  | ||||||
|   @include desktop { |  | ||||||
|     min-width: 175px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     max-width: 25vw; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .active { |  | ||||||
|     border: 2px solid unset; |  | ||||||
|  |  | ||||||
|     &.green { |  | ||||||
|       border-color: $green; |  | ||||||
|     } |  | ||||||
|     &.blue { |  | ||||||
|       border-color: $dark-blue; |  | ||||||
|     } |  | ||||||
|     &.red { |  | ||||||
|       border-color: $red; |  | ||||||
|     } |  | ||||||
|     &.yellow { |  | ||||||
|       border-color: $dark-yellow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   button { |  | ||||||
|     border: 2px solid transparent; |  | ||||||
|     display: inline-flex; |  | ||||||
|     flex-wrap: wrap; |  | ||||||
|     flex-direction: row; |  | ||||||
|     height: 2.5rem; |  | ||||||
|     width: 2.5rem; |  | ||||||
|  |  | ||||||
|     // disable-dbl-tap-zoom |  | ||||||
|     touch-action: manipulation; |  | ||||||
|  |  | ||||||
|     @include mobile { |  | ||||||
|       margin: 2px; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &.green { |  | ||||||
|       background: #c8f9df; |  | ||||||
|     } |  | ||||||
|     &.blue { |  | ||||||
|       background: #d4f2fe; |  | ||||||
|     } |  | ||||||
|     &.red { |  | ||||||
|       background: #fbd7de; |  | ||||||
|     } |  | ||||||
|     &.yellow { |  | ||||||
|       background: #fff6d6; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .colors { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   justify-content: center; |  | ||||||
|   max-width: 1400px; |  | ||||||
|   margin: 3rem auto 1rem; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     margin: 1.8rem auto 0; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .label-div { |  | ||||||
|     margin-top: 0.5rem; |  | ||||||
|     width: 100%; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .colors-box { |  | ||||||
|   width: 150px; |  | ||||||
|   height: 150px; |  | ||||||
|   margin: 20px; |  | ||||||
|   -webkit-mask-image: url(/public/assets/images/lodd.svg); |  | ||||||
|   background-repeat: no-repeat; |  | ||||||
|   mask-image: url(/public/assets/images/lodd.svg); |  | ||||||
|   -webkit-mask-repeat: no-repeat; |  | ||||||
|   mask-repeat: no-repeat; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     width: 120px; |  | ||||||
|     height: 120px; |  | ||||||
|     margin: 10px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .colors-overlay { |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   height: 100%; |  | ||||||
|   padding: 0 0.5rem; |  | ||||||
|   position: relative; |  | ||||||
|  |  | ||||||
|   p { |  | ||||||
|     margin: 0; |  | ||||||
|     font-size: 0.8rem; |  | ||||||
|     margin-bottom: 0.5rem; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|     font-weight: 600; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0.4rem; |  | ||||||
|     left: 0.5rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   input { |  | ||||||
|     width: 70%; |  | ||||||
|     border: 0; |  | ||||||
|     padding: 0; |  | ||||||
|     font-size: 3rem; |  | ||||||
|     height: unset; |  | ||||||
|     max-height: unset; |  | ||||||
|     position: absolute; |  | ||||||
|     bottom: 1.5rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .green, |  | ||||||
| .green .colors-overlay > input { |  | ||||||
|   background-color: $light-green; |  | ||||||
|   color: $green; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .blue, |  | ||||||
| .blue .colors-overlay > input { |  | ||||||
|   background-color: $light-blue; |  | ||||||
|   color: $blue; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .yellow, |  | ||||||
| .yellow .colors-overlay > input { |  | ||||||
|   background-color: $light-yellow; |  | ||||||
|   color: $yellow; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .red, |  | ||||||
| .red .colors-overlay > input { |  | ||||||
|   background-color: $light-red; |  | ||||||
|   color: $red; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,26 +1,29 @@ | |||||||
| <template> | <template> | ||||||
|   <section class="main-container"> |   <section class="main-container"> | ||||||
|     <Modal  |     <Modal | ||||||
|       v-if="showModal"  |       v-if="showModal" | ||||||
|       modalText="Ønsket ditt har blitt lagt til"  |       modalText="Ønsket ditt har blitt lagt til" | ||||||
|       :buttons="modalButtons" |       :buttons="modalButtons" | ||||||
|       @click="emitFromModalButton" |       @click="emitFromModalButton" | ||||||
|     ></Modal> |     ></Modal> | ||||||
|     <h1> |     <h1> | ||||||
|       Foreslå en vin! |       Foreslå en vin! | ||||||
|     </h1> |     </h1> | ||||||
|  |  | ||||||
|     <section class="search-container"> |     <section class="search-container"> | ||||||
|       <section class="search-section"> |       <section class="search-section"> | ||||||
|         <input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷" class="search-input-field"> |         <input | ||||||
|         <button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button> |           type="text" | ||||||
|       </section> |           v-model="searchString" | ||||||
|       <section v-for="(wine, index) in this.wines" :key="index" class="single-result"> |           @keyup.enter="searchWines()" | ||||||
|         <img |           placeholder="Søk etter en vin du liker her!🍷" | ||||||
|           v-if="wine.image" |           class="search-input-field" | ||||||
|           :src="wine.image" |  | ||||||
|           class="wine-image" |  | ||||||
|           :class="{ 'fullscreen': fullscreen }" |  | ||||||
|         /> |         /> | ||||||
|  |         <button :disabled="!searchString" @click="searchWines()" class="vin-button">Søk</button> | ||||||
|  |       </section> | ||||||
|  |  | ||||||
|  |       <section v-for="(wine, index) in wines" :key="index" class="single-result"> | ||||||
|  |         <img v-if="wine.image" :src="wine.image" class="wine-image" :class="{ fullscreen: fullscreen }" /> | ||||||
|         <img v-else class="wine-placeholder" alt="Wine image" /> |         <img v-else class="wine-placeholder" alt="Wine image" /> | ||||||
|         <section class="wine-info"> |         <section class="wine-info"> | ||||||
|           <h2 v-if="wine.name">{{ wine.name }}</h2> |           <h2 v-if="wine.name">{{ wine.name }}</h2> | ||||||
| @@ -29,37 +32,38 @@ | |||||||
|             <span v-if="wine.rating">{{ wine.rating }}%</span> |             <span v-if="wine.rating">{{ wine.rating }}%</span> | ||||||
|             <span v-if="wine.price">{{ wine.price }} NOK</span> |             <span v-if="wine.price">{{ wine.price }} NOK</span> | ||||||
|             <span v-if="wine.country">{{ wine.country }}</span> |             <span v-if="wine.country">{{ wine.country }}</span> | ||||||
|  |             <span v-if="wine.year">{{ wine.year }}</span> | ||||||
|           </div> |           </div> | ||||||
|         </section> |         </section> | ||||||
|           <button class="vin-button" @click="request(wine)">Foreslå denne</button> |         <button class="vin-button" @click="requestWine(wine)">Foreslå denne</button> | ||||||
|           <a |         <a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="wine-link">Les mer</a> | ||||||
|           v-if="wine.vivinoLink" |  | ||||||
|           :href="wine.vivinoLink" |  | ||||||
|           class="wine-link" |  | ||||||
|         >Les mer</a> |  | ||||||
|       </section> |       </section> | ||||||
|       <p v-if="this.wines && this.wines.length == 0"> |       <p v-if="loading == false && wines && wines.length == 0"> | ||||||
|         Fant ingen viner med det navnet! |         Fant ingen viner med det navnet! | ||||||
|       </p> |       </p> | ||||||
|  |       <p v-else-if="loading">Loading...</p> | ||||||
|     </section> |     </section> | ||||||
|   </section> |   </section> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { searchForWine, requestNewWine } from "@/api"; | import { searchForWine } from "@/api"; | ||||||
| import Wine from "@/ui/Wine"; | import Wine from "@/ui/Wine"; | ||||||
| import Modal from "@/ui/Modal"; | import Modal from "@/ui/Modal"; | ||||||
|  | import RequestedWineCard from "@/ui/RequestedWineCard"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     Wine, |     Wine, | ||||||
|     Modal |     Modal, | ||||||
|  |     RequestedWineCard | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       searchString: undefined, |       searchString: undefined, | ||||||
|       wines: undefined, |       wines: undefined, | ||||||
|       showModal: false, |       showModal: false, | ||||||
|  |       loading: false, | ||||||
|       modalButtons: [ |       modalButtons: [ | ||||||
|         { |         { | ||||||
|           text: "Legg til flere viner", |           text: "Legg til flere viner", | ||||||
| @@ -70,30 +74,59 @@ export default { | |||||||
|           action: "move" |           action: "move" | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     fetchWineFromVin(){ |     fetchWinesByQuery(query) { | ||||||
|       if(this.searchString){ |       let url = new URL("/api/vinmonopolet/wine/search", window.location); | ||||||
|         this.wines = [] |       url.searchParams.set("name", query); | ||||||
|         let localSearchString = this.searchString.replace(/ /g,"_"); |  | ||||||
|         searchForWine(localSearchString) |       this.wines = []; | ||||||
|           .then(res => this.wines = res) |       this.loading = true; | ||||||
|  |  | ||||||
|  |       return fetch(url.href) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.wines = response.wines)) | ||||||
|  |         .finally(wines => (this.loading = false)); | ||||||
|  |     }, | ||||||
|  |     searchWines() { | ||||||
|  |       if (this.searchString) { | ||||||
|  |         let localSearchString = this.searchString.replace(/ /g, "_"); | ||||||
|  |         this.fetchWinesByQuery(localSearchString); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     request(wine){ |     requestWine(wine) { | ||||||
|       requestNewWine(wine) |       const options = { | ||||||
|         .then(() => this.showModal = true) |         method: "POST", | ||||||
|  |         headers: { "Content-Type": "application/json" }, | ||||||
|  |         body: JSON.stringify({ wine: wine }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return fetch("/api/request", options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.showModal = true; | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: `Vinen ${wine.name} har blitt foreslått!` | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: "Obs, her oppsto det en feil! Feilen er logget.", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|     }, |     }, | ||||||
|     emitFromModalButton(action){ |     emitFromModalButton(action) { | ||||||
|       if(action == "stay"){ |       if (action == "stay") { | ||||||
|         this.showModal = false |         this.showModal = false; | ||||||
|       } else { |       } else { | ||||||
|         this.$router.push("/requested-wines"); |         this.$router.push("/requested-wines"); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -101,12 +134,11 @@ export default { | |||||||
| @import "@/styles/global"; | @import "@/styles/global"; | ||||||
| @import "@/styles/variables"; | @import "@/styles/variables"; | ||||||
|  |  | ||||||
|  | h1 { | ||||||
| h1{ |  | ||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
|  |  | ||||||
| .main-container{ | .main-container { | ||||||
|   margin: auto; |   margin: auto; | ||||||
|   max-width: 1200px; |   max-width: 1200px; | ||||||
| } | } | ||||||
| @@ -120,66 +152,63 @@ input[type="text"] { | |||||||
|   max-width: 90%; |   max-width: 90%; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .search-container { | ||||||
| .search-container{ |  | ||||||
|   margin: 1rem; |   margin: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .search-section{ | .search-section { | ||||||
|   display: grid; |   display: grid; | ||||||
|   grid: 1fr / 1fr .2fr; |   grid: 1fr / 1fr 0.2fr; | ||||||
|  |  | ||||||
|   @include mobile{ |   @include mobile { | ||||||
|     .vin-button{ |     .vin-button { | ||||||
|       display: none; |       display: none; | ||||||
|     } |     } | ||||||
|     .search-input-field{ |     .search-input-field { | ||||||
|       grid-column: 1 / -1; |       grid-column: 1 / -1; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .single-result{ | .single-result { | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
|   display: grid; |   display: grid; | ||||||
|   grid: 1fr / .5fr 2fr .5fr .5fr; |   grid: 1fr / 0.5fr 2fr 0.5fr 0.5fr; | ||||||
|   grid-template-areas: "picture details button-left button-right"; |   grid-template-areas: "picture details button-left button-right"; | ||||||
|   justify-items: center; |   justify-items: center; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   grid-gap: 1em; |   grid-gap: 1em; | ||||||
|   padding-bottom: 1em; |   padding-bottom: 1em; | ||||||
|   margin-bottom: 1em; |   margin-bottom: 1em; | ||||||
|   box-shadow: 0 1px 0 0 rgba(0,0,0,0.2); |   box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2); | ||||||
|  |  | ||||||
|   @include mobile{ |   @include mobile { | ||||||
|  |     grid: 1fr 0.5fr / 0.5fr 1fr; | ||||||
|  |     grid-template-areas: | ||||||
|  |       "picture details" | ||||||
|  |       "button-left button-right"; | ||||||
|  |     grid-gap: 0.5em; | ||||||
|  |  | ||||||
|     grid: 1fr .5fr / .5fr 1fr; |     .vin-button { | ||||||
|     grid-template-areas: "picture details" |  | ||||||
|                          "button-left button-right"; |  | ||||||
|     grid-gap: .5em; |  | ||||||
|  |  | ||||||
|     .vin-button{ |  | ||||||
|       grid-area: button-right; |       grid-area: button-right; | ||||||
|       padding: .5em; |       padding: 0.5em; | ||||||
|       font-size: 1em; |       font-size: 1em; | ||||||
|       line-height: 1em; |       line-height: 1em; | ||||||
|       height: 2em; |       height: 2em; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .wine-link{ |     .wine-link { | ||||||
|       grid-area: button-left; |       grid-area: button-left; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     h2{ |     h2 { | ||||||
|       font-size: 1em; |       font-size: 1em; | ||||||
|       max-width: 80%; |       max-width: 80%; | ||||||
|       white-space: nowrap; |       white-space: nowrap; | ||||||
|       overflow: hidden; |       overflow: hidden; | ||||||
|       text-overflow: ellipsis |       text-overflow: ellipsis; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   } |   } | ||||||
|   |  | ||||||
|  |  | ||||||
|   .wine-image { |   .wine-image { | ||||||
|     height: 100px; |     height: 100px; | ||||||
| @@ -192,14 +221,14 @@ input[type="text"] { | |||||||
|     grid-area: picture; |     grid-area: picture; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .wine-info{ |   .wine-info { | ||||||
|     grid-area: details; |     grid-area: details; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|      |  | ||||||
|     h2{ |     h2 { | ||||||
|       margin: 0; |       margin: 0; | ||||||
|     } |     } | ||||||
|     .details{ |     .details { | ||||||
|       top: 0; |       top: 0; | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
| @@ -216,22 +245,20 @@ input[type="text"] { | |||||||
|     width: max-content; |     width: max-content; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .vin-button{ |   .vin-button { | ||||||
|     grid-area: button-right; |     grid-area: button-right; | ||||||
|   } |   } | ||||||
|    |  | ||||||
|   @include tablet{ |   @include tablet { | ||||||
|     h2{ |     h2 { | ||||||
|       font-size: 1.2em; |       font-size: 1.2em; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |  | ||||||
|   @include desktop{ |   @include desktop { | ||||||
|     h2{ |     h2 { | ||||||
|       font-size: 1.6em; |       font-size: 1.6em; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | </style> | ||||||
|  |  | ||||||
| </style> |  | ||||||
|   | |||||||
| @@ -23,7 +23,9 @@ export default { | |||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     prelottery().then(wines => this.wines = wines); |     fetch("/api/lottery/wines") | ||||||
|  |       .then(resp => resp.json()) | ||||||
|  |       .then(response => (this.wines = response.wines)); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| @@ -42,19 +44,18 @@ h1 { | |||||||
| } | } | ||||||
|  |  | ||||||
| .wines-container { | .wines-container { | ||||||
|   display: grid; |   width: 90vw; | ||||||
|   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |   padding: 5vw; | ||||||
|   grid-gap: 2rem; |  | ||||||
|   gap: 2rem; |   @include desktop { | ||||||
|  |     width: 80vw; | ||||||
|  |     padding: 0 10vw; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @media (min-width: 1500px) { |   @media (min-width: 1500px) { | ||||||
|     max-width: 1500px; |     max-width: 1500px; | ||||||
|     margin: 0 auto; |     margin: 0 auto; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     flex-direction: column; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| h3 { | h3 { | ||||||
| @@ -65,23 +66,6 @@ h3 { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .inner-wine-container { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   margin: auto; |  | ||||||
|   width: 500px; |  | ||||||
|   font-family: Arial; |  | ||||||
|   margin-bottom: 30px; |  | ||||||
|  |  | ||||||
|   @include desktop { |  | ||||||
|     justify-content: center; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     width: auto; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .right { | .right { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <main class="main-container"> |   <main class="main-container"> | ||||||
|  |  | ||||||
|     <section class="top-container"> |     <section class="top-container"> | ||||||
|  |  | ||||||
|       <div class="want-to-win"> |       <div class="want-to-win"> | ||||||
|         <h1> |         <h1> | ||||||
|           Vil du også vinne? |           Vil du også vinne? | ||||||
| @@ -18,8 +16,8 @@ | |||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <router-link to="/lottery" class="participate-button"> |       <router-link to="/lottery" class="participate-button"> | ||||||
|           <i class="icon icon--arrow-right"></i> |         <i class="icon icon--arrow-right"></i> | ||||||
|           <p>Trykk her for å delta</p> |         <p>Trykk her for å delta</p> | ||||||
|       </router-link> |       </router-link> | ||||||
|  |  | ||||||
|       <router-link to="/generate" class="see-details-link"> |       <router-link to="/generate" class="see-details-link"> | ||||||
| @@ -38,17 +36,16 @@ | |||||||
|         <i class="icon icon--bottle"></i> |         <i class="icon icon--bottle"></i> | ||||||
|         <i class="icon icon--bottle"></i> |         <i class="icon icon--bottle"></i> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|     </section> |     </section> | ||||||
|  |  | ||||||
|     <section class="content-container"> |     <section class="content-container"> | ||||||
|  |  | ||||||
|       <div class="scroll-info"> |       <div class="scroll-info"> | ||||||
|         <i class ="icon icon--arrow-long-right"></i> |         <i class="icon icon--arrow-long-right"></i> | ||||||
|         <p>Scroll for å se vinnere og annen gøy statistikk</p> |         <p>Scroll for å se vinnere og annen gøy statistikk</p> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <Highscore class="highscore"/> |       <Highscore class="highscore" /> | ||||||
|  |  | ||||||
|       <TotalBought class="total-bought" /> |       <TotalBought class="total-bought" /> | ||||||
|  |  | ||||||
|       <section class="chart-container"> |       <section class="chart-container"> | ||||||
| @@ -56,12 +53,10 @@ | |||||||
|         <WinGraph class="win" /> |         <WinGraph class="win" /> | ||||||
|       </section> |       </section> | ||||||
|  |  | ||||||
|       <Wines class="wines-container" /> |       <Wines class="wine-container" /> | ||||||
|  |  | ||||||
|     </section> |     </section> | ||||||
|  |  | ||||||
|     <Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> |     <Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> | ||||||
|  |  | ||||||
|   </main> |   </main> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -96,11 +91,7 @@ export default { | |||||||
|       if (!("PushManager" in window)) { |       if (!("PushManager" in window)) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|       return ( |       return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null; | ||||||
|         Notification.permission !== "granted" || |  | ||||||
|         !this.pushAllowed || |  | ||||||
|         localStorage.getItem("push") == null |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
| @@ -120,7 +111,7 @@ export default { | |||||||
|       this.hardStart = way; |       this.hardStart = way; | ||||||
|     }, |     }, | ||||||
|     track() { |     track() { | ||||||
|       window.ga('send', 'pageview', '/'); |       window.ga("send", "pageview", "/"); | ||||||
|     }, |     }, | ||||||
|     startCountdown() { |     startCountdown() { | ||||||
|       this.hardStart = true; |       this.hardStart = true; | ||||||
| @@ -145,7 +136,7 @@ export default { | |||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-items: start; |   justify-items: start; | ||||||
|  |  | ||||||
|   @include mobile{ |   @include mobile { | ||||||
|     padding-bottom: 2em; |     padding-bottom: 2em; | ||||||
|     height: 15em; |     height: 15em; | ||||||
|     grid-template-rows: repeat(7, 1fr); |     grid-template-rows: repeat(7, 1fr); | ||||||
| @@ -156,13 +147,13 @@ export default { | |||||||
|     grid-column: 2 / -1; |     grid-column: 2 / -1; | ||||||
|     display: flex; |     display: flex; | ||||||
|  |  | ||||||
|     h1{ |     h1 { | ||||||
|       font-size: 2em; |       font-size: 2em; | ||||||
|       font-weight: 400; |       font-weight: 400; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @include tablet { |     @include tablet { | ||||||
|       h1{ |       h1 { | ||||||
|         font-size: 3em; |         font-size: 3em; | ||||||
|       } |       } | ||||||
|       grid-row: 2 / 4; |       grid-row: 2 / 4; | ||||||
| @@ -170,7 +161,7 @@ export default { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .notification-request-button{ |   .notification-request-button { | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -229,7 +220,7 @@ export default { | |||||||
|   .icons-container { |   .icons-container { | ||||||
|     grid-column: 1 / -1; |     grid-column: 1 / -1; | ||||||
|     grid-row: 7 / -1; |     grid-row: 7 / -1; | ||||||
|     @include mobile{ |     @include mobile { | ||||||
|       margin-top: 2em; |       margin-top: 2em; | ||||||
|       display: none; |       display: none; | ||||||
|     } |     } | ||||||
| @@ -239,7 +230,7 @@ export default { | |||||||
|       grid-column: 7 / -1; |       grid-column: 7 / -1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @include desktop{ |     @include desktop { | ||||||
|       grid-row: 4 / -3; |       grid-row: 4 / -3; | ||||||
|       grid-column: 7 / 11; |       grid-column: 7 / 11; | ||||||
|     } |     } | ||||||
| @@ -257,30 +248,27 @@ export default { | |||||||
|     i { |     i { | ||||||
|       font-size: 5em; |       font-size: 5em; | ||||||
|  |  | ||||||
|       &.icon--heart-sparks{ |       &.icon--heart-sparks { | ||||||
|         grid-column: 2 / 4; |         grid-column: 2 / 4; | ||||||
|         grid-row: 2 / 4; |         grid-row: 2 / 4; | ||||||
|         align-self: center; |         align-self: center; | ||||||
|         justify-self: center; |         justify-self: center; | ||||||
|  |  | ||||||
|       } |       } | ||||||
|       &.icon--face-1{ |       &.icon--face-1 { | ||||||
|         grid-column: 4 / 7; |         grid-column: 4 / 7; | ||||||
|         grid-row: 2 / 4; |         grid-row: 2 / 4; | ||||||
|         justify-self: center; |         justify-self: center; | ||||||
|  |  | ||||||
|       } |       } | ||||||
|       &.icon--face-3{ |       &.icon--face-3 { | ||||||
|         grid-column: 7 / 10; |         grid-column: 7 / 10; | ||||||
|         grid-row: 1 / 4; |         grid-row: 1 / 4; | ||||||
|         align-self: center; |         align-self: center; | ||||||
|       } |       } | ||||||
|       &.icon--ballon{ |       &.icon--ballon { | ||||||
|         grid-column: 9 / 11; |         grid-column: 9 / 11; | ||||||
|         grid-row: 3 / 5; |         grid-row: 3 / 5; | ||||||
|  |  | ||||||
|       } |       } | ||||||
|       &.icon--bottle{ |       &.icon--bottle { | ||||||
|         grid-row: 4 / -1; |         grid-row: 4 / -1; | ||||||
|  |  | ||||||
|         &:nth-of-type(5) { |         &:nth-of-type(5) { | ||||||
| @@ -297,14 +285,13 @@ export default { | |||||||
|         &:nth-of-type(8) { |         &:nth-of-type(8) { | ||||||
|           grid-column: 7 / 8; |           grid-column: 7 / 8; | ||||||
|         } |         } | ||||||
|         &:nth-of-type(9){ |         &:nth-of-type(9) { | ||||||
|           grid-column: 8 / 9; |           grid-column: 8 / 9; | ||||||
|           align-self: center; |           align-self: center; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| h1 { | h1 { | ||||||
| @@ -312,12 +299,12 @@ h1 { | |||||||
|   font-family: "knowit"; |   font-family: "knowit"; | ||||||
| } | } | ||||||
|  |  | ||||||
| .to-lottery{ | .to-lottery { | ||||||
|     color: #333; |   color: #333; | ||||||
|     text-decoration: none; |   text-decoration: none; | ||||||
|     display: block; |   display: block; | ||||||
|     text-align: center; |   text-align: center; | ||||||
|     margin-bottom: 0; |   margin-bottom: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| .content-container { | .content-container { | ||||||
| @@ -326,10 +313,10 @@ h1 { | |||||||
|   row-gap: 5em; |   row-gap: 5em; | ||||||
|  |  | ||||||
|   .scroll-info { |   .scroll-info { | ||||||
|       display: flex; |     display: flex; | ||||||
|       align-items: center; |     align-items: center; | ||||||
|       column-gap: 10px; |     column-gap: 10px; | ||||||
|       grid-column: 2 / -2; |     grid-column: 2 / -2; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .chart-container { |   .chart-container { | ||||||
| @@ -346,8 +333,8 @@ h1 { | |||||||
|     grid-column: 2 / -2; |     grid-column: 2 / -2; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .wines-container { |   .wine-container { | ||||||
|     grid-column: 2 / -2; |     grid-column: 3 / -3; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .icon--arrow-long-right { |   .icon--arrow-long-right { | ||||||
| @@ -356,8 +343,7 @@ h1 { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   @include tablet { |   @include tablet { | ||||||
|  |     .scroll-info { | ||||||
|     .scroll-info{ |  | ||||||
|       grid-column: 3 / -3; |       grid-column: 3 / -3; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,10 @@ | |||||||
|         <div class="instructions"> |         <div class="instructions"> | ||||||
|           <h1 class="title">Virtuelt lotteri</h1> |           <h1 class="title">Virtuelt lotteri</h1> | ||||||
|           <ol> |           <ol> | ||||||
|             <li>Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>, eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link></li> |             <li> | ||||||
|  |               Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>, | ||||||
|  |               eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link> | ||||||
|  |             </li> | ||||||
|             <li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li> |             <li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li> | ||||||
|             <li>Send gjerne melding om fargeønske også.</li> |             <li>Send gjerne melding om fargeønske også.</li> | ||||||
|           </ol> |           </ol> | ||||||
| @@ -15,18 +18,16 @@ | |||||||
|  |  | ||||||
|         <VippsPill class="vipps-pill mobile-only" /> |         <VippsPill class="vipps-pill mobile-only" /> | ||||||
|  |  | ||||||
|          <p class="call-to-action"> |         <p class="call-to-action"> | ||||||
|             <span class="vin-link">Følg med på utviklingen</span> og <span class="vin-link">chat om trekningen</span> |           <span class="vin-link" @click="scrollToContent">Følg med på utviklingen</span> og | ||||||
|             <i class="icon icon--arrow-left" @click="scrollToContent"></i></p> |           <span class="vin-link" @click="scrollToContent">chat om trekningen</span> | ||||||
|  |           <i class="icon icon--arrow-left" @click="scrollToContent"></i> | ||||||
|  |         </p> | ||||||
|       </div> |       </div> | ||||||
|     </header> |     </header> | ||||||
|  |  | ||||||
|     <div class="container" ref="content"> |     <div class="container" ref="content"> | ||||||
|       <WinnerDraw |       <WinnerDraw :currentWinnerDrawn="currentWinnerDrawn" :currentWinner="currentWinner" :attendees="attendees" /> | ||||||
|         :currentWinnerDrawn="currentWinnerDrawn" |  | ||||||
|         :currentWinner="currentWinner" |  | ||||||
|         :attendees="attendees" |  | ||||||
|       /> |  | ||||||
|  |  | ||||||
|       <div class="todays-raffles"> |       <div class="todays-raffles"> | ||||||
|         <h2>Liste av lodd kjøpt i dag</h2> |         <h2>Liste av lodd kjøpt i dag</h2> | ||||||
| @@ -51,15 +52,16 @@ | |||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="container wines-container"> |     <div class="todays-wines"> | ||||||
|       <h2>Dagens fangst ({{ wines.length }})</h2> |       <h2>Dagens fangst ({{ wines.length }})</h2> | ||||||
|       <Wine :wine="wine" v-for="wine in wines" :key="wine" /> |       <div class="wines-container"> | ||||||
|  |         <Wine :wine="wine" v-for="wine in wines" :key="wine" /> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { attendees, winners, prelottery } from "@/api"; |  | ||||||
| import Chat from "@/ui/Chat"; | import Chat from "@/ui/Chat"; | ||||||
| import Vipps from "@/ui/Vipps"; | import Vipps from "@/ui/Vipps"; | ||||||
| import VippsPill from "@/ui/VippsPill"; | import VippsPill from "@/ui/VippsPill"; | ||||||
| @@ -74,18 +76,18 @@ export default { | |||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       attendees: [], |       attendees: [], | ||||||
|  |       attendeesFetched: false, | ||||||
|       winners: [], |       winners: [], | ||||||
|       wines: [], |       wines: [], | ||||||
|       currentWinnerDrawn: false, |       currentWinnerDrawn: false, | ||||||
|       currentWinner: null, |       currentWinner: null, | ||||||
|       socket: null, |       socket: null, | ||||||
|       attendeesFetched: false, |  | ||||||
|       wasDisconnected: false, |       wasDisconnected: false, | ||||||
|       ticketsBought: { |       ticketsBought: { | ||||||
|         "red": 0, |         red: 0, | ||||||
|         "blue": 0, |         blue: 0, | ||||||
|         "green": 0, |         green: 0, | ||||||
|         "yellow": 0 |         yellow: 0 | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| @@ -129,42 +131,45 @@ export default { | |||||||
|     this.socket = null; |     this.socket = null; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     getWinners: async function() { |     getWinners() { | ||||||
|       let response = await winners(); |       fetch("/api/lottery/winners") | ||||||
|       if (response) { |         .then(resp => resp.json()) | ||||||
|         this.winners = response; |         .then(response => (this.winners = response.winners)); | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     getTodaysWines() { |     getTodaysWines() { | ||||||
|       prelottery() |       fetch("/api/lottery/wines") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response.wines) | ||||||
|         .then(wines => { |         .then(wines => { | ||||||
|           this.wines = wines; |           this.wines = wines; | ||||||
|           this.todayExists = wines.length > 0; |           this.todayExists = wines.length > 0; | ||||||
|         }) |         }) | ||||||
|         .catch(_ => this.todayExists = false) |         .catch(_ => (this.todayExists = false)); | ||||||
|     }, |     }, | ||||||
|     getAttendees: async function() { |     getAttendees() { | ||||||
|       let response = await attendees(); |       fetch("/api/lottery/attendees") | ||||||
|       if (response) { |         .then(resp => resp.json()) | ||||||
|         this.attendees = response; |         .then(response => { | ||||||
|         if (this.attendees == undefined || this.attendees.length == 0) { |           const { attendees } = response; | ||||||
|           this.attendeesFetched = true; |           this.attendees = attendees || []; | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         const addValueOfListObjectByKey = (list, key) => |  | ||||||
|           list.map(object => object[key]).reduce((a, b) => a + b); |  | ||||||
|  |  | ||||||
|         this.ticketsBought = { |           if (attendees == undefined || attendees.length == 0) { | ||||||
|           red: addValueOfListObjectByKey(response, "red"), |             return; | ||||||
|           blue: addValueOfListObjectByKey(response, "blue"), |           } | ||||||
|           green: addValueOfListObjectByKey(response, "green"), |  | ||||||
|           yellow: addValueOfListObjectByKey(response, "yellow") |           const addValueOfListObjectByKey = (list, key) => list.map(object => object[key]).reduce((a, b) => a + b); | ||||||
|         }; |  | ||||||
|       } |           this.ticketsBought = { | ||||||
|       this.attendeesFetched = true; |             red: addValueOfListObjectByKey(attendees, "red"), | ||||||
|  |             blue: addValueOfListObjectByKey(attendees, "blue"), | ||||||
|  |             green: addValueOfListObjectByKey(attendees, "green"), | ||||||
|  |             yellow: addValueOfListObjectByKey(attendees, "yellow") | ||||||
|  |           }; | ||||||
|  |         }) | ||||||
|  |         .finally(_ => (this.attendeesFetched = true)); | ||||||
|     }, |     }, | ||||||
|     scrollToContent() { |     scrollToContent() { | ||||||
|       console.log(window.scrollY) |       console.log(window.scrollY); | ||||||
|       const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50; |       const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50; | ||||||
|       const { scrollY } = window; |       const { scrollY } = window; | ||||||
|       let scrollHeight = intersectingHeaderHeight; |       let scrollHeight = intersectingHeaderHeight; | ||||||
| @@ -178,14 +183,13 @@ export default { | |||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     track() { |     track() { | ||||||
|       window.ga('send', 'pageview', '/lottery/game'); |       window.ga("send", "pageview", "/lottery/game"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
| @import "../styles/variables.scss"; | @import "../styles/variables.scss"; | ||||||
| @import "../styles/media-queries.scss"; | @import "../styles/media-queries.scss"; | ||||||
|  |  | ||||||
| @@ -201,7 +205,8 @@ export default { | |||||||
|   display: grid; |   display: grid; | ||||||
|   grid-template-columns: repeat(4, 1fr); |   grid-template-columns: repeat(4, 1fr); | ||||||
|  |  | ||||||
|   > div, > section { |   > div, | ||||||
|  |   > section { | ||||||
|     @include mobile { |     @include mobile { | ||||||
|       grid-column: span 5; |       grid-column: span 5; | ||||||
|     } |     } | ||||||
| @@ -343,6 +348,8 @@ header { | |||||||
|  |  | ||||||
|   > div { |   > div { | ||||||
|     padding: 1rem; |     padding: 1rem; | ||||||
|  |     max-height: 638px; | ||||||
|  |     overflow-y: scroll; | ||||||
|  |  | ||||||
|     -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); |     -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
|     -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); |     -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
| @@ -369,11 +376,14 @@ header { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .todays-wines { | ||||||
|  |   width: 80vw; | ||||||
|  |   padding: 0 10vw; | ||||||
|  |  | ||||||
| .wines-container { |   @include mobile { | ||||||
|   display: flex; |     width: 90vw; | ||||||
|   flex-wrap: wrap; |     padding: 0 5vw; | ||||||
|   margin-bottom: 4rem; |   } | ||||||
|  |  | ||||||
|   h2 { |   h2 { | ||||||
|     width: 100%; |     width: 100%; | ||||||
|   | |||||||
| @@ -1,439 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="page-container"> |  | ||||||
|     <h1 class="title">Virtuelt lotteri registrering</h1> |  | ||||||
|     <br /> |  | ||||||
|     <div class="draw-winner-container" v-if="attendees.length > 0"> |  | ||||||
|       <div v-if="drawingWinner"> |  | ||||||
|         <span> |  | ||||||
|           Trekker {{ currentWinners }} av {{ numberOfWinners }} vinnere. |  | ||||||
|           {{ secondsLeft }} sekunder av {{ drawTime }} igjen |  | ||||||
|         </span> |  | ||||||
|         <button class="vin-button no-margin" @click="stopDraw">Stopp trekning</button> |  | ||||||
|       </div> |  | ||||||
|       <div class="draw-container" v-if="!drawingWinner"> |  | ||||||
|         <button class="vin-button no-margin" @click="drawWinner">Trekk vinnere</button> |  | ||||||
|         <input type="number" v-model="numberOfWinners" /> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <h2 v-if="winners.length > 0">Vinnere</h2> |  | ||||||
|     <div class="winners" v-if="winners.length > 0"> |  | ||||||
|       <div class="winner" v-for="(winner, index) in winners" :key="index"> |  | ||||||
|         <div :class="winner.color + '-raffle'" class="raffle-element"> |  | ||||||
|           <span>{{ winner.name }}</span> |  | ||||||
|           <span>{{ winner.phoneNumber }}</span> |  | ||||||
|           <span>Rød: {{ winner.red }}</span> |  | ||||||
|           <span>Blå: {{ winner.blue }}</span> |  | ||||||
|           <span>Grønn: {{ winner.green }}</span> |  | ||||||
|           <span>Gul: {{ winner.yellow }}</span> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="delete-buttons" v-if="attendees.length > 0 || winners.length > 0"> |  | ||||||
|       <button |  | ||||||
|         class="vin-button" |  | ||||||
|         v-if="winners.length > 0" |  | ||||||
|         @click="deleteAllWinners" |  | ||||||
|       >Slett virtuelle vinnere</button> |  | ||||||
|       <button |  | ||||||
|         class="vin-button" |  | ||||||
|         v-if="attendees.length > 0" |  | ||||||
|         @click="deleteAllAttendees" |  | ||||||
|       >Slett virtuelle deltakere</button> |  | ||||||
|     </div> |  | ||||||
|     <div class="attendees" v-if="attendees.length > 0"> |  | ||||||
|       <h2>Deltakere ({{ attendees.length }})</h2> |  | ||||||
|       <div class="attendee" v-for="(attendee, index) in attendees" :key="index"> |  | ||||||
|         <div class="name-and-phone"> |  | ||||||
|           <span class="name">{{ attendee.name }}</span> |  | ||||||
|           <span class="phoneNumber">{{ attendee.phoneNumber }}</span> |  | ||||||
|         </div> |  | ||||||
|         <div class="raffles-container"> |  | ||||||
|           <div class="red-raffle raffle-element small">{{ attendee.red }}</div> |  | ||||||
|           <div class="blue-raffle raffle-element small">{{ attendee.blue }}</div> |  | ||||||
|           <div class="green-raffle raffle-element small">{{ attendee.green }}</div> |  | ||||||
|           <div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="attendee-registration-container"> |  | ||||||
|       <h2>Legg til deltaker</h2> |  | ||||||
|       <div class="label-div"> |  | ||||||
|         <label for="name">Navn</label> |  | ||||||
|         <input id="name" type="text" placeholder="Navn" v-model="name" /> |  | ||||||
|       </div> |  | ||||||
|       <br /> |  | ||||||
|       <div class="label-div"> |  | ||||||
|         <label for="phoneNumber">Telefonnummer</label> |  | ||||||
|         <input id="phoneNumber" type="text" placeholder="Telefonnummer" v-model="phoneNumber" /> |  | ||||||
|       </div> |  | ||||||
|       <br /> |  | ||||||
|       <br /> |  | ||||||
|       <div class="label-div"> |  | ||||||
|         <label for="randomColors">Tilfeldig farger?</label> |  | ||||||
|         <input |  | ||||||
|           id="randomColors" |  | ||||||
|           type="checkbox" |  | ||||||
|           placeholder="Tilfeldig farger" |  | ||||||
|           v-model="randomColors" |  | ||||||
|         /> |  | ||||||
|       </div> |  | ||||||
|       <div v-if="!randomColors"> |  | ||||||
|         <br /> |  | ||||||
|         <br /> |  | ||||||
|         <div class="label-div"> |  | ||||||
|           <label for="red">Rød</label> |  | ||||||
|           <input id="red" type="number" placeholder="Rød" v-model="red" /> |  | ||||||
|         </div> |  | ||||||
|         <br /> |  | ||||||
|         <div class="label-div"> |  | ||||||
|           <label for="blue">Blå</label> |  | ||||||
|           <input id="blue" type="number" placeholder="Blå" v-model="blue" /> |  | ||||||
|         </div> |  | ||||||
|         <br /> |  | ||||||
|         <div class="label-div"> |  | ||||||
|           <label for="green">Grønn</label> |  | ||||||
|           <input id="green" type="number" placeholder="Grønn" v-model="green" /> |  | ||||||
|         </div> |  | ||||||
|         <br /> |  | ||||||
|         <div class="label-div"> |  | ||||||
|           <label for="yellow">Gul</label> |  | ||||||
|           <input id="yellow" type="number" placeholder="Gul" v-model="yellow" /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <div v-else> |  | ||||||
|         <RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" /> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <br /> |  | ||||||
|     <button class="vin-button" @click="sendAttendee">Send deltaker</button> |  | ||||||
|  |  | ||||||
|     <TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" /> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import io from "socket.io-client"; |  | ||||||
| import { |  | ||||||
|   addAttendee, |  | ||||||
|   getVirtualWinner, |  | ||||||
|   attendeesSecure, |  | ||||||
|   attendees, |  | ||||||
|   winnersSecure, |  | ||||||
|   deleteWinners, |  | ||||||
|   deleteAttendees, |  | ||||||
|   finishedDraw, |  | ||||||
|   prelottery |  | ||||||
| } from "@/api"; |  | ||||||
| import TextToast from "@/ui/TextToast"; |  | ||||||
| import RaffleGenerator from "@/ui/RaffleGenerator"; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { |  | ||||||
|     RaffleGenerator, |  | ||||||
|     TextToast |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       name: null, |  | ||||||
|       phoneNumber: null, |  | ||||||
|       red: 0, |  | ||||||
|       blue: 0, |  | ||||||
|       green: 0, |  | ||||||
|       yellow: 0, |  | ||||||
|       raffles: 0, |  | ||||||
|       randomColors: false, |  | ||||||
|       attendees: [], |  | ||||||
|       winners: [], |  | ||||||
|       drawingWinner: false, |  | ||||||
|       secondsLeft: 20, |  | ||||||
|       drawTime: 20, |  | ||||||
|       currentWinners: 1, |  | ||||||
|       numberOfWinners: 4, |  | ||||||
|       socket: null, |  | ||||||
|       toastText: undefined, |  | ||||||
|       showToast: false |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     this.getAttendees(); |  | ||||||
|     this.getWinners(); |  | ||||||
|  |  | ||||||
|     this.socket = io(`${window.location.hostname}:${window.location.port}`); |  | ||||||
|  |  | ||||||
|     this.socket.on("winner", async msg => { |  | ||||||
|       this.getWinners(); |  | ||||||
|       this.getAttendees(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.socket.on("refresh_data", async msg => { |  | ||||||
|       this.getAttendees(); |  | ||||||
|       this.getWinners(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.socket.on("new_attendee", async msg => { |  | ||||||
|       this.getAttendees(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     window.finishedDraw = finishedDraw; |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     setWithRandomColors(colors) { |  | ||||||
|       Object.keys(colors).forEach(color => (this[color] = colors[color])); |  | ||||||
|     }, |  | ||||||
|     sendAttendee: async function() { |  | ||||||
|       if (this.red == 0 && this.blue == 0 && this.green == 0 && this.yellow == 0) { |  | ||||||
|         alert('Ingen farger valgt!') |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (this.name == 0 && this.phoneNumber) { |  | ||||||
|         alert('Ingen navn eller tlf satt!') |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let response = await addAttendee({ |  | ||||||
|         name: this.name, |  | ||||||
|         phoneNumber: this.phoneNumber, |  | ||||||
|         red: this.red, |  | ||||||
|         blue: this.blue, |  | ||||||
|         green: this.green, |  | ||||||
|         yellow: this.yellow, |  | ||||||
|         raffles: this.raffles |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (response == true) { |  | ||||||
|         this.toastText = `Sendt inn deltaker: ${this.name}`; |  | ||||||
|         this.showToast = true; |  | ||||||
|  |  | ||||||
|         this.name = null; |  | ||||||
|         this.phoneNumber = null; |  | ||||||
|         this.yellow = 0; |  | ||||||
|         this.green = 0; |  | ||||||
|         this.red = 0; |  | ||||||
|         this.blue = 0; |  | ||||||
|  |  | ||||||
|         this.getAttendees(); |  | ||||||
|       } else { |  | ||||||
|         alert("Klarte ikke sende inn.. Er du logget inn?"); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     getAttendees: async function() { |  | ||||||
|       let response = await attendeesSecure(); |  | ||||||
|       this.attendees = response; |  | ||||||
|     }, |  | ||||||
|     stopDraw: function() { |  | ||||||
|       this.drawingWinner = false; |  | ||||||
|       this.secondsLeft = this.drawTime; |  | ||||||
|     }, |  | ||||||
|     drawWinner: async function() { |  | ||||||
|       if (window.confirm("Er du sikker på at du vil trekke vinnere?")) { |  | ||||||
|         this.drawingWinner = true; |  | ||||||
|         let response = await getVirtualWinner(); |  | ||||||
|  |  | ||||||
|         if (response.success) { |  | ||||||
|           console.log("Winner:", response.winner); |  | ||||||
|           if (this.currentWinners < this.numberOfWinners) { |  | ||||||
|             this.countdown(); |  | ||||||
|           } else { |  | ||||||
|             this.drawingWinner = false; |  | ||||||
|             let finished = await finishedDraw(); |  | ||||||
|             if(finished) { |  | ||||||
|               alert("SMS'er er sendt ut!"); |  | ||||||
|             } else { |  | ||||||
|               alert("Noe gikk galt under SMS utsendelser.. Sjekk logg og database for id'er."); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|           this.getWinners(); |  | ||||||
|           this.getAttendees(); |  | ||||||
|         } else { |  | ||||||
|           this.drawingWinner = false; |  | ||||||
|           alert("Noe gikk galt under trekningen..! " + response["message"]); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     countdown: function() { |  | ||||||
|       this.secondsLeft -= 1; |  | ||||||
|       if (!this.drawingWinner) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       if (this.secondsLeft <= 0) { |  | ||||||
|         this.secondsLeft = this.drawTime; |  | ||||||
|         this.currentWinners += 1; |  | ||||||
|         if (this.currentWinners <= this.numberOfWinners) { |  | ||||||
|           this.drawWinner(); |  | ||||||
|         } else { |  | ||||||
|           this.drawingWinner = false; |  | ||||||
|         } |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       setTimeout(() => { |  | ||||||
|         this.countdown(); |  | ||||||
|       }, 1000); |  | ||||||
|     }, |  | ||||||
|     deleteAllWinners: async function() { |  | ||||||
|       if (window.confirm("Er du sikker på at du vil slette vinnere?")) { |  | ||||||
|         let response = await deleteWinners(); |  | ||||||
|         if (response) { |  | ||||||
|           this.getWinners(); |  | ||||||
|         } else { |  | ||||||
|           alert("Klarte ikke hente ut vinnere"); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     deleteAllAttendees: async function() { |  | ||||||
|       if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) { |  | ||||||
|         let response = await deleteAttendees(); |  | ||||||
|         if (response) { |  | ||||||
|           this.getAttendees(); |  | ||||||
|         } else { |  | ||||||
|           alert("Klarte ikke hente ut vinnere"); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     getWinners: async function() { |  | ||||||
|       let response = await winnersSecure(); |  | ||||||
|       if (response) { |  | ||||||
|         this.winners = response; |  | ||||||
|       } else { |  | ||||||
|         alert("Klarte ikke hente ut vinnere"); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "../styles/global.scss"; |  | ||||||
| @import "../styles/media-queries.scss"; |  | ||||||
|  |  | ||||||
| .draw-container { |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: space-around; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .draw-winner-container, |  | ||||||
| .delete-buttons { |  | ||||||
|   margin-bottom: 20px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .delete-buttons { |  | ||||||
|   display: flex; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h1 { |  | ||||||
|   width: 100%; |  | ||||||
|   text-align: center; |  | ||||||
|   font-family: knowit, Arial; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h2 { |  | ||||||
|   width: 100%; |  | ||||||
|   text-align: center; |  | ||||||
|   font-size: 1.6rem; |  | ||||||
|   font-family: knowit, Arial; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| hr { |  | ||||||
|   width: 90%; |  | ||||||
|   margin: 2rem auto; |  | ||||||
|   color: grey; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .page-container { |  | ||||||
|   padding: 0 1.5rem 3rem; |  | ||||||
|  |  | ||||||
|   @include desktop { |  | ||||||
|     max-width: 60vw; |  | ||||||
|     margin: 0 auto; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #randomColors { |  | ||||||
|   width: 40px; |  | ||||||
|   height: 40px; |  | ||||||
|   &:checked { |  | ||||||
|     background: green; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .raffle-element { |  | ||||||
|   width: 140px; |  | ||||||
|   height: 150px; |  | ||||||
|   margin: 20px 0; |  | ||||||
|   -webkit-mask-image: url(/public/assets/images/lodd.svg); |  | ||||||
|   background-repeat: no-repeat; |  | ||||||
|   mask-image: url(/public/assets/images/lodd.svg); |  | ||||||
|   -webkit-mask-repeat: no-repeat; |  | ||||||
|   mask-repeat: no-repeat; |  | ||||||
|   color: #333333; |  | ||||||
|   font-size: 0.75rem; |  | ||||||
|   font-weight: bold; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   align-items: center; |  | ||||||
|   text-align: center; |  | ||||||
|   flex-direction: column; |  | ||||||
|  |  | ||||||
|   &.small { |  | ||||||
|     width: 45px; |  | ||||||
|     height: 45px; |  | ||||||
|     font-size: 1rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.green-raffle { |  | ||||||
|     background-color: $light-green; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.blue-raffle { |  | ||||||
|     background-color: $light-blue; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.yellow-raffle { |  | ||||||
|     background-color: $light-yellow; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.red-raffle { |  | ||||||
|     background-color: $light-red; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| button { |  | ||||||
|   display: flex !important; |  | ||||||
|   margin: auto !important; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .winners { |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: space-around; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .attendees { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   justify-content: center; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .attendee { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   align-items: center; |  | ||||||
|   width: 50%; |  | ||||||
|   margin: 0 auto; |  | ||||||
|  |  | ||||||
|   & .name-and-phone, |  | ||||||
|   & .raffles-container { |  | ||||||
|     display: flex; |  | ||||||
|     justify-content: center; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   & .name-and-phone { |  | ||||||
|     flex-direction: column; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   & .raffles-container { |  | ||||||
|     flex-direction: row; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,21 +1,23 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="container"> |   <div> | ||||||
|     <div v-if="!posted"> |     <div v-if="!posted" class="container"> | ||||||
|       <h1 v-if="name">Gratulerer {{name}}!</h1> |       <h1 v-if="name">Gratulerer {{ name }}!</h1> | ||||||
|  |  | ||||||
|       <p v-if="name"> |       <p v-if="name"> | ||||||
|         Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en. |         Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en. | ||||||
|       </p> |       </p> | ||||||
|       <h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1> |  | ||||||
|  |       <h1 v-else-if="!turn && wines.length" class="sent-container">Finner ikke noen vinner her..</h1> | ||||||
|  |  | ||||||
|       <h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1> |       <h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1> | ||||||
|  |  | ||||||
|       <div class="wines-container" v-if="name"> |       <div class="wines-container" v-if="name"> | ||||||
|         <Wine :wine="wine" v-for="wine in wines" :key="wine"> |         <Wine :wine="wine" v-for="wine in wines" :key="wine"> | ||||||
|           <button |           <button @click="chooseWine(wine)" class="vin-button select-wine">Velg denne vinnen</button> | ||||||
|             @click="chooseWine(wine.name)" |  | ||||||
|             class="vin-button select-wine" |  | ||||||
|           >Velg denne vinnen</button> |  | ||||||
|         </Wine> |         </Wine> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div v-else-if="posted" class="sent-container"> |     <div v-else-if="posted" class="sent-container"> | ||||||
|       <h1>Valget ditt er sendt inn!</h1> |       <h1>Valget ditt er sendt inn!</h1> | ||||||
|       <p>Du får mer info om henting snarest!</p> |       <p>Du får mer info om henting snarest!</p> | ||||||
| @@ -24,15 +26,13 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { getAmIWinner, postWineChosen, prelottery } from "@/api"; |  | ||||||
| import Wine from "@/ui/Wine"; | import Wine from "@/ui/Wine"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { Wine }, |   components: { Wine }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       id: null, |       id: null, | ||||||
|       existing: false, |  | ||||||
|       fetched: false, |  | ||||||
|       turn: false, |       turn: false, | ||||||
|       name: null, |       name: null, | ||||||
|       wines: [], |       wines: [], | ||||||
| @@ -40,30 +40,43 @@ export default { | |||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     this.id = this.$router.currentRoute.params.id; |     const { id } = this.$router.currentRoute.params; | ||||||
|  |  | ||||||
|     let winnerObject = await getAmIWinner(this.id); |     this.id = id; | ||||||
|     this.fetched = true; |     this.getPrizes(id); | ||||||
|     if (!winnerObject || !winnerObject.existing) { |  | ||||||
|       console.error("non existing", winnerObject); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this.existing = true; |  | ||||||
|     if (winnerObject.existing && !winnerObject.turn) { |  | ||||||
|       console.error("not your turn yet", winnerObject); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this.turn = true; |  | ||||||
|     this.name = winnerObject.name; |  | ||||||
|     this.wines = await prelottery(); |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     chooseWine: async function(name) { |     getPrizes(id) { | ||||||
|       let posted = await postWineChosen(this.id, name); |       fetch(`/api/lottery/prize-distribution/prizes/${id}`) | ||||||
|       console.log("response", posted); |         .then(resp => resp.json()) | ||||||
|       if (posted.success) { |         .then(response => { | ||||||
|         this.posted = true; |           if (response.success) { | ||||||
|       } |             this.wines = response.wines; | ||||||
|  |             this.name = response.winner.name; | ||||||
|  |             this.turn = true; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     chooseWine(wine) { | ||||||
|  |       const options = { | ||||||
|  |         method: "POST", | ||||||
|  |         headers: { "Content-Type": "application/json" }, | ||||||
|  |         body: JSON.stringify({ wine }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       fetch(`/api/lottery/prize-distribution/prize/${this.id}`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.$toast.info({ title: `Valgt vin: ${wine.name}` }); | ||||||
|  |             this.posted = true; | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: "Klarte ikke velge vin :(", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| @@ -74,9 +87,19 @@ export default { | |||||||
| .container { | .container { | ||||||
|   display: flex; |   display: flex; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   flex-direction: column; | ||||||
|   margin-top: 2rem; |   margin-top: 2rem; | ||||||
|   padding: 2rem; |   padding: 2rem; | ||||||
|  |   width: 80%; | ||||||
|  |   margin: 0 auto; | ||||||
|  |   max-width: 2000px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .wines-container { | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
| .sent-container { | .sent-container { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 90vh; |   height: 90vh; | ||||||
| @@ -90,11 +113,4 @@ export default { | |||||||
| .select-wine { | .select-wine { | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
| } | } | ||||||
|  | </style> | ||||||
| .wines-container { |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   justify-content: space-evenly; |  | ||||||
|   align-items: flex-start; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|   | |||||||
							
								
								
									
										356
									
								
								frontend/components/admin/DrawWinnerPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								frontend/components/admin/DrawWinnerPage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,356 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="page-container"> | ||||||
|  |     <h1>Trekk vinnere</h1> | ||||||
|  |  | ||||||
|  |     <div class="draw-winner-container"> | ||||||
|  |       <div v-if="drawingWinner == false" class="draw-container"> | ||||||
|  |         <input type="number" v-model="winnersToDraw" /> | ||||||
|  |         <button class="vin-button no-margin" @click="startDrawingWinners">Trekk vinnere</button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-if="wines.length" class="wines-left"> | ||||||
|  |         <span>Antall vin igjen: {{ winnersToDraw }} av {{ wines.length }}</span> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-if="drawingWinner == true"> | ||||||
|  |         <p>Trekker vinner {{ winners.length }} av {{ wines.length }}.</p> | ||||||
|  |         <p>Neste trekning om {{ secondsLeft }} sekunder av {{ drawTime }}</p> | ||||||
|  |  | ||||||
|  |         <div class="button-container draw-winner-actions"> | ||||||
|  |           <button class="vin-button danger" @click="stopDraw"> | ||||||
|  |             Stopp trekning | ||||||
|  |           </button> | ||||||
|  |           <button | ||||||
|  |             class="vin-button" | ||||||
|  |             :class="{ 'pulse-button': secondsLeft == 0 }" | ||||||
|  |             :disabled="secondsLeft > 0" | ||||||
|  |             @click="drawWinner" | ||||||
|  |           > | ||||||
|  |             Trekk neste | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="prize-distribution"> | ||||||
|  |       <h2>Prisutdeling</h2> | ||||||
|  |  | ||||||
|  |       <div class="button-container"> | ||||||
|  |         <button class="vin-button" @click="startPrizeDistribution">Start automatisk prisutdeling med SMS</button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <h2 v-if="winners.length > 0">Vinnere</h2> | ||||||
|  |     <div class="winners" v-if="winners.length > 0"> | ||||||
|  |       <div :class="winner.color + '-raffle'" class="raffle-element" v-for="(winner, index) in winners" :key="index"> | ||||||
|  |         <span>{{ winner.name }}</span> | ||||||
|  |         <span>Phone: {{ winner.phoneNumber }}</span> | ||||||
|  |         <span>Rød: {{ winner.red }}</span> | ||||||
|  |         <span>Blå: {{ winner.blue }}</span> | ||||||
|  |         <span>Grønn: {{ winner.green }}</span> | ||||||
|  |         <span>Gul: {{ winner.yellow }}</span> | ||||||
|  |  | ||||||
|  |         <div class="button-container"> | ||||||
|  |           <button class="vin-button small" @click="editingWinner = editingWinner == winner ? false : winner"> | ||||||
|  |             {{ editingWinner == winner ? "Lukk" : "Rediger" }} | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div v-if="editingWinner == winner" class="edit"> | ||||||
|  |           <div class="label-div" v-for="key in Object.keys(winner)" :key="key"> | ||||||
|  |             <label>{{ key }}</label> | ||||||
|  |             <input type="text" v-model="winner[key]" :placeholder="key" /> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <div v-if="editingWinner == winner" class="button-container column"> | ||||||
|  |             <button class="vin-button small" @click="notifyWinner(winner)"> | ||||||
|  |               Send SMS | ||||||
|  |             </button> | ||||||
|  |             <button class="vin-button small warning" @click="updateWinner(winner)"> | ||||||
|  |               Oppdater | ||||||
|  |             </button> | ||||||
|  |             <button class="vin-button small danger" @click="deleteWinner(winner)"> | ||||||
|  |               Slett | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="button-container margin-md" v-if="winners.length > 0"> | ||||||
|  |       <button class="vin-button danger" v-if="winners.length > 0" @click="deleteAllWinners"> | ||||||
|  |         Slett virtuelle vinnere | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       wines: [], | ||||||
|  |       drawingWinner: false, | ||||||
|  |       secondsLeft: 20, | ||||||
|  |       drawTime: 20, | ||||||
|  |       winners: [], | ||||||
|  |       editingWinner: undefined | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.fetchLotterWines(); | ||||||
|  |     this.fetchLotterWinners(); | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     winnersToDraw() { | ||||||
|  |       if (this.wines.length == undefined || this.winners.length == undefined) { | ||||||
|  |         return 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return this.wines.length - this.winners.length; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     winners(val) { | ||||||
|  |       this.$emit("counter", val.length); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     fetchLotterWines() { | ||||||
|  |       return fetch("/api/lottery/wines") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.wines = response.wines)); | ||||||
|  |     }, | ||||||
|  |     fetchLotterWinners() { | ||||||
|  |       return fetch("/api/lottery/winners") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.winners = response.winners)); | ||||||
|  |     }, | ||||||
|  |     countdown() { | ||||||
|  |       if (this.drawingWinner == false) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (this.secondsLeft > 0) { | ||||||
|  |         this.secondsLeft -= 1; | ||||||
|  |  | ||||||
|  |         setTimeout(_ => { | ||||||
|  |           this.countdown(); | ||||||
|  |         }, 1000); | ||||||
|  |       } else { | ||||||
|  |         if (this.winners.length == this.wines.length) { | ||||||
|  |           this.drawingWinner = false; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     startDrawingWinners() { | ||||||
|  |       if (window.confirm("Er du sikker på at du vil trekke vinnere?")) { | ||||||
|  |         this.drawWinner(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     drawWinner() { | ||||||
|  |       if (this.winnersToDraw <= 0) { | ||||||
|  |         this.$toast.error({ title: "No more wines to draw" }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       this.secondsLeft = this.drawTime; | ||||||
|  |       this.drawingWinner = true; | ||||||
|  |  | ||||||
|  |       fetch("/api/lottery/draw") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           const { winner, color, success, message } = response; | ||||||
|  |  | ||||||
|  |           if (success == false) { | ||||||
|  |             this.$toast.error({ title: message }); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           winner.color = color; | ||||||
|  |           this.winners.push(winner); | ||||||
|  |           this.countdown(); | ||||||
|  |         }) | ||||||
|  |         .catch(error => { | ||||||
|  |           if (error) { | ||||||
|  |             this.$toast.error({ title: error.message }); | ||||||
|  |           } | ||||||
|  |           this.drawingWinner = false; | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     stopDraw() { | ||||||
|  |       this.drawingWinner = false; | ||||||
|  |       this.secondsLeft = this.drawTime; | ||||||
|  |     }, | ||||||
|  |     startPrizeDistribution() { | ||||||
|  |       if (!window.confirm("Er du sikker på at du vil starte prisutdeling?")) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.drawingWinner = false; | ||||||
|  |  | ||||||
|  |       const options = { method: "POST" }; | ||||||
|  |       fetch(`/api/lottery/prize-distribution/start`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: `Startet prisutdeling. SMS'er sendt ut!` | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: `Klarte ikke starte prisutdeling`, | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     notifyWinner(winner) { | ||||||
|  |       const options = { method: "POST" }; | ||||||
|  |  | ||||||
|  |       fetch(`/api/lottery/messages/winner/${winner.id}`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: `Sendte sms til vinner ${winner.name}.` | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: `Klarte ikke sende sms til vinner ${winner.name}`, | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     updateWinner(winner) { | ||||||
|  |       const options = { | ||||||
|  |         method: "PUT", | ||||||
|  |         headers: { "Content-Type": "application/json" }, | ||||||
|  |         body: JSON.stringify({ winner }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       fetch(`/api/lottery/winner/${winner.id}`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: `Oppdaterte vinner ${winner.name}.` | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: `Klarte ikke oppdatere vinner ${winner.name}`, | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     deleteWinner(winner) { | ||||||
|  |       if (winner._id != null && window.confirm(`Er du sikker på at du vil slette vinner ${winner.name}?`)) { | ||||||
|  |         const options = { method: "DELETE" }; | ||||||
|  |  | ||||||
|  |         fetch(`/api/lottery/winner/${winner.id}`, options) | ||||||
|  |           .then(resp => resp.json()) | ||||||
|  |           .then(response => { | ||||||
|  |             if (response.success) { | ||||||
|  |               this.winners = this.winners.filter(w => w.id != winner.id); | ||||||
|  |  | ||||||
|  |               this.$toast.info({ | ||||||
|  |                 title: `Slettet vinner ${winner.name}.` | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               this.$toast.error({ | ||||||
|  |                 title: `Klarte ikke slette vinner ${winner.name}`, | ||||||
|  |                 description: response.message | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     deleteAllWinners() { | ||||||
|  |       if (window.confirm("Er du sikker på at du vil slette alle vinnere?")) { | ||||||
|  |         const options = { method: "DELETE" }; | ||||||
|  |  | ||||||
|  |         fetch("/api/lottery/winners", options) | ||||||
|  |           .then(resp => resp.json()) | ||||||
|  |           .then(response => { | ||||||
|  |             if (response.success) { | ||||||
|  |               this.winners = []; | ||||||
|  |               this.$toast.info({ | ||||||
|  |                 title: "Slettet alle vinnere." | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               this.$toast.error({ | ||||||
|  |                 title: "Klarte ikke slette vinnere", | ||||||
|  |                 description: response.message | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .wines-left { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   margin-top: 1rem; | ||||||
|  |   font-size: 1.2rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .draw-container { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |  | ||||||
|  |   input { | ||||||
|  |     font-size: 1.7rem; | ||||||
|  |     padding: 7px; | ||||||
|  |     margin: 0; | ||||||
|  |     width: 10rem; | ||||||
|  |     height: 3rem; | ||||||
|  |     border: 1px solid rgba(#333333, 0.3); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .button-container { | ||||||
|  |   margin-top: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .draw-winner-actions { | ||||||
|  |   justify-content: left; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .winners { | ||||||
|  |   display: flex; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   justify-content: center; | ||||||
|  |  | ||||||
|  |   .raffle-element { | ||||||
|  |     width: 220px; | ||||||
|  |     height: 100%; | ||||||
|  |     min-height: 250px; | ||||||
|  |     font-size: 1.1rem; | ||||||
|  |     padding: 1rem; | ||||||
|  |     font-weight: 500; | ||||||
|  |     // text-align: center; | ||||||
|  |  | ||||||
|  |     -webkit-mask-size: cover; | ||||||
|  |     -moz-mask-size: cover; | ||||||
|  |     mask-size: cover; | ||||||
|  |     flex-direction: column; | ||||||
|  |  | ||||||
|  |     span:first-of-type { | ||||||
|  |       font-weight: 600; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     span.active { | ||||||
|  |       margin-top: 3rem; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .edit { | ||||||
|  |       padding: 1rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										59
									
								
								frontend/components/admin/PushPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								frontend/components/admin/PushPage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="page-container"> | ||||||
|  |     <h1>Send push melding</h1> | ||||||
|  |  | ||||||
|  |     <div class="notification-element"> | ||||||
|  |       <div class="label-div"> | ||||||
|  |         <label for="notification">Melding</label> | ||||||
|  |         <textarea id="notification" type="text" rows="3" v-model="pushMessage" placeholder="Push meldingtekst" /> | ||||||
|  |       </div> | ||||||
|  |       <div class="label-div"> | ||||||
|  |         <label for="notification-link">Push åpner lenke</label> | ||||||
|  |         <input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="button-container margin-top-sm"> | ||||||
|  |       <button class="vin-button" @click="sendPush">Send push</button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       pushMessage: "", | ||||||
|  |       pushLink: "/" | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     sendPush: async function() { | ||||||
|  |       const options = { | ||||||
|  |         method: "POST", | ||||||
|  |         headers: { | ||||||
|  |           "Content-Type": "application/json" | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |           message: this.pushMessage, | ||||||
|  |           link: this.pushLink | ||||||
|  |         }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return fetch("/subscription/send-notification", options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: "Sendt!" | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: "Noe gikk galt!", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
							
								
								
									
										308
									
								
								frontend/components/admin/RegisterWinePage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								frontend/components/admin/RegisterWinePage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <h1>Register vin</h1> | ||||||
|  |  | ||||||
|  |     <ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" /> | ||||||
|  |  | ||||||
|  |     <div class="button-container"> | ||||||
|  |       <button class="vin-button" @click="showCamera = !showCamera"> | ||||||
|  |         {{ showCamera ? "Skjul camera" : "Legg til vin med camera" }} | ||||||
|  |       </button> | ||||||
|  |  | ||||||
|  |       <button class="vin-button" @click="manualyFillInnWine"> | ||||||
|  |         Legg til en vin manuelt | ||||||
|  |       </button> | ||||||
|  |  | ||||||
|  |       <button class="vin-button" @click="showImportLink = !showImportLink"> | ||||||
|  |         {{ showImportLink ? "Skjul importer fra link" : "Importer fra link" }} | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="showImportLink" class="import-from-link"> | ||||||
|  |       <label>Importer vin fra vinmonopolet link:</label> | ||||||
|  |       <input | ||||||
|  |         type="text" | ||||||
|  |         placeholder="Vinmonopol lenke" | ||||||
|  |         ref="vinmonopoletLinkInput" | ||||||
|  |         autocapitalize="none" | ||||||
|  |         @input="addWineByUrl" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <div v-if="linkError" class="error"> | ||||||
|  |         {{ linkError }} | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="wines.length > 0" class="wine-edit-container"> | ||||||
|  |       <h2>Dagens registrerte viner</h2> | ||||||
|  |  | ||||||
|  |       <div> | ||||||
|  |         <button class="vin-button" @click="sendWines">Send inn dagens viner</button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="wines"> | ||||||
|  |         <wine v-for="wine in wines" :key="wine.id" :wine="wine"> | ||||||
|  |           <template v-slot:default> | ||||||
|  |             <div v-if="editingWine == wine" class="wine-edit"> | ||||||
|  |               <div class="label-div" v-for="key in Object.keys(wine)" :key="key"> | ||||||
|  |                 <label>{{ key }}</label> | ||||||
|  |                 <input type="text" v-model="wine[key]" :placeholder="key" /> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </template> | ||||||
|  |  | ||||||
|  |           <template v-slot:bottom> | ||||||
|  |             <div class="button-container row small"> | ||||||
|  |               <button v-if="editingWine == wine && wine._id" class="vin-button warning" @click="updateWine(wine)"> | ||||||
|  |                 Oppdater vin | ||||||
|  |               </button> | ||||||
|  |  | ||||||
|  |               <button class="vin-button" @click="editingWine = editingWine == wine ? false : wine"> | ||||||
|  |                 {{ editingWine == wine ? "Lukk" : "Rediger" }} | ||||||
|  |               </button> | ||||||
|  |  | ||||||
|  |               <button class="danger vin-button" @click="deleteWine(wine)"> | ||||||
|  |                 Slett | ||||||
|  |               </button> | ||||||
|  |             </div> | ||||||
|  |           </template> | ||||||
|  |         </wine> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="button-container" v-if="wines.length > 0"></div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet"; | ||||||
|  | import Wine from "@/ui/Wine"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { ScanToVinmonopolet, Wine }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       wines: [], | ||||||
|  |       editingWine: undefined, | ||||||
|  |       showCamera: false, | ||||||
|  |       showImportLink: false, | ||||||
|  |       linkError: undefined | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     wines() { | ||||||
|  |       this.$emit("counter", this.wines.length); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.fetchLotterWines(); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     fetchLotterWines() { | ||||||
|  |       fetch("/api/lottery/wines") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.wines = response.wines)); | ||||||
|  |     }, | ||||||
|  |     wineFromVinmonopoletScan(wineResponse) { | ||||||
|  |       if (this.wines.map(wine => wine.name).includes(wineResponse.name)) { | ||||||
|  |         this.toastText = "Vinen er allerede lagt til."; | ||||||
|  |         this.showToast = true; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.toastText = "Fant og la til vin:<br>" + wineResponse.name; | ||||||
|  |       this.showToast = true; | ||||||
|  |  | ||||||
|  |       this.wines.unshift(wineResponse); | ||||||
|  |     }, | ||||||
|  |     manualyFillInnWine() { | ||||||
|  |       fetch("/api/lottery/wine/schema") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response.schema) | ||||||
|  |         .then(wineSchema => { | ||||||
|  |           this.editingWine = wineSchema; | ||||||
|  |           this.wines.unshift(wineSchema); | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     addWineByUrl(event) { | ||||||
|  |       const url = event.target.value; | ||||||
|  |       this.linkError = null; | ||||||
|  |  | ||||||
|  |       if (!url.includes("vinmonopolet.no")) { | ||||||
|  |         this.linkError = "Dette er ikke en gydlig vinmonopolet lenke."; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const id = url.split("/").pop(); | ||||||
|  |  | ||||||
|  |       fetch(`/api/vinmonopolet/wine/by-id/${id}`) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           const { wine } = response; | ||||||
|  |           this.wines.unshift(wine); | ||||||
|  |           this.$refs.vinmonopoletLinkInput.value = ""; | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     sendWines() { | ||||||
|  |       const filterOutExistingWines = wine => wine["_id"] == null; | ||||||
|  |  | ||||||
|  |       const options = { | ||||||
|  |         method: "POST", | ||||||
|  |         headers: { | ||||||
|  |           "Content-Type": "application/json" | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |           wines: this.wines.filter(filterOutExistingWines) | ||||||
|  |         }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       fetch("/api/lottery/wines", options).then(resp => { | ||||||
|  |         try { | ||||||
|  |           if (resp.ok == false) { | ||||||
|  |             throw resp; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           resp.json().then(response => { | ||||||
|  |             if (response.success == false) { | ||||||
|  |               throw response; | ||||||
|  |             } else { | ||||||
|  |               this.$toast.info({ | ||||||
|  |                 title: "Viner sendt inn!", | ||||||
|  |                 timeout: 4000 | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |         } catch (error) { | ||||||
|  |           this.$toast.error({ | ||||||
|  |             title: "Feil oppsto ved innsending!", | ||||||
|  |             description: error.message, | ||||||
|  |             timeout: 4000 | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     updateWine(updatedWine) { | ||||||
|  |       const options = { | ||||||
|  |         method: "PUT", | ||||||
|  |         headers: { "Content-Type": "application/json" }, | ||||||
|  |         body: JSON.stringify({ wine: updatedWine }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       fetch(`/api/lottery/wine/${updatedWine._id}`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           this.editingWine = null; | ||||||
|  |  | ||||||
|  |           if (response.success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: response.message | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     deleteWine(deletedWine) { | ||||||
|  |       this.wines = this.wines.filter(wine => wine.name != deletedWine.name); | ||||||
|  |  | ||||||
|  |       if (deletedWine._id == null) return; | ||||||
|  |  | ||||||
|  |       const options = { method: "DELETE" }; | ||||||
|  |       fetch(`/api/lottery/wine/${deletedWine._id}`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           this.editingWine = null; | ||||||
|  |  | ||||||
|  |           this.$toast.info({ | ||||||
|  |             title: response.message | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  | @import "@/styles/variables.scss"; | ||||||
|  |  | ||||||
|  | h1 { | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .button-container { | ||||||
|  |   margin: 1.5rem 0 0; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .row { | ||||||
|  |   margin: 0.25rem 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .import-from-link { | ||||||
|  |   width: 70%; | ||||||
|  |   max-width: 800px; | ||||||
|  |   margin: 1.5rem auto 0; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |  | ||||||
|  |   label { | ||||||
|  |     display: inline-block; | ||||||
|  |     font-size: 1rem; | ||||||
|  |     text-transform: uppercase; | ||||||
|  |     letter-spacing: 0px; | ||||||
|  |     font-weight: 600; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   input { | ||||||
|  |     font-size: 1.5rem; | ||||||
|  |     min-height: 2rem; | ||||||
|  |     line-height: 2rem; | ||||||
|  |     border: none; | ||||||
|  |     border-bottom: 1px solid black; | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .error { | ||||||
|  |     margin-top: 0.5rem; | ||||||
|  |     padding: 1.25rem; | ||||||
|  |     background-color: $light-red; | ||||||
|  |     color: $red; | ||||||
|  |     font-size: 1.3rem; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       font-size: 1.1rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .wine-edit-container { | ||||||
|  |   max-width: 1500px; | ||||||
|  |   padding: 2rem; | ||||||
|  |   margin: 0 auto; | ||||||
|  |  | ||||||
|  |   .wines { | ||||||
|  |     display: flex; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     justify-content: center; | ||||||
|  |  | ||||||
|  |     > div { | ||||||
|  |       margin: 1rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   label { | ||||||
|  |     margin-top: 0.7rem; | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .button-container { | ||||||
|  |     margin-top: 1rem; | ||||||
|  |  | ||||||
|  |     button:not(:last-child) { | ||||||
|  |       margin-right: 0.5rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										441
									
								
								frontend/components/admin/archiveLotteryPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										441
									
								
								frontend/components/admin/archiveLotteryPage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,441 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="page-container"> | ||||||
|  |     <h1>Arkiver lotteri</h1> | ||||||
|  |  | ||||||
|  |     <h2>Registrer lodd kjøpt</h2> | ||||||
|  |  | ||||||
|  |     <div class="colors"> | ||||||
|  |       <div v-for="color in lotteryColors" :class="color.key + ' colors-box'" :key="color"> | ||||||
|  |         <div class="colors-overlay"> | ||||||
|  |           <p>{{ color.name }} kjøpt</p> | ||||||
|  |           <input v-model.number="color.value" min="0" :placeholder="0" type="number" /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="label-div"> | ||||||
|  |         <label>Penger mottatt på vipps:</label> | ||||||
|  |         <input v-model.number="payed" placeholder="NOK" type="number" :step="price || 1" min="0" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="wines.length > 0"> | ||||||
|  |       <h2>Vinneres vin-valg</h2> | ||||||
|  |  | ||||||
|  |       <div class="winner-container"> | ||||||
|  |         <wine v-for="wine in wines" :key="wine.id" :wine="wine"> | ||||||
|  |           <div class="label-div"> | ||||||
|  |             <label for="potential-winner-name">Virtuelle vinnere</label> | ||||||
|  |             <select id="potential-winner-name" type="text" placeholder="Navn" v-model="wine.winner"> | ||||||
|  |               <option v-for="winner in winners" :value="winner">{{ winner.name }}</option> | ||||||
|  |             </select> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <div class="winner-element"> | ||||||
|  |             <div class="color-selector"> | ||||||
|  |               <div class="label-div"> | ||||||
|  |                 <label>Farge vunnet</label> | ||||||
|  |               </div> | ||||||
|  |  | ||||||
|  |               <button | ||||||
|  |                 class="blue" | ||||||
|  |                 :class="{ active: wine.winner.color == 'blue' }" | ||||||
|  |                 @click="wine.winner.color = 'blue'" | ||||||
|  |               ></button> | ||||||
|  |               <button | ||||||
|  |                 class="red" | ||||||
|  |                 :class="{ active: wine.winner.color == 'red' }" | ||||||
|  |                 @click="wine.winner.color = 'red'" | ||||||
|  |               ></button> | ||||||
|  |               <button | ||||||
|  |                 class="green" | ||||||
|  |                 :class="{ active: wine.winner.color == 'green' }" | ||||||
|  |                 @click="wine.winner.color = 'green'" | ||||||
|  |               ></button> | ||||||
|  |               <button | ||||||
|  |                 class="yellow" | ||||||
|  |                 :class="{ active: wine.winner.color == 'yellow' }" | ||||||
|  |                 @click="wine.winner.color = 'yellow'" | ||||||
|  |               ></button> | ||||||
|  |             </div> | ||||||
|  |             <div class="label-div"> | ||||||
|  |               <label for="winner-name">Navn vinner</label> | ||||||
|  |               <input id="winner-name" type="text" placeholder="Navn" v-model="wine.winner.name" /> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </wine> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-if="wines.length > 0" class="button-container column"> | ||||||
|  |       <button class="vin-button" @click="archiveLottery">Send inn og arkiver</button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { dateString } from "@/utils"; | ||||||
|  | import Wine from "@/ui/Wine"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { Wine }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       payed: undefined, | ||||||
|  |       wines: [], | ||||||
|  |       winners: [], | ||||||
|  |       attendees: [], | ||||||
|  |       lotteryColors: [ | ||||||
|  |         { value: 0, name: "Blå", key: "blue" }, | ||||||
|  |         { value: 0, name: "Rød", key: "red" }, | ||||||
|  |         { value: 0, name: "Grønn", key: "green" }, | ||||||
|  |         { value: 0, name: "Gul", key: "yellow" } | ||||||
|  |       ], | ||||||
|  |       price: __PRICE__ || 10 | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.fetchLotteryWines(); | ||||||
|  |     this.fetchLotteryWinners(); | ||||||
|  |     this.fetchLotteryAttendees(); | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     lotteryColors: { | ||||||
|  |       deep: true, | ||||||
|  |       handler() { | ||||||
|  |         this.payed = this.getRaffleValue(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     payed(val) { | ||||||
|  |       this.$emit("counter", val); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     wineWithWinnerMapper(wine) { | ||||||
|  |       if (wine.winner == undefined) { | ||||||
|  |         wine.winner = { | ||||||
|  |           name: undefined, | ||||||
|  |           color: undefined | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       return wine; | ||||||
|  |     }, | ||||||
|  |     fetchLotteryWines() { | ||||||
|  |       return fetch("/api/lottery/wines") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.wines = response.wines.map(this.wineWithWinnerMapper); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: "Klarte ikke hente viner.", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     fetchLotteryWinners() { | ||||||
|  |       return fetch("/api/lottery/winners") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.winners = response.winners; | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: "Klarte ikke hente vinnere.", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     fetchLotteryAttendees() { | ||||||
|  |       return fetch("/api/lottery/attendees") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success && response.attendees) { | ||||||
|  |             this.attendees = response.attendees; | ||||||
|  |             this.updateLotteryColorsWithAttendees(response.attendees) | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: "Klarte ikke hente deltakere.", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     updateLotteryColorsWithAttendees(attendees) { | ||||||
|  |       this.attendees.map(attendee => { | ||||||
|  |         this.lotteryColors.map(color => (color.value += attendee[color.key])); | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     getRaffleValue() { | ||||||
|  |       let rafflesBought = 0; | ||||||
|  |       this.lotteryColors.map(color => rafflesBought += Number(color.value)); | ||||||
|  |  | ||||||
|  |       return rafflesBought * this.price; | ||||||
|  |     }, | ||||||
|  |     archiveLottery: async function(event) { | ||||||
|  |       const validation = this.wines.every(wine => { | ||||||
|  |         if (wine.winner.name == undefined || wine.winner.name == "") { | ||||||
|  |           this.$toast.error({ | ||||||
|  |             title: `Navn på vinner må defineres for vin: ${wine.name}` | ||||||
|  |           }); | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         if (wine.winner.color == undefined || wine.winner.color == "") { | ||||||
|  |           this.$toast.error({ | ||||||
|  |             title: `Farge vunnet må defineres for vin: ${wine.name}` | ||||||
|  |           }); | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (validation == false) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let rafflesPayload = {}; | ||||||
|  |       this.lotteryColors.map(el => rafflesPayload.[el.key] = el.value); | ||||||
|  |  | ||||||
|  |       let stolen = 0; | ||||||
|  |       const payedDiff = this.payed - this.getRaffleValue() | ||||||
|  |       if (payedDiff) { | ||||||
|  |         stolen = payedDiff / this.price; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const payload = { | ||||||
|  |         wines: this.wines, | ||||||
|  |         raffles: rafflesPayload, | ||||||
|  |         stolen: stolen | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const options = { | ||||||
|  |         method: "POST", | ||||||
|  |         headers: { "Content-Type": "application/json" }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |           lottery: payload | ||||||
|  |         }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return fetch("/api/lottery/archive", options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: "Lotteriet er sendt inn og arkivert! Du kan nå slette viner, deltakere & vinnere slettes.", | ||||||
|  |               timeout: 10000 | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: "Noe gikk galt under innsending!", | ||||||
|  |               description: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "@/styles/global.scss"; | ||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
|  | select { | ||||||
|  |   margin: 0 0 auto; | ||||||
|  |   height: 2rem; | ||||||
|  |   min-width: 0; | ||||||
|  |   width: 98%; | ||||||
|  |   padding: 1%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .button-container { | ||||||
|  |   margin-top: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .page-container { | ||||||
|  |   padding: 0 1.5rem 3rem; | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     max-width: 60vw; | ||||||
|  |     margin: 0 auto; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | .winner-container { | ||||||
|  |   width: 100%; | ||||||
|  |   display: flex; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   justify-content: space-around; | ||||||
|  |  | ||||||
|  |   > div { | ||||||
|  |     margin: 1rem; | ||||||
|  |     max-width: 350px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .button-container { | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .winner-element { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |  | ||||||
|  |   > div { | ||||||
|  |     margin-bottom: 1rem; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .color-selector { | ||||||
|  |   margin-bottom: 0.65rem; | ||||||
|  |   margin-right: 1rem; | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     min-width: 175px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     max-width: 25vw; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .active { | ||||||
|  |     border: 2px solid unset; | ||||||
|  |  | ||||||
|  |     &.green { | ||||||
|  |       border-color: $green; | ||||||
|  |     } | ||||||
|  |     &.blue { | ||||||
|  |       border-color: $dark-blue; | ||||||
|  |     } | ||||||
|  |     &.red { | ||||||
|  |       border-color: $red; | ||||||
|  |     } | ||||||
|  |     &.yellow { | ||||||
|  |       border-color: $dark-yellow; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   button { | ||||||
|  |     border: 2px solid transparent; | ||||||
|  |     display: inline-flex; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     flex-direction: row; | ||||||
|  |     height: 2.5rem; | ||||||
|  |     width: 2.5rem; | ||||||
|  |  | ||||||
|  |     // disable-dbl-tap-zoom | ||||||
|  |     touch-action: manipulation; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       margin: 2px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.green { | ||||||
|  |       background: #c8f9df; | ||||||
|  |     } | ||||||
|  |     &.blue { | ||||||
|  |       background: #d4f2fe; | ||||||
|  |     } | ||||||
|  |     &.red { | ||||||
|  |       background: #fbd7de; | ||||||
|  |     } | ||||||
|  |     &.yellow { | ||||||
|  |       background: #fff6d6; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .colors { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   justify-content: center; | ||||||
|  |   max-width: 1400px; | ||||||
|  |   margin: 0 auto; | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     margin: 1.8rem auto 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .label-div { | ||||||
|  |     margin-top: 0.5rem; | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .colors-box { | ||||||
|  |   width: 150px; | ||||||
|  |   height: 150px; | ||||||
|  |   margin: 20px; | ||||||
|  |   -webkit-mask-image: url(/public/assets/images/lodd.svg); | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   mask-image: url(/public/assets/images/lodd.svg); | ||||||
|  |   -webkit-mask-repeat: no-repeat; | ||||||
|  |   mask-repeat: no-repeat; | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     width: 120px; | ||||||
|  |     height: 120px; | ||||||
|  |     margin: 10px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .colors-overlay { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   height: 100%; | ||||||
|  |   padding: 0 0.5rem; | ||||||
|  |   position: relative; | ||||||
|  |  | ||||||
|  |   p { | ||||||
|  |     margin: 0; | ||||||
|  |     font-size: 0.8rem; | ||||||
|  |     margin-bottom: 0.5rem; | ||||||
|  |     text-transform: uppercase; | ||||||
|  |     font-weight: 600; | ||||||
|  |     position: absolute; | ||||||
|  |     top: 0.4rem; | ||||||
|  |     left: 0.5rem; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   input { | ||||||
|  |     width: 70%; | ||||||
|  |     border: 0; | ||||||
|  |     padding: 0; | ||||||
|  |     font-size: 3rem; | ||||||
|  |     height: unset; | ||||||
|  |     max-height: unset; | ||||||
|  |     position: absolute; | ||||||
|  |     bottom: 1.5rem; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .green, | ||||||
|  | .green .colors-overlay > input { | ||||||
|  |   background-color: $light-green; | ||||||
|  |   color: $green; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .blue, | ||||||
|  | .blue .colors-overlay > input { | ||||||
|  |   background-color: $light-blue; | ||||||
|  |   color: $blue; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .yellow, | ||||||
|  | .yellow .colors-overlay > input { | ||||||
|  |   background-color: $light-yellow; | ||||||
|  |   color: $yellow; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .red, | ||||||
|  | .red .colors-overlay > input { | ||||||
|  |   background-color: $light-red; | ||||||
|  |   color: $red; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										329
									
								
								frontend/components/admin/registerAttendeePage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										329
									
								
								frontend/components/admin/registerAttendeePage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,329 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="page-container"> | ||||||
|  |     <h1>Legg til deltaker</h1> | ||||||
|  |  | ||||||
|  |     <div class="attendee-registration-container"> | ||||||
|  |       <div class="row flex"> | ||||||
|  |         <div class="label-div"> | ||||||
|  |           <label for="name" ref="name">Navn</label> | ||||||
|  |           <input id="name" type="text" placeholder="Navn" v-model="name" /> | ||||||
|  |  | ||||||
|  |           <ul class="autocomplete" v-if="autocompleteAttendees.length"> | ||||||
|  |             <a | ||||||
|  |               v-for="attendee in autocompleteAttendees" | ||||||
|  |               tabindex="0" | ||||||
|  |               @keydown.enter="setName(attendee)" | ||||||
|  |               @keydown.space="setName(attendee)" | ||||||
|  |             > | ||||||
|  |               <li @click="setName(attendee)"> | ||||||
|  |                 {{ attendee }} | ||||||
|  |               </li> | ||||||
|  |             </a> | ||||||
|  |           </ul> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="label-div"> | ||||||
|  |           <label for="phoneNumber">Telefonnummer</label> | ||||||
|  |           <input | ||||||
|  |             id="phoneNumber" | ||||||
|  |             ref="phone" | ||||||
|  |             type="phone" | ||||||
|  |             pattern="[0-9]" | ||||||
|  |             placeholder="Telefonnummer" | ||||||
|  |             v-model="phoneNumber" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="label-div"> | ||||||
|  |           <label for="randomColors">Tilfeldig farger?</label> | ||||||
|  |           <input id="randomColors" type="checkbox" placeholder="Tilfeldig farger" v-model="randomColors" /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-if="!randomColors"> | ||||||
|  |         <div class="row flex"> | ||||||
|  |           <div class="label-div" v-for="color in colors"> | ||||||
|  |             <label :for="color.key">{{ color.name }}</label> | ||||||
|  |             <input :id="color.key" type="number" :placeholder="color.name" v-model="color.value" /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <button class="vin-button" @click="sendAttendee">Send deltaker</button> | ||||||
|  |  | ||||||
|  |       <div v-if="randomColors"> | ||||||
|  |         <RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" :compact="true" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <Attendees :attendees="attendees" :admin="isAdmin" /> | ||||||
|  |  | ||||||
|  |     <div v-if="attendees.length" class="button-container" style="margin-top: 2rem;"> | ||||||
|  |       <button class="vin-button danger" @click="deleteAllAttendees"> | ||||||
|  |         Slett alle deltakere | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import io from "socket.io-client"; | ||||||
|  | import Attendees from "@/ui/Attendees"; | ||||||
|  | import RaffleGenerator from "@/ui/RaffleGenerator"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     Attendees, | ||||||
|  |     RaffleGenerator | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       red: { | ||||||
|  |         name: "Rød", | ||||||
|  |         key: "red", | ||||||
|  |         value: 0 | ||||||
|  |       }, | ||||||
|  |       blue: { | ||||||
|  |         name: "Blå", | ||||||
|  |         key: "blue", | ||||||
|  |         value: 0 | ||||||
|  |       }, | ||||||
|  |       green: { | ||||||
|  |         name: "Grønn", | ||||||
|  |         key: "green", | ||||||
|  |         value: 0 | ||||||
|  |       }, | ||||||
|  |       yellow: { | ||||||
|  |         name: "Gul", | ||||||
|  |         key: "yellow", | ||||||
|  |         value: 0 | ||||||
|  |       }, | ||||||
|  |       isAdmin: false, | ||||||
|  |       name: null, | ||||||
|  |       phoneNumber: null, | ||||||
|  |       raffles: 0, | ||||||
|  |       randomColors: false, | ||||||
|  |       attendees: [], | ||||||
|  |       autocompleteAttendees: [], | ||||||
|  |       socket: null, | ||||||
|  |       previousAttendees: [] | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     attendees() { | ||||||
|  |       this.$emit("counter", this.attendees.length || 0); | ||||||
|  |     }, | ||||||
|  |     randomColors(val) { | ||||||
|  |       if (val == false) { | ||||||
|  |         this.colors.map(color => (color.value = 0)); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     name(newVal, oldVal) { | ||||||
|  |       if (newVal == "" || newVal == null) { | ||||||
|  |         this.autocompleteAttendees = []; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (this.autocompleteAttendees.includes(newVal)) { | ||||||
|  |         this.autocompleteAttendees = []; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (this.previousAttendees.length == 0) { | ||||||
|  |         fetch(`/api/history`) | ||||||
|  |           .then(resp => resp.json()) | ||||||
|  |           .then(response => (this.previousAttendees = response.winners)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.autocompleteAttendees = this.previousAttendees | ||||||
|  |         .filter(attendee => attendee.name.toLowerCase().includes(newVal)) | ||||||
|  |         .map(attendee => attendee.name); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.getAttendees(); | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     colors() { | ||||||
|  |       return [this.red, this.blue, this.green, this.yellow]; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     setName(name) { | ||||||
|  |       this.name = name; | ||||||
|  |       this.$refs.phone.focus(); | ||||||
|  |     }, | ||||||
|  |     setWithRandomColors(colors) { | ||||||
|  |       Object.keys(colors).forEach(color => (this[color].value = colors[color])); | ||||||
|  |     }, | ||||||
|  |     checkIfAdmin(resp) { | ||||||
|  |       this.isAdmin = resp.headers.get("vinlottis-admin") == "true" || false; | ||||||
|  |       return resp; | ||||||
|  |     }, | ||||||
|  |     getAttendees: async function() { | ||||||
|  |       return fetch("/api/lottery/attendees") | ||||||
|  |         .then(resp => this.checkIfAdmin(resp)) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => (this.attendees = response.attendees)); | ||||||
|  |     }, | ||||||
|  |     sendAttendee: async function() { | ||||||
|  |       const { red, blue, green, yellow } = this; | ||||||
|  |  | ||||||
|  |       if (red.value == 0 && blue.value == 0 && green.value == 0 && yellow.value == 0) { | ||||||
|  |         this.$toast.error({ title: "Ingen farger valgt!" }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       if (this.name == 0 && this.phoneNumber) { | ||||||
|  |         this.$toast.error({ title: "Ingen navn eller tlf satt!" }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const attendee = { | ||||||
|  |         name: this.name, | ||||||
|  |         phoneNumber: Number(this.phoneNumber), | ||||||
|  |         red: Number(red.value), | ||||||
|  |         blue: Number(blue.value), | ||||||
|  |         green: Number(green.value), | ||||||
|  |         yellow: Number(yellow.value), | ||||||
|  |         raffles: Number(this.raffles) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const options = { | ||||||
|  |         method: "POST", | ||||||
|  |         headers: { "Content-Type": "application/json" }, | ||||||
|  |         body: JSON.stringify({ attendee }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return fetch("/api/lottery/attendee", options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           if (response.success == true) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: `Sendt inn deltaker: ${this.name}`, | ||||||
|  |               timeout: 4000 | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             this.name = ""; | ||||||
|  |             this.phoneNumber = null; | ||||||
|  |             this.yellow.value = 0; | ||||||
|  |             this.green.value = 0; | ||||||
|  |             this.red.value = 0; | ||||||
|  |             this.blue.value = 0; | ||||||
|  |             this.randomColors = false; | ||||||
|  |  | ||||||
|  |             this.$refs.name.focus(); | ||||||
|  |             this.getAttendees(); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: `Klarte ikke sende deltaker`, | ||||||
|  |               description: response.message, | ||||||
|  |               timeout: 4000 | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     deleteAllAttendees() { | ||||||
|  |       if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) { | ||||||
|  |         const options = { method: "DELETE" }; | ||||||
|  |  | ||||||
|  |         fetch("/api/lottery/attendees", options) | ||||||
|  |           .then(resp => resp.json()) | ||||||
|  |           .then(response => { | ||||||
|  |             if (response.success) { | ||||||
|  |               this.attendees = []; | ||||||
|  |               this.$toast.info({ | ||||||
|  |                 title: "Slettet alle deltakere." | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               this.$toast.error({ | ||||||
|  |                 title: "Klarte ikke slette deltakere", | ||||||
|  |                 description: response.message | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style lang="scss"> | ||||||
|  | // global styling for disabling height of attendee class | ||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
|  | .attendee { | ||||||
|  |   max-height: unset; | ||||||
|  |  | ||||||
|  |   .raffle-element { | ||||||
|  |     margin: 0; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       margin: 10px 0; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "@/styles/global.scss"; | ||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
|  | .attendee-registration-container { | ||||||
|  |   margin-bottom: 2rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .row.flex .label-div { | ||||||
|  |   margin-right: 1rem; | ||||||
|  |   margin-bottom: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .autocomplete { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 100%; | ||||||
|  |   margin: 0; | ||||||
|  |   list-style: none; | ||||||
|  |   padding: 0; | ||||||
|  |   z-index: 10; | ||||||
|  |   background-color: white; | ||||||
|  |   border: 1px solid #e1e4e8; | ||||||
|  |  | ||||||
|  |   & li { | ||||||
|  |     padding: 1rem; | ||||||
|  |     font-size: 1.1rem; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       background-color: #e1e4e8; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | hr { | ||||||
|  |   width: 90%; | ||||||
|  |   margin: 2rem auto; | ||||||
|  |   color: grey; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .page-container { | ||||||
|  |   padding: 0 1.5rem 3rem; | ||||||
|  |  | ||||||
|  |   @include desktop { | ||||||
|  |     max-width: 60vw; | ||||||
|  |     margin: 0 auto; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #randomColors { | ||||||
|  |   width: 40px; | ||||||
|  |   height: 40px; | ||||||
|  |   border: none; | ||||||
|  |   cursor: pointer; | ||||||
|  |  | ||||||
|  |   &:checked::after { | ||||||
|  |     content: "✅"; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &::after { | ||||||
|  |     font-size: 2.1rem; | ||||||
|  |     content: "❌"; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										166
									
								
								frontend/plugins/Toast/Toast.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								frontend/plugins/Toast/Toast.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | |||||||
|  | <template> | ||||||
|  |   <transition name="slide"> | ||||||
|  |     <div class="toast" :class="type" v-if="show" ref="toast"> | ||||||
|  |       <div class="message"> | ||||||
|  |         <span v-html="title"></span> | ||||||
|  |         <span class="description" v-if="description"> | ||||||
|  |           {{ description }} | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="button-container"> | ||||||
|  |         <button @click="dismiss">Lukk</button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </transition> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       type: this.$root.type || "info", | ||||||
|  |       title: this.$root.title || undefined, | ||||||
|  |       description: this.$root.description || undefined, | ||||||
|  |       image: this.$root.image || undefined, | ||||||
|  |       link: this.$root.link || undefined, | ||||||
|  |       timeout: this.$root.timeout || 4500, | ||||||
|  |       show: false, | ||||||
|  |       mouseover: false, | ||||||
|  |       timedOut: false | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     // Here we set show when mounted in-order to get the transition animation to be displayed correctly | ||||||
|  |     this.show = true; | ||||||
|  |     const timeout = setTimeout(() => { | ||||||
|  |       console.log("Your toast time is up 👋"); | ||||||
|  |  | ||||||
|  |       if (this.mouseover === false) { | ||||||
|  |         this.show = false; | ||||||
|  |       } else { | ||||||
|  |         this.timedOut = true; | ||||||
|  |       } | ||||||
|  |     }, this.timeout); | ||||||
|  |  | ||||||
|  |     setTimeout(() => { | ||||||
|  |       const { toast } = this.$refs; | ||||||
|  |  | ||||||
|  |       if (toast) { | ||||||
|  |         toast.addEventListener("mouseenter", _ => { | ||||||
|  |           this.mouseover = true; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         toast.addEventListener("mouseleave", _ => { | ||||||
|  |           this.mouseover = false; | ||||||
|  |  | ||||||
|  |           if (this.timedOut === true) { | ||||||
|  |             this.show = false; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }, 10); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     dismiss() { | ||||||
|  |       this.show = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
|  | .slide-enter-active { | ||||||
|  |   transition: all 0.3s ease; | ||||||
|  | } | ||||||
|  | .slide-enter, | ||||||
|  | .slide-leave-to { | ||||||
|  |   transform: translateY(100vh); | ||||||
|  |   opacity: 0; | ||||||
|  | } | ||||||
|  | .slide-leave-active { | ||||||
|  |   transition: all 2s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .toast { | ||||||
|  |   position: fixed; | ||||||
|  |   bottom: 1.3rem; | ||||||
|  |   left: 0; | ||||||
|  |   right: 0; | ||||||
|  |   margin: auto; | ||||||
|  |   background: #2d2d2d; | ||||||
|  |   border-radius: 5px; | ||||||
|  |   padding: 15px; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|  |   width: 80vw; | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     width: 85vw; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .message { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   & span { | ||||||
|  |     color: white; | ||||||
|  |  | ||||||
|  |     &.description { | ||||||
|  |       margin-top: 0.5rem; | ||||||
|  |       font-size: 0.9rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   & .button-container { | ||||||
|  |     & button { | ||||||
|  |       color: #2d2d2d; | ||||||
|  |       background-color: white; | ||||||
|  |       border-radius: 5px; | ||||||
|  |       padding: 10px; | ||||||
|  |       margin: 0 3px; | ||||||
|  |       font-size: 0.8rem; | ||||||
|  |       height: max-content; | ||||||
|  |       border: 0; | ||||||
|  |       font-size: 0.9rem; | ||||||
|  |  | ||||||
|  |       &:active { | ||||||
|  |         background: #2d2d2d; | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &.success { | ||||||
|  |     background-color: #5bc2a1; | ||||||
|  |     color: white; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &.info { | ||||||
|  |     background: #2d2d2d; | ||||||
|  |     color: white; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &.warning { | ||||||
|  |     border-left: 6px solid #f6993f; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &.error { | ||||||
|  |     background-color: var(--red); | ||||||
|  |  | ||||||
|  |     button { | ||||||
|  |       color: var(--dark-red); | ||||||
|  |  | ||||||
|  |       &:active { | ||||||
|  |         background-color: var(--dark-red); | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										51
									
								
								frontend/plugins/Toast/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/plugins/Toast/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | import Vue from "vue"; | ||||||
|  | import ToastComponent from "./Toast"; | ||||||
|  |  | ||||||
|  | const optionsDefaults = { | ||||||
|  |   data: { | ||||||
|  |     type: "info", | ||||||
|  |     show: true, | ||||||
|  |     timeout: 4500, | ||||||
|  |  | ||||||
|  |     onCreate(created = null) {}, | ||||||
|  |     onEdit(editted = null) {}, | ||||||
|  |     onRemove(removed = null) {} | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function toast(options) { | ||||||
|  |   // merge the default options with the passed options. | ||||||
|  |   const root = new Vue({ | ||||||
|  |     data: { | ||||||
|  |       ...optionsDefaults.data, | ||||||
|  |       ...options | ||||||
|  |     }, | ||||||
|  |     render: createElement => createElement(ToastComponent) | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   root.$mount(document.body.appendChild(document.createElement("div"))); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   install(vue) { | ||||||
|  |     console.log("Installing toast plugin!"); | ||||||
|  |  | ||||||
|  |     Vue.prototype.$toast = { | ||||||
|  |       info(options) { | ||||||
|  |         toast({ type: "info", ...options }); | ||||||
|  |       }, | ||||||
|  |       success(options) { | ||||||
|  |         toast({ type: "success", ...options }); | ||||||
|  |       }, | ||||||
|  |       warning(options) { | ||||||
|  |         toast({ type: "warning", ...options }); | ||||||
|  |       }, | ||||||
|  |       error(options) { | ||||||
|  |         toast({ type: "error", ...options }); | ||||||
|  |       }, | ||||||
|  |       simple(options) { | ||||||
|  |         toast({ type: "simple", ...options }); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -20,6 +20,8 @@ body { | |||||||
|  |  | ||||||
| a { | a { | ||||||
|   text-decoration: none; |   text-decoration: none; | ||||||
|  |   cursor: pointer; | ||||||
|  |   color: inherit; | ||||||
| } | } | ||||||
|  |  | ||||||
| .title { | .title { | ||||||
| @@ -51,8 +53,10 @@ a { | |||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|  |   position: relative; | ||||||
|  |  | ||||||
|   label { |   label { | ||||||
|  |     margin-top: 0.7rem; | ||||||
|     margin-bottom: 0.25rem; |     margin-bottom: 0.25rem; | ||||||
|     font-weight: 600; |     font-weight: 600; | ||||||
|     text-transform: uppercase; |     text-transform: uppercase; | ||||||
| @@ -76,6 +80,7 @@ a { | |||||||
|  |  | ||||||
|   > *:not(:last-child) { |   > *:not(:last-child) { | ||||||
|     margin-right: 2rem; |     margin-right: 2rem; | ||||||
|  |     margin-bottom: 0.75rem; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.column { |   &.column { | ||||||
| @@ -95,7 +100,7 @@ a { | |||||||
|  |  | ||||||
|       > *:not(:last-child) { |       > *:not(:last-child) { | ||||||
|         margin-right: unset; |         margin-right: unset; | ||||||
|         margin-bottom: .75rem; |         margin-bottom: 0.75rem; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -105,6 +110,8 @@ input, | |||||||
| textarea { | textarea { | ||||||
|   border-radius: 0; |   border-radius: 0; | ||||||
|   box-shadow: none; |   box-shadow: none; | ||||||
|  |   padding: 0; | ||||||
|  |   margin: 0; | ||||||
|   -webkit-appearance: none; |   -webkit-appearance: none; | ||||||
|   font-size: 1.1rem; |   font-size: 1.1rem; | ||||||
|   border: 1px solid rgba(#333333, 0.3); |   border: 1px solid rgba(#333333, 0.3); | ||||||
| @@ -136,6 +143,11 @@ textarea { | |||||||
|     height: auto; |     height: auto; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   &.warning { | ||||||
|  |     background-color: #f9826c; | ||||||
|  |     color: white; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   &.danger { |   &.danger { | ||||||
|     background-color: $red; |     background-color: $red; | ||||||
|     color: white; |     color: white; | ||||||
| @@ -151,9 +163,12 @@ textarea { | |||||||
|     top: 0; |     top: 0; | ||||||
|     left: 0; |     left: 0; | ||||||
|     opacity: 0; |     opacity: 0; | ||||||
|     box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), |     box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), 0 4px 8px rgba(0, 0, 0, 0.07), | ||||||
|       0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07), |       0 8px 16px rgba(0, 0, 0, 0.07), 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); |   } | ||||||
|  |  | ||||||
|  |   &.active { | ||||||
|  |     font-weight: bold; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:hover:not(:disabled) { |   &:hover:not(:disabled) { | ||||||
| @@ -163,7 +178,7 @@ textarea { | |||||||
|       opacity: 1; |       opacity: 1; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   &:disabled{ |   &:disabled { | ||||||
|     opacity: 0.25; |     opacity: 0.25; | ||||||
|     cursor: not-allowed; |     cursor: not-allowed; | ||||||
|   } |   } | ||||||
| @@ -173,6 +188,21 @@ textarea { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .pulse-button:not(:hover) { | ||||||
|  |   animation: pulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes pulse { | ||||||
|  |   from { | ||||||
|  |     transform: scale(1); | ||||||
|  |   } | ||||||
|  |   50% { | ||||||
|  |     transform: scale(1.12); | ||||||
|  |   } | ||||||
|  |   to { | ||||||
|  |     transform: scale(1); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .cursor { | .cursor { | ||||||
|   &-pointer { |   &-pointer { | ||||||
| @@ -193,11 +223,23 @@ textarea { | |||||||
|   text-decoration: none; |   text-decoration: none; | ||||||
|   color: $matte-text-color; |   color: $matte-text-color; | ||||||
|  |  | ||||||
|   &:focus, &:hover { |   &:focus, | ||||||
|  |   &:hover { | ||||||
|     border-color: $link-color; |     border-color: $link-color; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .margin { | ||||||
|  |   &-md { | ||||||
|  |     margin: 3rem; | ||||||
|  |   } | ||||||
|  |   &-sm { | ||||||
|  |     margin: 1rem; | ||||||
|  |   } | ||||||
|  |   &-0 { | ||||||
|  |     margin: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .margin-top { | .margin-top { | ||||||
|   &-md { |   &-md { | ||||||
| @@ -269,14 +311,29 @@ textarea { | |||||||
|   margin: 0 !important; |   margin: 0 !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .wines-container { | ||||||
|  |   display: grid; | ||||||
|  |   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||||||
|  |   grid-gap: 2rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| .raffle-element { | .raffle-element { | ||||||
|  |   width: 45px; | ||||||
|  |   height: 45px; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   font-size: 0.75rem; | ||||||
|  |   font-weight: bold; | ||||||
|  |  | ||||||
|   margin: 20px 0; |   margin: 20px 0; | ||||||
|  |   color: #333333; | ||||||
|  |  | ||||||
|   -webkit-mask-image: url(/public/assets/images/lodd.svg); |   -webkit-mask-image: url(/public/assets/images/lodd.svg); | ||||||
|   background-repeat: no-repeat; |   background-repeat: no-repeat; | ||||||
|   mask-image: url(/public/assets/images/lodd.svg); |   mask-image: url(/public/assets/images/lodd.svg); | ||||||
|   -webkit-mask-repeat: no-repeat; |   -webkit-mask-repeat: no-repeat; | ||||||
|   mask-repeat: no-repeat; |   mask-repeat: no-repeat; | ||||||
|   color: #333333; |  | ||||||
|  |  | ||||||
|   &.green-raffle { |   &.green-raffle { | ||||||
|     background-color: $light-green; |     background-color: $light-green; | ||||||
| @@ -293,11 +350,16 @@ textarea { | |||||||
|   &.red-raffle { |   &.red-raffle { | ||||||
|     background-color: $light-red; |     background-color: $light-red; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   &:not(:last-of-type) { | ||||||
|  |     margin-right: 1rem; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @mixin raffle { | @mixin raffle { | ||||||
|   padding-bottom: 50px; |   padding-bottom: 50px; | ||||||
|   &::before, &::after { |   &::before, | ||||||
|  |   &::after { | ||||||
|     content: ""; |     content: ""; | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     left: 0; |     left: 0; | ||||||
| @@ -309,11 +371,11 @@ textarea { | |||||||
|     background-position: 0 25px; |     background-position: 0 25px; | ||||||
|     background-repeat: repeat-x; |     background-repeat: repeat-x; | ||||||
|   } |   } | ||||||
|   &::after{ |   &::after { | ||||||
|     background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%); |     background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%); | ||||||
|     background-size: 50px 50px; |     background-size: 50px 50px; | ||||||
|     background-position: 25px -25px; |     background-position: 25px -25px; | ||||||
|     bottom: -25px |     bottom: -25px; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -327,4 +389,4 @@ textarea { | |||||||
|   @include desktop { |   @include desktop { | ||||||
|     display: none; |     display: none; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
| .flex { | .flex { | ||||||
|   display: flex; |   display: flex; | ||||||
|  |  | ||||||
| @@ -7,6 +9,10 @@ | |||||||
|  |  | ||||||
|   &.row { |   &.row { | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       flex-direction: column; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.wrap { |   &.wrap { | ||||||
| @@ -43,4 +49,4 @@ | |||||||
|   &-right { |   &-right { | ||||||
|     float: right; |     float: right; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,21 +1,49 @@ | |||||||
| $primary: #b7debd; | body { | ||||||
|  |   --primary: #b7debd; | ||||||
|  |  | ||||||
| $light-green: #c8f9df; |   --light-green: #c8f9df; | ||||||
| $green: #0be881; |   --green: #0be881; | ||||||
| $dark-green: #0ed277; |   --dark-green: #0ed277; | ||||||
|  |  | ||||||
| $light-blue: #d4f2fe; |   --light-blue: #d4f2fe; | ||||||
| $blue: #4bcffa; |   --blue: #4bcffa; | ||||||
| $dark-blue: #24acda; |   --dark-blue: #24acda; | ||||||
|  |  | ||||||
| $light-yellow: #fff6d6; |   --light-yellow: #fff6d6; | ||||||
| $yellow: #ffde5d; |   --yellow: #ffde5d; | ||||||
| $dark-yellow: #ecc31d; |   --dark-yellow: #ecc31d; | ||||||
|  |  | ||||||
| $light-red: #fbd7de; |   --light-red: #fbd7de; | ||||||
| $red: #ef5878; |   --red: #ef5878; | ||||||
| $dark-red: #ec3b61; |   --dark-red: #ec3b61; | ||||||
|  |  | ||||||
| $link-color: #ff5fff; |   --link-color: #ff5fff; | ||||||
|  |   --underlinenav-text: #e1e4e8; | ||||||
|  |   --underlinenav-text-active: #f9826c; | ||||||
|  |   --underlinenav-text-hover: #d1d5da; | ||||||
|  |  | ||||||
| $matte-text-color: #333333; |   --matte-text-color: #333333; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $primary: var(--primary); | ||||||
|  |  | ||||||
|  | $light-green: var(--light-green); | ||||||
|  | $green: var(--green); | ||||||
|  | $dark-green: var(--dark-green); | ||||||
|  |  | ||||||
|  | $light-blue: var(--light-blue); | ||||||
|  | $blue: var(--blue); | ||||||
|  | $dark-blue: var(--dark-blue); | ||||||
|  |  | ||||||
|  | $light-yellow: var(--light-yellow); | ||||||
|  | $yellow: var(--yellow); | ||||||
|  | $dark-yellow: var(--dark-yellow); | ||||||
|  |  | ||||||
|  | $light-red: var(--light-red); | ||||||
|  | $red: var(--red); | ||||||
|  | $dark-red: var(--dark-red); | ||||||
|  |  | ||||||
|  | $link-color: var(--link-color); | ||||||
|  | $underlinenav-text-active: var(--underlinenav-text-active); | ||||||
|  |  | ||||||
|  | $matte-text-color: var(--matte-text-color); | ||||||
|   | |||||||
| @@ -1,12 +1,45 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="attendees" v-if="attendees.length > 0"> |   <div v-if="attendees.length > 0" class="attendee-container"> | ||||||
|     <div class="attendees-container" ref="attendees"> |     <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> | ||||||
|       <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> |       <div class="attendee-info"> | ||||||
|         <span class="attendee-name">{{ attendee.name }}</span> |         <router-link class="attendee-name" :to="`/highscore/${attendee.name}`"> | ||||||
|         <div class="red-raffle raffle-element small">{{ attendee.red }}</div> |           {{ attendee.name }} | ||||||
|         <div class="blue-raffle raffle-element small">{{ attendee.blue }}</div> |         </router-link> | ||||||
|         <div class="green-raffle raffle-element small">{{ attendee.green }}</div> |  | ||||||
|         <div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div> |         <div v-if="admin" class="flex column justify-center margin-top-sm"> | ||||||
|  |           <span>Phone: {{ attendee.phoneNumber }}</span> | ||||||
|  |           <span>Has won: {{ attendee.winner }}</span> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="raffle-container"> | ||||||
|  |           <div class="red-raffle raffle-element small">{{ attendee.red }}</div> | ||||||
|  |           <div class="blue-raffle raffle-element small">{{ attendee.blue }}</div> | ||||||
|  |           <div class="green-raffle raffle-element small">{{ attendee.green }}</div> | ||||||
|  |           <div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-if="admin" class="attendee-admin"> | ||||||
|  |         <button class="vin-button small" @click="editingAttendee = editingAttendee == attendee ? false : attendee"> | ||||||
|  |           {{ editingAttendee == attendee ? "Lukk" : "Rediger" }} | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-if="editingAttendee == attendee" class="attendee-edit"> | ||||||
|  |         <div class="label-div" v-for="key in Object.keys(attendee)" :key="key"> | ||||||
|  |           <label>{{ key }}</label> | ||||||
|  |           <input type="text" v-model="attendee[key]" :placeholder="key" /> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div v-if="editingAttendee == attendee"> | ||||||
|  |           <button class="vin-button small warning" @click="updateAttendee(attendee)"> | ||||||
|  |             Oppdater deltaker | ||||||
|  |           </button> | ||||||
|  |  | ||||||
|  |           <button class="vin-button small danger" @click="deleteAttendee(attendee)"> | ||||||
|  |             Slett deltaker | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @@ -17,33 +50,79 @@ export default { | |||||||
|   props: { |   props: { | ||||||
|     attendees: { |     attendees: { | ||||||
|       type: Array |       type: Array | ||||||
|  |     }, | ||||||
|  |     admin: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   data() { | ||||||
|     flipList: (list) => list.slice().reverse() |     return { | ||||||
|  |       editingAttendee: undefined | ||||||
|  |     }; | ||||||
|   }, |   }, | ||||||
|   watch: { |   methods: { | ||||||
|     attendees: { |     flipList: list => list.slice().reverse(), | ||||||
|       deep: true, |     updateAttendee(updatedAttendee) { | ||||||
|       handler() { |       const options = { | ||||||
|         if (this.$refs && this.$refs.history) { |         method: "PUT", | ||||||
|           setTimeout(() => { |         headers: { "Content-Type": "application/json" }, | ||||||
|             this.$refs.attendees.scrollTop = this.$refs.attendees.scrollHeight; |         body: JSON.stringify({ attendee: updatedAttendee }) | ||||||
|           }, 50); |       }; | ||||||
|         } |  | ||||||
|       } |       fetch(`/api/lottery/attendee/${updatedAttendee._id}`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           this.editingAttendee = null; | ||||||
|  |  | ||||||
|  |           const { message, success } = response; | ||||||
|  |  | ||||||
|  |           if (success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: response.message | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  |     deleteAttendee(deletedAttendee) { | ||||||
|  |       const options = { | ||||||
|  |         method: "DELETE", | ||||||
|  |         headers: { "Content-Type": "application/json" }, | ||||||
|  |         body: JSON.stringify({ attendee: deletedAttendee }) | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       fetch(`/api/lottery/attendee/${deletedAttendee._id}`, options) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           this.editingAttendee = null; | ||||||
|  |  | ||||||
|  |           const { message, success } = response; | ||||||
|  |  | ||||||
|  |           if (success) { | ||||||
|  |             this.$toast.info({ | ||||||
|  |               title: response.message | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             this.$toast.error({ | ||||||
|  |               title: response.message | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/global.scss"; | @import "@/styles/variables.scss"; | ||||||
| @import "../styles/variables.scss"; | @import "@/styles/media-queries.scss"; | ||||||
| @import "../styles/media-queries.scss"; |  | ||||||
|  |  | ||||||
| .attendee-name { | .attendee-name { | ||||||
|   width: 60%; |   font-size: 1.1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| hr { | hr { | ||||||
| @@ -51,45 +130,60 @@ hr { | |||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
|  |  | ||||||
| .raffle-element { | .attendee-container { | ||||||
|   font-size: 0.75rem; |  | ||||||
|   width: 45px; |  | ||||||
|   height: 45px; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   font-weight: bold; |  | ||||||
|   font-size: 0.75rem; |  | ||||||
|   text-align: center; |  | ||||||
|  |  | ||||||
|   &:not(:last-of-type) { |  | ||||||
|     margin-right: 1rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .attendees { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   align-items: center; |  | ||||||
|   height: auto; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .attendees-container { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   overflow-y: scroll; |  | ||||||
|   max-height: 550px; |   padding: 1rem; | ||||||
|  |  | ||||||
|  |   -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
|  |   -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
|  |   box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
| } | } | ||||||
|  |  | ||||||
| .attendee { | .attendee { | ||||||
|  |   padding: 0.5rem; | ||||||
|   display: flex; |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|   align-items: center; |  | ||||||
|   width: 100%; |   @include mobile { | ||||||
|   margin: 0 auto; |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   &:not(:last-of-type) { |   &:not(:last-of-type) { | ||||||
|     border-bottom: 2px solid #d7d8d7; |     border-bottom: 2px solid #d7d8d7; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   &:not(:first-of-type) { | ||||||
|  |     margin-top: 0.5rem; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   button { | ||||||
|  |     margin-bottom: 0.5rem; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &-info { | ||||||
|  |     display: flex; | ||||||
|  |     width: 100%; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     align-items: center; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       flex-direction: column; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &-edit { | ||||||
|  |     button { | ||||||
|  |       margin-top: 0.5rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .raffle-container { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -5,14 +5,14 @@ | |||||||
|       <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 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> |         <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> | ||||||
| @@ -21,8 +21,9 @@ | |||||||
|     <div class="clock"> |     <div class="clock"> | ||||||
|       <h2 v-if="!fiveMinutesLeft || !tenMinutesOver"> |       <h2 v-if="!fiveMinutesLeft || !tenMinutesOver"> | ||||||
|         <span v-if="days > 0">{{ pad(days) }}:</span> |         <span v-if="days > 0">{{ pad(days) }}:</span> | ||||||
|         <span>{{ pad(hours) }}</span>: |         <span>{{ pad(hours) }}</span | ||||||
|         <span>{{ pad(minutes) }}</span>: |         >: <span>{{ pad(minutes) }}</span | ||||||
|  |         >: | ||||||
|         <span>{{ pad(seconds) }}</span> |         <span>{{ pad(seconds) }}</span> | ||||||
|       </h2> |       </h2> | ||||||
|       <h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2> |       <h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2> | ||||||
| @@ -41,7 +42,7 @@ export default { | |||||||
|       minutes: 0, |       minutes: 0, | ||||||
|       seconds: 0, |       seconds: 0, | ||||||
|       distance: 0, |       distance: 0, | ||||||
|       interval: null, |       interval: null | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
| @@ -68,7 +69,7 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     toggleMenu(){ |     toggleMenu() { | ||||||
|       this.isOpen = this.isOpen ? false : true; |       this.isOpen = this.isOpen ? false : true; | ||||||
|     }, |     }, | ||||||
|     pad: function(num) { |     pad: function(num) { | ||||||
| @@ -91,10 +92,7 @@ export default { | |||||||
|       let nowDate = new Date(); |       let nowDate = new Date(); | ||||||
|       let now = nowDate.getTime(); |       let now = nowDate.getTime(); | ||||||
|       if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) { |       if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) { | ||||||
|         let _diff = |         let _diff = (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * 60 * -1; | ||||||
|           (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * |  | ||||||
|           60 * |  | ||||||
|           -1; |  | ||||||
|         nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff); |         nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff); | ||||||
|       } |       } | ||||||
|       this.nextLottery = nextDayOfLottery; |       this.nextLottery = nextDayOfLottery; | ||||||
| @@ -110,12 +108,8 @@ export default { | |||||||
|  |  | ||||||
|       // Time calculations for days, hours, minutes and seconds |       // Time calculations for days, hours, minutes and seconds | ||||||
|       this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24)); |       this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24)); | ||||||
|       this.hours = Math.floor( |       this.hours = Math.floor((this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); | ||||||
|         (this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) |       this.minutes = Math.floor((this.distance % (1000 * 60 * 60)) / (1000 * 60)); | ||||||
|       ); |  | ||||||
|       this.minutes = Math.floor( |  | ||||||
|         (this.distance % (1000 * 60 * 60)) / (1000 * 60) |  | ||||||
|       ); |  | ||||||
|       this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000); |       this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000); | ||||||
|       if (this.days == 7) { |       if (this.days == 7) { | ||||||
|         this.days = 0; |         this.days = 0; | ||||||
| @@ -124,7 +118,7 @@ export default { | |||||||
|         this.initialize(); |         this.initialize(); | ||||||
|       } |       } | ||||||
|       this.interval = setTimeout(this.countdown, 500); |       this.interval = setTimeout(this.countdown, 500); | ||||||
|     }, |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="chat-container"> |   <div class="chat-container"> | ||||||
|     <span class="logged-in-username" v-if="username">Logget inn som: <span class="username">{{ username }}</span> <button @click="removeUsername">Logg ut</button></span> |     <span class="logged-in-username" v-if="username" | ||||||
|  |       >Logget inn som: <span class="username">{{ username }}</span> | ||||||
|  |       <button @click="removeUsername">Logg ut</button></span | ||||||
|  |     > | ||||||
|  |  | ||||||
|     <div class="history" ref="history" v-if="chatHistory.length > 0"> |     <div class="history" ref="history" v-if="chatHistory.length > 0"> | ||||||
|       <div class="opaque-skirt"></div> |       <div class="opaque-skirt"></div> | ||||||
| @@ -8,7 +11,8 @@ | |||||||
|         <button @click="loadMoreHistory">Hent eldre meldinger</button> |         <button @click="loadMoreHistory">Hent eldre meldinger</button> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="history-message" |       <div | ||||||
|  |         class="history-message" | ||||||
|         v-for="(history, index) in chatHistory" |         v-for="(history, index) in chatHistory" | ||||||
|         :key="`${history.username}-${history.timestamp}-${index}`" |         :key="`${history.username}-${history.timestamp}-${index}`" | ||||||
|       > |       > | ||||||
| @@ -61,12 +65,11 @@ export default { | |||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   created() { |   created() { | ||||||
|     getChatHistory(1, this.pageSize) |     getChatHistory(1, this.pageSize).then(resp => { | ||||||
|       .then(resp => { |       this.chatHistory = resp.messages; | ||||||
|         this.chatHistory = resp.messages; |       this.hasMorePages = resp.total != resp.messages.length; | ||||||
|         this.hasMorePages = resp.total != resp.messages.length; |     }); | ||||||
|       }); |     const username = window.localStorage.getItem("username"); | ||||||
|     const username = window.localStorage.getItem('username'); |  | ||||||
|     if (username) { |     if (username) { | ||||||
|       this.username = username; |       this.username = username; | ||||||
|       this.emitUsernameOnConnect = true; |       this.emitUsernameOnConnect = true; | ||||||
| @@ -77,8 +80,7 @@ export default { | |||||||
|       handler: function(newVal, oldVal) { |       handler: function(newVal, oldVal) { | ||||||
|         if (oldVal.length == 0) { |         if (oldVal.length == 0) { | ||||||
|           this.scrollToBottomOfHistory(); |           this.scrollToBottomOfHistory(); | ||||||
|         } |         } else if (newVal && newVal.length == oldVal.length) { | ||||||
|         else if (newVal && newVal.length == oldVal.length) { |  | ||||||
|           if (this.isScrollPositionAtBottom()) { |           if (this.isScrollPositionAtBottom()) { | ||||||
|             this.scrollToBottomOfHistory(); |             this.scrollToBottomOfHistory(); | ||||||
|           } |           } | ||||||
| @@ -105,10 +107,7 @@ export default { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.socket.on("connect", msg => { |     this.socket.on("connect", msg => { | ||||||
|       if ( |       if (this.emitUsernameOnConnect || (this.wasDisconnected && this.username != null)) { | ||||||
|         this.emitUsernameOnConnect || |  | ||||||
|         (this.wasDisconnected && this.username != null) |  | ||||||
|       ) { |  | ||||||
|         this.setUsername(this.username); |         this.setUsername(this.username); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
| @@ -133,12 +132,11 @@ export default { | |||||||
|       let { page, pageSize } = this; |       let { page, pageSize } = this; | ||||||
|       page = page + 1; |       page = page + 1; | ||||||
|  |  | ||||||
|       getChatHistory(page, pageSize) |       getChatHistory(page, pageSize).then(resp => { | ||||||
|         .then(resp => { |         this.chatHistory = resp.messages.concat(this.chatHistory); | ||||||
|           this.chatHistory = resp.messages.concat(this.chatHistory); |         this.page = page; | ||||||
|           this.page = page; |         this.hasMorePages = resp.total != this.chatHistory.length; | ||||||
|           this.hasMorePages = resp.total != this.chatHistory.length; |       }); | ||||||
|         }); |  | ||||||
|     }, |     }, | ||||||
|     pad(num) { |     pad(num) { | ||||||
|       if (num > 9) return num; |       if (num > 9) return num; | ||||||
| @@ -146,9 +144,7 @@ export default { | |||||||
|     }, |     }, | ||||||
|     getTime(timestamp) { |     getTime(timestamp) { | ||||||
|       let date = new Date(timestamp); |       let date = new Date(timestamp); | ||||||
|       const timeString = `${this.pad(date.getHours())}:${this.pad( |       const timeString = `${this.pad(date.getHours())}:${this.pad(date.getMinutes())}:${this.pad(date.getSeconds())}`; | ||||||
|         date.getMinutes() |  | ||||||
|       )}:${this.pad(date.getSeconds())}`; |  | ||||||
|  |  | ||||||
|       if (date.getDate() == new Date().getDate()) { |       if (date.getDate() == new Date().getDate()) { | ||||||
|         return timeString; |         return timeString; | ||||||
| @@ -158,10 +154,10 @@ export default { | |||||||
|     sendMessage() { |     sendMessage() { | ||||||
|       const message = { message: this.message }; |       const message = { message: this.message }; | ||||||
|       this.socket.emit("chat", message); |       this.socket.emit("chat", message); | ||||||
|       this.message = ''; |       this.message = ""; | ||||||
|       this.scrollToBottomOfHistory(); |       this.scrollToBottomOfHistory(); | ||||||
|     }, |     }, | ||||||
|     setUsername(username=undefined) { |     setUsername(username = undefined) { | ||||||
|       if (this.temporaryUsername) { |       if (this.temporaryUsername) { | ||||||
|         username = this.temporaryUsername; |         username = this.temporaryUsername; | ||||||
|       } |       } | ||||||
| @@ -178,7 +174,7 @@ export default { | |||||||
|       if (history) { |       if (history) { | ||||||
|         return history.offsetHeight + history.scrollTop >= history.scrollHeight; |         return history.offsetHeight + history.scrollTop >= history.scrollHeight; | ||||||
|       } |       } | ||||||
|       return false |       return false; | ||||||
|     }, |     }, | ||||||
|     scrollToBottomOfHistory() { |     scrollToBottomOfHistory() { | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
| @@ -189,15 +185,15 @@ export default { | |||||||
|     scrollToMessageElement(message) { |     scrollToMessageElement(message) { | ||||||
|       const elemTimestamp = this.getTime(message.timestamp); |       const elemTimestamp = this.getTime(message.timestamp); | ||||||
|       const self = this; |       const self = this; | ||||||
|       const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText; |       const getTimeStamp = elem => elem.getElementsByClassName("timestamp")[0].innerText; | ||||||
|       const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp; |       const prevOldestMessageInNewList = elem => getTimeStamp(elem) == elemTimestamp; | ||||||
|  |  | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
|         const { history } = self.$refs; |         const { history } = self.$refs; | ||||||
|         const childrenElements = Array.from(history.getElementsByClassName('history-message')); |         const childrenElements = Array.from(history.getElementsByClassName("history-message")); | ||||||
|  |  | ||||||
|         const elemInNewList = childrenElements.find(prevOldestMessageInNewList); |         const elemInNewList = childrenElements.find(prevOldestMessageInNewList); | ||||||
|         history.scrollTop = elemInNewList.offsetTop - 70 |         history.scrollTop = elemInNewList.offsetTop - 70; | ||||||
|       }, 1); |       }, 1); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -210,7 +206,7 @@ export default { | |||||||
|  |  | ||||||
| .chat-container { | .chat-container { | ||||||
|   position: relative; |   position: relative; | ||||||
|   transform: translate3d(0,0,0); |   transform: translate3d(0, 0, 0); | ||||||
| } | } | ||||||
|  |  | ||||||
| input { | input { | ||||||
| @@ -241,7 +237,6 @@ input { | |||||||
|   display: flex; |   display: flex; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| .history { | .history { | ||||||
|   height: 75%; |   height: 75%; | ||||||
|   overflow-y: scroll; |   overflow-y: scroll; | ||||||
| @@ -276,11 +271,7 @@ input { | |||||||
|     position: fixed; |     position: fixed; | ||||||
|     height: 2rem; |     height: 2rem; | ||||||
|     z-index: 1; |     z-index: 1; | ||||||
|     background: linear-gradient( |     background: linear-gradient(to bottom, white, rgba(255, 255, 255, 0)); | ||||||
|       to bottom, |  | ||||||
|       white, |  | ||||||
|       rgba(255, 255, 255, 0) |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   & .fetch-older-history { |   & .fetch-older-history { | ||||||
| @@ -310,7 +301,7 @@ input { | |||||||
|     border-radius: 4px; |     border-radius: 4px; | ||||||
|  |  | ||||||
|     &::before { |     &::before { | ||||||
|       content: ''; |       content: ""; | ||||||
|       position: absolute; |       position: absolute; | ||||||
|       top: 2.1rem; |       top: 2.1rem; | ||||||
|       left: 2rem; |       left: 2rem; | ||||||
|   | |||||||
| @@ -1,60 +1,48 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="highscores" v-if="highscore.length > 0"> |   <div class="highscores" v-if="highscore.length > 0"> | ||||||
|  |  | ||||||
|     <section class="heading"> |     <section class="heading"> | ||||||
|       <h3> |       <h3> | ||||||
|         Topp 5 vinnere |         Topp vinnere | ||||||
|       </h3> |       </h3> | ||||||
|       <router-link to="highscore" class=""> |       <router-link to="highscore" class=""> | ||||||
|         <span class="vin-link">Se alle vinnere</span> |         <span class="vin-link">Se alle vinnere</span> | ||||||
|       </router-link> |       </router-link> | ||||||
|     </section> |     </section> | ||||||
|  |  | ||||||
|     <ol class="winner-list-container"> |     <ol class="winner-list-container"> | ||||||
|       <li v-for="(person, index) in highscore" :key="person._id" class="single-winner"> |       <li v-for="(person, index) in highscore" :key="person._id"> | ||||||
|         <span class="placement">{{index + 1}}.</span> |         <router-link :to="`/highscore/${person.name}`" class="single-winner"> | ||||||
|         <i class="icon icon--medal"></i> |           <span class="placement">{{ index + 1 }}.</span> | ||||||
|         <p class="winner-name">{{ person.name }}</p> |           <i class="icon icon--medal"></i> | ||||||
|  |           <p class="winner-name">{{ person.name }}</p> | ||||||
|  |         </router-link> | ||||||
|       </li> |       </li> | ||||||
|     </ol> |     </ol> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  |  | ||||||
| import { highscoreStatistics } from "@/api"; | import { highscoreStatistics } from "@/api"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   data() { |   data() { | ||||||
|     return { highscore: [] }; |     return { | ||||||
|  |       highscore: [], | ||||||
|  |       limit: 22 | ||||||
|  |     }; | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     let response = await highscoreStatistics(); |     return fetch(`/api/history/by-wins?limit=${this.limit}`) | ||||||
|     response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1) |       .then(resp => resp.json()) | ||||||
|     this.highscore = this.generateScoreBoard(response.slice(0, 5)); |       .then(response => { | ||||||
|   }, |         this.highscore = response.winners; | ||||||
|   methods: { |       }); | ||||||
|     generateScoreBoard(highscore=this.highscore) { |  | ||||||
|       let place = 0; |  | ||||||
|       let highestWinCount = -1; |  | ||||||
|  |  | ||||||
|       return highscore.map(win => { |  | ||||||
|         const wins = win.wins.length |  | ||||||
|         if (wins != highestWinCount) { |  | ||||||
|           place += 1 |  | ||||||
|           highestWinCount = wins |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const placeString = place.toString().padStart(2, "0"); |  | ||||||
|         win.rank = placeString; |  | ||||||
|         return win |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/variables.scss"; | @import "@/styles/variables.scss"; | ||||||
| .heading { | .heading { | ||||||
|   display: flex; |   display: flex; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
| @@ -81,8 +69,8 @@ ol { | |||||||
|  |  | ||||||
| .winner-list-container { | .winner-list-container { | ||||||
|   display: grid; |   display: grid; | ||||||
|   grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr)); |   grid-template-columns: repeat(auto-fit, minmax(12em, 1fr)); | ||||||
|   gap: 5%; |   gap: 2rem; | ||||||
|  |  | ||||||
|   .single-winner { |   .single-winner { | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
| @@ -91,7 +79,7 @@ ol { | |||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-columns: 1fr 1fr 1fr; |     grid-template-columns: 1fr 1fr 1fr; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     padding: 1em;  |     padding: 1em; | ||||||
|  |  | ||||||
|     i { |     i { | ||||||
|       font-size: 3em; |       font-size: 3em; | ||||||
| @@ -110,11 +98,71 @@ ol { | |||||||
|       grid-column: 1 / -1; |       grid-column: 1 / -1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     .winner-count { | ||||||
|  |       grid-row: 3; | ||||||
|  |       grid-column: 1 / -1; | ||||||
|  |       margin: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     .winner-icon { |     .winner-icon { | ||||||
|       grid-row: 1; |       grid-row: 1; | ||||||
|       grid-column: 3; |       grid-column: 3; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // I'm sorry mama | ||||||
|  |   @media (max-width: 550px) { | ||||||
|  |     *:nth-child(n + 7) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 1295px) { | ||||||
|  |     *:nth-child(n + 7) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 1630px) { | ||||||
|  |     *:nth-child(n + 9) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 1968px) { | ||||||
|  |     *:nth-child(n + 11) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 2300px) { | ||||||
|  |     *:nth-child(n + 13) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 2645px) { | ||||||
|  |     *:nth-child(n + 15) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 2975px) { | ||||||
|  |     *:nth-child(n + 17) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 3311px) { | ||||||
|  |     *:nth-child(n + 19) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 3647px) { | ||||||
|  |     *:nth-child(n + 21) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -2,89 +2,47 @@ | |||||||
|   <div class="chart"> |   <div class="chart"> | ||||||
|     <canvas ref="purchase-chart" width="100" height="50"></canvas> |     <canvas ref="purchase-chart" width="100" height="50"></canvas> | ||||||
|     <div ref="chartjsLegend" class="chartjsLegend"></div> |     <div ref="chartjsLegend" class="chartjsLegend"></div> | ||||||
|  |     <div class="year-select" v-if="years.length"> | ||||||
|  |       <button | ||||||
|  |         class="vin-button small" | ||||||
|  |         v-for="year in years" | ||||||
|  |         :class="{ active: yearSelected == year }" | ||||||
|  |         @click="yearFilterClicked(year)" | ||||||
|  |       > | ||||||
|  |         {{ year }} | ||||||
|  |       </button> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import Chartjs from "chart.js"; | import Chartjs from "chart.js"; | ||||||
| import { chartPurchaseByColor } from "@/api"; |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       lotteries: [], | ||||||
|  |       years: [], | ||||||
|  |       yearSelected: undefined, | ||||||
|  |       chart: undefined | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     let canvas = this.$refs["purchase-chart"].getContext("2d"); |     let canvas = this.$refs["purchase-chart"].getContext("2d"); | ||||||
|  |  | ||||||
|     let response = await chartPurchaseByColor(); |     this.lotteries = await this.chartPurchaseByColor(); | ||||||
|     let labels = []; |     if (this.lotteries?.length) this.years = [...new Set(this.lotteries.map(lot => lot.date.slice(0, 4)))]; | ||||||
|     let blue = { |  | ||||||
|       label: "Blå", |     const dataset = this.calculateChartDatapoints(); | ||||||
|       borderColor: "#57d2fb", |  | ||||||
|       backgroundColor: "#d4f2fe", |     let chartData = { | ||||||
|       borderWidth: 2, |       labels: dataset.labels, | ||||||
|       data: [] |       datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow] | ||||||
|     }; |  | ||||||
|     let yellow = { |  | ||||||
|       label: "Gul", |  | ||||||
|       borderColor: "#ffde5d", |  | ||||||
|       backgroundColor: "#fff6d6", |  | ||||||
|       borderWidth: 2, |  | ||||||
|       data: [] |  | ||||||
|     }; |  | ||||||
|     let red = { |  | ||||||
|       label: "Rød", |  | ||||||
|       borderColor: "#ef5878", |  | ||||||
|       backgroundColor: "#fbd7de", |  | ||||||
|       borderWidth: 2, |  | ||||||
|       data: [] |  | ||||||
|     }; |  | ||||||
|     let green = { |  | ||||||
|       label: "Grønn", |  | ||||||
|       borderColor: "#10e783", |  | ||||||
|       backgroundColor: "#c8f9df", |  | ||||||
|       borderWidth: 2, |  | ||||||
|       data: [] |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (response.length == 1) { |     this.chart = new Chart(canvas, { | ||||||
|       labels.push(""); |  | ||||||
|       blue.data.push(0); |  | ||||||
|       yellow.data.push(0); |  | ||||||
|       red.data.push(0); |  | ||||||
|       green.data.push(0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let highestNumber = 0; |  | ||||||
|  |  | ||||||
|     for (let i = 0; i < response.length; i++) { |  | ||||||
|       let thisDate = response[i]; |  | ||||||
|       let dateObject = new Date(thisDate.date); |  | ||||||
|       labels.push(this.getPrettierDateString(dateObject)); |  | ||||||
|  |  | ||||||
|       blue.data.push(thisDate.blue); |  | ||||||
|       yellow.data.push(thisDate.yellow); |  | ||||||
|       red.data.push(thisDate.red); |  | ||||||
|       green.data.push(thisDate.green); |  | ||||||
|  |  | ||||||
|       if (thisDate.blue > highestNumber) { |  | ||||||
|         highestNumber = thisDate.blue; |  | ||||||
|       } |  | ||||||
|       if (thisDate.yellow > highestNumber) { |  | ||||||
|         highestNumber = thisDate.yellow; |  | ||||||
|       } |  | ||||||
|       if (thisDate.green > highestNumber) { |  | ||||||
|         highestNumber = thisDate.green; |  | ||||||
|       } |  | ||||||
|       if (thisDate.red > highestNumber) { |  | ||||||
|         highestNumber = thisDate.red; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     let datasets = [blue, yellow, green, red]; |  | ||||||
|     let chartdata = { |  | ||||||
|       labels: labels, |  | ||||||
|       datasets: datasets |  | ||||||
|     }; |  | ||||||
|     let chart = new Chart(canvas, { |  | ||||||
|       type: "line", |       type: "line", | ||||||
|       data: chartdata, |       data: chartData, | ||||||
|       options: { |       options: { | ||||||
|         maintainAspectRatio: false, |         maintainAspectRatio: false, | ||||||
|         animation: { |         animation: { | ||||||
| @@ -110,8 +68,7 @@ export default { | |||||||
|           yAxes: [ |           yAxes: [ | ||||||
|             { |             { | ||||||
|               ticks: { |               ticks: { | ||||||
|                 beginAtZero: true, |                 beginAtZero: true | ||||||
|                 suggestedMax: highestNumber + 5 |  | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           ] |           ] | ||||||
| @@ -120,10 +77,82 @@ export default { | |||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     async yearFilterClicked(year) { | ||||||
|  |       this.yearSelected = this.yearSelected === year ? null : year; | ||||||
|  |  | ||||||
|  |       this.lotteries = await this.chartPurchaseByColor(); | ||||||
|  |       const dataset = this.calculateChartDatapoints(); | ||||||
|  |       let chartData = { | ||||||
|  |         labels: dataset.labels, | ||||||
|  |         datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow] | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       this.chart.data = chartData; | ||||||
|  |       this.chart.update(); | ||||||
|  |     }, | ||||||
|  |     setupDataset() { | ||||||
|  |       let blue = { | ||||||
|  |         label: "Blå", | ||||||
|  |         borderColor: "#57d2fb", | ||||||
|  |         backgroundColor: "#d4f2fe", | ||||||
|  |         borderWidth: 2, | ||||||
|  |         data: [] | ||||||
|  |       }; | ||||||
|  |       let yellow = { | ||||||
|  |         label: "Gul", | ||||||
|  |         borderColor: "#ffde5d", | ||||||
|  |         backgroundColor: "#fff6d6", | ||||||
|  |         borderWidth: 2, | ||||||
|  |         data: [] | ||||||
|  |       }; | ||||||
|  |       let red = { | ||||||
|  |         label: "Rød", | ||||||
|  |         borderColor: "#ef5878", | ||||||
|  |         backgroundColor: "#fbd7de", | ||||||
|  |         borderWidth: 2, | ||||||
|  |         data: [] | ||||||
|  |       }; | ||||||
|  |       let green = { | ||||||
|  |         label: "Grønn", | ||||||
|  |         borderColor: "#10e783", | ||||||
|  |         backgroundColor: "#c8f9df", | ||||||
|  |         borderWidth: 2, | ||||||
|  |         data: [] | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         labels: [""], | ||||||
|  |         blue, | ||||||
|  |         green, | ||||||
|  |         red, | ||||||
|  |         yellow | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |     calculateChartDatapoints() { | ||||||
|  |       let dataset = this.setupDataset(); | ||||||
|  |  | ||||||
|  |       this.lotteries.map(lottery => { | ||||||
|  |         const date = new Date(lottery.date); | ||||||
|  |         dataset.labels.push(this.getPrettierDateString(date)); | ||||||
|  |  | ||||||
|  |         dataset.blue.data.push(lottery.blue); | ||||||
|  |         dataset.green.data.push(lottery.green); | ||||||
|  |         dataset.red.data.push(lottery.red); | ||||||
|  |         dataset.yellow.data.push(lottery.yellow); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       return dataset; | ||||||
|  |     }, | ||||||
|  |     chartPurchaseByColor() { | ||||||
|  |       const url = new URL("/api/lotteries", window.location); | ||||||
|  |       if (this.yearSelected != null) url.searchParams.set("year", this.yearSelected); | ||||||
|  |  | ||||||
|  |       return fetch(url.href) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response.lotteries); | ||||||
|  |     }, | ||||||
|     getPrettierDateString(date) { |     getPrettierDateString(date) { | ||||||
|       return `${this.pad(date.getDate())}.${this.pad( |       return `${this.pad(date.getDate())}.${this.pad(date.getMonth() + 1)}.${this.pad(date.getYear() - 100)}`; | ||||||
|         date.getMonth() + 1 |  | ||||||
|       )}.${this.pad(date.getYear() - 100)}`; |  | ||||||
|     }, |     }, | ||||||
|     pad(num) { |     pad(num) { | ||||||
|       if (num < 10) { |       if (num < 10) { | ||||||
| @@ -136,11 +165,19 @@ export default { | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/media-queries.scss"; | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
| .chart { | .chart { | ||||||
|   height: 40vh; |   height: 40vh; | ||||||
|   max-height: 500px; |   max-height: 500px; | ||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .year-select { | ||||||
|  |   margin-top: 1rem; | ||||||
|  |  | ||||||
|  |   button:not(:first-of-type) { | ||||||
|  |     margin-left: 0.5rem; | ||||||
|  |   } | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -2,28 +2,28 @@ | |||||||
|   <div class="container"> |   <div class="container"> | ||||||
|     <div class="input-line"> |     <div class="input-line"> | ||||||
|       <label for="redCheckbox"> |       <label for="redCheckbox"> | ||||||
|         <input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/> |         <input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors" /> | ||||||
|         <span class="border"> |         <span class="border"> | ||||||
|           <span class="checkmark"></span> |           <span class="checkmark"></span> | ||||||
|         </span> |         </span> | ||||||
|         <span class="text">Rød</span> |         <span class="text">Rød</span> | ||||||
|       </label> |       </label> | ||||||
|       <label for="blueCheckbox"> |       <label for="blueCheckbox"> | ||||||
|         <input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors"/> |         <input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors" /> | ||||||
|         <span class="border"> |         <span class="border"> | ||||||
|           <span class="checkmark"></span> |           <span class="checkmark"></span> | ||||||
|         </span> |         </span> | ||||||
|         <span class="text">Blå</span> |         <span class="text">Blå</span> | ||||||
|       </label> |       </label> | ||||||
|       <label for="yellowCheckbox"> |       <label for="yellowCheckbox"> | ||||||
|         <input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors"/> |         <input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors" /> | ||||||
|         <span class="border"> |         <span class="border"> | ||||||
|           <span class="checkmark"></span> |           <span class="checkmark"></span> | ||||||
|         </span> |         </span> | ||||||
|         <span class="text">Gul</span> |         <span class="text">Gul</span> | ||||||
|       </label> |       </label> | ||||||
|       <label for="greenCheckbox"> |       <label for="greenCheckbox"> | ||||||
|         <input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors"/> |         <input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors" /> | ||||||
|         <span class="border"> |         <span class="border"> | ||||||
|           <span class="checkmark"></span> |           <span class="checkmark"></span> | ||||||
|         </span> |         </span> | ||||||
| @@ -31,15 +31,10 @@ | |||||||
|       </label> |       </label> | ||||||
|     </div> |     </div> | ||||||
|     <div class="input-line"> |     <div class="input-line"> | ||||||
|       <input |       <input type="number" placeholder="Antall lodd" @keyup.enter="generateColors" v-model="numberOfRaffles" /> | ||||||
|         type="number" |  | ||||||
|         placeholder="Antall lodd" |  | ||||||
|         @keyup.enter="generateColors" |  | ||||||
|         v-model="numberOfRaffles" |  | ||||||
|       /> |  | ||||||
|       <button class="vin-button" @click="generateColors">Generer</button> |       <button class="vin-button" @click="generateColors">Generer</button> | ||||||
|     </div> |     </div> | ||||||
|     <div class="colors"> |     <div class="colors" :class="{ compact }"> | ||||||
|       <div |       <div | ||||||
|         v-for="color in colors" |         v-for="color in colors" | ||||||
|         :class="getColorClass(color)" |         :class="getColorClass(color)" | ||||||
| @@ -47,13 +42,6 @@ | |||||||
|         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" |         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" | ||||||
|       ></div> |       ></div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="color-count-container" v-if="generated"> |  | ||||||
|       <span>Rød: {{ red }}</span> |  | ||||||
|       <span>Blå: {{ blue }}</span> |  | ||||||
|       <span>Gul: {{ yellow }}</span> |  | ||||||
|       <span>Grønn: {{ green }}</span> |  | ||||||
|     </div> |  | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -64,11 +52,15 @@ export default { | |||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       required: false, |       required: false, | ||||||
|       default: false |       default: false | ||||||
|  |     }, | ||||||
|  |     compact: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       numberOfRaffles: 4, |       numberOfRaffles: 6, | ||||||
|       colors: [], |       colors: [], | ||||||
|       blue: 0, |       blue: 0, | ||||||
|       red: 0, |       red: 0, | ||||||
| @@ -101,18 +93,21 @@ export default { | |||||||
|       if (time == 5) { |       if (time == 5) { | ||||||
|         this.generating = false; |         this.generating = false; | ||||||
|         this.generated = true; |         this.generated = true; | ||||||
|         if (this.numberOfRaffles > 1 && |         if ( | ||||||
|           [this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) { |           this.numberOfRaffles > 1 && | ||||||
|           return |           [this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true) | ||||||
|  |             .length == 1 | ||||||
|  |         ) { | ||||||
|  |           return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (new Set(this.colors).size == 1) { |         if (new Set(this.colors).size == 1) { | ||||||
|           alert("BINGO"); |           alert("BINGO"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.emitColors() |         this.emitColors(); | ||||||
|  |  | ||||||
|         window.ga('send', { |         window.ga("send", { | ||||||
|           hitType: "event", |           hitType: "event", | ||||||
|           eventCategory: "Raffles", |           eventCategory: "Raffles", | ||||||
|           eventAction: "Generate", |           eventAction: "Generate", | ||||||
| @@ -147,8 +142,7 @@ export default { | |||||||
|       } |       } | ||||||
|       if (this.numberOfRaffles > 0) { |       if (this.numberOfRaffles > 0) { | ||||||
|         for (let i = 0; i < this.numberOfRaffles; i++) { |         for (let i = 0; i < this.numberOfRaffles; i++) { | ||||||
|           let color = |           let color = randomArray[Math.floor(Math.random() * randomArray.length)]; | ||||||
|             randomArray[Math.floor(Math.random() * randomArray.length)]; |  | ||||||
|           this.colors.push(color); |           this.colors.push(color); | ||||||
|           if (color == 1) { |           if (color == 1) { | ||||||
|             this.red += 1; |             this.red += 1; | ||||||
| @@ -201,12 +195,12 @@ export default { | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/variables.scss"; | @import "@/styles/variables.scss"; | ||||||
| @import "../styles/global.scss"; | @import "@/styles/global.scss"; | ||||||
| @import "../styles/media-queries.scss"; | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
| .container { | .container { | ||||||
|   margin: auto; |   // margin: auto; | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| } | } | ||||||
| @@ -282,6 +276,15 @@ label .text { | |||||||
|   max-width: 1400px; |   max-width: 1400px; | ||||||
|   margin: 3rem auto 0; |   margin: 3rem auto 0; | ||||||
|  |  | ||||||
|  |   &.compact { | ||||||
|  |     margin-top: 0.5rem; | ||||||
|  |  | ||||||
|  |     > .color-box { | ||||||
|  |       width: 100px; | ||||||
|  |       height: 100px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @include mobile { |   @include mobile { | ||||||
|     margin: 1.8rem auto 0; |     margin: 1.8rem auto 0; | ||||||
|   } |   } | ||||||
| @@ -309,20 +312,6 @@ label .text { | |||||||
|   justify-content: space-around; |   justify-content: space-around; | ||||||
| } | } | ||||||
|  |  | ||||||
| .color-count-container { |  | ||||||
|   margin: auto; |  | ||||||
|   width: 300px; |  | ||||||
|   justify-content: space-around; |  | ||||||
|   align-items: center; |  | ||||||
|   display: flex; |  | ||||||
|   font-family: Arial; |  | ||||||
|   margin-top: 35px; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     width: 80vw; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .green { | .green { | ||||||
|   background-color: $light-green; |   background-color: $light-green; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|       <div class="flex justify-end"> |       <div class="flex justify-end"> | ||||||
|         <div class="requested-count cursor-pointer" @click="request"> |         <div class="requested-count cursor-pointer" @click="request"> | ||||||
|           <span>{{ requestedElement.count }}</span> |           <span>{{ requestedElement.count }}</span> | ||||||
|           <i class="icon icon--heart" :class="{ 'active': locallyRequested }" /> |           <i class="icon icon--heart" :class="{ active: locallyRequested }" /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
| @@ -17,10 +17,9 @@ | |||||||
|  |  | ||||||
|     <template v-slot:bottom> |     <template v-slot:bottom> | ||||||
|       <div class="float-left request"> |       <div class="float-left request"> | ||||||
|         <i class="icon icon--heart request-icon" :class="{ 'active': locallyRequested }"></i> |         <i class="icon icon--heart request-icon" :class="{ active: locallyRequested }"></i> | ||||||
|         <a aria-role="button" tabindex="0" class="link" @click="request" |         <a aria-role="button" tabindex="0" class="link" @click="request" :class="{ active: locallyRequested }"> | ||||||
|            :class="{ 'active': locallyRequested }"> |           {{ locallyRequested ? "Anbefalt" : "Anbefal" }} | ||||||
|           {{ locallyRequested ? 'Anbefalt' : 'Anbefal' }} |  | ||||||
|         </a> |         </a> | ||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
| @@ -35,14 +34,14 @@ export default { | |||||||
|   components: { |   components: { | ||||||
|     Wine |     Wine | ||||||
|   }, |   }, | ||||||
|   data(){ |   data() { | ||||||
|     return { |     return { | ||||||
|       wine: this.requestedElement.wine, |       wine: this.requestedElement.wine, | ||||||
|       locallyRequested: false |       locallyRequested: false | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|     requestedElement: { |     requestedElement: { | ||||||
|       required: true, |       required: true, | ||||||
|       type: Object |       type: Object | ||||||
|     }, |     }, | ||||||
| @@ -53,27 +52,26 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     request(){ |     request() { | ||||||
|       if (this.locallyRequested) |       if (this.locallyRequested) return; | ||||||
|         return |  | ||||||
|       console.log("requesting", this.wine) |       this.locallyRequested = true; | ||||||
|       this.locallyRequested = true |       this.requestedElement.count = this.requestedElement.count + 1; | ||||||
|       this.requestedElement.count = this.requestedElement.count +1 |       requestNewWine(this.wine); | ||||||
|       requestNewWine(this.wine) |  | ||||||
|     }, |     }, | ||||||
|     async deleteWine() { |     async deleteWine() { | ||||||
|       const wine = this.wine |       const wine = this.wine; | ||||||
|       if (window.confirm("Er du sikker på at du vil slette vinen?")) { |       if (window.confirm("Er du sikker på at du vil slette vinen?")) { | ||||||
|         let response = await deleteRequestedWine(wine); |         let response = await deleteRequestedWine(wine); | ||||||
|         if (response['success'] == true) { |         if (response["success"] == true) { | ||||||
|           this.$emit('wineDeleted', wine); |           this.$emit("wineDeleted", wine); | ||||||
|         } else { |         } else { | ||||||
|           alert("Klarte ikke slette vinen"); |           alert("Klarte ikke slette vinen"); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     } | ||||||
|   }, |   } | ||||||
| } | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @@ -83,7 +81,7 @@ export default { | |||||||
|   display: flex; |   display: flex; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   margin-top: -0.5rem; |   margin-top: -0.5rem; | ||||||
|   background-color: rgb(244,244,244); |   background-color: rgb(244, 244, 244); | ||||||
|   border-radius: 1.1rem; |   border-radius: 1.1rem; | ||||||
|   padding: 0.25rem 1rem; |   padding: 0.25rem 1rem; | ||||||
|   font-size: 1.25em; |   font-size: 1.25em; | ||||||
| @@ -93,14 +91,14 @@ export default { | |||||||
|     line-height: 1.25em; |     line-height: 1.25em; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .icon--heart{ |   .icon--heart { | ||||||
|     color: grey; |     color: grey; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .active { | .active { | ||||||
|   &.link { |   &.link { | ||||||
|     border-color: $link-color |     border-color: $link-color; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.icon--heart { |   &.icon--heart { | ||||||
| @@ -121,4 +119,4 @@ export default { | |||||||
|     margin-left: 0.5rem; |     margin-left: 0.5rem; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div id="camera-stream"> | ||||||
|     <h2 v-if="errorMessage">{{ errorMessage }}</h2> |     <h2 v-if="errorMessage">{{ errorMessage }}</h2> | ||||||
|     <video playsinline autoplay class="hidden"></video> |     <video playsinline autoplay class="hidden"></video> | ||||||
|   </div> |   </div> | ||||||
| @@ -47,13 +47,8 @@ export default { | |||||||
|       this.searchVideoForBarcode(this.video); |       this.searchVideoForBarcode(this.video); | ||||||
|     }, |     }, | ||||||
|     handleError(error) { |     handleError(error) { | ||||||
|       console.log( |       console.log("navigator.MediaDevices.getUserMedia error: ", error.message, error.name); | ||||||
|         "navigator.MediaDevices.getUserMedia error: ", |       this.errorMessage = "Feil ved oppstart av kamera! Feilmelding: " + error.message; | ||||||
|         error.message, |  | ||||||
|         error.name |  | ||||||
|       ); |  | ||||||
|       this.errorMessage = |  | ||||||
|         "Feil ved oppstart av kamera! Feilmelding: " + error.message; |  | ||||||
|     }, |     }, | ||||||
|     searchVideoForBarcode(video) { |     searchVideoForBarcode(video) { | ||||||
|       const codeReader = new BrowserBarcodeReader(); |       const codeReader = new BrowserBarcodeReader(); | ||||||
| @@ -84,10 +79,7 @@ export default { | |||||||
|       this.errorMessage = "Feil! " + error.message || error; |       this.errorMessage = "Feil! " + error.message || error; | ||||||
|     }, |     }, | ||||||
|     scrollIntoView() { |     scrollIntoView() { | ||||||
|       window.scrollTo( |       window.scrollTo(0, document.getElementById("camera-stream").offsetTop - 10); | ||||||
|         0, |  | ||||||
|         document.getElementById("addwine-title").offsetTop - 10 |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| @@ -112,4 +104,4 @@ h2 { | |||||||
|   text-align: center; |   text-align: center; | ||||||
|   color: $red; |   color: $red; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,51 +1,80 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <div class="tab-container"> |     <nav class="tab-container"> | ||||||
|       <div |       <a | ||||||
|         class="tab" |         class="tab" | ||||||
|         v-for="(tab, index) in tabs" |         v-for="(tab, index) in tabs" | ||||||
|         :key="index" |         :key="index" | ||||||
|         @click="changeTab(index)" |         @click="changeTab(index)" | ||||||
|  |         @keydown.enter="changeTab(index)" | ||||||
|  |         tabindex="0" | ||||||
|         :class="chosenTab == index ? 'active' : null" |         :class="chosenTab == index ? 'active' : null" | ||||||
|       >{{ tab.name }}</div> |       > | ||||||
|     </div> |         {{ tab.name }} | ||||||
|  |  | ||||||
|  |         <span v-if="tab.counter" class="counter">{{ tab.counter }}</span> | ||||||
|  |       </a> | ||||||
|  |     </nav> | ||||||
|  |  | ||||||
|     <div class="tab-elements"> |     <div class="tab-elements"> | ||||||
|       <component :is="tabs[chosenTab].component" /> |       <component :is="tabs[chosenTab].component" @counter="updateCounter" /> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import eventBus from "@/mixins/EventBus"; |  | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|     tabs: { |     tabs: { | ||||||
|       type: Array |       type: Array | ||||||
|     }, |  | ||||||
|     active: { |  | ||||||
|       type: Number, |  | ||||||
|       default: 0 |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   beforeMount() { |   beforeMount() { | ||||||
|     this.chosenTab = this.active; |     const url = location.href; | ||||||
|  |  | ||||||
|  |     if (url.includes("tab=")) { | ||||||
|  |       const tabParameter = url.split("tab=")[1]; | ||||||
|  |       const matchingSlug = this.tabs.findIndex(tab => tab.slug == tabParameter); | ||||||
|  |       console.log("matchingSlug:", matchingSlug); | ||||||
|  |       this.chosenTab = matchingSlug; | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       chosenTab: 0 |       chosenTab: 0 | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |   computed: { | ||||||
|  |     activeTab() { | ||||||
|  |       return this.tabs[this.chosenTab]; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     changeTab: function(num) { |     changeTab(num) { | ||||||
|       this.chosenTab = num; |       this.chosenTab = num; | ||||||
|       this.$emit("tabChange", num); |  | ||||||
|       eventBus.$emit("tab-change"); |       let url = location.href; | ||||||
|  |       const tabParameterIndex = url.indexOf("tab="); | ||||||
|  |  | ||||||
|  |       if (tabParameterIndex > 0) { | ||||||
|  |         url = url.split("tab=")[0] + `tab=${this.activeTab.slug}`; | ||||||
|  |       } else { | ||||||
|  |         url = url + `?tab=${this.activeTab.slug}`; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       window.history.pushState({}, "", url); | ||||||
|  |     }, | ||||||
|  |     updateCounter(val) { | ||||||
|  |       this.activeTab.counter = val; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  | @import "@/styles/variables.scss"; | ||||||
|  | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
| h1 { | h1 { | ||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
| @@ -54,28 +83,50 @@ h1 { | |||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: row; |   flex-direction: row; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   margin-top: 25px; |   // margin-top: 25px; | ||||||
|   border-bottom: 1px solid #333333; |   border-bottom: 1px solid var(--underlinenav-text); | ||||||
|  |  | ||||||
|  |   margin-top: 2rem; | ||||||
|  |  | ||||||
|  |   @include mobile { | ||||||
|  |     flex-direction: column; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .tab { | .tab { | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   font-size: 1.2rem; |   font-size: 1.1rem; | ||||||
|   display: flex; |   padding: 8px 16px; | ||||||
|   justify-content: center; |   border-bottom: 2px solid transparent; | ||||||
|   align-items: center; |   color: rgba($matte-text-color, 0.9); | ||||||
|   padding: 20px; |  | ||||||
|   margin: 0 15px; |  | ||||||
|   border: 1px solid #333333; |  | ||||||
|   border-top-left-radius: 5px; |  | ||||||
|   border-top-right-radius: 5px; |  | ||||||
|   background: #00000008; |  | ||||||
|   border-bottom: 1px solid #333333; |  | ||||||
|   margin-bottom: -1px; |  | ||||||
|  |  | ||||||
|   &.active { |   &.active { | ||||||
|     border-bottom: 1px solid white; |     color: $matte-text-color; | ||||||
|  |     border-color: var(--underlinenav-text-active) !important; | ||||||
|     background: white; |     background: white; | ||||||
|  |     font-weight: 600; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   &:hover, | ||||||
|  |   &:focus { | ||||||
|  |     border-color: var(--underlinenav-text-hover); | ||||||
|  |     outline: 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   & .counter { | ||||||
|  |     margin-left: 4px; | ||||||
|  |     box-sizing: border-box; | ||||||
|  |  | ||||||
|  |     display: inline-block; | ||||||
|  |     min-width: 20px; | ||||||
|  |     padding: 0 6px; | ||||||
|  |     font-size: 14px; | ||||||
|  |     font-weight: 600; | ||||||
|  |     line-height: 18px; | ||||||
|  |     text-align: center; | ||||||
|  |     background-color: rgba(209, 213, 218, 0.5); | ||||||
|  |     border: 1px solid transparent; | ||||||
|  |     border-radius: 2em; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,99 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="update-toast" :class="showClass"> |  | ||||||
|     <span v-html="text"></span> |  | ||||||
|     <div class="button-container"> |  | ||||||
|       <button @click="closeToast">Lukk</button> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     text: { type: String, required: true }, |  | ||||||
|     refreshButton: { type: Boolean, required: false } |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { showClass: null }; |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     this.showClass = "show"; |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     if (this.refreshButton) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     setTimeout(() => { |  | ||||||
|       this.$emit("closeToast"); |  | ||||||
|     }, 5000); |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     refresh: function() { |  | ||||||
|       location.reload(); |  | ||||||
|     }, |  | ||||||
|     closeToast: function() { |  | ||||||
|       this.$emit("closeToast"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "../styles/media-queries.scss"; |  | ||||||
|  |  | ||||||
| .update-toast { |  | ||||||
|   position: fixed; |  | ||||||
|   bottom: 1.3rem; |  | ||||||
|   left: 0; |  | ||||||
|   right: 0; |  | ||||||
|   margin: auto; |  | ||||||
|   background: #2d2d2d; |  | ||||||
|   border-radius: 5px; |  | ||||||
|   padding: 15px; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   align-items: center; |  | ||||||
|   width: 80vw; |  | ||||||
|   opacity: 0; |  | ||||||
|   pointer-events: none; |  | ||||||
|  |  | ||||||
|   &.show { |  | ||||||
|     pointer-events: all; |  | ||||||
|     opacity: 1; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   -webkit-transition: opacity 0.5s ease-in-out; |  | ||||||
|   -moz-transition: opacity 0.5s ease-in-out; |  | ||||||
|   -ms-transition: opacity 0.5s ease-in-out; |  | ||||||
|   -o-transition: opacity 0.5s ease-in-out; |  | ||||||
|   transition: opacity 0.5s ease-in-out; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     width: 85vw; |  | ||||||
|     border-bottom-left-radius: 0px; |  | ||||||
|     border-bottom-right-radius: 0px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   & span { |  | ||||||
|     color: white; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   & .button-container { |  | ||||||
|     & button { |  | ||||||
|       color: #2d2d2d; |  | ||||||
|       background-color: white; |  | ||||||
|       border-radius: 5px; |  | ||||||
|       padding: 10px; |  | ||||||
|       margin: 0 3px; |  | ||||||
|       font-size: 0.8rem; |  | ||||||
|       height: max-content; |  | ||||||
|  |  | ||||||
|       &:active { |  | ||||||
|         background: #2d2d2d; |  | ||||||
|         color: white; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,37 +1,30 @@ | |||||||
| <template> | <template> | ||||||
|   <section class="outer-bought"> |   <div> | ||||||
|     <h3>Loddstatistikk</h3> |     <h3>Loddstatistikk</h3> | ||||||
|  |  | ||||||
|     <div class="total-raffles"> |     <div class="total-raffles"> | ||||||
|         Totalt  |       Totalt  | ||||||
|         <span class="total">{{ total }}</span> |       <span class="total">{{ total }}</span> | ||||||
|          kjøpte,  |        kjøpte,  | ||||||
|         <span>{{ totalWin }} vinn og </span> |       <span>{{ totalWin }} vinn og </span> | ||||||
|         <span> {{ stolen }} stjålet </span> |       <span> {{ stolen }} stjålet </span> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  |  | ||||||
|     <div class="bought-container"> |     <div class="bought-container"> | ||||||
|       <div |       <div | ||||||
|         v-for="color in colors" |         v-for="color in colors" | ||||||
|         :class=" |         :class="color.name + '-container ' + color.name + '-raffle raffle-element-local'" | ||||||
|           color.name + |  | ||||||
|             '-container ' + |  | ||||||
|             color.name + |  | ||||||
|             '-raffle raffle-element-local' |  | ||||||
|         " |  | ||||||
|         :key="color.name" |         :key="color.name" | ||||||
|         > |       > | ||||||
|         <p class="winner-chance"> |         <p class="winner-chance">{{ translate(color.name) }} vinnersjanse</p> | ||||||
|           {{translate(color.name)}} vinnersjanse |  | ||||||
|         </p> |  | ||||||
|         <span class="win-percentage">{{ color.totalPercentage }}% </span> |         <span class="win-percentage">{{ color.totalPercentage }}% </span> | ||||||
|         <p class="total-bought-color">{{ color.total }} kjøpte</p> |         <p class="total-bought-color">{{ color.total }} kjøpte</p> | ||||||
|         <p class="amount-of-wins"> {{ color.win }} vinn </p> |         <p class="amount-of-wins">{{ color.win }} vinn</p> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </section> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { colorStatistics } from "@/api"; | import { colorStatistics } from "@/api"; | ||||||
|  |  | ||||||
| @@ -45,109 +38,128 @@ export default { | |||||||
|       green: 0, |       green: 0, | ||||||
|       total: 0, |       total: 0, | ||||||
|       totalWin: 0, |       totalWin: 0, | ||||||
|       stolen: 0, |       stolen: 0 | ||||||
|       wins: 0, |  | ||||||
|       redPercentage: 0, |  | ||||||
|       yellowPercentage: 0, |  | ||||||
|       greenPercentage: 0, |  | ||||||
|       bluePercentage: 0 |  | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     let response = await colorStatistics(); |     this.allLotteries().then(this.computeColors); | ||||||
|  |  | ||||||
|     this.red = response.red; |  | ||||||
|     this.blue = response.blue; |  | ||||||
|     this.green = response.green; |  | ||||||
|     this.yellow = response.yellow; |  | ||||||
|     this.total = response.total; |  | ||||||
|  |  | ||||||
|     this.totalWin = |  | ||||||
|       this.red.win + this.yellow.win + this.blue.win + this.green.win; |  | ||||||
|     this.stolen = response.stolen; |  | ||||||
|  |  | ||||||
|     this.redPercentage = this.round( |  | ||||||
|       this.red.win == 0 ? 0 : (this.red.win / this.totalWin) * 100 |  | ||||||
|     ); |  | ||||||
|     this.greenPercentage = this.round( |  | ||||||
|       this.green.win == 0 ? 0 : (this.green.win / this.totalWin) * 100 |  | ||||||
|     ); |  | ||||||
|     this.bluePercentage = this.round( |  | ||||||
|       this.blue.win == 0 ? 0 : (this.blue.win / this.totalWin) * 100 |  | ||||||
|     ); |  | ||||||
|     this.yellowPercentage = this.round( |  | ||||||
|       this.yellow.win == 0 ? 0 : (this.yellow.win / this.totalWin) * 100 |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     this.colors.push({ |  | ||||||
|       name: "red", |  | ||||||
|       total: this.red.total, |  | ||||||
|       win: this.red.win, |  | ||||||
|       totalPercentage: this.getPercentage(this.red.win, this.totalWin), |  | ||||||
|       percentage: this.getPercentage(this.red.win, this.red.total) |  | ||||||
|     }); |  | ||||||
|     this.colors.push({ |  | ||||||
|       name: "blue", |  | ||||||
|       total: this.blue.total, |  | ||||||
|       win: this.blue.win, |  | ||||||
|       totalPercentage: this.getPercentage(this.blue.win, this.totalWin), |  | ||||||
|       percentage: this.getPercentage(this.blue.win, this.blue.total) |  | ||||||
|     }); |  | ||||||
|     this.colors.push({ |  | ||||||
|       name: "yellow", |  | ||||||
|       total: this.yellow.total, |  | ||||||
|       win: this.yellow.win, |  | ||||||
|       totalPercentage: this.getPercentage(this.yellow.win, this.totalWin), |  | ||||||
|       percentage: this.getPercentage(this.yellow.win, this.yellow.total) |  | ||||||
|     }); |  | ||||||
|     this.colors.push({ |  | ||||||
|       name: "green", |  | ||||||
|       total: this.green.total, |  | ||||||
|       win: this.green.win, |  | ||||||
|       totalPercentage: this.getPercentage(this.green.win, this.totalWin), |  | ||||||
|       percentage: this.getPercentage(this.green.win, this.green.total) |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1)); |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     translate(color){ |     allLotteries() { | ||||||
|       switch(color) { |       return fetch("/api/lotteries?includeWinners=true") | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => response.lotteries); | ||||||
|  |     }, | ||||||
|  |     translate(color) { | ||||||
|  |       switch (color) { | ||||||
|         case "blue": |         case "blue": | ||||||
|           return "Blå" |           return "Blå"; | ||||||
|           break; |           break; | ||||||
|         case "red": |         case "red": | ||||||
|           return "Rød" |           return "Rød"; | ||||||
|           break; |           break; | ||||||
|         case "green": |         case "green": | ||||||
|           return "Grønn" |           return "Grønn"; | ||||||
|           break; |           break; | ||||||
|         case "yellow": |         case "yellow": | ||||||
|           return "Gul" |           return "Gul"; | ||||||
|  |           break; | ||||||
|           break; |           break; | ||||||
|         break; |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     getPercentage: function(win, total) { |     getPercentage: function(win, total) { | ||||||
|       return this.round(win == 0 ? 0 : (win / total) * 100); |       return this.round(win == 0 ? 0 : (win / total) * 100); | ||||||
|     }, |     }, | ||||||
|     round: function(number) { |     round: function(number) { | ||||||
|  |  | ||||||
|       //this can make the odds added together more than 100%, maybe rework? |       //this can make the odds added together more than 100%, maybe rework? | ||||||
|       let actualPercentage = Math.round(number * 100) / 100; |       let actualPercentage = Math.round(number * 100) / 100; | ||||||
|       let rounded = actualPercentage.toFixed(0); |       let rounded = actualPercentage.toFixed(0); | ||||||
|       return rounded; |       return rounded; | ||||||
|  |     }, | ||||||
|  |     computeColors(lotteries) { | ||||||
|  |       let totalRed = 0; | ||||||
|  |       let totalGreen = 0; | ||||||
|  |       let totalYellow = 0; | ||||||
|  |       let totalBlue = 0; | ||||||
|  |       let total = 0; | ||||||
|  |       let stolen = 0; | ||||||
|  |  | ||||||
|  |       const colorAccumulatedWins = { | ||||||
|  |         blue: 0, | ||||||
|  |         green: 0, | ||||||
|  |         red: 0, | ||||||
|  |         yellow: 0 | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const accumelatedColors = (winners, colorAccumulatedWins) => { | ||||||
|  |         winners.forEach(winner => { | ||||||
|  |           const winnerColor = winner.color; | ||||||
|  |           colorAccumulatedWins[winnerColor] += 1; | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       lotteries.forEach(lottery => { | ||||||
|  |         totalRed += lottery.red; | ||||||
|  |         totalGreen += lottery.green; | ||||||
|  |         totalYellow += lottery.yellow; | ||||||
|  |         totalBlue += lottery.blue; | ||||||
|  |         total += lottery.bought; | ||||||
|  |         stolen += lottery.stolen; | ||||||
|  |  | ||||||
|  |         accumelatedColors(lottery.winners, colorAccumulatedWins); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       this.red = totalRed; | ||||||
|  |       this.yellow = totalYellow; | ||||||
|  |       this.green = totalGreen; | ||||||
|  |       this.blue = totalBlue; | ||||||
|  |       this.total = total; | ||||||
|  |  | ||||||
|  |       this.totalWin = | ||||||
|  |         colorAccumulatedWins.red + colorAccumulatedWins.yellow + colorAccumulatedWins.blue + colorAccumulatedWins.green; | ||||||
|  |       this.stolen = stolen; | ||||||
|  |  | ||||||
|  |       this.colors.push({ | ||||||
|  |         name: "red", | ||||||
|  |         total: totalRed, | ||||||
|  |         win: colorAccumulatedWins.red, | ||||||
|  |         totalPercentage: this.getPercentage(colorAccumulatedWins.red, this.totalWin), | ||||||
|  |         percentage: this.getPercentage(colorAccumulatedWins.red, this.red.total) | ||||||
|  |       }); | ||||||
|  |       this.colors.push({ | ||||||
|  |         name: "blue", | ||||||
|  |         total: totalBlue, | ||||||
|  |         win: colorAccumulatedWins.blue, | ||||||
|  |         totalPercentage: this.getPercentage(colorAccumulatedWins.blue, this.totalWin), | ||||||
|  |         percentage: this.getPercentage(colorAccumulatedWins.blue, this.blue.total) | ||||||
|  |       }); | ||||||
|  |       this.colors.push({ | ||||||
|  |         name: "yellow", | ||||||
|  |         total: totalYellow, | ||||||
|  |         win: colorAccumulatedWins.yellow, | ||||||
|  |         totalPercentage: this.getPercentage(colorAccumulatedWins.yellow, this.totalWin), | ||||||
|  |         percentage: this.getPercentage(colorAccumulatedWins.yellow, this.yellow.total) | ||||||
|  |       }); | ||||||
|  |       this.colors.push({ | ||||||
|  |         name: "green", | ||||||
|  |         total: totalGreen, | ||||||
|  |         win: colorAccumulatedWins.green, | ||||||
|  |         totalPercentage: this.getPercentage(colorAccumulatedWins.green, this.totalWin), | ||||||
|  |         percentage: this.getPercentage(colorAccumulatedWins.green, this.green.total) | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/variables.scss"; | @import "@/styles/variables.scss"; | ||||||
| @import "../styles/media-queries.scss"; | @import "@/styles/media-queries.scss"; | ||||||
| @import "../styles/global.scss"; | @import "@/styles/global.scss"; | ||||||
|  |  | ||||||
| @include mobile{ | @include mobile { | ||||||
|   section { |   section { | ||||||
|     margin-top: 5em; |     margin-top: 5em; | ||||||
|   } |   } | ||||||
| @@ -182,7 +194,7 @@ export default { | |||||||
|         margin-top: 40px; |         margin-top: 40px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       &.total-bought-color{ |       &.total-bought-color { | ||||||
|         font-weight: bold; |         font-weight: bold; | ||||||
|         margin-top: 25px; |         margin-top: 25px; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -5,13 +5,17 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { chartWinsByColor } from "@/api"; |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|  |   methods: { | ||||||
|  |     fetchWinsByColor() { | ||||||
|  |       return fetch("/api/history/by-color").then(resp => resp.json()); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     let canvas = this.$refs["win-chart"].getContext("2d"); |     let canvas = this.$refs["win-chart"].getContext("2d"); | ||||||
|  |  | ||||||
|     let response = await chartWinsByColor(); |     let response = await this.fetchWinsByColor(); | ||||||
|  |     const { colors } = response; | ||||||
|     let labels = ["Vunnet"]; |     let labels = ["Vunnet"]; | ||||||
|     let blue = { |     let blue = { | ||||||
|       label: "Blå", |       label: "Blå", | ||||||
| @@ -42,23 +46,26 @@ export default { | |||||||
|       data: [] |       data: [] | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     blue.data.push(response.blue.win); |     const findColorWinners = (colorSelect, colors) => { | ||||||
|     yellow.data.push(response.yellow.win); |       return colors.filter(color => color.color == colorSelect)[0].count; | ||||||
|     red.data.push(response.red.win); |     }; | ||||||
|     green.data.push(response.green.win); |  | ||||||
|  |     const blueWinCount = findColorWinners("blue", colors); | ||||||
|  |     const redWinCount = findColorWinners("red", colors); | ||||||
|  |     const greenWinCount = findColorWinners("green", colors); | ||||||
|  |     const yellowWinCount = findColorWinners("yellow", colors); | ||||||
|  |  | ||||||
|  |     blue.data.push(blueWinCount); | ||||||
|  |     red.data.push(redWinCount); | ||||||
|  |     green.data.push(greenWinCount); | ||||||
|  |     yellow.data.push(yellowWinCount); | ||||||
|  |  | ||||||
|     let highestNumber = 0; |     let highestNumber = 0; | ||||||
|     if (response.blue.win > highestNumber) { |     [blueWinCount, redWinCount, greenWinCount, greenWinCount].forEach(winCount => { | ||||||
|       highestNumber = response.blue.win; |       if (winCount > highestNumber) { | ||||||
|     } |         highestNumber = winCount; | ||||||
|     if (response.red.win > highestNumber) { |       } | ||||||
|       highestNumber = response.red.win; |     }); | ||||||
|     } |  | ||||||
|     if (response.green.win > highestNumber) { |  | ||||||
|       highestNumber = response.green.win; |  | ||||||
|     } |  | ||||||
|     if (response.yellow.win > highestNumber) { |  | ||||||
|       highestNumber = response.yellow.win; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let datasets = [blue, yellow, green, red]; |     let datasets = [blue, yellow, green, red]; | ||||||
|     let chartdata = { |     let chartdata = { | ||||||
| @@ -102,8 +109,6 @@ export default { | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "../styles/media-queries.scss"; |  | ||||||
|  |  | ||||||
| .chart { | .chart { | ||||||
|   height: 40vh; |   height: 40vh; | ||||||
|   max-height: 500px; |   max-height: 500px; | ||||||
|   | |||||||
| @@ -2,10 +2,7 @@ | |||||||
|   <div class="wine"> |   <div class="wine"> | ||||||
|     <slot name="top"></slot> |     <slot name="top"></slot> | ||||||
|     <div class="wine-image"> |     <div class="wine-image"> | ||||||
|       <img |       <img v-if="wine.image && loadImage" :src="wine.image" /> | ||||||
|         v-if="wine.image && loadImage" |  | ||||||
|         :src="wine.image" |  | ||||||
|       /> |  | ||||||
|       <img v-else class="wine-placeholder" alt="Wine image" /> |       <img v-else class="wine-placeholder" alt="Wine image" /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
| @@ -38,7 +35,7 @@ export default { | |||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       loadImage: false |       loadImage: false | ||||||
|     } |     }; | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     setImage(entries) { |     setImage(entries) { | ||||||
| @@ -53,7 +50,7 @@ export default { | |||||||
|     this.observer = new IntersectionObserver(this.setImage, { |     this.observer = new IntersectionObserver(this.setImage, { | ||||||
|       root: this.$el, |       root: this.$el, | ||||||
|       threshold: 0 |       threshold: 0 | ||||||
|     }) |     }); | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.observer.observe(this.$el); |     this.observer.observe(this.$el); | ||||||
| @@ -66,16 +63,17 @@ export default { | |||||||
| @import "@/styles/variables"; | @import "@/styles/variables"; | ||||||
|  |  | ||||||
| .wine { | .wine { | ||||||
|  |   align-self: flex-start; | ||||||
|   padding: 1rem; |   padding: 1rem; | ||||||
|   box-sizing: border-box; |   box-sizing: border-box; | ||||||
|   position: relative; |   position: relative; | ||||||
|   -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); |   -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
|   -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); |   -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
|   box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); |   box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); | ||||||
|  |   width: 100%; | ||||||
|  |  | ||||||
|   @include tablet { |   @include tablet { | ||||||
|     width: 250px; |     max-width: 280px; | ||||||
|     height: 100%; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -85,19 +83,18 @@ export default { | |||||||
|   margin-top: 10px; |   margin-top: 10px; | ||||||
|  |  | ||||||
|   img { |   img { | ||||||
|     height: 250px; |     height: 280px; | ||||||
|     @include mobile { |     @include mobile { | ||||||
|       object-fit: cover; |       object-fit: cover; | ||||||
|       max-width: 90px; |       max-width: 90px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   .wine-placeholder { |   .wine-placeholder { | ||||||
|     height: 250px; |     height: 280px; | ||||||
|     width: 70px; |     width: 70px; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| .wine-details { | .wine-details { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| @@ -107,7 +104,7 @@ export default { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .wine-name{ | .wine-name { | ||||||
|   font-size: 20px; |   font-size: 20px; | ||||||
|   margin: 1em 0; |   margin: 1em 0; | ||||||
| } | } | ||||||
| @@ -120,6 +117,7 @@ export default { | |||||||
| .bottom-section { | .bottom-section { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
|  |   align-self: flex-end; | ||||||
|  |  | ||||||
|   .link { |   .link { | ||||||
|     color: $matte-text-color; |     color: $matte-text-color; | ||||||
| @@ -135,4 +133,4 @@ export default { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -2,18 +2,18 @@ | |||||||
|   <div v-if="wines.length > 0" class="wines-main-container"> |   <div v-if="wines.length > 0" class="wines-main-container"> | ||||||
|     <div class="info-and-link"> |     <div class="info-and-link"> | ||||||
|       <h3> |       <h3> | ||||||
|         Topp 5 viner |         Topp viner | ||||||
|       </h3> |       </h3> | ||||||
|       <router-link to="viner"> |       <router-link to="viner"> | ||||||
|         <span class="vin-link">Se alle viner </span> |         <span class="vin-link">Se alle viner </span> | ||||||
|       </router-link> |       </router-link> | ||||||
|     </div> |     </div> | ||||||
|     <div class="wine-container"> |     <div class="wines-container"> | ||||||
|       <Wine v-for="wine in wines" :key="wine" :wine="wine"> |       <Wine v-for="wine in wines" :key="wine" :wine="wine"> | ||||||
|         <template v-slot:top> |         <template v-slot:top> | ||||||
|           <div class="flex justify-end"> |           <div class="flex justify-end"> | ||||||
|             <div class="requested-count cursor-pointer"> |             <div class="requested-count cursor-pointer"> | ||||||
|               <span> {{ wine.occurences }} </span> |               <span> {{ wine.occurences }} </span> | ||||||
|               <i class="icon icon--heart" /> |               <i class="icon icon--heart" /> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
| @@ -32,32 +32,36 @@ export default { | |||||||
|     Wine |     Wine | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return {  |     return { | ||||||
|       wines: [],  |       wines: [], | ||||||
|       clickedWine: null,  |       clickedWine: null, | ||||||
|  |       limit: 18 | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
|     let response = await overallWineStatistics(); |     this.getAllWines(); | ||||||
|  |  | ||||||
|     response.sort(); |  | ||||||
|     response = response |  | ||||||
|       .filter(wine => wine.name != null && wine.name != "") |  | ||||||
|       .sort( |  | ||||||
|         this.predicate( |  | ||||||
|           { |  | ||||||
|             name: "occurences", |  | ||||||
|             reverse: true |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             name: "rating", |  | ||||||
|             reverse: true |  | ||||||
|           } |  | ||||||
|         ) |  | ||||||
|       ); |  | ||||||
|     this.wines = response.slice(0, 5); |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     getAllWines() { | ||||||
|  |       return fetch(`/api/wines?limit=${this.limit}`) | ||||||
|  |         .then(resp => resp.json()) | ||||||
|  |         .then(response => { | ||||||
|  |           let { wines, success } = response; | ||||||
|  |  | ||||||
|  |           this.wines = wines.sort( | ||||||
|  |             this.predicate( | ||||||
|  |               { | ||||||
|  |                 name: "occurences", | ||||||
|  |                 reverse: true | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 name: "rating", | ||||||
|  |                 reverse: true | ||||||
|  |               } | ||||||
|  |             ) | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|     predicate: function() { |     predicate: function() { | ||||||
|       var fields = [], |       var fields = [], | ||||||
|         n_fields = arguments.length, |         n_fields = arguments.length, | ||||||
| @@ -125,42 +129,72 @@ export default { | |||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "@/styles/variables.scss"; | @import "@/styles/variables.scss"; | ||||||
| @import "@/styles/global.scss"; | @import "@/styles/global.scss"; | ||||||
| @import "../styles/media-queries.scss"; | @import "@/styles/media-queries.scss"; | ||||||
|  |  | ||||||
| .wines-main-container { | .wines-main-container { | ||||||
|   margin-bottom: 10em; |   margin-bottom: 10em; | ||||||
| } | } | ||||||
|  |  | ||||||
| .info-and-link{ | .info-and-link { | ||||||
|   display: flex; |   display: flex; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
| } | } | ||||||
|  |  | ||||||
| .wine-container { | .requested-count { | ||||||
|   display: grid; |   display: flex; | ||||||
|   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |   align-items: center; | ||||||
|   grid-gap: 2rem; |   margin-top: -0.5rem; | ||||||
|  |   background-color: rgb(244, 244, 244); | ||||||
|  |   border-radius: 1.1rem; | ||||||
|  |   padding: 0.25rem 1rem; | ||||||
|  |   font-size: 1.25em; | ||||||
|  |  | ||||||
|   .requested-count { |   span { | ||||||
|     display: flex; |     padding-right: 0.5rem; | ||||||
|     align-items: center; |     line-height: 1.25em; | ||||||
|     margin-top: -0.5rem; |   } | ||||||
|     background-color: rgb(244,244,244); |   .icon--heart { | ||||||
|     border-radius: 1.1rem; |     font-size: 1.5rem; | ||||||
|     padding: 0.25rem 1rem; |     color: var(--link-color); | ||||||
|     font-size: 1.25em; |  | ||||||
|  |  | ||||||
|     span { |  | ||||||
|       padding-right: 0.5rem; |  | ||||||
|       line-height: 1.25em; |  | ||||||
|     } |  | ||||||
|     .icon--heart{ |  | ||||||
|       font-size: 1.5rem; |  | ||||||
|       color: $link-color; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Call for help | ||||||
|  | .wines-container { | ||||||
|  |   @media (max-width: 1643px) { | ||||||
|  |     *:nth-child(n + 7) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 2066px) { | ||||||
|  |     *:nth-child(n + 9) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 2490px) { | ||||||
|  |     *:nth-child(n + 11) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 2915px) { | ||||||
|  |     *:nth-child(n + 13) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 3335px) { | ||||||
|  |     *:nth-child(n + 15) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @media (max-width: 3758px) { | ||||||
|  |     *:nth-child(n + 17) { | ||||||
|  |       display: none; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -85,9 +85,7 @@ export default { | |||||||
|         this.startConfetti(this.currentName); |         this.startConfetti(this.currentName); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       this.currentName = this.attendees[ |       this.currentName = this.attendees[this.nameRounds % this.attendees.length].name; | ||||||
|         this.nameRounds % this.attendees.length |  | ||||||
|       ].name; |  | ||||||
|       this.nameRounds += 1; |       this.nameRounds += 1; | ||||||
|       clearTimeout(this.nameTimeout); |       clearTimeout(this.nameTimeout); | ||||||
|       this.nameTimeout = setTimeout(() => { |       this.nameTimeout = setTimeout(() => { | ||||||
| @@ -136,8 +134,8 @@ export default { | |||||||
|       //duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds. |       //duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds. | ||||||
|       var duration = 7 * 1000; |       var duration = 7 * 1000; | ||||||
|       var animationEnd = Date.now() + duration; |       var animationEnd = Date.now() + duration; | ||||||
|       var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20}; |       var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20 }; | ||||||
|       var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35} |       var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35 }; | ||||||
|  |  | ||||||
|       function randomInRange(min, max) { |       function randomInRange(min, max) { | ||||||
|         return Math.random() * (max - min) + min; |         return Math.random() * (max - min) + min; | ||||||
| @@ -148,27 +146,27 @@ export default { | |||||||
|         var timeLeft = animationEnd - Date.now(); |         var timeLeft = animationEnd - Date.now(); | ||||||
|         if (timeLeft <= 0) { |         if (timeLeft <= 0) { | ||||||
|           self.drawing = false; |           self.drawing = false; | ||||||
|           console.time("drawing finished") |           console.time("drawing finished"); | ||||||
|           return clearInterval(interval); |           return clearInterval(interval); | ||||||
|         } |         } | ||||||
|         if (currentName == "Amund Brandsrud") { |         if (currentName == "Amund Brandsrud") { | ||||||
|           runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135}); |           runCannon(uberDefaults, { x: 1, y: 1 }, { angle: 135 }); | ||||||
|           runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45}); |           runCannon(uberDefaults, { x: 0, y: 1 }, { angle: 45 }); | ||||||
|           runCannon(uberDefaults, {y: 1 }, {angle: 90}); |           runCannon(uberDefaults, { y: 1 }, { angle: 90 }); | ||||||
|           runCannon(uberDefaults, {x: 0 }, {angle: 45}); |           runCannon(uberDefaults, { x: 0 }, { angle: 45 }); | ||||||
|           runCannon(uberDefaults, {x: 1 }, {angle: 135}); |           runCannon(uberDefaults, { x: 1 }, { angle: 135 }); | ||||||
|         } else { |         } else { | ||||||
|           runCannon(defaults, {x: 0 }, {angle: 45}); |           runCannon(defaults, { x: 0 }, { angle: 45 }); | ||||||
|           runCannon(defaults, {x: 1 }, {angle: 135}); |           runCannon(defaults, { x: 1 }, { angle: 135 }); | ||||||
|           runCannon(defaults, {y: 1 }, {angle: 90}); |           runCannon(defaults, { y: 1 }, { angle: 90 }); | ||||||
|         } |         } | ||||||
|       }, 250); |       }, 250); | ||||||
|  |  | ||||||
|       function runCannon(confettiDefaultValues, originPoint, launchAngle){ |       function runCannon(confettiDefaultValues, originPoint, launchAngle) { | ||||||
|         confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle)) |         confetti(Object.assign({}, confettiDefaultValues, { origin: originPoint }, launchAngle)); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     ordinalNumber(number=this.currentWinnerLocal.winnerCount) { |     ordinalNumber(number = this.currentWinnerLocal.winnerCount) { | ||||||
|       const dictonary = { |       const dictonary = { | ||||||
|         1: "første", |         1: "første", | ||||||
|         2: "andre", |         2: "andre", | ||||||
| @@ -187,7 +185,6 @@ export default { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| <template> | <template> | ||||||
|   <section> |   <section> | ||||||
|     <h2>{{ title ? title : 'Vinnere' }}</h2> |     <h2>{{ title ? title : "Vinnere" }}</h2> | ||||||
|     <div class="winning-raffles" v-if="winners.length > 0"> |     <div class="winning-raffles" v-if="winners.length > 0"> | ||||||
|       <div v-for="(winner, index) in winners" :key="index"> |       <div v-for="(winner, index) in winners" :key="index"> | ||||||
|         <router-link :to="`/highscore/${ encodeURIComponent(winner.name) }`"> |         <router-link :to="`/highscore/${winner.name}`"> | ||||||
|           <div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div> |           <div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div> | ||||||
|         </router-link> |         </router-link> | ||||||
|       </div> |       </div> | ||||||
| @@ -26,7 +26,7 @@ export default { | |||||||
|       type: Array |       type: Array | ||||||
|     }, |     }, | ||||||
|     drawing: { |     drawing: { | ||||||
|       type: Boolean, |       type: Boolean | ||||||
|     }, |     }, | ||||||
|     title: { |     title: { | ||||||
|       type: String, |       type: String, | ||||||
|   | |||||||
| @@ -1,17 +1,16 @@ | |||||||
|  | const dateString = date => { | ||||||
| const dateString = (date) => { |   if (typeof date == "string") { | ||||||
|   if (typeof(date) == "string") { |  | ||||||
|     date = new Date(date); |     date = new Date(date); | ||||||
|   } |   } | ||||||
|   const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date) |   const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date); | ||||||
|   const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date) |   const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date); | ||||||
|   const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date) |   const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date); | ||||||
|  |  | ||||||
|   return `${ye}-${mo}-${da}` |   return `${ye}-${mo}-${da}`; | ||||||
| } | }; | ||||||
|  |  | ||||||
| function humanReadableDate(date) { | function humanReadableDate(date) { | ||||||
|   const options = { year: 'numeric', month: 'long', day: 'numeric' }; |   const options = { year: "numeric", month: "long", day: "numeric" }; | ||||||
|   return new Date(date).toLocaleDateString(undefined, options); |   return new Date(date).toLocaleDateString(undefined, options); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -20,8 +19,4 @@ 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 { | export { dateString, humanReadableDate, daysAgo }; | ||||||
|   dateString, |  | ||||||
|   humanReadableDate, |  | ||||||
|   daysAgo |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -3,43 +3,47 @@ import VueRouter from "vue-router"; | |||||||
| import { routes } from "@/router.js"; | import { routes } from "@/router.js"; | ||||||
| import Vinlottis from "@/Vinlottis"; | import Vinlottis from "@/Vinlottis"; | ||||||
|  |  | ||||||
|  | import Toast from "@/plugins/Toast"; | ||||||
|  |  | ||||||
| import * as Sentry from "@sentry/browser"; | import * as Sentry from "@sentry/browser"; | ||||||
| import { Vue as VueIntegration } from "@sentry/integrations"; | import { Vue as VueIntegration } from "@sentry/integrations"; | ||||||
|  |  | ||||||
| Vue.use(VueRouter); | Vue.use(VueRouter); | ||||||
|  |  | ||||||
|  | // Plugins | ||||||
|  | Vue.use(Toast); | ||||||
|  |  | ||||||
| const ENV = window.location.href.includes("localhost") ? "development" : "production"; | const ENV = window.location.href.includes("localhost") ? "development" : "production"; | ||||||
| if (ENV !== "development") { | if (ENV !== "development") { | ||||||
|   Sentry.init({ |   Sentry.init({ | ||||||
|     dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091", |     dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091", | ||||||
|     integrations: [ |     integrations: [new VueIntegration({ Vue })], | ||||||
|       new VueIntegration({ Vue }) |  | ||||||
|     ], |  | ||||||
|     beforeSend: event => { |     beforeSend: event => { | ||||||
|       console.error(event); |       console.error(event); | ||||||
|       return event; |       return event; | ||||||
|     } |     } | ||||||
|   }) |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Add global GA variables | // Add global GA variables | ||||||
| window.ga = window.ga || function(){ | window.ga = | ||||||
|   window.ga.q = window.ga.q || []; |   window.ga || | ||||||
|   window.ga.q.push(arguments); |   function() { | ||||||
| }; |     window.ga.q = window.ga.q || []; | ||||||
|  |     window.ga.q.push(arguments); | ||||||
|  |   }; | ||||||
| ga.l = 1 * new Date(); | ga.l = 1 * new Date(); | ||||||
|  |  | ||||||
| // Initiate | // Initiate | ||||||
| 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"); | ||||||
|  |  | ||||||
| if (ENV == 'development') | if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true; | ||||||
|   window[`ga-disable-${__GA_TRACKINGID__}`] = true; |  | ||||||
|  |  | ||||||
| const router = new VueRouter({ | const router = new VueRouter({ | ||||||
|   routes: routes |   routes: routes | ||||||
|   | |||||||
| @@ -6,9 +6,9 @@ | |||||||
|   "scripts": { |   "scripts": { | ||||||
|     "build": "cross-env NODE_ENV=production webpack --progress", |     "build": "cross-env NODE_ENV=production webpack --progress", | ||||||
|     "build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress", |     "build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress", | ||||||
|     "dev": "yarn webpack serve --mode development --env development", |     "watch": "yarn webpack serve --mode development --env development", | ||||||
|     "start": "node server.js", |     "start": "node server.js", | ||||||
|     "start-noauth": "cross-env NODE_ENV=development node server.js", |     "dev": "cross-env NODE_ENV=development node server.js", | ||||||
|     "test": "echo \"Error: no test specified\" && exit 1" |     "test": "echo \"Error: no test specified\" && exit 1" | ||||||
|   }, |   }, | ||||||
|   "author": "", |   "author": "", | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								server.js
									
									
									
									
									
								
							| @@ -18,24 +18,36 @@ const MongoStore = require("connect-mongo")(session); | |||||||
| // mongoose / database | // mongoose / database | ||||||
| console.log("Trying to connect with mongodb.."); | console.log("Trying to connect with mongodb.."); | ||||||
| mongoose.promise = global.Promise; | mongoose.promise = global.Promise; | ||||||
| mongoose.connect("mongodb://localhost/vinlottis", { | mongoose | ||||||
|   useCreateIndex: true, |   .connect("mongodb://localhost/vinlottis", { | ||||||
|   useNewUrlParser: true, |     useCreateIndex: true, | ||||||
|   useUnifiedTopology: true, |     useNewUrlParser: true, | ||||||
|   serverSelectionTimeoutMS: 10000 // initial connection timeout |     useUnifiedTopology: true, | ||||||
| }).then(_ => console.log("Mongodb connection established!")) |     serverSelectionTimeoutMS: 10000 // initial connection timeout | ||||||
| .catch(err => { |   }) | ||||||
|   console.log(err); |   .then(_ => console.log("Mongodb connection established!")) | ||||||
|   console.error("ERROR! Mongodb required to run."); |   .catch(err => { | ||||||
|   process.exit(1); |     console.log(err); | ||||||
| }) |     console.error("ERROR! Mongodb required to run."); | ||||||
|  |     process.exit(1); | ||||||
|  |   }); | ||||||
| mongoose.set("debug", false); | mongoose.set("debug", false); | ||||||
|  |  | ||||||
| // middleware | // middleware | ||||||
| const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS")); | const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS")); | ||||||
| const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders")); | const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders")); | ||||||
| app.use(setupCORS) | app.use(setupCORS); | ||||||
| app.use(setupHeaders) | app.use(setupHeaders); | ||||||
|  |  | ||||||
|  | if (process.env.NODE_ENV == "development") { | ||||||
|  |   console.info(`NODE_ENV=development set, your are now always an authenticated user.`); | ||||||
|  |   const alwaysAuthenticatedWhenLocalhost = require(path.join( | ||||||
|  |     __dirname, | ||||||
|  |     "/api/middleware/alwaysAuthenticatedWhenLocalhost" | ||||||
|  |   )); | ||||||
|  |  | ||||||
|  |   app.use(alwaysAuthenticatedWhenLocalhost); | ||||||
|  | } | ||||||
|  |  | ||||||
| // parse application/json | // parse application/json | ||||||
| app.use(express.json()); | app.use(express.json()); | ||||||
| @@ -52,7 +64,7 @@ app.use( | |||||||
|   }) |   }) | ||||||
| ); | ); | ||||||
|  |  | ||||||
| app.set('socketio', io);  // set io instance to key "socketio" | app.set("socketio", io); // set io instance to key "socketio" | ||||||
|  |  | ||||||
| const passport = require("passport"); | const passport = require("passport"); | ||||||
| const LocalStrategy = require("passport-local"); | const LocalStrategy = require("passport-local"); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user