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 { history, clearHistory } = require(path.join(__dirname + "/../api/redis")); | ||||
| const { history, clearHistory } = require(path.join(__dirname + "/../redis")); | ||||
| console.log("loading chat"); | ||||
| 
 | ||||
| const getAllHistory = (req, res) => { | ||||
|   let { page, limit } = req.query; | ||||
| @@ -8,19 +9,23 @@ const getAllHistory = (req, res) => { | ||||
| 
 | ||||
|   return history(page, limit) | ||||
|     .then(messages => res.json(messages)) | ||||
|     .catch(error =>  res.status(500).json({ | ||||
|       message: error.message, | ||||
|       success: false | ||||
|     })); | ||||
|     .catch(error => | ||||
|       res.status(500).json({ | ||||
|         message: error.message, | ||||
|         success: false | ||||
|       }) | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const deleteHistory = (req, res) => { | ||||
|   return clearHistory() | ||||
|     .then(message => res.json(message)) | ||||
|     .catch(error => res.status(500).json({ | ||||
|       message: error.message, | ||||
|       success: false | ||||
|     })); | ||||
|     .catch(error => | ||||
|       res.status(500).json({ | ||||
|         message: error.message, | ||||
|         success: false | ||||
|       }) | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| 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 Wine = require(path.join(__dirname, '/schemas/Wine')); | ||||
| const Attendee = require(path.join(__dirname, "/schemas/Attendee")); | ||||
| 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 epochToDateString = date => new Date(parseInt(date)).toDateString(); | ||||
| const Message = require(path.join(__dirname, "/message")); | ||||
| const historyRepository = require(path.join(__dirname, "/history")); | ||||
| const wineRepository = require(path.join(__dirname, "/wine")); | ||||
|  | ||||
| const sortNewestFirst = (lotteries) => { | ||||
|   return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1) | ||||
| } | ||||
| const { | ||||
|   WinnerNotFound, | ||||
|   NoMoreAttendeesToWin, | ||||
|   CouldNotFindNewWinnerAfterNTries, | ||||
|   LotteryByDateNotFound | ||||
| } = require(path.join(__dirname, "/vinlottisErrors")); | ||||
|  | ||||
| const groupHighscoreByDate = async (highscore=undefined) => { | ||||
|   if (highscore == undefined) | ||||
|     highscore = await Highscore.find(); | ||||
| const archive = (date, raffles, stolen, wines) => { | ||||
|   const { blue, red, yellow, green } = raffles; | ||||
|   const bought = blue + red + yellow + green; | ||||
|  | ||||
|   const highscoreByDate = []; | ||||
|  | ||||
|   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}`, | ||||
|   return Promise.all(wines.map(wine => wineRepository.findWine(wine))).then(resolvedWines => { | ||||
|     const lottery = new Lottery({ | ||||
|       date, | ||||
|       winners: lottery.winners | ||||
|     })) | ||||
| } | ||||
|       blue, | ||||
|       red, | ||||
|       yellow, | ||||
|       green, | ||||
|       bought, | ||||
|       stolen, | ||||
|       wines: resolvedWines | ||||
|     }); | ||||
|  | ||||
| const byName = (req, res) => { | ||||
|   const { name } = req.params; | ||||
|   const regexName = new RegExp(name, "i"); // lowercase regex of the name | ||||
|     return lottery.save(); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
|   return Highscore.find({ name }) | ||||
|     .then(highscore => { | ||||
|       if (highscore.length > 0) { | ||||
|         return highscore[0] | ||||
|       } else { | ||||
|         return res.status(404).send({ | ||||
|           message: `Name: ${ name } not found in leaderboards.` | ||||
|         }) | ||||
| const lotteryByDate = date => { | ||||
|   const startOfDay = new Date(date.setHours(0, 0, 0, 0)); | ||||
|   const endOfDay = new Date(date.setHours(24, 59, 59, 99)); | ||||
|  | ||||
|   const query = [ | ||||
|     { | ||||
|       $match: { | ||||
|         date: { | ||||
|           $gte: startOfDay, | ||||
|           $lte: endOfDay | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     .then(highscore => resolveWineReferences(highscore, "wins")) | ||||
|     .then(highscore => res.send({ | ||||
|       message: `Lottery winnings for name: ${ name }.`, | ||||
|       name: highscore.name, | ||||
|       highscore: sortNewestFirst(highscore.wins) | ||||
|     })) | ||||
|     }, | ||||
|     { | ||||
|       $lookup: { | ||||
|         from: "wines", | ||||
|         localField: "wines", | ||||
|         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 = { | ||||
|   all, | ||||
|   latest, | ||||
|   byEpochDate, | ||||
|   byName | ||||
|   drawWinner, | ||||
|   archive, | ||||
|   lotteryByDate, | ||||
|   allLotteries, | ||||
|   allLotteriesIncludingWinners | ||||
| }; | ||||
|   | ||||
							
								
								
									
										123
									
								
								api/message.js
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								api/message.js
									
									
									
									
									
								
							| @@ -2,34 +2,50 @@ const https = require("https"); | ||||
| const path = require("path"); | ||||
| const config = require(path.join(__dirname + "/../config/defaults/lottery")); | ||||
|  | ||||
| const dateString = (date) => { | ||||
|   if (typeof(date) == "string") { | ||||
| const dateString = date => { | ||||
|   if (typeof date == "string") { | ||||
|     date = new Date(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 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 `${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) { | ||||
|   winnerObject.timestamp_sent = new Date().getTime(); | ||||
|   winnerObject.timestamp_limit = new Date().getTime() * 600000; | ||||
|   await winnerObject.save(); | ||||
| async function sendPrizeSelectionLink(winner) { | ||||
|   winner.timestamp_sent = new Date().getTime(); | ||||
|   winner.timestamp_limit = new Date().getTime() + 1000 * 600; | ||||
|   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( | ||||
|     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.)` | ||||
|   ) | ||||
|   return sendMessageToNumber(phoneNumber, message); | ||||
| } | ||||
|  | ||||
| async function sendWineConfirmation(winnerObject, wineObject, date) { | ||||
|   date = dateString(date); | ||||
|   return sendMessageToUser(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!`) | ||||
|   return sendMessageToNumber( | ||||
|     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) { | ||||
| @@ -38,84 +54,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) { | ||||
|   winnerObject.timestamp_limit = new Date().getTime(); | ||||
|   await winnerObject.save(); | ||||
|  | ||||
|   return sendMessageToUser( | ||||
|   return sendMessageToNumber( | ||||
|     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) { | ||||
|   return sendMessageToUser( | ||||
|   return sendMessageToNumber( | ||||
|     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) { | ||||
|   console.log(`Attempting to send message to ${ phoneNumber }.`) | ||||
| async function sendMessageToNumber(phoneNumber, message) { | ||||
|   console.log(`Attempting to send message to ${phoneNumber}.`); | ||||
|  | ||||
|   const body = { | ||||
|     sender: "Vinlottis", | ||||
|     message: message, | ||||
|     recipients: [{ msisdn: `47${ phoneNumber }`}] | ||||
|     recipients: [{ msisdn: `47${phoneNumber}` }] | ||||
|   }; | ||||
|  | ||||
|   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) { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     const options = { | ||||
|       hostname: "gatewayapi.com", | ||||
|       post: 443, | ||||
|       path: `/rest/mtsms?token=${ config.gatewayToken }`, | ||||
|       path: `/rest/mtsms?token=${config.gatewayToken}`, | ||||
|       method: "POST", | ||||
|       headers: { | ||||
|         "Content-Type": "application/json" | ||||
|       } | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     const req = https.request(options, (res) => { | ||||
|       console.log(`statusCode: ${ res.statusCode }`); | ||||
|       console.log(`statusMessage: ${ res.statusMessage }`); | ||||
|     const req = https.request(options, res => { | ||||
|       console.log(`statusCode: ${res.statusCode}`); | ||||
|       console.log(`statusMessage: ${res.statusMessage}`); | ||||
|  | ||||
|       res.setEncoding('utf8'); | ||||
|       res.setEncoding("utf8"); | ||||
|  | ||||
|       if (res.statusCode == 200) { | ||||
|         res.on("data", (data) => { | ||||
|           console.log("Response from message gateway:", data) | ||||
|         res.on("data", data => { | ||||
|           console.log("Response from message gateway:", data); | ||||
|  | ||||
|           resolve(JSON.parse(data)) | ||||
|           resolve(JSON.parse(data)); | ||||
|         }); | ||||
|       } else { | ||||
|         res.on("data", (data) => { | ||||
|         res.on("data", data => { | ||||
|           data = JSON.parse(data); | ||||
|           return reject('Gateway error: ' + data['message'] || data) | ||||
|           return reject("Gateway error: " + data["message"] || data); | ||||
|         }); | ||||
|       } | ||||
|     }) | ||||
|     }); | ||||
|  | ||||
|     req.on("error", (error) => { | ||||
|       console.error(`Error from sms service: ${ error }`); | ||||
|       reject(`Error from sms service: ${ error }`); | ||||
|     }) | ||||
|     req.on("error", error => { | ||||
|       console.error(`Error from sms service: ${error}`); | ||||
|       reject(`Error from sms service: ${error}`); | ||||
|     }); | ||||
|  | ||||
|     req.write(JSON.stringify(body)); | ||||
|     req.end(); | ||||
| @@ -123,9 +124,9 @@ async function gatewayRequest(body) { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   sendWineSelectMessage, | ||||
|   sendInitialMessageToWinners, | ||||
|   sendPrizeSelectionLink, | ||||
|   sendWineConfirmation, | ||||
|   sendLastWinnerMessage, | ||||
|   sendWineSelectMessageTooLate, | ||||
|   sendInitialMessageToWinners | ||||
| } | ||||
|   sendWineSelectMessageTooLate | ||||
| }; | ||||
|   | ||||
							
								
								
									
										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) => { | ||||
|   if (process.env.NODE_ENV == "development") { | ||||
|     console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`) | ||||
|     req.isAuthenticated = () => true; | ||||
|     return next(); | ||||
|   } | ||||
|  | ||||
|   if (!req.isAuthenticated()) { | ||||
|     return res.status(401).send({ | ||||
|       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 RequestedWine = require(path.join( | ||||
|   __dirname, "/schemas/RequestedWine" | ||||
| )); | ||||
| const Wine = require(path.join( | ||||
|   __dirname, "/schemas/Wine" | ||||
| )); | ||||
| const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine")); | ||||
| const Wine = require(path.join(__dirname, "/schemas/Wine")); | ||||
|  | ||||
| const deleteRequestedWineById = async (req, res) => { | ||||
|   const { id } = req.params; | ||||
|   if(id == null){ | ||||
|     return res.json({ | ||||
|       message: "Id er ikke definert", | ||||
|       success: false | ||||
|     }) | ||||
| class RequestedWineNotFound extends Error { | ||||
|   constructor(message = "Wine with this id was not found.") { | ||||
|     super(message); | ||||
|     this.name = "RequestedWineNotFound"; | ||||
|     this.statusCode = 404; | ||||
|   } | ||||
|  | ||||
|   await RequestedWine.deleteOne({wineId: id}) | ||||
|   return res.json({ | ||||
|     message: `Slettet vin med id: ${id}`, | ||||
|     success: true | ||||
|   }); | ||||
| } | ||||
|  | ||||
| const getAllRequestedWines = async (req, res) => { | ||||
|   const allWines = await RequestedWine.find({}).populate("wine"); | ||||
| const addNew = async wine => { | ||||
|   let foundWine = await Wine.findOne({ id: wine.id }); | ||||
|  | ||||
|   return res.json(allWines); | ||||
| } | ||||
|  | ||||
| const requestNewWine = async (req, res) => { | ||||
|   const {wine} = req.body | ||||
|  | ||||
|   let thisWineIsLOKO = await Wine.findOne({id: wine.id}) | ||||
|  | ||||
|   if(thisWineIsLOKO == undefined){ | ||||
|     thisWineIsLOKO = new Wine({ | ||||
|   if (foundWine == undefined) { | ||||
|     foundWine = new Wine({ | ||||
|       name: wine.name, | ||||
|       vivinoLink: wine.vivinoLink, | ||||
|       rating: null, | ||||
| @@ -43,27 +22,47 @@ const requestNewWine = async (req, res) => { | ||||
|       image: wine.image, | ||||
|       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({ | ||||
|       count: 1, | ||||
|       wineId: wine.id, | ||||
|       wine: thisWineIsLOKO | ||||
|     }) | ||||
|       wine: foundWine | ||||
|     }); | ||||
|   } else { | ||||
|     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 = { | ||||
|   requestNewWine, | ||||
|   getAllRequestedWines, | ||||
|   deleteRequestedWineById | ||||
|   addNew, | ||||
|   getAll, | ||||
|   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 setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated")); | ||||
|  | ||||
| const update = require(path.join(__dirname, "/update")); | ||||
| const retrieve = require(path.join(__dirname, "/retrieve")); | ||||
| const request = require(path.join(__dirname, "/request")); | ||||
| const subscriptionApi = require(path.join(__dirname, "/subscriptions")); | ||||
| const userApi = require(path.join(__dirname, "/user")); | ||||
| const wineinfo = require(path.join(__dirname, "/wineinfo")); | ||||
| const virtualApi = require(path.join(__dirname, "/virtualLottery")); | ||||
| const virtualRegistrationApi = require(path.join( | ||||
|   __dirname, "/virtualRegistration" | ||||
| )); | ||||
| const lottery = require(path.join(__dirname, "/lottery")); | ||||
| const chatHistoryApi = require(path.join(__dirname, "/chatHistory")); | ||||
| const requestController = require(path.join(__dirname, "/controllers/requestController")); | ||||
| const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController")); | ||||
| const chatController = require(path.join(__dirname, "/controllers/chatController")); | ||||
| const userController = require(path.join(__dirname, "/controllers/userController")); | ||||
| const historyController = require(path.join(__dirname, "/controllers/historyController")); | ||||
| const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController")); | ||||
| const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController")); | ||||
| const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController")); | ||||
| const lotteryController = require(path.join(__dirname, "/controllers/lotteryController")); | ||||
| const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController")); | ||||
| const wineController = require(path.join(__dirname, "/controllers/wineController")); | ||||
| const messageController = require(path.join(__dirname, "/controllers/messageController")); | ||||
|  | ||||
| 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.post("/request/new-wine", request.requestNewWine); | ||||
| router.delete("/request/:id", request.deleteRequestedWineById); | ||||
| router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests); | ||||
| router.post("/request", requestController.addRequest); | ||||
| router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest); | ||||
|  | ||||
| router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); | ||||
| router.get("/wineinfo/:ean", wineinfo.byEAN); | ||||
| router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences | ||||
| 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.post("/lottery", update.submitLottery); | ||||
| router.post("/lottery/wines", update.submitWinesToLottery); | ||||
| // router.delete("/lottery/wine/:id", update.deleteWineFromLottery); | ||||
| router.post("/lottery/winners", update.submitWinnersToLottery); | ||||
| router.get("/history", historyController.all); | ||||
| router.get("/history/latest", historyController.latest); | ||||
| router.get("/history/by-wins/", historyController.orderByWins); | ||||
| router.get("/history/by-color/", historyController.groupByColor); | ||||
| 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("/purchase/statistics", retrieve.allPurchase); | ||||
| router.get("/purchase/statistics/color", retrieve.purchaseByColor); | ||||
| router.get("/highscore/statistics", retrieve.highscore) | ||||
| router.get("/wines/statistics", retrieve.allWines); | ||||
| router.get("/wines/statistics/overall", retrieve.allWinesSummary); | ||||
| // router.get("/purchases", purchaseController.lotteryPurchases); | ||||
| // // returns list per date and count of each colors that where bought | ||||
| // router.get("/purchases/summary", purchaseController.lotteryPurchases); | ||||
| // // returns total, wins?, stolen | ||||
| // router.get("/purchase/:date", purchaseController.lotteryPurchaseByDate); | ||||
|  | ||||
| router.get("/lottery/all", lottery.all); | ||||
| router.get("/lottery/latest", lottery.latest); | ||||
| router.get("/lottery/by-date/:date", lottery.byEpochDate); | ||||
| router.get("/lottery/by-name/:name", lottery.byName); | ||||
| router.get("/lottery/wines", prelotteryWineController.allWines); | ||||
| router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema); | ||||
| router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById); | ||||
| 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.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees); | ||||
| router.get('/virtual/winner/draw', virtualApi.drawWinner); | ||||
| router.get('/virtual/winner/all', virtualApi.winners); | ||||
| router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure); | ||||
| 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.get("/lottery/attendees", setAdminHeaderIfAuthenticated, attendeeController.allAttendees); | ||||
| router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees); | ||||
| router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee); | ||||
| router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById); | ||||
| router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById); | ||||
|  | ||||
| router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById); | ||||
| router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById); | ||||
| router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection); | ||||
| router.get("/lottery/winners", winnerController.allWinners); | ||||
| router.get("/lottery/winner/:id", winnerController.winnerById); | ||||
| 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.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory) | ||||
| router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner); | ||||
| router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery); | ||||
| router.get("/lottery/:epoch", lotteryController.lotteryByDate); | ||||
| router.get("/lotteries/", lotteryController.allLotteries); | ||||
|  | ||||
| router.post('/login', userApi.login); | ||||
| router.post('/register', mustBeAuthenticated, userApi.register); | ||||
| router.get('/logout', userApi.logout); | ||||
| // router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status); | ||||
| router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start); | ||||
| // 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; | ||||
|   | ||||
| @@ -6,9 +6,14 @@ const PreLotteryWine = new Schema({ | ||||
|   vivinoLink: String, | ||||
|   rating: Number, | ||||
|   id: String, | ||||
|   year: Number, | ||||
|   image: String, | ||||
|   price: String, | ||||
|   country: String | ||||
|   country: String, | ||||
|   winner: { | ||||
|     type: Schema.Types.ObjectId, | ||||
|     ref: "VirtualWinner" | ||||
|   } | ||||
| }); | ||||
|  | ||||
| module.exports = mongoose.model("PreLotteryWine", PreLotteryWine); | ||||
|   | ||||
| @@ -10,6 +10,10 @@ const VirtualWinner = new Schema({ | ||||
|   red: Number, | ||||
|   yellow: Number, | ||||
|   id: String, | ||||
|   prize_selected: { | ||||
|     type: Boolean, | ||||
|     default: false | ||||
|   }, | ||||
|   timestamp_drawn: Number, | ||||
|   timestamp_sent: Number, | ||||
|   timestamp_limit: Number | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| const mongoose = require("mongoose"); | ||||
| const Schema = mongoose.Schema; | ||||
|  | ||||
| const Wine = new Schema({ | ||||
| const WineSchema = new Schema({ | ||||
|   name: String, | ||||
|   vivinoLink: String, | ||||
|   rating: Number, | ||||
|   occurences: Number, | ||||
|   id: String, | ||||
|   year: Number, | ||||
|   image: String, | ||||
|   price: 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 path = require("path"); | ||||
| const User = require(path.join(__dirname, "/schemas/User")); | ||||
| const router = require("express").Router(); | ||||
|  | ||||
| const register = (req, res, next) => { | ||||
|   User.register( | ||||
|     new User({ username: req.body.username }), | ||||
|     req.body.password, | ||||
|     function(err) { | ||||
| class UserExistsError extends Error { | ||||
|   constructor(message = "Username already exists.") { | ||||
|     super(message); | ||||
|     this.name = "UserExists"; | ||||
|     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.name == "UserExistsError") | ||||
|           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); | ||||
|         reject(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) => { | ||||
|   passport.authenticate("local", function(err, user, info) { | ||||
|     if (err) { | ||||
|       if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") | ||||
|         return res.status(400).send({ message: err.message, success: false }) | ||||
|       return next(err); | ||||
|     } | ||||
| const login = (req, user) => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     req.logIn(user, err => { | ||||
|       if (err) { | ||||
|         reject(err); | ||||
|       } | ||||
|  | ||||
|     if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false }) | ||||
|  | ||||
|     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("/"); | ||||
|       resolve(user); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| module.exports = { | ||||
|   register, | ||||
|   login, | ||||
|   logout | ||||
|   authenticate, | ||||
|   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 Wine = require(path.join(__dirname, "/schemas/Wine")); | ||||
|  | ||||
| async function findSaveWine(prelotteryWine) { | ||||
|   let wonWine = await Wine.findOne({ name: prelotteryWine.name }); | ||||
|   if (wonWine == undefined) { | ||||
|     let newWonWine = new Wine({ | ||||
|       name: prelotteryWine.name, | ||||
|       vivinoLink: prelotteryWine.vivinoLink, | ||||
|       rating: prelotteryWine.rating, | ||||
| const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors")); | ||||
|  | ||||
| const addWine = async wine => { | ||||
|   let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }); | ||||
|  | ||||
|   if (existingWine == undefined) { | ||||
|     let newWine = new Wine({ | ||||
|       name: wine.name, | ||||
|       vivinoLink: wine.vivinoLink, | ||||
|       rating: wine.rating, | ||||
|       occurences: 1, | ||||
|       image: prelotteryWine.image, | ||||
|       id: prelotteryWine.id | ||||
|       id: wine.id, | ||||
|       year: wine.year, | ||||
|       image: wine.image, | ||||
|       price: wine.price, | ||||
|       country: wine.country | ||||
|     }); | ||||
|     await newWonWine.save(); | ||||
|     wonWine = newWonWine; | ||||
|     await newWine.save(); | ||||
|     return newWine; | ||||
|   } else { | ||||
|     wonWine.occurences += 1; | ||||
|     wonWine.image = prelotteryWine.image; | ||||
|     wonWine.id = prelotteryWine.id; | ||||
|     await wonWine.save(); | ||||
|     existingWine.occurences += 1; | ||||
|     await existingWine.save(); | ||||
|     return existingWine; | ||||
|   } | ||||
| }; | ||||
|  | ||||
|   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") | ||||
|     .then(resp => { | ||||
|       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: { | ||||
|       "Content-Type": "application/json" | ||||
|     }, | ||||
|     method: "DELETE", | ||||
|     body: JSON.stringify(wineToBeDeleted) | ||||
|     method: "DELETE" | ||||
|   }; | ||||
|  | ||||
|   return fetch("api/request/" + wineToBeDeleted.id, options) | ||||
| @@ -148,14 +148,12 @@ const attendees = () => { | ||||
|  | ||||
| const requestNewWine = (wine) => { | ||||
|   const options = { | ||||
|     body: JSON.stringify({ | ||||
|       wine: wine | ||||
|     }), | ||||
|     method: "POST", | ||||
|      headers: { | ||||
|       'Accept': 'application/json', | ||||
|       'Content-Type': 'application/json' | ||||
|     }, | ||||
|     method: "post" | ||||
|     body: JSON.stringify({ wine }) | ||||
|   } | ||||
|  | ||||
|   return fetch("/api/request/new-wine", options) | ||||
|   | ||||
| @@ -1,34 +1,72 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <h1>Admin-side</h1> | ||||
|     <Tabs :tabs="tabs" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Tabs from "@/ui/Tabs"; | ||||
| import RegisterPage from "@/components/RegisterPage"; | ||||
| import VirtualLotteryRegistrationPage from "@/components/VirtualLotteryRegistrationPage"; | ||||
| import RegisterWinePage from "@/components/admin/RegisterWinePage"; | ||||
| 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 { | ||||
|   components: { | ||||
|     Tabs, | ||||
|     RegisterPage, | ||||
|     VirtualLotteryRegistrationPage | ||||
|     Tabs | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       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> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| h1 { | ||||
|   text-align: center; | ||||
| <style lang="scss"> | ||||
| @import "@/styles/media-queries"; | ||||
|  | ||||
| .page-container { | ||||
|   padding: 0 1.5rem 3rem; | ||||
|  | ||||
|   h1 { | ||||
|     text-align: center; | ||||
|   } | ||||
|  | ||||
|   @include desktop { | ||||
|     max-width: 60vw; | ||||
|     margin: 0 auto; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,40 +2,50 @@ | ||||
|   <main class="container"> | ||||
|     <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> | ||||
|  | ||||
|       <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> | ||||
|   </main> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { allRequestedWines } from "@/api"; | ||||
| import RequestedWineCard from "@/ui/RequestedWineCard"; | ||||
| export default { | ||||
|   components: { | ||||
|     RequestedWineCard | ||||
|   }, | ||||
|   data(){ | ||||
|     return{ | ||||
|   data() { | ||||
|     return { | ||||
|       wines: undefined, | ||||
|       canRequest: true, | ||||
|       isAdmin: false | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     filterOutDeletedWine(wine){ | ||||
|       this.wines = this.wines.filter(item => item.wine._id !== wine._id) | ||||
|     }, | ||||
|     async refreshData(){ | ||||
|       [this.wines, this.isAdmin] = await allRequestedWines() || [[], false] | ||||
|     } | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.refreshData() | ||||
|     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> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @@ -55,10 +65,4 @@ h1 { | ||||
|   color: $matte-text-color; | ||||
|   font-weight: normal; | ||||
| } | ||||
|  | ||||
| .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"> | ||||
|     <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"> | ||||
|         <div class="winners-container"> | ||||
|  | ||||
|           <span class="label">Vinnende lodd:</span> | ||||
|           <div class="flex row"> | ||||
|             <span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span> | ||||
| @@ -19,43 +18,44 @@ | ||||
|             <ul class="names"> | ||||
|               <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="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> | ||||
|             </ul> | ||||
|           </div> | ||||
|         </div> | ||||
|       </Wine> | ||||
|  | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Banner from "@/ui/Banner"; | ||||
| import Wine from "@/ui/Wine"; | ||||
| import { overallWineStatistics } from "@/api"; | ||||
| import { dateString } from "@/utils"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     Banner, | ||||
|     Wine | ||||
|   }, | ||||
|   components: { Wine }, | ||||
|   data() { | ||||
|     return { | ||||
|       wines: [] | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.overallWineStatistics(); | ||||
|   }, | ||||
|   methods: { | ||||
|     winDateUrl(date) { | ||||
|       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 | ||||
|   }, | ||||
|   async mounted() { | ||||
|     this.wines = await overallWineStatistics(); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| @@ -84,18 +84,6 @@ h1 { | ||||
|   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 { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|       Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer' | ||||
|     </p> | ||||
|  | ||||
|     <RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" /> | ||||
|     <RaffleGenerator @numberOfRaffles="val => (this.numberOfRaffles = val)" /> | ||||
|  | ||||
|     <Vipps class="vipps" :amount="numberOfRaffles" /> | ||||
|     <Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> | ||||
| @@ -43,16 +43,16 @@ export default { | ||||
|       this.hardStart = true; | ||||
|     }, | ||||
|     track() { | ||||
|       window.ga('send', 'pageview', '/lottery/generate'); | ||||
|       window.ga("send", "pageview", "/lottery/generate"); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/variables.scss"; | ||||
| @import "../styles/global.scss"; | ||||
| @import "../styles/media-queries.scss"; | ||||
| @import "@/styles/variables.scss"; | ||||
| @import "@/styles/global.scss"; | ||||
| @import "@/styles/media-queries.scss"; | ||||
| h1 { | ||||
|   cursor: pointer; | ||||
| } | ||||
| @@ -67,7 +67,9 @@ p { | ||||
| } | ||||
|  | ||||
| .vipps { | ||||
|   margin: 5rem auto 2.5rem auto; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   margin-top: 4rem; | ||||
|  | ||||
|   @include mobile { | ||||
|     margin-top: 2rem; | ||||
| @@ -75,7 +77,6 @@ p { | ||||
| } | ||||
|  | ||||
| .container { | ||||
|   margin: auto; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,12 @@ | ||||
|     <p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p> | ||||
|  | ||||
|     <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 }} | ||||
|       </li> | ||||
|     </ol> | ||||
| @@ -24,8 +29,6 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| import { highscoreStatistics } from "@/api"; | ||||
| import { humanReadableDate, daysAgo } from "@/utils"; | ||||
| import Wine from "@/ui/Wine"; | ||||
|  | ||||
| @@ -34,18 +37,12 @@ export default { | ||||
|   data() { | ||||
|     return { | ||||
|       highscore: [], | ||||
|       filterInput: '' | ||||
|     } | ||||
|       filterInput: "" | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     let response = await highscoreStatistics(); | ||||
|     response.sort((a, b) => { | ||||
|       return a.wins.length > b.wins.length ? -1 : 1; | ||||
|     }); | ||||
|     response = response.filter( | ||||
|       person => person.name != null && person.name != "" | ||||
|     ); | ||||
|     this.highscore = this.generateScoreBoard(response); | ||||
|     const winners = await this.highscoreStatistics(); | ||||
|     this.highscore = this.generateScoreBoard(winners); | ||||
|   }, | ||||
|   computed: { | ||||
|     filteredResults() { | ||||
| @@ -53,37 +50,42 @@ export default { | ||||
|       let val = this.filterInput; | ||||
|  | ||||
|       if (val.length) { | ||||
|         val = val.toLowerCase() | ||||
|         const nameIncludesString = (person) => person.name.toLowerCase().includes(val); | ||||
|         highscore = highscore.filter(nameIncludesString) | ||||
|         val = val.toLowerCase(); | ||||
|         const nameIncludesString = person => person.name.toLowerCase().includes(val); | ||||
|         highscore = highscore.filter(nameIncludesString); | ||||
|       } | ||||
|  | ||||
|       return highscore | ||||
|       return highscore; | ||||
|     } | ||||
|   }, | ||||
|   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 highestWinCount = -1; | ||||
|  | ||||
|       return highscore.map(win => { | ||||
|         const wins = win.wins.length | ||||
|         const wins = win.wins.length; | ||||
|         if (wins != highestWinCount) { | ||||
|           place += 1 | ||||
|           highestWinCount = wins | ||||
|           place += 1; | ||||
|           highestWinCount = wins; | ||||
|         } | ||||
|  | ||||
|         const placeString = place.toString().padStart(2, "0"); | ||||
|         win.rank = placeString; | ||||
|         return win | ||||
|       }) | ||||
|         return win; | ||||
|       }); | ||||
|     }, | ||||
|     resetFilter() { | ||||
|       this.filterInput = ''; | ||||
|       document.getElementsByTagName('input')[0].focus(); | ||||
|       this.filterInput = ""; | ||||
|       document.getElementsByTagName("input")[0].focus(); | ||||
|     }, | ||||
|     selectWinner(winner) { | ||||
|       const path = "/highscore/" + encodeURIComponent(winner.name) | ||||
|     goToWinner(winner) { | ||||
|       const path = "/highscore/" + encodeURIComponent(winner.name); | ||||
|       this.$router.push(path); | ||||
|     }, | ||||
|     humanReadableDate: humanReadableDate, | ||||
| @@ -152,7 +154,8 @@ h1 { | ||||
|     cursor: pointer; | ||||
|  | ||||
|     border-bottom: 2px solid transparent; | ||||
|     &:hover, &:focus { | ||||
|     &:hover, | ||||
|     &:focus { | ||||
|       border-color: $link-color; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -3,38 +3,47 @@ | ||||
|     <h1>Historie fra tidligere lotteri</h1> | ||||
|  | ||||
|     <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> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { historyByDate, historyAll } from '@/api' | ||||
| import { historyByDate, historyAll } from "@/api"; | ||||
| import { humanReadableDate } from "@/utils"; | ||||
| import Winners from '@/ui/Winners' | ||||
| import Winners from "@/ui/Winners"; | ||||
|  | ||||
| export default { | ||||
|   name: 'History page of prev lotteries', | ||||
|   name: "History page of prev lotteries", | ||||
|   components: { Winners }, | ||||
|   data() { | ||||
|     return { | ||||
|       lotteries: [], | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     humanReadableDate: humanReadableDate | ||||
|       lotteries: [] | ||||
|     }; | ||||
|   }, | ||||
|   created() { | ||||
|     const dateFromUrl = this.$route.params.date; | ||||
|  | ||||
|     if (dateFromUrl !== undefined) | ||||
|       historyByDate(dateFromUrl) | ||||
|         .then(history => this.lotteries = { "lottery": history }) | ||||
|     else | ||||
|       historyAll() | ||||
|         .then(history => this.lotteries = history.lotteries) | ||||
|     if (dateFromUrl !== undefined) { | ||||
|       this.fetchHistoryByDate(dateFromUrl).then(history => (this.lotteries = [history])); | ||||
|     } else { | ||||
|       this.fetchHistory().then(history => (this.lotteries = history)); | ||||
|     } | ||||
|   }, | ||||
|   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> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
|         <input | ||||
|           type="text" | ||||
|           v-model="username" | ||||
|           ref="username" | ||||
|           placeholder="Brukernavn" | ||||
|           autocapitalize="none" | ||||
|           @keyup.enter="submit" | ||||
| @@ -34,6 +35,9 @@ export default { | ||||
|       error: undefined | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$refs.username.focus(); | ||||
|   }, | ||||
|   methods: { | ||||
|     submit() { | ||||
|       login(this.username, this.password) | ||||
|   | ||||
| @@ -14,20 +14,25 @@ | ||||
|  | ||||
|         <h4 class="margin-bottom-0">Vinnende farger:</h4> | ||||
|         <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 }} | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <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"> | ||||
|             {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden | ||||
|             {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden | ||||
|           </router-link> | ||||
|  | ||||
|           <div class="won-wine"> | ||||
|             <img :src="smallerWineImage(win.wine.image)"> | ||||
|           <div class="won-wine" v-if="win.wine"> | ||||
|             <img :src="smallerWineImage(win.wine.image)" /> | ||||
|  | ||||
|             <div class="won-wine-details"> | ||||
|               <h3>{{ win.wine.name }}</h3> | ||||
| @@ -38,6 +43,11 @@ | ||||
|  | ||||
|             <div class="raffle-element small" :class="win.color + `-raffle`"></div> | ||||
|           </div> | ||||
|           <div class="won-wine" v-else> | ||||
|             <div class="won-wine-details"> | ||||
|               <h3>Oisann! Klarte ikke finne vin.</h3> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </section> | ||||
|  | ||||
| @@ -49,67 +59,71 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { getWinnerByName } from "@/api"; | ||||
| import { humanReadableDate, daysAgo } from "@/utils"; | ||||
| import { dateString, humanReadableDate, daysAgo } from "@/utils"; | ||||
|  | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       winner: undefined, | ||||
|       name: undefined, | ||||
|       error: undefined, | ||||
|       previousRoute: { | ||||
|         default: true, | ||||
|         name: "topplisten", | ||||
|         path: "/highscore" | ||||
|       } | ||||
|     } | ||||
|     }; | ||||
|   }, | ||||
|   beforeRouteEnter(to, from, next) { | ||||
|     next(vm => { | ||||
|       if (from.name != null) | ||||
|         vm.previousRoute = from | ||||
|     }) | ||||
|       if (from.name != null) vm.previousRoute = from; | ||||
|     }); | ||||
|   }, | ||||
|   computed: { | ||||
|     numberOfWins() { | ||||
|       return this.winner.highscore.length | ||||
|       return this.winner.wins.length; | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     const nameFromURL = this.$route.params.name; | ||||
|     getWinnerByName(nameFromURL) | ||||
|     this.name = this.$route.params.name; | ||||
|     this.getWinnerByName(this.name) | ||||
|       .then(winner => this.setWinner(winner)) | ||||
|       .catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`) | ||||
|       .catch(err => (this.error = `Ingen med navn: "${nameFromURL}" funnet.`)); | ||||
|   }, | ||||
|   methods: { | ||||
|     getWinnerByName(name) { | ||||
|       return fetch(`/api/history/by-name/${name}`) | ||||
|         .then(resp => resp.json()) | ||||
|         .then(response => response.winner); | ||||
|     }, | ||||
|     setWinner(winner) { | ||||
|       this.winner = { | ||||
|         name: winner.name, | ||||
|         highscore: [], | ||||
|         ...winner | ||||
|       } | ||||
|       this.winningColors = this.findWinningColors() | ||||
|       }; | ||||
|       this.winningColors = this.findWinningColors(); | ||||
|     }, | ||||
|     smallerWineImage(image) { | ||||
|       if (image && image.includes(`515x515`)) | ||||
|         return image.replace(`515x515`, `175x175`) | ||||
|       return image | ||||
|       if (image && image.includes(`515x515`)) return image.replace(`515x515`, `175x175`); | ||||
|       if (image && image.includes(`500x500`)) return image.replace(`500x500`, `175x175`); | ||||
|       return image; | ||||
|     }, | ||||
|     findWinningColors() { | ||||
|       const colors = this.winner.highscore.map(win => win.color) | ||||
|       const colorOccurences = {} | ||||
|       const colors = this.winner.wins.map(win => win.color); | ||||
|       const colorOccurences = {}; | ||||
|       colors.forEach(color => { | ||||
|         if (colorOccurences[color] == undefined) { | ||||
|           colorOccurences[color] = 1 | ||||
|           colorOccurences[color] = 1; | ||||
|         } else { | ||||
|           colorOccurences[color] += 1 | ||||
|           colorOccurences[color] += 1; | ||||
|         } | ||||
|       }) | ||||
|       return colorOccurences | ||||
|       }); | ||||
|       return colorOccurences; | ||||
|     }, | ||||
|     winDateUrl(date) { | ||||
|       const timestamp = new Date(date).getTime(); | ||||
|       return `/history/${timestamp}` | ||||
|       const dateParameter = dateString(new Date(date)); | ||||
|       return `/history/${dateParameter}`; | ||||
|     }, | ||||
|     navigateBack() { | ||||
|       if (this.previousRoute.default) { | ||||
| @@ -121,7 +135,7 @@ export default { | ||||
|     humanReadableDate: humanReadableDate, | ||||
|     daysAgo: daysAgo | ||||
|   } | ||||
| } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @@ -259,9 +273,8 @@ h1 { | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| .backdrop { | ||||
|   $background: rgb(244,244,244); | ||||
|   $background: rgb(244, 244, 244); | ||||
|  | ||||
|   --padding: 2rem; | ||||
|   @include desktop { | ||||
|   | ||||
| @@ -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> | ||||
| @@ -9,18 +9,21 @@ | ||||
|     <h1> | ||||
|       Foreslå en vin! | ||||
|     </h1> | ||||
|  | ||||
|     <section class="search-container"> | ||||
|       <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"> | ||||
|         <button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button> | ||||
|       </section> | ||||
|       <section v-for="(wine, index) in this.wines" :key="index" class="single-result"> | ||||
|         <img | ||||
|           v-if="wine.image" | ||||
|           :src="wine.image" | ||||
|           class="wine-image" | ||||
|           :class="{ 'fullscreen': fullscreen }" | ||||
|         <input | ||||
|           type="text" | ||||
|           v-model="searchString" | ||||
|           @keyup.enter="searchWines()" | ||||
|           placeholder="Søk etter en vin du liker her!🍷" | ||||
|           class="search-input-field" | ||||
|         /> | ||||
|         <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" /> | ||||
|         <section class="wine-info"> | ||||
|           <h2 v-if="wine.name">{{ wine.name }}</h2> | ||||
| @@ -29,37 +32,38 @@ | ||||
|             <span v-if="wine.rating">{{ wine.rating }}%</span> | ||||
|             <span v-if="wine.price">{{ wine.price }} NOK</span> | ||||
|             <span v-if="wine.country">{{ wine.country }}</span> | ||||
|             <span v-if="wine.year">{{ wine.year }}</span> | ||||
|           </div> | ||||
|         </section> | ||||
|           <button class="vin-button" @click="request(wine)">Foreslå denne</button> | ||||
|           <a | ||||
|           v-if="wine.vivinoLink" | ||||
|           :href="wine.vivinoLink" | ||||
|           class="wine-link" | ||||
|         >Les mer</a> | ||||
|         <button class="vin-button" @click="requestWine(wine)">Foreslå denne</button> | ||||
|         <a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="wine-link">Les mer</a> | ||||
|       </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! | ||||
|       </p> | ||||
|       <p v-else-if="loading">Loading...</p> | ||||
|     </section> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { searchForWine, requestNewWine } from "@/api"; | ||||
| import { searchForWine } from "@/api"; | ||||
| import Wine from "@/ui/Wine"; | ||||
| import Modal from "@/ui/Modal"; | ||||
| import RequestedWineCard from "@/ui/RequestedWineCard"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     Wine, | ||||
|     Modal | ||||
|     Modal, | ||||
|     RequestedWineCard | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       searchString: undefined, | ||||
|       wines: undefined, | ||||
|       showModal: false, | ||||
|       loading: false, | ||||
|       modalButtons: [ | ||||
|         { | ||||
|           text: "Legg til flere viner", | ||||
| @@ -70,30 +74,59 @@ export default { | ||||
|           action: "move" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     fetchWineFromVin(){ | ||||
|       if(this.searchString){ | ||||
|         this.wines = [] | ||||
|         let localSearchString = this.searchString.replace(/ /g,"_"); | ||||
|         searchForWine(localSearchString) | ||||
|           .then(res => this.wines = res) | ||||
|     fetchWinesByQuery(query) { | ||||
|       let url = new URL("/api/vinmonopolet/wine/search", window.location); | ||||
|       url.searchParams.set("name", query); | ||||
|  | ||||
|       this.wines = []; | ||||
|       this.loading = true; | ||||
|  | ||||
|       return fetch(url.href) | ||||
|         .then(resp => resp.json()) | ||||
|         .then(response => (this.wines = response.wines)) | ||||
|         .finally(wines => (this.loading = false)); | ||||
|     }, | ||||
|     searchWines() { | ||||
|       if (this.searchString) { | ||||
|         let localSearchString = this.searchString.replace(/ /g, "_"); | ||||
|         this.fetchWinesByQuery(localSearchString); | ||||
|       } | ||||
|     }, | ||||
|     request(wine){ | ||||
|       requestNewWine(wine) | ||||
|         .then(() => this.showModal = true) | ||||
|     requestWine(wine) { | ||||
|       const options = { | ||||
|         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){ | ||||
|       if(action == "stay"){ | ||||
|         this.showModal = false | ||||
|     emitFromModalButton(action) { | ||||
|       if (action == "stay") { | ||||
|         this.showModal = false; | ||||
|       } else { | ||||
|         this.$router.push("/requested-wines"); | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @@ -101,12 +134,11 @@ export default { | ||||
| @import "@/styles/global"; | ||||
| @import "@/styles/variables"; | ||||
|  | ||||
|  | ||||
| h1{ | ||||
| h1 { | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .main-container{ | ||||
| .main-container { | ||||
|   margin: auto; | ||||
|   max-width: 1200px; | ||||
| } | ||||
| @@ -120,67 +152,64 @@ input[type="text"] { | ||||
|   max-width: 90%; | ||||
| } | ||||
|  | ||||
|  | ||||
| .search-container{ | ||||
| .search-container { | ||||
|   margin: 1rem; | ||||
| } | ||||
|  | ||||
| .search-section{ | ||||
| .search-section { | ||||
|   display: grid; | ||||
|   grid: 1fr / 1fr .2fr; | ||||
|   grid: 1fr / 1fr 0.2fr; | ||||
|  | ||||
|   @include mobile{ | ||||
|     .vin-button{ | ||||
|   @include mobile { | ||||
|     .vin-button { | ||||
|       display: none; | ||||
|     } | ||||
|     .search-input-field{ | ||||
|     .search-input-field { | ||||
|       grid-column: 1 / -1; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .single-result{ | ||||
| .single-result { | ||||
|   margin-top: 1rem; | ||||
|   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"; | ||||
|   justify-items: center; | ||||
|   align-items: center; | ||||
|   grid-gap: 1em; | ||||
|   padding-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; | ||||
|     grid-template-areas: "picture details" | ||||
|                          "button-left button-right"; | ||||
|     grid-gap: .5em; | ||||
|  | ||||
|     .vin-button{ | ||||
|     .vin-button { | ||||
|       grid-area: button-right; | ||||
|       padding: .5em; | ||||
|       padding: 0.5em; | ||||
|       font-size: 1em; | ||||
|       line-height: 1em; | ||||
|       height: 2em; | ||||
|     } | ||||
|  | ||||
|     .wine-link{ | ||||
|     .wine-link { | ||||
|       grid-area: button-left; | ||||
|     } | ||||
|  | ||||
|     h2{ | ||||
|     h2 { | ||||
|       font-size: 1em; | ||||
|       max-width: 80%; | ||||
|       white-space: nowrap; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis | ||||
|       text-overflow: ellipsis; | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|  | ||||
|   .wine-image { | ||||
|     height: 100px; | ||||
|     grid-area: picture; | ||||
| @@ -192,14 +221,14 @@ input[type="text"] { | ||||
|     grid-area: picture; | ||||
|   } | ||||
|  | ||||
|   .wine-info{ | ||||
|   .wine-info { | ||||
|     grid-area: details; | ||||
|     width: 100%; | ||||
|  | ||||
|     h2{ | ||||
|     h2 { | ||||
|       margin: 0; | ||||
|     } | ||||
|     .details{ | ||||
|     .details { | ||||
|       top: 0; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
| @@ -216,22 +245,20 @@ input[type="text"] { | ||||
|     width: max-content; | ||||
|   } | ||||
|  | ||||
|   .vin-button{ | ||||
|   .vin-button { | ||||
|     grid-area: button-right; | ||||
|   } | ||||
|  | ||||
|   @include tablet{ | ||||
|     h2{ | ||||
|   @include tablet { | ||||
|     h2 { | ||||
|       font-size: 1.2em; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @include desktop{ | ||||
|     h2{ | ||||
|   @include desktop { | ||||
|     h2 { | ||||
|       font-size: 1.6em; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| </style> | ||||
| @@ -23,7 +23,9 @@ export default { | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     prelottery().then(wines => this.wines = wines); | ||||
|     fetch("/api/lottery/wines") | ||||
|       .then(resp => resp.json()) | ||||
|       .then(response => (this.wines = response.wines)); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| @@ -42,19 +44,18 @@ h1 { | ||||
| } | ||||
|  | ||||
| .wines-container { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||||
|   grid-gap: 2rem; | ||||
|   gap: 2rem; | ||||
|   width: 90vw; | ||||
|   padding: 5vw; | ||||
|  | ||||
|   @include desktop { | ||||
|     width: 80vw; | ||||
|     padding: 0 10vw; | ||||
|   } | ||||
|  | ||||
|   @media (min-width: 1500px) { | ||||
|     max-width: 1500px; | ||||
|     margin: 0 auto; | ||||
|   } | ||||
|  | ||||
|   @include mobile { | ||||
|     flex-direction: column; | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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 { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| <template> | ||||
|   <main class="main-container"> | ||||
|  | ||||
|     <section class="top-container"> | ||||
|  | ||||
|       <div class="want-to-win"> | ||||
|         <h1> | ||||
|           Vil du også vinne? | ||||
| @@ -18,8 +16,8 @@ | ||||
|       </div> | ||||
|  | ||||
|       <router-link to="/lottery" class="participate-button"> | ||||
|           <i class="icon icon--arrow-right"></i> | ||||
|           <p>Trykk her for å delta</p> | ||||
|         <i class="icon icon--arrow-right"></i> | ||||
|         <p>Trykk her for å delta</p> | ||||
|       </router-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> | ||||
|       </div> | ||||
|  | ||||
|     </section> | ||||
|  | ||||
|     <section class="content-container"> | ||||
|  | ||||
|       <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> | ||||
|       </div> | ||||
|  | ||||
|       <Highscore class="highscore"/> | ||||
|       <Highscore class="highscore" /> | ||||
|  | ||||
|       <TotalBought class="total-bought" /> | ||||
|  | ||||
|       <section class="chart-container"> | ||||
| @@ -56,12 +53,10 @@ | ||||
|         <WinGraph class="win" /> | ||||
|       </section> | ||||
|  | ||||
|       <Wines class="wines-container" /> | ||||
|  | ||||
|       <Wines class="wine-container" /> | ||||
|     </section> | ||||
|  | ||||
|     <Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> | ||||
|  | ||||
|   </main> | ||||
| </template> | ||||
|  | ||||
| @@ -96,11 +91,7 @@ export default { | ||||
|       if (!("PushManager" in window)) { | ||||
|         return false; | ||||
|       } | ||||
|       return ( | ||||
|         Notification.permission !== "granted" || | ||||
|         !this.pushAllowed || | ||||
|         localStorage.getItem("push") == null | ||||
|       ); | ||||
|       return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null; | ||||
|     } | ||||
|   }, | ||||
|   async mounted() { | ||||
| @@ -120,7 +111,7 @@ export default { | ||||
|       this.hardStart = way; | ||||
|     }, | ||||
|     track() { | ||||
|       window.ga('send', 'pageview', '/'); | ||||
|       window.ga("send", "pageview", "/"); | ||||
|     }, | ||||
|     startCountdown() { | ||||
|       this.hardStart = true; | ||||
| @@ -145,7 +136,7 @@ export default { | ||||
|   align-items: center; | ||||
|   justify-items: start; | ||||
|  | ||||
|   @include mobile{ | ||||
|   @include mobile { | ||||
|     padding-bottom: 2em; | ||||
|     height: 15em; | ||||
|     grid-template-rows: repeat(7, 1fr); | ||||
| @@ -156,13 +147,13 @@ export default { | ||||
|     grid-column: 2 / -1; | ||||
|     display: flex; | ||||
|  | ||||
|     h1{ | ||||
|     h1 { | ||||
|       font-size: 2em; | ||||
|       font-weight: 400; | ||||
|     } | ||||
|  | ||||
|     @include tablet { | ||||
|       h1{ | ||||
|       h1 { | ||||
|         font-size: 3em; | ||||
|       } | ||||
|       grid-row: 2 / 4; | ||||
| @@ -170,7 +161,7 @@ export default { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .notification-request-button{ | ||||
|   .notification-request-button { | ||||
|     cursor: pointer; | ||||
|   } | ||||
|  | ||||
| @@ -229,7 +220,7 @@ export default { | ||||
|   .icons-container { | ||||
|     grid-column: 1 / -1; | ||||
|     grid-row: 7 / -1; | ||||
|     @include mobile{ | ||||
|     @include mobile { | ||||
|       margin-top: 2em; | ||||
|       display: none; | ||||
|     } | ||||
| @@ -239,7 +230,7 @@ export default { | ||||
|       grid-column: 7 / -1; | ||||
|     } | ||||
|  | ||||
|     @include desktop{ | ||||
|     @include desktop { | ||||
|       grid-row: 4 / -3; | ||||
|       grid-column: 7 / 11; | ||||
|     } | ||||
| @@ -257,30 +248,27 @@ export default { | ||||
|     i { | ||||
|       font-size: 5em; | ||||
|  | ||||
|       &.icon--heart-sparks{ | ||||
|       &.icon--heart-sparks { | ||||
|         grid-column: 2 / 4; | ||||
|         grid-row: 2 / 4; | ||||
|         align-self: center; | ||||
|         justify-self: center; | ||||
|  | ||||
|       } | ||||
|       &.icon--face-1{ | ||||
|       &.icon--face-1 { | ||||
|         grid-column: 4 / 7; | ||||
|         grid-row: 2 / 4; | ||||
|         justify-self: center; | ||||
|  | ||||
|       } | ||||
|       &.icon--face-3{ | ||||
|       &.icon--face-3 { | ||||
|         grid-column: 7 / 10; | ||||
|         grid-row: 1 / 4; | ||||
|         align-self: center; | ||||
|       } | ||||
|       &.icon--ballon{ | ||||
|       &.icon--ballon { | ||||
|         grid-column: 9 / 11; | ||||
|         grid-row: 3 / 5; | ||||
|  | ||||
|       } | ||||
|       &.icon--bottle{ | ||||
|       &.icon--bottle { | ||||
|         grid-row: 4 / -1; | ||||
|  | ||||
|         &:nth-of-type(5) { | ||||
| @@ -297,14 +285,13 @@ export default { | ||||
|         &:nth-of-type(8) { | ||||
|           grid-column: 7 / 8; | ||||
|         } | ||||
|         &:nth-of-type(9){ | ||||
|         &:nth-of-type(9) { | ||||
|           grid-column: 8 / 9; | ||||
|           align-self: center; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| h1 { | ||||
| @@ -312,12 +299,12 @@ h1 { | ||||
|   font-family: "knowit"; | ||||
| } | ||||
|  | ||||
| .to-lottery{ | ||||
|     color: #333; | ||||
|     text-decoration: none; | ||||
|     display: block; | ||||
|     text-align: center; | ||||
|     margin-bottom: 0; | ||||
| .to-lottery { | ||||
|   color: #333; | ||||
|   text-decoration: none; | ||||
|   display: block; | ||||
|   text-align: center; | ||||
|   margin-bottom: 0; | ||||
| } | ||||
|  | ||||
| .content-container { | ||||
| @@ -326,10 +313,10 @@ h1 { | ||||
|   row-gap: 5em; | ||||
|  | ||||
|   .scroll-info { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       column-gap: 10px; | ||||
|       grid-column: 2 / -2; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     column-gap: 10px; | ||||
|     grid-column: 2 / -2; | ||||
|   } | ||||
|  | ||||
|   .chart-container { | ||||
| @@ -346,8 +333,8 @@ h1 { | ||||
|     grid-column: 2 / -2; | ||||
|   } | ||||
|  | ||||
|   .wines-container { | ||||
|     grid-column: 2 / -2; | ||||
|   .wine-container { | ||||
|     grid-column: 3 / -3; | ||||
|   } | ||||
|  | ||||
|   .icon--arrow-long-right { | ||||
| @@ -356,8 +343,7 @@ h1 { | ||||
|   } | ||||
|  | ||||
|   @include tablet { | ||||
|  | ||||
|     .scroll-info{ | ||||
|     .scroll-info { | ||||
|       grid-column: 3 / -3; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,10 @@ | ||||
|         <div class="instructions"> | ||||
|           <h1 class="title">Virtuelt lotteri</h1> | ||||
|           <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 gjerne melding om fargeønske også.</li> | ||||
|           </ol> | ||||
| @@ -15,18 +18,16 @@ | ||||
|  | ||||
|         <VippsPill class="vipps-pill mobile-only" /> | ||||
|  | ||||
|          <p class="call-to-action"> | ||||
|             <span class="vin-link">Følg med på utviklingen</span> og <span class="vin-link">chat om trekningen</span> | ||||
|             <i class="icon icon--arrow-left" @click="scrollToContent"></i></p> | ||||
|         <p class="call-to-action"> | ||||
|           <span class="vin-link" @click="scrollToContent">Følg med på utviklingen</span> og | ||||
|           <span class="vin-link" @click="scrollToContent">chat om trekningen</span> | ||||
|           <i class="icon icon--arrow-left" @click="scrollToContent"></i> | ||||
|         </p> | ||||
|       </div> | ||||
|     </header> | ||||
|  | ||||
|     <div class="container" ref="content"> | ||||
|       <WinnerDraw | ||||
|         :currentWinnerDrawn="currentWinnerDrawn" | ||||
|         :currentWinner="currentWinner" | ||||
|         :attendees="attendees" | ||||
|       /> | ||||
|       <WinnerDraw :currentWinnerDrawn="currentWinnerDrawn" :currentWinner="currentWinner" :attendees="attendees" /> | ||||
|  | ||||
|       <div class="todays-raffles"> | ||||
|         <h2>Liste av lodd kjøpt i dag</h2> | ||||
| @@ -51,15 +52,16 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="container wines-container"> | ||||
|     <div class="todays-wines"> | ||||
|       <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> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { attendees, winners, prelottery } from "@/api"; | ||||
| import Chat from "@/ui/Chat"; | ||||
| import Vipps from "@/ui/Vipps"; | ||||
| import VippsPill from "@/ui/VippsPill"; | ||||
| @@ -74,18 +76,18 @@ export default { | ||||
|   data() { | ||||
|     return { | ||||
|       attendees: [], | ||||
|       attendeesFetched: false, | ||||
|       winners: [], | ||||
|       wines: [], | ||||
|       currentWinnerDrawn: false, | ||||
|       currentWinner: null, | ||||
|       socket: null, | ||||
|       attendeesFetched: false, | ||||
|       wasDisconnected: false, | ||||
|       ticketsBought: { | ||||
|         "red": 0, | ||||
|         "blue": 0, | ||||
|         "green": 0, | ||||
|         "yellow": 0 | ||||
|         red: 0, | ||||
|         blue: 0, | ||||
|         green: 0, | ||||
|         yellow: 0 | ||||
|       } | ||||
|     }; | ||||
|   }, | ||||
| @@ -129,42 +131,45 @@ export default { | ||||
|     this.socket = null; | ||||
|   }, | ||||
|   methods: { | ||||
|     getWinners: async function() { | ||||
|       let response = await winners(); | ||||
|       if (response) { | ||||
|         this.winners = response; | ||||
|       } | ||||
|     getWinners() { | ||||
|       fetch("/api/lottery/winners") | ||||
|         .then(resp => resp.json()) | ||||
|         .then(response => (this.winners = response.winners)); | ||||
|     }, | ||||
|     getTodaysWines() { | ||||
|       prelottery() | ||||
|       fetch("/api/lottery/wines") | ||||
|         .then(resp => resp.json()) | ||||
|         .then(response => response.wines) | ||||
|         .then(wines => { | ||||
|           this.wines = wines; | ||||
|           this.todayExists = wines.length > 0; | ||||
|         }) | ||||
|         .catch(_ => this.todayExists = false) | ||||
|         .catch(_ => (this.todayExists = false)); | ||||
|     }, | ||||
|     getAttendees: async function() { | ||||
|       let response = await attendees(); | ||||
|       if (response) { | ||||
|         this.attendees = response; | ||||
|         if (this.attendees == undefined || this.attendees.length == 0) { | ||||
|           this.attendeesFetched = true; | ||||
|           return; | ||||
|         } | ||||
|         const addValueOfListObjectByKey = (list, key) => | ||||
|           list.map(object => object[key]).reduce((a, b) => a + b); | ||||
|     getAttendees() { | ||||
|       fetch("/api/lottery/attendees") | ||||
|         .then(resp => resp.json()) | ||||
|         .then(response => { | ||||
|           const { attendees } = response; | ||||
|           this.attendees = attendees || []; | ||||
|  | ||||
|         this.ticketsBought = { | ||||
|           red: addValueOfListObjectByKey(response, "red"), | ||||
|           blue: addValueOfListObjectByKey(response, "blue"), | ||||
|           green: addValueOfListObjectByKey(response, "green"), | ||||
|           yellow: addValueOfListObjectByKey(response, "yellow") | ||||
|         }; | ||||
|       } | ||||
|       this.attendeesFetched = true; | ||||
|           if (attendees == undefined || attendees.length == 0) { | ||||
|             return; | ||||
|           } | ||||
|  | ||||
|           const addValueOfListObjectByKey = (list, key) => list.map(object => object[key]).reduce((a, b) => a + b); | ||||
|  | ||||
|           this.ticketsBought = { | ||||
|             red: addValueOfListObjectByKey(attendees, "red"), | ||||
|             blue: addValueOfListObjectByKey(attendees, "blue"), | ||||
|             green: addValueOfListObjectByKey(attendees, "green"), | ||||
|             yellow: addValueOfListObjectByKey(attendees, "yellow") | ||||
|           }; | ||||
|         }) | ||||
|         .finally(_ => (this.attendeesFetched = true)); | ||||
|     }, | ||||
|     scrollToContent() { | ||||
|       console.log(window.scrollY) | ||||
|       console.log(window.scrollY); | ||||
|       const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50; | ||||
|       const { scrollY } = window; | ||||
|       let scrollHeight = intersectingHeaderHeight; | ||||
| @@ -178,14 +183,13 @@ export default { | ||||
|       }); | ||||
|     }, | ||||
|     track() { | ||||
|       window.ga('send', 'pageview', '/lottery/game'); | ||||
|       window.ga("send", "pageview", "/lottery/game"); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|  | ||||
| @import "../styles/variables.scss"; | ||||
| @import "../styles/media-queries.scss"; | ||||
|  | ||||
| @@ -201,7 +205,8 @@ export default { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(4, 1fr); | ||||
|  | ||||
|   > div, > section { | ||||
|   > div, | ||||
|   > section { | ||||
|     @include mobile { | ||||
|       grid-column: span 5; | ||||
|     } | ||||
| @@ -343,6 +348,8 @@ header { | ||||
|  | ||||
|   > div { | ||||
|     padding: 1rem; | ||||
|     max-height: 638px; | ||||
|     overflow-y: scroll; | ||||
|  | ||||
|     -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); | ||||
| @@ -369,11 +376,14 @@ header { | ||||
|   } | ||||
| } | ||||
|  | ||||
| .todays-wines { | ||||
|   width: 80vw; | ||||
|   padding: 0 10vw; | ||||
|  | ||||
| .wines-container { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   margin-bottom: 4rem; | ||||
|   @include mobile { | ||||
|     width: 90vw; | ||||
|     padding: 0 5vw; | ||||
|   } | ||||
|  | ||||
|   h2 { | ||||
|     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> | ||||
|   <div class="container"> | ||||
|     <div v-if="!posted"> | ||||
|       <h1 v-if="name">Gratulerer {{name}}!</h1> | ||||
|   <div> | ||||
|     <div v-if="!posted" class="container"> | ||||
|       <h1 v-if="name">Gratulerer {{ name }}!</h1> | ||||
|  | ||||
|       <p v-if="name"> | ||||
|         Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en. | ||||
|       </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> | ||||
|  | ||||
|       <div class="wines-container" v-if="name"> | ||||
|         <Wine :wine="wine" v-for="wine in wines" :key="wine"> | ||||
|           <button | ||||
|             @click="chooseWine(wine.name)" | ||||
|             class="vin-button select-wine" | ||||
|           >Velg denne vinnen</button> | ||||
|           <button @click="chooseWine(wine)" class="vin-button select-wine">Velg denne vinnen</button> | ||||
|         </Wine> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div v-else-if="posted" class="sent-container"> | ||||
|       <h1>Valget ditt er sendt inn!</h1> | ||||
|       <p>Du får mer info om henting snarest!</p> | ||||
| @@ -24,15 +26,13 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { getAmIWinner, postWineChosen, prelottery } from "@/api"; | ||||
| import Wine from "@/ui/Wine"; | ||||
|  | ||||
| export default { | ||||
|   components: { Wine }, | ||||
|   data() { | ||||
|     return { | ||||
|       id: null, | ||||
|       existing: false, | ||||
|       fetched: false, | ||||
|       turn: false, | ||||
|       name: null, | ||||
|       wines: [], | ||||
| @@ -40,30 +40,43 @@ export default { | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     this.id = this.$router.currentRoute.params.id; | ||||
|     const { id } = this.$router.currentRoute.params; | ||||
|  | ||||
|     let winnerObject = await getAmIWinner(this.id); | ||||
|     this.fetched = true; | ||||
|     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(); | ||||
|     this.id = id; | ||||
|     this.getPrizes(id); | ||||
|   }, | ||||
|   methods: { | ||||
|     chooseWine: async function(name) { | ||||
|       let posted = await postWineChosen(this.id, name); | ||||
|       console.log("response", posted); | ||||
|       if (posted.success) { | ||||
|         this.posted = true; | ||||
|       } | ||||
|     getPrizes(id) { | ||||
|       fetch(`/api/lottery/prize-distribution/prizes/${id}`) | ||||
|         .then(resp => resp.json()) | ||||
|         .then(response => { | ||||
|           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 { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   flex-direction: column; | ||||
|   margin-top: 2rem; | ||||
|   padding: 2rem; | ||||
|   width: 80%; | ||||
|   margin: 0 auto; | ||||
|   max-width: 2000px; | ||||
| } | ||||
|  | ||||
| .wines-container { | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .sent-container { | ||||
|   width: 100%; | ||||
|   height: 90vh; | ||||
| @@ -90,11 +113,4 @@ export default { | ||||
| .select-wine { | ||||
|   margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| .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 { | ||||
|   text-decoration: none; | ||||
|   cursor: pointer; | ||||
|   color: inherit; | ||||
| } | ||||
|  | ||||
| .title { | ||||
| @@ -51,8 +53,10 @@ a { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: space-between; | ||||
|   position: relative; | ||||
|  | ||||
|   label { | ||||
|     margin-top: 0.7rem; | ||||
|     margin-bottom: 0.25rem; | ||||
|     font-weight: 600; | ||||
|     text-transform: uppercase; | ||||
| @@ -76,6 +80,7 @@ a { | ||||
|  | ||||
|   > *:not(:last-child) { | ||||
|     margin-right: 2rem; | ||||
|     margin-bottom: 0.75rem; | ||||
|   } | ||||
|  | ||||
|   &.column { | ||||
| @@ -95,7 +100,7 @@ a { | ||||
|  | ||||
|       > *:not(:last-child) { | ||||
|         margin-right: unset; | ||||
|         margin-bottom: .75rem; | ||||
|         margin-bottom: 0.75rem; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -105,6 +110,8 @@ input, | ||||
| textarea { | ||||
|   border-radius: 0; | ||||
|   box-shadow: none; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
|   -webkit-appearance: none; | ||||
|   font-size: 1.1rem; | ||||
|   border: 1px solid rgba(#333333, 0.3); | ||||
| @@ -136,6 +143,11 @@ textarea { | ||||
|     height: auto; | ||||
|   } | ||||
|  | ||||
|   &.warning { | ||||
|     background-color: #f9826c; | ||||
|     color: white; | ||||
|   } | ||||
|  | ||||
|   &.danger { | ||||
|     background-color: $red; | ||||
|     color: white; | ||||
| @@ -151,9 +163,12 @@ textarea { | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     opacity: 0; | ||||
|     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 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); | ||||
|     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 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); | ||||
|   } | ||||
|  | ||||
|   &.active { | ||||
|     font-weight: bold; | ||||
|   } | ||||
|  | ||||
|   &:hover:not(:disabled) { | ||||
| @@ -163,7 +178,7 @@ textarea { | ||||
|       opacity: 1; | ||||
|     } | ||||
|   } | ||||
|   &:disabled{ | ||||
|   &:disabled { | ||||
|     opacity: 0.25; | ||||
|     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 { | ||||
|   &-pointer { | ||||
| @@ -193,11 +223,23 @@ textarea { | ||||
|   text-decoration: none; | ||||
|   color: $matte-text-color; | ||||
|  | ||||
|   &:focus, &:hover { | ||||
|   &:focus, | ||||
|   &:hover { | ||||
|     border-color: $link-color; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .margin { | ||||
|   &-md { | ||||
|     margin: 3rem; | ||||
|   } | ||||
|   &-sm { | ||||
|     margin: 1rem; | ||||
|   } | ||||
|   &-0 { | ||||
|     margin: 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .margin-top { | ||||
|   &-md { | ||||
| @@ -269,14 +311,29 @@ textarea { | ||||
|   margin: 0 !important; | ||||
| } | ||||
|  | ||||
| .wines-container { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||||
|   grid-gap: 2rem; | ||||
| } | ||||
|  | ||||
| .raffle-element { | ||||
|   width: 45px; | ||||
|   height: 45px; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   font-size: 0.75rem; | ||||
|   font-weight: bold; | ||||
|  | ||||
|   margin: 20px 0; | ||||
|   color: #333333; | ||||
|  | ||||
|   -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; | ||||
|  | ||||
|   &.green-raffle { | ||||
|     background-color: $light-green; | ||||
| @@ -293,11 +350,16 @@ textarea { | ||||
|   &.red-raffle { | ||||
|     background-color: $light-red; | ||||
|   } | ||||
|  | ||||
|   &:not(:last-of-type) { | ||||
|     margin-right: 1rem; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @mixin raffle { | ||||
|   padding-bottom: 50px; | ||||
|   &::before, &::after { | ||||
|   &::before, | ||||
|   &::after { | ||||
|     content: ""; | ||||
|     position: absolute; | ||||
|     left: 0; | ||||
| @@ -309,11 +371,11 @@ textarea { | ||||
|     background-position: 0 25px; | ||||
|     background-repeat: repeat-x; | ||||
|   } | ||||
|   &::after{ | ||||
|   &::after { | ||||
|     background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%); | ||||
|     background-size: 50px 50px; | ||||
|     background-position: 25px -25px; | ||||
|     bottom: -25px | ||||
|     bottom: -25px; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| @import "@/styles/media-queries.scss"; | ||||
|  | ||||
| .flex { | ||||
|   display: flex; | ||||
|  | ||||
| @@ -7,6 +9,10 @@ | ||||
|  | ||||
|   &.row { | ||||
|     flex-direction: row; | ||||
|  | ||||
|     @include mobile { | ||||
|       flex-direction: column; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.wrap { | ||||
|   | ||||
| @@ -1,21 +1,49 @@ | ||||
| $primary: #b7debd; | ||||
| body { | ||||
|   --primary: #b7debd; | ||||
|  | ||||
| $light-green: #c8f9df; | ||||
| $green: #0be881; | ||||
| $dark-green: #0ed277; | ||||
|   --light-green: #c8f9df; | ||||
|   --green: #0be881; | ||||
|   --dark-green: #0ed277; | ||||
|  | ||||
| $light-blue: #d4f2fe; | ||||
| $blue: #4bcffa; | ||||
| $dark-blue: #24acda; | ||||
|   --light-blue: #d4f2fe; | ||||
|   --blue: #4bcffa; | ||||
|   --dark-blue: #24acda; | ||||
|  | ||||
| $light-yellow: #fff6d6; | ||||
| $yellow: #ffde5d; | ||||
| $dark-yellow: #ecc31d; | ||||
|   --light-yellow: #fff6d6; | ||||
|   --yellow: #ffde5d; | ||||
|   --dark-yellow: #ecc31d; | ||||
|  | ||||
| $light-red: #fbd7de; | ||||
| $red: #ef5878; | ||||
| $dark-red: #ec3b61; | ||||
|   --light-red: #fbd7de; | ||||
|   --red: #ef5878; | ||||
|   --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> | ||||
|   <div class="attendees" v-if="attendees.length > 0"> | ||||
|     <div class="attendees-container" ref="attendees"> | ||||
|       <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> | ||||
|         <span class="attendee-name">{{ attendee.name }}</span> | ||||
|         <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 v-if="attendees.length > 0" class="attendee-container"> | ||||
|     <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> | ||||
|       <div class="attendee-info"> | ||||
|         <router-link class="attendee-name" :to="`/highscore/${attendee.name}`"> | ||||
|           {{ attendee.name }} | ||||
|         </router-link> | ||||
|  | ||||
|         <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> | ||||
| @@ -17,33 +50,79 @@ export default { | ||||
|   props: { | ||||
|     attendees: { | ||||
|       type: Array | ||||
|     }, | ||||
|     admin: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     flipList: (list) => list.slice().reverse() | ||||
|   data() { | ||||
|     return { | ||||
|       editingAttendee: undefined | ||||
|     }; | ||||
|   }, | ||||
|   watch: { | ||||
|     attendees: { | ||||
|       deep: true, | ||||
|       handler() { | ||||
|         if (this.$refs && this.$refs.history) { | ||||
|           setTimeout(() => { | ||||
|             this.$refs.attendees.scrollTop = this.$refs.attendees.scrollHeight; | ||||
|           }, 50); | ||||
|         } | ||||
|       } | ||||
|   methods: { | ||||
|     flipList: list => list.slice().reverse(), | ||||
|     updateAttendee(updatedAttendee) { | ||||
|       const options = { | ||||
|         method: "PUT", | ||||
|         headers: { "Content-Type": "application/json" }, | ||||
|         body: JSON.stringify({ attendee: updatedAttendee }) | ||||
|       }; | ||||
|  | ||||
|       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> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/global.scss"; | ||||
| @import "../styles/variables.scss"; | ||||
| @import "../styles/media-queries.scss"; | ||||
| @import "@/styles/variables.scss"; | ||||
| @import "@/styles/media-queries.scss"; | ||||
|  | ||||
| .attendee-name { | ||||
|   width: 60%; | ||||
|   font-size: 1.1rem; | ||||
| } | ||||
|  | ||||
| hr { | ||||
| @@ -51,45 +130,60 @@ hr { | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .raffle-element { | ||||
|   font-size: 0.75rem; | ||||
|   width: 45px; | ||||
|   height: 45px; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
| .attendee-container { | ||||
|   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%; | ||||
|   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 { | ||||
|   padding: 0.5rem; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   width: 100%; | ||||
|   margin: 0 auto; | ||||
|  | ||||
|   @include mobile { | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|   } | ||||
|  | ||||
|   &:not(:last-of-type) { | ||||
|     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> | ||||
|   | ||||
| @@ -5,14 +5,14 @@ | ||||
|       <img src="/public/assets/images/knowit.svg" alt="knowit logo" /> | ||||
|     </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> | ||||
|     </a> | ||||
|  | ||||
|     <nav class="menu" :class="isOpen ? 'open' : 'collapsed'" > | ||||
|       <router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link" > | ||||
|     <nav class="menu" :class="isOpen ? 'open' : 'collapsed'"> | ||||
|       <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> | ||||
|         <i class="icon icon--arrow-right"></i> | ||||
|       </router-link> | ||||
| @@ -21,8 +21,9 @@ | ||||
|     <div class="clock"> | ||||
|       <h2 v-if="!fiveMinutesLeft || !tenMinutesOver"> | ||||
|         <span v-if="days > 0">{{ pad(days) }}:</span> | ||||
|         <span>{{ pad(hours) }}</span>: | ||||
|         <span>{{ pad(minutes) }}</span>: | ||||
|         <span>{{ pad(hours) }}</span | ||||
|         >: <span>{{ pad(minutes) }}</span | ||||
|         >: | ||||
|         <span>{{ pad(seconds) }}</span> | ||||
|       </h2> | ||||
|       <h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2> | ||||
| @@ -41,7 +42,7 @@ export default { | ||||
|       minutes: 0, | ||||
|       seconds: 0, | ||||
|       distance: 0, | ||||
|       interval: null, | ||||
|       interval: null | ||||
|     }; | ||||
|   }, | ||||
|   props: { | ||||
| @@ -68,7 +69,7 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleMenu(){ | ||||
|     toggleMenu() { | ||||
|       this.isOpen = this.isOpen ? false : true; | ||||
|     }, | ||||
|     pad: function(num) { | ||||
| @@ -91,10 +92,7 @@ export default { | ||||
|       let nowDate = new Date(); | ||||
|       let now = nowDate.getTime(); | ||||
|       if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) { | ||||
|         let _diff = | ||||
|           (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * | ||||
|           60 * | ||||
|           -1; | ||||
|         let _diff = (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * 60 * -1; | ||||
|         nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff); | ||||
|       } | ||||
|       this.nextLottery = nextDayOfLottery; | ||||
| @@ -110,12 +108,8 @@ export default { | ||||
|  | ||||
|       // Time calculations for days, hours, minutes and seconds | ||||
|       this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24)); | ||||
|       this.hours = Math.floor( | ||||
|         (this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) | ||||
|       ); | ||||
|       this.minutes = Math.floor( | ||||
|         (this.distance % (1000 * 60 * 60)) / (1000 * 60) | ||||
|       ); | ||||
|       this.hours = Math.floor((this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); | ||||
|       this.minutes = Math.floor((this.distance % (1000 * 60 * 60)) / (1000 * 60)); | ||||
|       this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000); | ||||
|       if (this.days == 7) { | ||||
|         this.days = 0; | ||||
| @@ -124,7 +118,7 @@ export default { | ||||
|         this.initialize(); | ||||
|       } | ||||
|       this.interval = setTimeout(this.countdown, 500); | ||||
|     }, | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| <template> | ||||
|   <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="opaque-skirt"></div> | ||||
| @@ -8,7 +11,8 @@ | ||||
|         <button @click="loadMoreHistory">Hent eldre meldinger</button> | ||||
|       </div> | ||||
|  | ||||
|       <div class="history-message" | ||||
|       <div | ||||
|         class="history-message" | ||||
|         v-for="(history, index) in chatHistory" | ||||
|         :key="`${history.username}-${history.timestamp}-${index}`" | ||||
|       > | ||||
| @@ -61,12 +65,11 @@ export default { | ||||
|     }; | ||||
|   }, | ||||
|   created() { | ||||
|     getChatHistory(1, this.pageSize) | ||||
|       .then(resp => { | ||||
|         this.chatHistory = resp.messages; | ||||
|         this.hasMorePages = resp.total != resp.messages.length; | ||||
|       }); | ||||
|     const username = window.localStorage.getItem('username'); | ||||
|     getChatHistory(1, this.pageSize).then(resp => { | ||||
|       this.chatHistory = resp.messages; | ||||
|       this.hasMorePages = resp.total != resp.messages.length; | ||||
|     }); | ||||
|     const username = window.localStorage.getItem("username"); | ||||
|     if (username) { | ||||
|       this.username = username; | ||||
|       this.emitUsernameOnConnect = true; | ||||
| @@ -77,8 +80,7 @@ export default { | ||||
|       handler: function(newVal, oldVal) { | ||||
|         if (oldVal.length == 0) { | ||||
|           this.scrollToBottomOfHistory(); | ||||
|         } | ||||
|         else if (newVal && newVal.length == oldVal.length) { | ||||
|         } else if (newVal && newVal.length == oldVal.length) { | ||||
|           if (this.isScrollPositionAtBottom()) { | ||||
|             this.scrollToBottomOfHistory(); | ||||
|           } | ||||
| @@ -105,10 +107,7 @@ export default { | ||||
|     }); | ||||
|  | ||||
|     this.socket.on("connect", msg => { | ||||
|       if ( | ||||
|         this.emitUsernameOnConnect || | ||||
|         (this.wasDisconnected && this.username != null) | ||||
|       ) { | ||||
|       if (this.emitUsernameOnConnect || (this.wasDisconnected && this.username != null)) { | ||||
|         this.setUsername(this.username); | ||||
|       } | ||||
|     }); | ||||
| @@ -133,12 +132,11 @@ export default { | ||||
|       let { page, pageSize } = this; | ||||
|       page = page + 1; | ||||
|  | ||||
|       getChatHistory(page, pageSize) | ||||
|         .then(resp => { | ||||
|           this.chatHistory = resp.messages.concat(this.chatHistory); | ||||
|           this.page = page; | ||||
|           this.hasMorePages = resp.total != this.chatHistory.length; | ||||
|         }); | ||||
|       getChatHistory(page, pageSize).then(resp => { | ||||
|         this.chatHistory = resp.messages.concat(this.chatHistory); | ||||
|         this.page = page; | ||||
|         this.hasMorePages = resp.total != this.chatHistory.length; | ||||
|       }); | ||||
|     }, | ||||
|     pad(num) { | ||||
|       if (num > 9) return num; | ||||
| @@ -146,9 +144,7 @@ export default { | ||||
|     }, | ||||
|     getTime(timestamp) { | ||||
|       let date = new Date(timestamp); | ||||
|       const timeString = `${this.pad(date.getHours())}:${this.pad( | ||||
|         date.getMinutes() | ||||
|       )}:${this.pad(date.getSeconds())}`; | ||||
|       const timeString = `${this.pad(date.getHours())}:${this.pad(date.getMinutes())}:${this.pad(date.getSeconds())}`; | ||||
|  | ||||
|       if (date.getDate() == new Date().getDate()) { | ||||
|         return timeString; | ||||
| @@ -158,10 +154,10 @@ export default { | ||||
|     sendMessage() { | ||||
|       const message = { message: this.message }; | ||||
|       this.socket.emit("chat", message); | ||||
|       this.message = ''; | ||||
|       this.message = ""; | ||||
|       this.scrollToBottomOfHistory(); | ||||
|     }, | ||||
|     setUsername(username=undefined) { | ||||
|     setUsername(username = undefined) { | ||||
|       if (this.temporaryUsername) { | ||||
|         username = this.temporaryUsername; | ||||
|       } | ||||
| @@ -178,7 +174,7 @@ export default { | ||||
|       if (history) { | ||||
|         return history.offsetHeight + history.scrollTop >= history.scrollHeight; | ||||
|       } | ||||
|       return false | ||||
|       return false; | ||||
|     }, | ||||
|     scrollToBottomOfHistory() { | ||||
|       setTimeout(() => { | ||||
| @@ -189,15 +185,15 @@ export default { | ||||
|     scrollToMessageElement(message) { | ||||
|       const elemTimestamp = this.getTime(message.timestamp); | ||||
|       const self = this; | ||||
|       const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText; | ||||
|       const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp; | ||||
|       const getTimeStamp = elem => elem.getElementsByClassName("timestamp")[0].innerText; | ||||
|       const prevOldestMessageInNewList = elem => getTimeStamp(elem) == elemTimestamp; | ||||
|  | ||||
|       setTimeout(() => { | ||||
|         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); | ||||
|         history.scrollTop = elemInNewList.offsetTop - 70 | ||||
|         history.scrollTop = elemInNewList.offsetTop - 70; | ||||
|       }, 1); | ||||
|     } | ||||
|   } | ||||
| @@ -210,7 +206,7 @@ export default { | ||||
|  | ||||
| .chat-container { | ||||
|   position: relative; | ||||
|   transform: translate3d(0,0,0); | ||||
|   transform: translate3d(0, 0, 0); | ||||
| } | ||||
|  | ||||
| input { | ||||
| @@ -241,7 +237,6 @@ input { | ||||
|   display: flex; | ||||
| } | ||||
|  | ||||
|  | ||||
| .history { | ||||
|   height: 75%; | ||||
|   overflow-y: scroll; | ||||
| @@ -276,11 +271,7 @@ input { | ||||
|     position: fixed; | ||||
|     height: 2rem; | ||||
|     z-index: 1; | ||||
|     background: linear-gradient( | ||||
|       to bottom, | ||||
|       white, | ||||
|       rgba(255, 255, 255, 0) | ||||
|     ); | ||||
|     background: linear-gradient(to bottom, white, rgba(255, 255, 255, 0)); | ||||
|   } | ||||
|  | ||||
|   & .fetch-older-history { | ||||
| @@ -310,7 +301,7 @@ input { | ||||
|     border-radius: 4px; | ||||
|  | ||||
|     &::before { | ||||
|       content: ''; | ||||
|       content: ""; | ||||
|       position: absolute; | ||||
|       top: 2.1rem; | ||||
|       left: 2rem; | ||||
|   | ||||
| @@ -1,60 +1,48 @@ | ||||
| <template> | ||||
|   <div class="highscores" v-if="highscore.length > 0"> | ||||
|  | ||||
|     <section class="heading"> | ||||
|       <h3> | ||||
|         Topp 5 vinnere | ||||
|         Topp vinnere | ||||
|       </h3> | ||||
|       <router-link to="highscore" class=""> | ||||
|         <span class="vin-link">Se alle vinnere</span> | ||||
|       </router-link> | ||||
|     </section> | ||||
|  | ||||
|     <ol class="winner-list-container"> | ||||
|       <li v-for="(person, index) in highscore" :key="person._id" class="single-winner"> | ||||
|         <span class="placement">{{index + 1}}.</span> | ||||
|         <i class="icon icon--medal"></i> | ||||
|         <p class="winner-name">{{ person.name }}</p> | ||||
|       <li v-for="(person, index) in highscore" :key="person._id"> | ||||
|         <router-link :to="`/highscore/${person.name}`" class="single-winner"> | ||||
|           <span class="placement">{{ index + 1 }}.</span> | ||||
|           <i class="icon icon--medal"></i> | ||||
|           <p class="winner-name">{{ person.name }}</p> | ||||
|         </router-link> | ||||
|       </li> | ||||
|     </ol> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| import { highscoreStatistics } from "@/api"; | ||||
|  | ||||
| export default { | ||||
|   data() { | ||||
|     return { highscore: [] }; | ||||
|     return { | ||||
|       highscore: [], | ||||
|       limit: 22 | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     let response = await highscoreStatistics(); | ||||
|     response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1) | ||||
|     this.highscore = this.generateScoreBoard(response.slice(0, 5)); | ||||
|   }, | ||||
|   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 | ||||
|       }) | ||||
|     } | ||||
|     return fetch(`/api/history/by-wins?limit=${this.limit}`) | ||||
|       .then(resp => resp.json()) | ||||
|       .then(response => { | ||||
|         this.highscore = response.winners; | ||||
|       }); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/variables.scss"; | ||||
| @import "@/styles/variables.scss"; | ||||
| .heading { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
| @@ -81,8 +69,8 @@ ol { | ||||
|  | ||||
| .winner-list-container { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr)); | ||||
|   gap: 5%; | ||||
|   grid-template-columns: repeat(auto-fit, minmax(12em, 1fr)); | ||||
|   gap: 2rem; | ||||
|  | ||||
|   .single-winner { | ||||
|     box-sizing: border-box; | ||||
| @@ -110,11 +98,71 @@ ol { | ||||
|       grid-column: 1 / -1; | ||||
|     } | ||||
|  | ||||
|     .winner-count { | ||||
|       grid-row: 3; | ||||
|       grid-column: 1 / -1; | ||||
|       margin: 0; | ||||
|     } | ||||
|  | ||||
|     .winner-icon { | ||||
|       grid-row: 1; | ||||
|       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> | ||||
|   | ||||
| @@ -2,89 +2,47 @@ | ||||
|   <div class="chart"> | ||||
|     <canvas ref="purchase-chart" width="100" height="50"></canvas> | ||||
|     <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> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Chartjs from "chart.js"; | ||||
| import { chartPurchaseByColor } from "@/api"; | ||||
|  | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       lotteries: [], | ||||
|       years: [], | ||||
|       yearSelected: undefined, | ||||
|       chart: undefined | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     let canvas = this.$refs["purchase-chart"].getContext("2d"); | ||||
|  | ||||
|     let response = await chartPurchaseByColor(); | ||||
|     let labels = []; | ||||
|     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: [] | ||||
|     this.lotteries = await this.chartPurchaseByColor(); | ||||
|     if (this.lotteries?.length) this.years = [...new Set(this.lotteries.map(lot => lot.date.slice(0, 4)))]; | ||||
|  | ||||
|     const dataset = this.calculateChartDatapoints(); | ||||
|  | ||||
|     let chartData = { | ||||
|       labels: dataset.labels, | ||||
|       datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow] | ||||
|     }; | ||||
|  | ||||
|     if (response.length == 1) { | ||||
|       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, { | ||||
|     this.chart = new Chart(canvas, { | ||||
|       type: "line", | ||||
|       data: chartdata, | ||||
|       data: chartData, | ||||
|       options: { | ||||
|         maintainAspectRatio: false, | ||||
|         animation: { | ||||
| @@ -110,8 +68,7 @@ export default { | ||||
|           yAxes: [ | ||||
|             { | ||||
|               ticks: { | ||||
|                 beginAtZero: true, | ||||
|                 suggestedMax: highestNumber + 5 | ||||
|                 beginAtZero: true | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
| @@ -120,10 +77,82 @@ export default { | ||||
|     }); | ||||
|   }, | ||||
|   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) { | ||||
|       return `${this.pad(date.getDate())}.${this.pad( | ||||
|         date.getMonth() + 1 | ||||
|       )}.${this.pad(date.getYear() - 100)}`; | ||||
|       return `${this.pad(date.getDate())}.${this.pad(date.getMonth() + 1)}.${this.pad(date.getYear() - 100)}`; | ||||
|     }, | ||||
|     pad(num) { | ||||
|       if (num < 10) { | ||||
| @@ -136,11 +165,19 @@ export default { | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/media-queries.scss"; | ||||
| @import "@/styles/media-queries.scss"; | ||||
|  | ||||
| .chart { | ||||
|   height: 40vh; | ||||
|   max-height: 500px; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .year-select { | ||||
|   margin-top: 1rem; | ||||
|  | ||||
|   button:not(:first-of-type) { | ||||
|     margin-left: 0.5rem; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,28 +2,28 @@ | ||||
|   <div class="container"> | ||||
|     <div class="input-line"> | ||||
|       <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="checkmark"></span> | ||||
|         </span> | ||||
|         <span class="text">Rød</span> | ||||
|       </label> | ||||
|       <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="checkmark"></span> | ||||
|         </span> | ||||
|         <span class="text">Blå</span> | ||||
|       </label> | ||||
|       <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="checkmark"></span> | ||||
|         </span> | ||||
|         <span class="text">Gul</span> | ||||
|       </label> | ||||
|       <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="checkmark"></span> | ||||
|         </span> | ||||
| @@ -31,15 +31,10 @@ | ||||
|       </label> | ||||
|     </div> | ||||
|     <div class="input-line"> | ||||
|       <input | ||||
|         type="number" | ||||
|         placeholder="Antall lodd" | ||||
|         @keyup.enter="generateColors" | ||||
|         v-model="numberOfRaffles" | ||||
|       /> | ||||
|       <input type="number" placeholder="Antall lodd" @keyup.enter="generateColors" v-model="numberOfRaffles" /> | ||||
|       <button class="vin-button" @click="generateColors">Generer</button> | ||||
|     </div> | ||||
|     <div class="colors"> | ||||
|     <div class="colors" :class="{ compact }"> | ||||
|       <div | ||||
|         v-for="color in colors" | ||||
|         :class="getColorClass(color)" | ||||
| @@ -47,13 +42,6 @@ | ||||
|         :style="{ transform: 'rotate(' + getRotation() + 'deg)' }" | ||||
|       ></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> | ||||
| </template> | ||||
|  | ||||
| @@ -64,11 +52,15 @@ export default { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false | ||||
|     }, | ||||
|     compact: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       numberOfRaffles: 4, | ||||
|       numberOfRaffles: 6, | ||||
|       colors: [], | ||||
|       blue: 0, | ||||
|       red: 0, | ||||
| @@ -101,18 +93,21 @@ export default { | ||||
|       if (time == 5) { | ||||
|         this.generating = false; | ||||
|         this.generated = true; | ||||
|         if (this.numberOfRaffles > 1 && | ||||
|           [this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) { | ||||
|           return | ||||
|         if ( | ||||
|           this.numberOfRaffles > 1 && | ||||
|           [this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true) | ||||
|             .length == 1 | ||||
|         ) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (new Set(this.colors).size == 1) { | ||||
|           alert("BINGO"); | ||||
|         } | ||||
|  | ||||
|         this.emitColors() | ||||
|         this.emitColors(); | ||||
|  | ||||
|         window.ga('send', { | ||||
|         window.ga("send", { | ||||
|           hitType: "event", | ||||
|           eventCategory: "Raffles", | ||||
|           eventAction: "Generate", | ||||
| @@ -147,8 +142,7 @@ export default { | ||||
|       } | ||||
|       if (this.numberOfRaffles > 0) { | ||||
|         for (let i = 0; i < this.numberOfRaffles; i++) { | ||||
|           let color = | ||||
|             randomArray[Math.floor(Math.random() * randomArray.length)]; | ||||
|           let color = randomArray[Math.floor(Math.random() * randomArray.length)]; | ||||
|           this.colors.push(color); | ||||
|           if (color == 1) { | ||||
|             this.red += 1; | ||||
| @@ -201,12 +195,12 @@ export default { | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/variables.scss"; | ||||
| @import "../styles/global.scss"; | ||||
| @import "../styles/media-queries.scss"; | ||||
| @import "@/styles/variables.scss"; | ||||
| @import "@/styles/global.scss"; | ||||
| @import "@/styles/media-queries.scss"; | ||||
|  | ||||
| .container { | ||||
|   margin: auto; | ||||
|   // margin: auto; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
| @@ -282,6 +276,15 @@ label .text { | ||||
|   max-width: 1400px; | ||||
|   margin: 3rem auto 0; | ||||
|  | ||||
|   &.compact { | ||||
|     margin-top: 0.5rem; | ||||
|  | ||||
|     > .color-box { | ||||
|       width: 100px; | ||||
|       height: 100px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @include mobile { | ||||
|     margin: 1.8rem auto 0; | ||||
|   } | ||||
| @@ -309,20 +312,6 @@ label .text { | ||||
|   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 { | ||||
|   background-color: $light-green; | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|       <div class="flex justify-end"> | ||||
|         <div class="requested-count cursor-pointer" @click="request"> | ||||
|           <span>{{ requestedElement.count }}</span> | ||||
|           <i class="icon icon--heart" :class="{ 'active': locallyRequested }" /> | ||||
|           <i class="icon icon--heart" :class="{ active: locallyRequested }" /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </template> | ||||
| @@ -17,10 +17,9 @@ | ||||
|  | ||||
|     <template v-slot:bottom> | ||||
|       <div class="float-left request"> | ||||
|         <i class="icon icon--heart request-icon" :class="{ 'active': locallyRequested }"></i> | ||||
|         <a aria-role="button" tabindex="0" class="link" @click="request" | ||||
|            :class="{ 'active': locallyRequested }"> | ||||
|           {{ locallyRequested ? 'Anbefalt' : 'Anbefal' }} | ||||
|         <i class="icon icon--heart request-icon" :class="{ active: locallyRequested }"></i> | ||||
|         <a aria-role="button" tabindex="0" class="link" @click="request" :class="{ active: locallyRequested }"> | ||||
|           {{ locallyRequested ? "Anbefalt" : "Anbefal" }} | ||||
|         </a> | ||||
|       </div> | ||||
|     </template> | ||||
| @@ -35,14 +34,14 @@ export default { | ||||
|   components: { | ||||
|     Wine | ||||
|   }, | ||||
|   data(){ | ||||
|   data() { | ||||
|     return { | ||||
|       wine: this.requestedElement.wine, | ||||
|       locallyRequested: false | ||||
|     } | ||||
|     }; | ||||
|   }, | ||||
|   props: { | ||||
|     requestedElement: { | ||||
|     requestedElement: { | ||||
|       required: true, | ||||
|       type: Object | ||||
|     }, | ||||
| @@ -53,27 +52,26 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     request(){ | ||||
|       if (this.locallyRequested) | ||||
|         return | ||||
|       console.log("requesting", this.wine) | ||||
|       this.locallyRequested = true | ||||
|       this.requestedElement.count = this.requestedElement.count +1 | ||||
|       requestNewWine(this.wine) | ||||
|     request() { | ||||
|       if (this.locallyRequested) return; | ||||
|  | ||||
|       this.locallyRequested = true; | ||||
|       this.requestedElement.count = this.requestedElement.count + 1; | ||||
|       requestNewWine(this.wine); | ||||
|     }, | ||||
|     async deleteWine() { | ||||
|       const wine = this.wine | ||||
|       const wine = this.wine; | ||||
|       if (window.confirm("Er du sikker på at du vil slette vinen?")) { | ||||
|         let response = await deleteRequestedWine(wine); | ||||
|         if (response['success'] == true) { | ||||
|           this.$emit('wineDeleted', wine); | ||||
|         if (response["success"] == true) { | ||||
|           this.$emit("wineDeleted", wine); | ||||
|         } else { | ||||
|           alert("Klarte ikke slette vinen"); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @@ -83,7 +81,7 @@ export default { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-top: -0.5rem; | ||||
|   background-color: rgb(244,244,244); | ||||
|   background-color: rgb(244, 244, 244); | ||||
|   border-radius: 1.1rem; | ||||
|   padding: 0.25rem 1rem; | ||||
|   font-size: 1.25em; | ||||
| @@ -93,14 +91,14 @@ export default { | ||||
|     line-height: 1.25em; | ||||
|   } | ||||
|  | ||||
|   .icon--heart{ | ||||
|   .icon--heart { | ||||
|     color: grey; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .active { | ||||
|   &.link { | ||||
|     border-color: $link-color | ||||
|     border-color: $link-color; | ||||
|   } | ||||
|  | ||||
|   &.icon--heart { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div> | ||||
|   <div id="camera-stream"> | ||||
|     <h2 v-if="errorMessage">{{ errorMessage }}</h2> | ||||
|     <video playsinline autoplay class="hidden"></video> | ||||
|   </div> | ||||
| @@ -47,13 +47,8 @@ export default { | ||||
|       this.searchVideoForBarcode(this.video); | ||||
|     }, | ||||
|     handleError(error) { | ||||
|       console.log( | ||||
|         "navigator.MediaDevices.getUserMedia error: ", | ||||
|         error.message, | ||||
|         error.name | ||||
|       ); | ||||
|       this.errorMessage = | ||||
|         "Feil ved oppstart av kamera! Feilmelding: " + error.message; | ||||
|       console.log("navigator.MediaDevices.getUserMedia error: ", error.message, error.name); | ||||
|       this.errorMessage = "Feil ved oppstart av kamera! Feilmelding: " + error.message; | ||||
|     }, | ||||
|     searchVideoForBarcode(video) { | ||||
|       const codeReader = new BrowserBarcodeReader(); | ||||
| @@ -84,10 +79,7 @@ export default { | ||||
|       this.errorMessage = "Feil! " + error.message || error; | ||||
|     }, | ||||
|     scrollIntoView() { | ||||
|       window.scrollTo( | ||||
|         0, | ||||
|         document.getElementById("addwine-title").offsetTop - 10 | ||||
|       ); | ||||
|       window.scrollTo(0, document.getElementById("camera-stream").offsetTop - 10); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -1,51 +1,80 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <div class="tab-container"> | ||||
|       <div | ||||
|     <nav class="tab-container"> | ||||
|       <a | ||||
|         class="tab" | ||||
|         v-for="(tab, index) in tabs" | ||||
|         :key="index" | ||||
|         @click="changeTab(index)" | ||||
|         @keydown.enter="changeTab(index)" | ||||
|         tabindex="0" | ||||
|         :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"> | ||||
|       <component :is="tabs[chosenTab].component" /> | ||||
|       <component :is="tabs[chosenTab].component" @counter="updateCounter" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import eventBus from "@/mixins/EventBus"; | ||||
| export default { | ||||
|   props: { | ||||
|     tabs: { | ||||
|       type: Array | ||||
|     }, | ||||
|     active: { | ||||
|       type: Number, | ||||
|       default: 0 | ||||
|     } | ||||
|   }, | ||||
|   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() { | ||||
|     return { | ||||
|       chosenTab: 0 | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     activeTab() { | ||||
|       return this.tabs[this.chosenTab]; | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     changeTab: function(num) { | ||||
|     changeTab(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> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "@/styles/variables.scss"; | ||||
| @import "@/styles/media-queries.scss"; | ||||
|  | ||||
| h1 { | ||||
|   text-align: center; | ||||
| } | ||||
| @@ -54,28 +83,50 @@ h1 { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   justify-content: center; | ||||
|   margin-top: 25px; | ||||
|   border-bottom: 1px solid #333333; | ||||
|   // margin-top: 25px; | ||||
|   border-bottom: 1px solid var(--underlinenav-text); | ||||
|  | ||||
|   margin-top: 2rem; | ||||
|  | ||||
|   @include mobile { | ||||
|     flex-direction: column; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .tab { | ||||
|   cursor: pointer; | ||||
|   font-size: 1.2rem; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   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; | ||||
|   font-size: 1.1rem; | ||||
|   padding: 8px 16px; | ||||
|   border-bottom: 2px solid transparent; | ||||
|   color: rgba($matte-text-color, 0.9); | ||||
|  | ||||
|   &.active { | ||||
|     border-bottom: 1px solid white; | ||||
|     color: $matte-text-color; | ||||
|     border-color: var(--underlinenav-text-active) !important; | ||||
|     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> | ||||
|   | ||||
| @@ -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> | ||||
|   <section class="outer-bought"> | ||||
|   <div> | ||||
|     <h3>Loddstatistikk</h3> | ||||
|  | ||||
|     <div class="total-raffles"> | ||||
|         Totalt  | ||||
|         <span class="total">{{ total }}</span> | ||||
|          kjøpte,  | ||||
|         <span>{{ totalWin }} vinn og </span> | ||||
|         <span> {{ stolen }} stjålet </span> | ||||
|       Totalt  | ||||
|       <span class="total">{{ total }}</span> | ||||
|        kjøpte,  | ||||
|       <span>{{ totalWin }} vinn og </span> | ||||
|       <span> {{ stolen }} stjålet </span> | ||||
|     </div> | ||||
|  | ||||
|  | ||||
|     <div class="bought-container"> | ||||
|       <div | ||||
|         v-for="color in colors" | ||||
|         :class=" | ||||
|           color.name + | ||||
|             '-container ' + | ||||
|             color.name + | ||||
|             '-raffle raffle-element-local' | ||||
|         " | ||||
|         :class="color.name + '-container ' + color.name + '-raffle raffle-element-local'" | ||||
|         :key="color.name" | ||||
|         > | ||||
|         <p class="winner-chance"> | ||||
|           {{translate(color.name)}} vinnersjanse | ||||
|         </p> | ||||
|       > | ||||
|         <p class="winner-chance">{{ translate(color.name) }} vinnersjanse</p> | ||||
|         <span class="win-percentage">{{ color.totalPercentage }}% </span> | ||||
|         <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> | ||||
|   </section> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { colorStatistics } from "@/api"; | ||||
|  | ||||
| @@ -45,109 +38,128 @@ export default { | ||||
|       green: 0, | ||||
|       total: 0, | ||||
|       totalWin: 0, | ||||
|       stolen: 0, | ||||
|       wins: 0, | ||||
|       redPercentage: 0, | ||||
|       yellowPercentage: 0, | ||||
|       greenPercentage: 0, | ||||
|       bluePercentage: 0 | ||||
|       stolen: 0 | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     let response = await colorStatistics(); | ||||
|  | ||||
|     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)); | ||||
|     this.allLotteries().then(this.computeColors); | ||||
|   }, | ||||
|   methods: { | ||||
|     translate(color){ | ||||
|       switch(color) { | ||||
|     allLotteries() { | ||||
|       return fetch("/api/lotteries?includeWinners=true") | ||||
|         .then(resp => resp.json()) | ||||
|         .then(response => response.lotteries); | ||||
|     }, | ||||
|     translate(color) { | ||||
|       switch (color) { | ||||
|         case "blue": | ||||
|           return "Blå" | ||||
|           return "Blå"; | ||||
|           break; | ||||
|         case "red": | ||||
|           return "Rød" | ||||
|           return "Rød"; | ||||
|           break; | ||||
|         case "green": | ||||
|           return "Grønn" | ||||
|           return "Grønn"; | ||||
|           break; | ||||
|         case "yellow": | ||||
|           return "Gul" | ||||
|           return "Gul"; | ||||
|           break; | ||||
|           break; | ||||
|         break; | ||||
|       } | ||||
|     }, | ||||
|     getPercentage: function(win, total) { | ||||
|       return this.round(win == 0 ? 0 : (win / total) * 100); | ||||
|     }, | ||||
|     round: function(number) { | ||||
|  | ||||
|       //this can make the odds added together more than 100%, maybe rework? | ||||
|       let actualPercentage = Math.round(number * 100) / 100; | ||||
|       let rounded = actualPercentage.toFixed(0); | ||||
|       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> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/variables.scss"; | ||||
| @import "../styles/media-queries.scss"; | ||||
| @import "../styles/global.scss"; | ||||
| @import "@/styles/variables.scss"; | ||||
| @import "@/styles/media-queries.scss"; | ||||
| @import "@/styles/global.scss"; | ||||
|  | ||||
| @include mobile{ | ||||
| @include mobile { | ||||
|   section { | ||||
|     margin-top: 5em; | ||||
|   } | ||||
| @@ -182,7 +194,7 @@ export default { | ||||
|         margin-top: 40px; | ||||
|       } | ||||
|  | ||||
|       &.total-bought-color{ | ||||
|       &.total-bought-color { | ||||
|         font-weight: bold; | ||||
|         margin-top: 25px; | ||||
|       } | ||||
|   | ||||
| @@ -5,13 +5,17 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { chartWinsByColor } from "@/api"; | ||||
|  | ||||
| export default { | ||||
|   methods: { | ||||
|     fetchWinsByColor() { | ||||
|       return fetch("/api/history/by-color").then(resp => resp.json()); | ||||
|     } | ||||
|   }, | ||||
|   async mounted() { | ||||
|     let canvas = this.$refs["win-chart"].getContext("2d"); | ||||
|  | ||||
|     let response = await chartWinsByColor(); | ||||
|     let response = await this.fetchWinsByColor(); | ||||
|     const { colors } = response; | ||||
|     let labels = ["Vunnet"]; | ||||
|     let blue = { | ||||
|       label: "Blå", | ||||
| @@ -42,23 +46,26 @@ export default { | ||||
|       data: [] | ||||
|     }; | ||||
|  | ||||
|     blue.data.push(response.blue.win); | ||||
|     yellow.data.push(response.yellow.win); | ||||
|     red.data.push(response.red.win); | ||||
|     green.data.push(response.green.win); | ||||
|     const findColorWinners = (colorSelect, colors) => { | ||||
|       return colors.filter(color => color.color == colorSelect)[0].count; | ||||
|     }; | ||||
|  | ||||
|     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; | ||||
|     if (response.blue.win > highestNumber) { | ||||
|       highestNumber = response.blue.win; | ||||
|     } | ||||
|     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; | ||||
|     } | ||||
|     [blueWinCount, redWinCount, greenWinCount, greenWinCount].forEach(winCount => { | ||||
|       if (winCount > highestNumber) { | ||||
|         highestNumber = winCount; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     let datasets = [blue, yellow, green, red]; | ||||
|     let chartdata = { | ||||
| @@ -102,8 +109,6 @@ export default { | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../styles/media-queries.scss"; | ||||
|  | ||||
| .chart { | ||||
|   height: 40vh; | ||||
|   max-height: 500px; | ||||
|   | ||||
| @@ -2,10 +2,7 @@ | ||||
|   <div class="wine"> | ||||
|     <slot name="top"></slot> | ||||
|     <div class="wine-image"> | ||||
|       <img | ||||
|         v-if="wine.image && loadImage" | ||||
|         :src="wine.image" | ||||
|       /> | ||||
|       <img v-if="wine.image && loadImage" :src="wine.image" /> | ||||
|       <img v-else class="wine-placeholder" alt="Wine image" /> | ||||
|     </div> | ||||
|  | ||||
| @@ -38,7 +35,7 @@ export default { | ||||
|   data() { | ||||
|     return { | ||||
|       loadImage: false | ||||
|     } | ||||
|     }; | ||||
|   }, | ||||
|   methods: { | ||||
|     setImage(entries) { | ||||
| @@ -53,7 +50,7 @@ export default { | ||||
|     this.observer = new IntersectionObserver(this.setImage, { | ||||
|       root: this.$el, | ||||
|       threshold: 0 | ||||
|     }) | ||||
|     }); | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.observer.observe(this.$el); | ||||
| @@ -66,16 +63,17 @@ export default { | ||||
| @import "@/styles/variables"; | ||||
|  | ||||
| .wine { | ||||
|   align-self: flex-start; | ||||
|   padding: 1rem; | ||||
|   box-sizing: border-box; | ||||
|   position: relative; | ||||
|   -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); | ||||
|   width: 100%; | ||||
|  | ||||
|   @include tablet { | ||||
|     width: 250px; | ||||
|     height: 100%; | ||||
|     max-width: 280px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -85,19 +83,18 @@ export default { | ||||
|   margin-top: 10px; | ||||
|  | ||||
|   img { | ||||
|     height: 250px; | ||||
|     height: 280px; | ||||
|     @include mobile { | ||||
|       object-fit: cover; | ||||
|       max-width: 90px; | ||||
|     } | ||||
|   } | ||||
|   .wine-placeholder { | ||||
|     height: 250px; | ||||
|     height: 280px; | ||||
|     width: 70px; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| .wine-details { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| @@ -107,7 +104,7 @@ export default { | ||||
|   } | ||||
| } | ||||
|  | ||||
| .wine-name{ | ||||
| .wine-name { | ||||
|   font-size: 20px; | ||||
|   margin: 1em 0; | ||||
| } | ||||
| @@ -120,6 +117,7 @@ export default { | ||||
| .bottom-section { | ||||
|   width: 100%; | ||||
|   margin-top: 1rem; | ||||
|   align-self: flex-end; | ||||
|  | ||||
|   .link { | ||||
|     color: $matte-text-color; | ||||
|   | ||||
| @@ -2,18 +2,18 @@ | ||||
|   <div v-if="wines.length > 0" class="wines-main-container"> | ||||
|     <div class="info-and-link"> | ||||
|       <h3> | ||||
|         Topp 5 viner | ||||
|         Topp viner | ||||
|       </h3> | ||||
|       <router-link to="viner"> | ||||
|         <span class="vin-link">Se alle viner </span> | ||||
|       </router-link> | ||||
|     </div> | ||||
|     <div class="wine-container"> | ||||
|     <div class="wines-container"> | ||||
|       <Wine v-for="wine in wines" :key="wine" :wine="wine"> | ||||
|         <template v-slot:top> | ||||
|           <div class="flex justify-end"> | ||||
|             <div class="requested-count cursor-pointer"> | ||||
|               <span> {{ wine.occurences }} </span> | ||||
|               <span> {{ wine.occurences }} </span> | ||||
|               <i class="icon icon--heart" /> | ||||
|             </div> | ||||
|           </div> | ||||
| @@ -35,29 +35,33 @@ export default { | ||||
|     return { | ||||
|       wines: [], | ||||
|       clickedWine: null, | ||||
|       limit: 18 | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     let response = await overallWineStatistics(); | ||||
|  | ||||
|     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); | ||||
|     this.getAllWines(); | ||||
|   }, | ||||
|   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() { | ||||
|       var fields = [], | ||||
|         n_fields = arguments.length, | ||||
| @@ -125,42 +129,72 @@ export default { | ||||
| <style lang="scss" scoped> | ||||
| @import "@/styles/variables.scss"; | ||||
| @import "@/styles/global.scss"; | ||||
| @import "../styles/media-queries.scss"; | ||||
| @import "@/styles/media-queries.scss"; | ||||
|  | ||||
| .wines-main-container { | ||||
|   margin-bottom: 10em; | ||||
| } | ||||
|  | ||||
| .info-and-link{ | ||||
| .info-and-link { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
| } | ||||
|  | ||||
| .wine-container { | ||||
|   display: grid; | ||||
|   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||||
|   grid-gap: 2rem; | ||||
| .requested-count { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-top: -0.5rem; | ||||
|   background-color: rgb(244, 244, 244); | ||||
|   border-radius: 1.1rem; | ||||
|   padding: 0.25rem 1rem; | ||||
|   font-size: 1.25em; | ||||
|  | ||||
|   .requested-count { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     margin-top: -0.5rem; | ||||
|     background-color: rgb(244,244,244); | ||||
|     border-radius: 1.1rem; | ||||
|     padding: 0.25rem 1rem; | ||||
|     font-size: 1.25em; | ||||
|  | ||||
|     span { | ||||
|       padding-right: 0.5rem; | ||||
|       line-height: 1.25em; | ||||
|     } | ||||
|     .icon--heart{ | ||||
|       font-size: 1.5rem; | ||||
|       color: $link-color; | ||||
|     } | ||||
|   span { | ||||
|     padding-right: 0.5rem; | ||||
|     line-height: 1.25em; | ||||
|   } | ||||
|   .icon--heart { | ||||
|     font-size: 1.5rem; | ||||
|     color: var(--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> | ||||
|   | ||||
| @@ -85,9 +85,7 @@ export default { | ||||
|         this.startConfetti(this.currentName); | ||||
|         return; | ||||
|       } | ||||
|       this.currentName = this.attendees[ | ||||
|         this.nameRounds % this.attendees.length | ||||
|       ].name; | ||||
|       this.currentName = this.attendees[this.nameRounds % this.attendees.length].name; | ||||
|       this.nameRounds += 1; | ||||
|       clearTimeout(this.nameTimeout); | ||||
|       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. | ||||
|       var duration = 7 * 1000; | ||||
|       var animationEnd = Date.now() + duration; | ||||
|       var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20}; | ||||
|       var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35} | ||||
|       var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20 }; | ||||
|       var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35 }; | ||||
|  | ||||
|       function randomInRange(min, max) { | ||||
|         return Math.random() * (max - min) + min; | ||||
| @@ -148,27 +146,27 @@ export default { | ||||
|         var timeLeft = animationEnd - Date.now(); | ||||
|         if (timeLeft <= 0) { | ||||
|           self.drawing = false; | ||||
|           console.time("drawing finished") | ||||
|           console.time("drawing finished"); | ||||
|           return clearInterval(interval); | ||||
|         } | ||||
|         if (currentName == "Amund Brandsrud") { | ||||
|           runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135}); | ||||
|           runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45}); | ||||
|           runCannon(uberDefaults, {y: 1 }, {angle: 90}); | ||||
|           runCannon(uberDefaults, {x: 0 }, {angle: 45}); | ||||
|           runCannon(uberDefaults, {x: 1 }, {angle: 135}); | ||||
|           runCannon(uberDefaults, { x: 1, y: 1 }, { angle: 135 }); | ||||
|           runCannon(uberDefaults, { x: 0, y: 1 }, { angle: 45 }); | ||||
|           runCannon(uberDefaults, { y: 1 }, { angle: 90 }); | ||||
|           runCannon(uberDefaults, { x: 0 }, { angle: 45 }); | ||||
|           runCannon(uberDefaults, { x: 1 }, { angle: 135 }); | ||||
|         } else { | ||||
|           runCannon(defaults, {x: 0 }, {angle: 45}); | ||||
|           runCannon(defaults, {x: 1 }, {angle: 135}); | ||||
|           runCannon(defaults, {y: 1 }, {angle: 90}); | ||||
|           runCannon(defaults, { x: 0 }, { angle: 45 }); | ||||
|           runCannon(defaults, { x: 1 }, { angle: 135 }); | ||||
|           runCannon(defaults, { y: 1 }, { angle: 90 }); | ||||
|         } | ||||
|       }, 250); | ||||
|  | ||||
|       function runCannon(confettiDefaultValues, originPoint, launchAngle){ | ||||
|         confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle)) | ||||
|       function runCannon(confettiDefaultValues, originPoint, launchAngle) { | ||||
|         confetti(Object.assign({}, confettiDefaultValues, { origin: originPoint }, launchAngle)); | ||||
|       } | ||||
|     }, | ||||
|     ordinalNumber(number=this.currentWinnerLocal.winnerCount) { | ||||
|     ordinalNumber(number = this.currentWinnerLocal.winnerCount) { | ||||
|       const dictonary = { | ||||
|         1: "første", | ||||
|         2: "andre", | ||||
| @@ -187,7 +185,6 @@ export default { | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| <template> | ||||
|   <section> | ||||
|     <h2>{{ title ? title : 'Vinnere' }}</h2> | ||||
|     <h2>{{ title ? title : "Vinnere" }}</h2> | ||||
|     <div class="winning-raffles" v-if="winners.length > 0"> | ||||
|       <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> | ||||
|         </router-link> | ||||
|       </div> | ||||
| @@ -26,7 +26,7 @@ export default { | ||||
|       type: Array | ||||
|     }, | ||||
|     drawing: { | ||||
|       type: Boolean, | ||||
|       type: Boolean | ||||
|     }, | ||||
|     title: { | ||||
|       type: String, | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
|  | ||||
| const dateString = (date) => { | ||||
|   if (typeof(date) == "string") { | ||||
| const dateString = date => { | ||||
|   if (typeof date == "string") { | ||||
|     date = new Date(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 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}` | ||||
| } | ||||
|   return `${ye}-${mo}-${da}`; | ||||
| }; | ||||
|  | ||||
| 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); | ||||
| } | ||||
|  | ||||
| @@ -20,8 +19,4 @@ function daysAgo(date) { | ||||
|   return Math.round(Math.abs((new Date() - new Date(date)) / day)); | ||||
| } | ||||
|  | ||||
| export { | ||||
|   dateString, | ||||
|   humanReadableDate, | ||||
|   daysAgo | ||||
| } | ||||
| export { dateString, humanReadableDate, daysAgo }; | ||||
|   | ||||
| @@ -3,43 +3,47 @@ import VueRouter from "vue-router"; | ||||
| import { routes } from "@/router.js"; | ||||
| import Vinlottis from "@/Vinlottis"; | ||||
|  | ||||
| import Toast from "@/plugins/Toast"; | ||||
|  | ||||
| import * as Sentry from "@sentry/browser"; | ||||
| import { Vue as VueIntegration } from "@sentry/integrations"; | ||||
|  | ||||
| Vue.use(VueRouter); | ||||
|  | ||||
| // Plugins | ||||
| Vue.use(Toast); | ||||
|  | ||||
| const ENV = window.location.href.includes("localhost") ? "development" : "production"; | ||||
| if (ENV !== "development") { | ||||
|   Sentry.init({ | ||||
|     dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091", | ||||
|     integrations: [ | ||||
|       new VueIntegration({ Vue }) | ||||
|     ], | ||||
|     integrations: [new VueIntegration({ Vue })], | ||||
|     beforeSend: event => { | ||||
|       console.error(event); | ||||
|       return event; | ||||
|     } | ||||
|   }) | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Add global GA variables | ||||
| window.ga = window.ga || function(){ | ||||
|   window.ga.q = window.ga.q || []; | ||||
|   window.ga.q.push(arguments); | ||||
| }; | ||||
| window.ga = | ||||
|   window.ga || | ||||
|   function() { | ||||
|     window.ga.q = window.ga.q || []; | ||||
|     window.ga.q.push(arguments); | ||||
|   }; | ||||
| ga.l = 1 * new Date(); | ||||
|  | ||||
| // Initiate | ||||
| ga('create', __GA_TRACKINGID__, { | ||||
|   'allowAnchor': false, | ||||
|   'cookieExpires': __GA_COOKIELIFETIME__, // Time in seconds | ||||
|   'cookieFlags': 'SameSite=Strict; Secure' | ||||
| ga("create", __GA_TRACKINGID__, { | ||||
|   allowAnchor: false, | ||||
|   cookieExpires: __GA_COOKIELIFETIME__, // Time in seconds | ||||
|   cookieFlags: "SameSite=Strict; Secure" | ||||
| }); | ||||
| ga('set', 'anonymizeIp', true); // Enable IP Anonymization/IP masking | ||||
| ga('send', 'pageview'); | ||||
| ga("set", "anonymizeIp", true); // Enable IP Anonymization/IP masking | ||||
| ga("send", "pageview"); | ||||
|  | ||||
| if (ENV == 'development') | ||||
|   window[`ga-disable-${__GA_TRACKINGID__}`] = true; | ||||
| if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true; | ||||
|  | ||||
| const router = new VueRouter({ | ||||
|   routes: routes | ||||
|   | ||||
| @@ -6,9 +6,9 @@ | ||||
|   "scripts": { | ||||
|     "build": "cross-env NODE_ENV=production 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-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" | ||||
|   }, | ||||
|   "author": "", | ||||
|   | ||||
							
								
								
									
										40
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								server.js
									
									
									
									
									
								
							| @@ -18,24 +18,36 @@ const MongoStore = require("connect-mongo")(session); | ||||
| // mongoose / database | ||||
| console.log("Trying to connect with mongodb.."); | ||||
| mongoose.promise = global.Promise; | ||||
| mongoose.connect("mongodb://localhost/vinlottis", { | ||||
|   useCreateIndex: true, | ||||
|   useNewUrlParser: true, | ||||
|   useUnifiedTopology: true, | ||||
|   serverSelectionTimeoutMS: 10000 // initial connection timeout | ||||
| }).then(_ => console.log("Mongodb connection established!")) | ||||
| .catch(err => { | ||||
|   console.log(err); | ||||
|   console.error("ERROR! Mongodb required to run."); | ||||
|   process.exit(1); | ||||
| }) | ||||
| mongoose | ||||
|   .connect("mongodb://localhost/vinlottis", { | ||||
|     useCreateIndex: true, | ||||
|     useNewUrlParser: true, | ||||
|     useUnifiedTopology: true, | ||||
|     serverSelectionTimeoutMS: 10000 // initial connection timeout | ||||
|   }) | ||||
|   .then(_ => console.log("Mongodb connection established!")) | ||||
|   .catch(err => { | ||||
|     console.log(err); | ||||
|     console.error("ERROR! Mongodb required to run."); | ||||
|     process.exit(1); | ||||
|   }); | ||||
| mongoose.set("debug", false); | ||||
|  | ||||
| // middleware | ||||
| const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS")); | ||||
| const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders")); | ||||
| app.use(setupCORS) | ||||
| app.use(setupHeaders) | ||||
| app.use(setupCORS); | ||||
| 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 | ||||
| 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 LocalStrategy = require("passport-local"); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user