Merge pull request #75 from KevinMidboe/feat/controllers

Feat/controllers - refactor entire backend and new admin interface
This commit is contained in:
2021-02-19 01:19:52 +01:00
committed by GitHub
81 changed files with 6097 additions and 3453 deletions

81
api/attendee.js Normal file
View 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
};

View File

@@ -1,5 +1,6 @@
const path = require("path"); const path = require("path");
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis")); const { history, clearHistory } = require(path.join(__dirname + "/../redis"));
console.log("loading chat");
const getAllHistory = (req, res) => { const getAllHistory = (req, res) => {
let { page, limit } = req.query; let { page, limit } = req.query;
@@ -8,19 +9,23 @@ const getAllHistory = (req, res) => {
return history(page, limit) return history(page, limit)
.then(messages => res.json(messages)) .then(messages => res.json(messages))
.catch(error => res.status(500).json({ .catch(error =>
message: error.message, res.status(500).json({
success: false message: error.message,
})); success: false
})
);
}; };
const deleteHistory = (req, res) => { const deleteHistory = (req, res) => {
return clearHistory() return clearHistory()
.then(message => res.json(message)) .then(message => res.json(message))
.catch(error => res.status(500).json({ .catch(error =>
message: error.message, res.status(500).json({
success: false message: error.message,
})); success: false
})
);
}; };
module.exports = { module.exports = {

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
View 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
};

View File

@@ -1,132 +1,263 @@
const path = require('path'); const path = require("path");
const crypto = require("crypto");
const Highscore = require(path.join(__dirname, '/schemas/Highscore')); const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
const Wine = require(path.join(__dirname, '/schemas/Wine')); const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
// Utils const Message = require(path.join(__dirname, "/message"));
const epochToDateString = date => new Date(parseInt(date)).toDateString(); const historyRepository = require(path.join(__dirname, "/history"));
const wineRepository = require(path.join(__dirname, "/wine"));
const sortNewestFirst = (lotteries) => { const {
return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1) WinnerNotFound,
} NoMoreAttendeesToWin,
CouldNotFindNewWinnerAfterNTries,
LotteryByDateNotFound
} = require(path.join(__dirname, "/vinlottisErrors"));
const groupHighscoreByDate = async (highscore=undefined) => { const archive = (date, raffles, stolen, wines) => {
if (highscore == undefined) const { blue, red, yellow, green } = raffles;
highscore = await Highscore.find(); const bought = blue + red + yellow + green;
const highscoreByDate = []; return Promise.all(wines.map(wine => wineRepository.findWine(wine))).then(resolvedWines => {
const lottery = new Lottery({
highscore.forEach(person => {
person.wins.map(win => {
const epochDate = new Date(win.date).setHours(0,0,0,0);
const winnerObject = {
name: person.name,
color: win.color,
wine: win.wine,
date: epochDate
}
const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate)
if (existingDateIndex > -1)
highscoreByDate[existingDateIndex].winners.push(winnerObject);
else
highscoreByDate.push({
date: epochDate,
winners: [winnerObject]
})
})
})
return sortNewestFirst(highscoreByDate);
}
const resolveWineReferences = (highscoreObject, key) => {
const listWithWines = highscoreObject[key]
return Promise.all(listWithWines.map(element =>
Wine.findById(element.wine)
.then(wine => {
element.wine = wine
return element
}))
)
.then(resolvedListWithWines => {
highscoreObject[key] = resolvedListWithWines;
return highscoreObject
})
}
// end utils
// Routes
const all = (req, res) => {
return Highscore.find()
.then(highscore => groupHighscoreByDate(highscore))
.then(lotteries => res.send({
message: "Lotteries by date!",
lotteries
}))
}
const latest = (req, res) => {
return groupHighscoreByDate()
.then(lotteries => lotteries.shift()) // first element in list
.then(latestLottery => resolveWineReferences(latestLottery, "winners"))
.then(lottery => res.send({
message: "Latest lottery!",
winners: lottery.winners
})
)
}
const byEpochDate = (req, res) => {
let { date } = req.params;
date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime()
const dateString = epochToDateString(date);
return groupHighscoreByDate()
.then(lotteries => {
const lottery = lotteries.filter(lottery => lottery.date == date)
if (lottery.length > 0) {
return lottery[0]
} else {
return res.status(404).send({
message: `No lottery found for date: ${ dateString }`
})
}
})
.then(lottery => resolveWineReferences(lottery, "winners"))
.then(lottery => res.send({
message: `Lottery for date: ${ dateString}`,
date, date,
winners: lottery.winners blue,
})) red,
} yellow,
green,
bought,
stolen,
wines: resolvedWines
});
const byName = (req, res) => { return lottery.save();
const { name } = req.params; });
const regexName = new RegExp(name, "i"); // lowercase regex of the name };
return Highscore.find({ name }) const lotteryByDate = date => {
.then(highscore => { const startOfDay = new Date(date.setHours(0, 0, 0, 0));
if (highscore.length > 0) { const endOfDay = new Date(date.setHours(24, 59, 59, 99));
return highscore[0]
} else { const query = [
return res.status(404).send({ {
message: `Name: ${ name } not found in leaderboards.` $match: {
}) date: {
$gte: startOfDay,
$lte: endOfDay
}
} }
}) },
.then(highscore => resolveWineReferences(highscore, "wins")) {
.then(highscore => res.send({ $lookup: {
message: `Lottery winnings for name: ${ name }.`, from: "wines",
name: highscore.name, localField: "wines",
highscore: sortNewestFirst(highscore.wins) foreignField: "_id",
})) as: "wines"
}
}
];
const aggregateLottery = Lottery.aggregate(query);
return aggregateLottery.project("-_id -__v").then(lotteries => {
if (lotteries.length == 0) {
throw new LotteryByDateNotFound(date);
}
return lotteries[0];
});
};
const allLotteries = (sort = "asc", yearFilter = undefined) => {
const sortDirection = sort == "asc" ? 1 : -1;
let startQueryDate = new Date("1970-01-01");
let endQueryDate = new Date("2999-01-01");
if (yearFilter) {
startQueryDate = new Date(`${yearFilter}-01-01`);
endQueryDate = new Date(`${Number(yearFilter) + 1}-01-01`);
}
const query = [
{
$match: {
date: {
$gte: startQueryDate,
$lte: endQueryDate
}
}
},
{
$sort: {
date: sortDirection
}
},
{
$unset: ["_id", "__v"]
},
{
$lookup: {
from: "wines",
localField: "wines",
foreignField: "_id",
as: "wines"
}
}
];
return Lottery.aggregate(query);
};
const allLotteriesIncludingWinners = async (sort = "asc", yearFilter = undefined) => {
const lotteries = await allLotteries(sort, yearFilter);
const allWinners = await historyRepository.groupByDate(false, sort);
return lotteries.map(lottery => {
const { winners } = allWinners.pop();
return {
wines: lottery.wines,
date: lottery.date,
blue: lottery.blue,
green: lottery.green,
yellow: lottery.yellow,
red: lottery.red,
bought: lottery.bought,
stolen: lottery.stolen,
winners: winners
};
});
};
const drawWinner = async () => {
let allContestants = await Attendee.find({ winner: false });
if (allContestants.length == 0) {
throw new NoMoreAttendeesToWin();
}
let raffleColors = [];
for (let i = 0; i < allContestants.length; i++) {
let currentContestant = allContestants[i];
for (let blue = 0; blue < currentContestant.blue; blue++) {
raffleColors.push("blue");
}
for (let red = 0; red < currentContestant.red; red++) {
raffleColors.push("red");
}
for (let green = 0; green < currentContestant.green; green++) {
raffleColors.push("green");
}
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
raffleColors.push("yellow");
}
}
raffleColors = shuffle(raffleColors);
let colorToChooseFrom = raffleColors[Math.floor(Math.random() * raffleColors.length)];
let findObject = { winner: false };
findObject[colorToChooseFrom] = { $gt: 0 };
let tries = 0;
const maxTries = 3;
let contestantsToChooseFrom = undefined;
while (contestantsToChooseFrom == undefined && tries < maxTries) {
const hit = await Attendee.find(findObject);
if (hit && hit.length) {
contestantsToChooseFrom = hit;
break;
}
tries++;
}
if (contestantsToChooseFrom == undefined) {
throw new CouldNotFindNewWinnerAfterNTries(maxTries);
}
let attendeeListDemocratic = [];
let currentContestant;
for (let i = 0; i < contestantsToChooseFrom.length; i++) {
currentContestant = contestantsToChooseFrom[i];
for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) {
attendeeListDemocratic.push({
name: currentContestant.name,
phoneNumber: currentContestant.phoneNumber,
red: currentContestant.red,
blue: currentContestant.blue,
green: currentContestant.green,
yellow: currentContestant.yellow
});
}
}
attendeeListDemocratic = shuffle(attendeeListDemocratic);
let winner = attendeeListDemocratic[Math.floor(Math.random() * attendeeListDemocratic.length)];
let newWinnerElement = new VirtualWinner({
name: winner.name,
phoneNumber: winner.phoneNumber,
color: colorToChooseFrom,
red: winner.red,
blue: winner.blue,
green: winner.green,
yellow: winner.yellow,
id: sha512(winner.phoneNumber, genRandomString(10)),
timestamp_drawn: new Date().getTime()
});
await newWinnerElement.save();
await Attendee.updateOne({ name: winner.name, phoneNumber: winner.phoneNumber }, { $set: { winner: true } });
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
timestamp_drawn: 1
});
return { winner, color: colorToChooseFrom, winners };
};
/** - - UTILS - - **/
const genRandomString = function(length) {
return crypto
.randomBytes(Math.ceil(length / 2))
.toString("hex") /** convert to hexadecimal format */
.slice(0, length); /** return required number of characters */
};
const sha512 = function(password, salt) {
var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */
hash.update(password);
var value = hash.digest("hex");
return value;
};
function shuffle(array) {
let currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
} }
module.exports = { module.exports = {
all, drawWinner,
latest, archive,
byEpochDate, lotteryByDate,
byName allLotteries,
allLotteriesIncludingWinners
}; };

View File

@@ -2,34 +2,50 @@ const https = require("https");
const path = require("path"); const path = require("path");
const config = require(path.join(__dirname + "/../config/defaults/lottery")); const config = require(path.join(__dirname + "/../config/defaults/lottery"));
const dateString = (date) => { const dateString = date => {
if (typeof(date) == "string") { if (typeof date == "string") {
date = new Date(date); date = new Date(date);
} }
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date) const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date) const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date) const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
return `${da}-${mo}-${ye}` return `${da}-${mo}-${ye}`;
};
async function sendInitialMessageToWinners(winners) {
const numbers = winners.map(winner => ({ msisdn: `47${winner.phoneNumber}` }));
const body = {
sender: "Vinlottis",
message: "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
recipients: numbers
};
return gatewayRequest(body);
} }
async function sendWineSelectMessage(winnerObject) { async function sendPrizeSelectionLink(winner) {
winnerObject.timestamp_sent = new Date().getTime(); winner.timestamp_sent = new Date().getTime();
winnerObject.timestamp_limit = new Date().getTime() * 600000; winner.timestamp_limit = new Date().getTime() + 1000 * 600;
await winnerObject.save(); await winner.save();
let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin"); const { id, name, phoneNumber } = winner;
const url = new URL(`/#/winner/${id}`, "https://lottis.vin");
const message = `Gratulerer som heldig vinner av vinlotteriet ${name}! Her er linken for \
å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst \
i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.`;
return sendMessageToUser( return sendMessageToNumber(phoneNumber, message);
winnerObject.phoneNumber,
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Her er linken for å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.)`
)
} }
async function sendWineConfirmation(winnerObject, wineObject, date) { async function sendWineConfirmation(winnerObject, wineObject, date) {
date = dateString(date); date = dateString(date);
return sendMessageToUser(winnerObject.phoneNumber, return sendMessageToNumber(
`Bekreftelse på din vin ${ winnerObject.name }.\nDato vunnet: ${ date }.\nVin valgt: ${ wineObject.name }.\nDu vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`) winnerObject.phoneNumber,
`Bekreftelse på din vin ${winnerObject.name}.\nDato vunnet: ${date}.\nVin valgt: ${wineObject.name}.\
\nDu vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
);
} }
async function sendLastWinnerMessage(winnerObject, wineObject) { async function sendLastWinnerMessage(winnerObject, wineObject) {
@@ -38,84 +54,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) {
winnerObject.timestamp_limit = new Date().getTime(); winnerObject.timestamp_limit = new Date().getTime();
await winnerObject.save(); await winnerObject.save();
return sendMessageToUser( return sendMessageToNumber(
winnerObject.phoneNumber, winnerObject.phoneNumber,
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!` `Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, \
du vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
); );
} }
async function sendWineSelectMessageTooLate(winnerObject) { async function sendWineSelectMessageTooLate(winnerObject) {
return sendMessageToUser( return sendMessageToNumber(
winnerObject.phoneNumber, winnerObject.phoneNumber,
`Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.` `Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor \
puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.`
); );
} }
async function sendMessageToUser(phoneNumber, message) { async function sendMessageToNumber(phoneNumber, message) {
console.log(`Attempting to send message to ${ phoneNumber }.`) console.log(`Attempting to send message to ${phoneNumber}.`);
const body = { const body = {
sender: "Vinlottis", sender: "Vinlottis",
message: message, message: message,
recipients: [{ msisdn: `47${ phoneNumber }`}] recipients: [{ msisdn: `47${phoneNumber}` }]
}; };
return gatewayRequest(body); return gatewayRequest(body);
} }
async function sendInitialMessageToWinners(winners) {
let numbers = [];
for (let i = 0; i < winners.length; i++) {
numbers.push({ msisdn: `47${winners[i].phoneNumber}` });
}
const body = {
sender: "Vinlottis",
message:
"Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
recipients: numbers
}
return gatewayRequest(body);
}
async function gatewayRequest(body) { async function gatewayRequest(body) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const options = { const options = {
hostname: "gatewayapi.com", hostname: "gatewayapi.com",
post: 443, post: 443,
path: `/rest/mtsms?token=${ config.gatewayToken }`, path: `/rest/mtsms?token=${config.gatewayToken}`,
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
} };
const req = https.request(options, (res) => { const req = https.request(options, res => {
console.log(`statusCode: ${ res.statusCode }`); console.log(`statusCode: ${res.statusCode}`);
console.log(`statusMessage: ${ res.statusMessage }`); console.log(`statusMessage: ${res.statusMessage}`);
res.setEncoding('utf8'); res.setEncoding("utf8");
if (res.statusCode == 200) { if (res.statusCode == 200) {
res.on("data", (data) => { res.on("data", data => {
console.log("Response from message gateway:", data) console.log("Response from message gateway:", data);
resolve(JSON.parse(data)) resolve(JSON.parse(data));
}); });
} else { } else {
res.on("data", (data) => { res.on("data", data => {
data = JSON.parse(data); data = JSON.parse(data);
return reject('Gateway error: ' + data['message'] || data) return reject("Gateway error: " + data["message"] || data);
}); });
} }
}) });
req.on("error", (error) => { req.on("error", error => {
console.error(`Error from sms service: ${ error }`); console.error(`Error from sms service: ${error}`);
reject(`Error from sms service: ${ error }`); reject(`Error from sms service: ${error}`);
}) });
req.write(JSON.stringify(body)); req.write(JSON.stringify(body));
req.end(); req.end();
@@ -123,9 +124,9 @@ async function gatewayRequest(body) {
} }
module.exports = { module.exports = {
sendWineSelectMessage, sendInitialMessageToWinners,
sendPrizeSelectionLink,
sendWineConfirmation, sendWineConfirmation,
sendLastWinnerMessage, sendLastWinnerMessage,
sendWineSelectMessageTooLate, sendWineSelectMessageTooLate
sendInitialMessageToWinners };
}

View File

@@ -0,0 +1,6 @@
const alwaysAuthenticatedWhenLocalhost = (req, res, next) => {
req.isAuthenticated = () => true;
return next();
};
module.exports = alwaysAuthenticatedWhenLocalhost;

View File

@@ -1,10 +1,4 @@
const mustBeAuthenticated = (req, res, next) => { const mustBeAuthenticated = (req, res, next) => {
if (process.env.NODE_ENV == "development") {
console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
req.isAuthenticated = () => true;
return next();
}
if (!req.isAuthenticated()) { if (!req.isAuthenticated()) {
return res.status(401).send({ return res.status(401).send({
success: false, success: false,

103
api/prelotteryWine.js Normal file
View 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
View 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
};

View File

@@ -1,41 +1,20 @@
const express = require("express");
const path = require("path"); const path = require("path");
const RequestedWine = require(path.join( const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine"));
__dirname, "/schemas/RequestedWine" const Wine = require(path.join(__dirname, "/schemas/Wine"));
));
const Wine = require(path.join(
__dirname, "/schemas/Wine"
));
const deleteRequestedWineById = async (req, res) => { class RequestedWineNotFound extends Error {
const { id } = req.params; constructor(message = "Wine with this id was not found.") {
if(id == null){ super(message);
return res.json({ this.name = "RequestedWineNotFound";
message: "Id er ikke definert", this.statusCode = 404;
success: false
})
} }
await RequestedWine.deleteOne({wineId: id})
return res.json({
message: `Slettet vin med id: ${id}`,
success: true
});
} }
const getAllRequestedWines = async (req, res) => { const addNew = async wine => {
const allWines = await RequestedWine.find({}).populate("wine"); let foundWine = await Wine.findOne({ id: wine.id });
return res.json(allWines); if (foundWine == undefined) {
} foundWine = new Wine({
const requestNewWine = async (req, res) => {
const {wine} = req.body
let thisWineIsLOKO = await Wine.findOne({id: wine.id})
if(thisWineIsLOKO == undefined){
thisWineIsLOKO = new Wine({
name: wine.name, name: wine.name,
vivinoLink: wine.vivinoLink, vivinoLink: wine.vivinoLink,
rating: null, rating: null,
@@ -43,27 +22,47 @@ const requestNewWine = async (req, res) => {
image: wine.image, image: wine.image,
id: wine.id id: wine.id
}); });
await thisWineIsLOKO.save() await foundWine.save();
} }
let requestedWine = await RequestedWine.findOne({ "wineId": wine.id}) let requestedWine = await RequestedWine.findOne({ wineId: wine.id });
if(requestedWine == undefined){ if (requestedWine == undefined) {
requestedWine = new RequestedWine({ requestedWine = new RequestedWine({
count: 1, count: 1,
wineId: wine.id, wineId: wine.id,
wine: thisWineIsLOKO wine: foundWine
}) });
} else { } else {
requestedWine.count += 1; requestedWine.count += 1;
} }
await requestedWine.save() await requestedWine.save();
return res.send(requestedWine); return requestedWine;
} };
const getById = id => {
return RequestedWine.findOne({ wineId: id })
.populate("wine")
.then(wine => {
if (wine == null) {
throw new RequestedWineNotFound();
}
return wine;
});
};
const deleteById = id => {
return getById(id).then(requestedWine => RequestedWine.deleteOne({ _id: requestedWine._id }));
};
const getAll = () => {
return RequestedWine.find({}).populate("wine");
};
module.exports = { module.exports = {
requestNewWine, addNew,
getAllRequestedWines, getAll,
deleteRequestedWineById deleteById
}; };

View File

@@ -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
};

View File

@@ -4,67 +4,97 @@ const path = require("path");
const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated")); const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated"));
const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated")); const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated"));
const update = require(path.join(__dirname, "/update")); const requestController = require(path.join(__dirname, "/controllers/requestController"));
const retrieve = require(path.join(__dirname, "/retrieve")); const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController"));
const request = require(path.join(__dirname, "/request")); const chatController = require(path.join(__dirname, "/controllers/chatController"));
const subscriptionApi = require(path.join(__dirname, "/subscriptions")); const userController = require(path.join(__dirname, "/controllers/userController"));
const userApi = require(path.join(__dirname, "/user")); const historyController = require(path.join(__dirname, "/controllers/historyController"));
const wineinfo = require(path.join(__dirname, "/wineinfo")); const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController"));
const virtualApi = require(path.join(__dirname, "/virtualLottery")); const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController"));
const virtualRegistrationApi = require(path.join( const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController"));
__dirname, "/virtualRegistration" const lotteryController = require(path.join(__dirname, "/controllers/lotteryController"));
)); const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController"));
const lottery = require(path.join(__dirname, "/lottery")); const wineController = require(path.join(__dirname, "/controllers/wineController"));
const chatHistoryApi = require(path.join(__dirname, "/chatHistory")); const messageController = require(path.join(__dirname, "/controllers/messageController"));
const router = express.Router(); const router = express.Router();
router.get("/wineinfo/search", wineinfo.wineSearch); router.get("/vinmonopolet/wine/search", vinmonopoletController.searchWines);
router.get("/vinmonopolet/wine/by-ean/:ean", vinmonopoletController.wineByEAN);
router.get("/vinmonopolet/wine/by-id/:id", vinmonopoletController.wineById);
router.get("/vinmonopolet/stores/", vinmonopoletController.allStores);
router.get("/vinmonopolet/stores/search", vinmonopoletController.searchStores);
router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines); router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests);
router.post("/request/new-wine", request.requestNewWine); router.post("/request", requestController.addRequest);
router.delete("/request/:id", request.deleteRequestedWineById); router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest);
router.get("/wineinfo/schema", mustBeAuthenticated, update.schema); router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences
router.get("/wineinfo/:ean", wineinfo.byEAN); router.get("/wine/:id", wineController.wineById); // sort = by-date, by-name, by-occurences
// router.update("/wine/:id", mustBeAuthenticated, wineController.update);
router.post("/log/wines", mustBeAuthenticated, update.submitWines); router.get("/history", historyController.all);
router.post("/lottery", update.submitLottery); router.get("/history/latest", historyController.latest);
router.post("/lottery/wines", update.submitWinesToLottery); router.get("/history/by-wins/", historyController.orderByWins);
// router.delete("/lottery/wine/:id", update.deleteWineFromLottery); router.get("/history/by-color/", historyController.groupByColor);
router.post("/lottery/winners", update.submitWinnersToLottery); router.get("/history/by-date/:date", historyController.byDate);
router.get("/history/by-name/:name", historyController.byName);
router.get("/history/search/", historyController.search);
router.get("/history/by-date/", historyController.groupByDate);
router.get("/wines/prelottery", retrieve.prelotteryWines); // router.get("/purchases", purchaseController.lotteryPurchases);
router.get("/purchase/statistics", retrieve.allPurchase); // // returns list per date and count of each colors that where bought
router.get("/purchase/statistics/color", retrieve.purchaseByColor); // router.get("/purchases/summary", purchaseController.lotteryPurchases);
router.get("/highscore/statistics", retrieve.highscore) // // returns total, wins?, stolen
router.get("/wines/statistics", retrieve.allWines); // router.get("/purchase/:date", purchaseController.lotteryPurchaseByDate);
router.get("/wines/statistics/overall", retrieve.allWinesSummary);
router.get("/lottery/all", lottery.all); router.get("/lottery/wines", prelotteryWineController.allWines);
router.get("/lottery/latest", lottery.latest); router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema);
router.get("/lottery/by-date/:date", lottery.byEpochDate); router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById);
router.get("/lottery/by-name/:name", lottery.byName); router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines);
router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines);
router.put("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.updateWineById);
router.delete("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.deleteWineById);
router.delete('/virtual/winner/all', mustBeAuthenticated, virtualApi.deleteWinners); router.get("/lottery/attendees", setAdminHeaderIfAuthenticated, attendeeController.allAttendees);
router.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees); router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees);
router.get('/virtual/winner/draw', virtualApi.drawWinner); router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee);
router.get('/virtual/winner/all', virtualApi.winners); router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById);
router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure); router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById);
router.post('/virtual/finish', mustBeAuthenticated, virtualApi.finish);
router.get('/virtual/attendee/all', virtualApi.attendees);
router.get('/virtual/attendee/all/secure', mustBeAuthenticated, virtualApi.attendeesSecure);
router.post('/virtual/attendee/add', mustBeAuthenticated, virtualApi.addAttendee);
router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById); router.get("/lottery/winners", winnerController.allWinners);
router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById); router.get("/lottery/winner/:id", winnerController.winnerById);
router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection); router.post("/lottery/winners", mustBeAuthenticated, winnerController.addWinners);
router.delete("/lottery/winners", mustBeAuthenticated, winnerController.deleteWinners);
router.put("/lottery/winner/:id", mustBeAuthenticated, winnerController.updateWinnerById);
router.delete("/lottery/winner/:id", mustBeAuthenticated, winnerController.deleteWinnerById);
router.get('/chat/history', chatHistoryApi.getAllHistory) router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner);
router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory) router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery);
router.get("/lottery/:epoch", lotteryController.lotteryByDate);
router.get("/lotteries/", lotteryController.allLotteries);
router.post('/login', userApi.login); // router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status);
router.post('/register', mustBeAuthenticated, userApi.register); router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start);
router.get('/logout', userApi.logout); // router.post("/lottery/prize-distribution/stop", mustBeAuthenticated, prizeDistributionController.stop);
router.get("/lottery/prize-distribution/prizes/:id", prizeDistributionController.getPrizesForWinnerById);
router.post("/lottery/prize-distribution/prize/:id", prizeDistributionController.submitPrizeForWinnerById);
router.post("/lottery/messages/winner/:id", mustBeAuthenticated, messageController.notifyWinnerById);
router.get("/chat/history", chatController.getAllHistory);
router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory);
router.post("/login", userController.login);
router.post("/register", mustBeAuthenticated, userController.register);
router.get("/logout", userController.logout);
// router.get("/", documentation.apiInfo);
// router.get("/wine/schema", mustBeAuthenticated, update.schema);
// router.get("/purchase/statistics", retrieve.allPurchase);
// router.get("/highscore/statistics", retrieve.highscore);
// router.get("/wines/statistics", retrieve.allWines);
// router.get("/wines/statistics/overall", retrieve.allWinesSummary);
module.exports = router; module.exports = router;

View File

@@ -6,9 +6,14 @@ const PreLotteryWine = new Schema({
vivinoLink: String, vivinoLink: String,
rating: Number, rating: Number,
id: String, id: String,
year: Number,
image: String, image: String,
price: String, price: String,
country: String country: String,
winner: {
type: Schema.Types.ObjectId,
ref: "VirtualWinner"
}
}); });
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine); module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);

View File

@@ -10,6 +10,10 @@ const VirtualWinner = new Schema({
red: Number, red: Number,
yellow: Number, yellow: Number,
id: String, id: String,
prize_selected: {
type: Boolean,
default: false
},
timestamp_drawn: Number, timestamp_drawn: Number,
timestamp_sent: Number, timestamp_sent: Number,
timestamp_limit: Number timestamp_limit: Number

View File

@@ -1,15 +1,16 @@
const mongoose = require("mongoose"); const mongoose = require("mongoose");
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
const Wine = new Schema({ const WineSchema = new Schema({
name: String, name: String,
vivinoLink: String, vivinoLink: String,
rating: Number, rating: Number,
occurences: Number, occurences: Number,
id: String, id: String,
year: Number,
image: String, image: String,
price: String, price: String,
country: String country: String
}); });
module.exports = mongoose.model("Wine", Wine); module.exports = mongoose.model("Wine", WineSchema);

View File

@@ -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
};

View File

@@ -1,51 +1,90 @@
const passport = require("passport"); const passport = require("passport");
const path = require("path"); const path = require("path");
const User = require(path.join(__dirname, "/schemas/User")); const User = require(path.join(__dirname, "/schemas/User"));
const router = require("express").Router();
const register = (req, res, next) => { class UserExistsError extends Error {
User.register( constructor(message = "Username already exists.") {
new User({ username: req.body.username }), super(message);
req.body.password, this.name = "UserExists";
function(err) { this.statusCode = 409;
}
}
class MissingUsernameError extends Error {
constructor(message = "No username given.") {
super(message);
this.name = "MissingUsernameError";
this.statusCode = 400;
}
}
class MissingPasswordError extends Error {
constructor(message = "No password given.") {
super(message);
this.name = "MissingPasswordError";
this.statusCode = 400;
}
}
class IncorrectUserCredentialsError extends Error {
constructor(message = "Incorrect username or password") {
super(message);
this.name = "IncorrectUserCredentialsError";
this.statusCode = 404;
}
}
function userAuthenticationErrorHandler(err) {
if (err.name == "UserExistsError") {
throw new UserExistsError(err.message);
} else if (err.name == "MissingUsernameError") {
throw new MissingUsernameError(err.message);
} else if (err.name == "MissingPasswordError") {
throw new MissingPasswordError(err.message);
}
throw err;
}
const register = (username, password) => {
return User.register(new User({ username: username }), password).catch(userAuthenticationErrorHandler);
};
const authenticate = req => {
return new Promise((resolve, reject) => {
const { username, password } = req.body;
if (username == undefined) throw new MissingUsernameError();
if (password == undefined) throw new MissingPasswordError();
passport.authenticate("local", function(err, user, info) {
if (err) { if (err) {
if (err.name == "UserExistsError") reject(err);
res.status(409).send({ success: false, message: err.message })
else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
res.status(400).send({ success: false, message: err.message })
return next(err);
} }
return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true }) if (!user) {
} reject(new IncorrectUserCredentialsError());
); }
resolve(user);
})(req);
});
}; };
const login = (req, res, next) => { const login = (req, user) => {
passport.authenticate("local", function(err, user, info) { return new Promise((resolve, reject) => {
if (err) { req.logIn(user, err => {
if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError") if (err) {
return res.status(400).send({ message: err.message, success: false }) reject(err);
return next(err); }
}
if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false }) resolve(user);
});
req.logIn(user, (err) => { });
if (err) { return next(err) }
return res.status(200).send({ message: "Velkommen " + user.username, success: true })
})
})(req, res, next);
};
const logout = (req, res) => {
req.logout();
res.redirect("/");
}; };
module.exports = { module.exports = {
register, register,
login, authenticate,
logout login
}; };

90
api/vinlottisErrors.js Normal file
View 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
View 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
};

View File

@@ -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
}

View File

@@ -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
};

View File

@@ -1,27 +1,63 @@
const path = require("path"); const path = require("path");
const Wine = require(path.join(__dirname, "/schemas/Wine")); const Wine = require(path.join(__dirname, "/schemas/Wine"));
async function findSaveWine(prelotteryWine) { const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
let wonWine = await Wine.findOne({ name: prelotteryWine.name });
if (wonWine == undefined) { const addWine = async wine => {
let newWonWine = new Wine({ let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year });
name: prelotteryWine.name,
vivinoLink: prelotteryWine.vivinoLink, if (existingWine == undefined) {
rating: prelotteryWine.rating, let newWine = new Wine({
name: wine.name,
vivinoLink: wine.vivinoLink,
rating: wine.rating,
occurences: 1, occurences: 1,
image: prelotteryWine.image, id: wine.id,
id: prelotteryWine.id year: wine.year,
image: wine.image,
price: wine.price,
country: wine.country
}); });
await newWonWine.save(); await newWine.save();
wonWine = newWonWine; return newWine;
} else { } else {
wonWine.occurences += 1; existingWine.occurences += 1;
wonWine.image = prelotteryWine.image; await existingWine.save();
wonWine.id = prelotteryWine.id; return existingWine;
await wonWine.save();
} }
};
return wonWine; const allWines = (limit = undefined) => {
} if (limit) {
return Wine.find().limit(limit);
} else {
return Wine.find();
}
};
module.exports.findSaveWine = findSaveWine; const wineById = id => {
return Wine.findOne({ _id: id }).then(wine => {
if (wine == null) {
throw new WineNotFound();
}
return wine;
});
};
const findWine = wine => {
return Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }).then(wine => {
if (wine == null) {
throw new WineNotFound();
}
return wine;
});
};
module.exports = {
addWine,
allWines,
wineById,
findWine
};

View File

@@ -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
View 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
};

View File

@@ -26,7 +26,8 @@ const allRequestedWines = () => {;
return fetch("/api/request/all") return fetch("/api/request/all")
.then(resp => { .then(resp => {
const isAdmin = resp.headers.get("vinlottis-admin") == "true"; const isAdmin = resp.headers.get("vinlottis-admin") == "true";
return Promise.all([resp.json(), isAdmin]); const getWinesFromBody = (resp) => resp.json().then(body => body.wines);
return Promise.all([getWinesFromBody(resp), isAdmin]);
}); });
}; };
@@ -109,8 +110,7 @@ const deleteRequestedWine = wineToBeDeleted => {
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
method: "DELETE", method: "DELETE"
body: JSON.stringify(wineToBeDeleted)
}; };
return fetch("api/request/" + wineToBeDeleted.id, options) return fetch("api/request/" + wineToBeDeleted.id, options)
@@ -148,14 +148,12 @@ const attendees = () => {
const requestNewWine = (wine) => { const requestNewWine = (wine) => {
const options = { const options = {
body: JSON.stringify({ method: "POST",
wine: wine
}),
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
method: "post" body: JSON.stringify({ wine })
} }
return fetch("/api/request/new-wine", options) return fetch("/api/request/new-wine", options)

View File

@@ -1,34 +1,72 @@
<template> <template>
<div> <div>
<h1>Admin-side</h1>
<Tabs :tabs="tabs" /> <Tabs :tabs="tabs" />
</div> </div>
</template> </template>
<script> <script>
import Tabs from "@/ui/Tabs"; import Tabs from "@/ui/Tabs";
import RegisterPage from "@/components/RegisterPage"; import RegisterWinePage from "@/components/admin/RegisterWinePage";
import VirtualLotteryRegistrationPage from "@/components/VirtualLotteryRegistrationPage"; import archiveLotteryPage from "@/components/admin/archiveLotteryPage";
import registerAttendeePage from "@/components/admin/registerAttendeePage";
import DrawWinnerPage from "@/components/admin/DrawWinnerPage";
import PushPage from "@/components/admin/PushPage";
export default { export default {
components: { components: {
Tabs, Tabs
RegisterPage,
VirtualLotteryRegistrationPage
}, },
data() { data() {
return { return {
tabs: [ tabs: [
{ name: "Registrering", component: RegisterPage }, {
{ name: "Virtuelt lotteri", component: VirtualLotteryRegistrationPage } name: "Vin",
component: RegisterWinePage,
slug: "vin",
counter: null
},
{
name: "Legg til deltakere",
component: registerAttendeePage,
slug: "attendees",
counter: null
},
{
name: "Trekk vinner",
component: DrawWinnerPage,
slug: "draw",
counter: null
},
{
name: "Arkiver lotteri",
component: archiveLotteryPage,
slug: "reg",
counter: null
},
{
name: "Push meldinger",
component: PushPage,
slug: "push"
}
] ]
}; };
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss">
h1 { @import "@/styles/media-queries";
text-align: center;
.page-container {
padding: 0 1.5rem 3rem;
h1 {
text-align: center;
}
@include desktop {
max-width: 60vw;
margin: 0 auto;
}
} }
</style> </style>

View File

@@ -2,40 +2,50 @@
<main class="container"> <main class="container">
<h1>Alle foreslåtte viner</h1> <h1>Alle foreslåtte viner</h1>
<section class="requested-wines-container"> <section class="wines-container">
<p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p> <p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p>
<RequestedWineCard v-for="requestedEl in wines" :key="requestedEl.wine._id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/> <RequestedWineCard
v-for="requestedWine in wines"
:key="requestedWine.wine._id"
:requestedElement="requestedWine"
@wineDeleted="filterOutDeletedWine"
:showDeleteButton="isAdmin"
/>
</section> </section>
</main> </main>
</template> </template>
<script> <script>
import { allRequestedWines } from "@/api";
import RequestedWineCard from "@/ui/RequestedWineCard"; import RequestedWineCard from "@/ui/RequestedWineCard";
export default { export default {
components: { components: {
RequestedWineCard RequestedWineCard
}, },
data(){ data() {
return{ return {
wines: undefined, wines: undefined,
canRequest: true,
isAdmin: false isAdmin: false
} };
},
methods: {
filterOutDeletedWine(wine){
this.wines = this.wines.filter(item => item.wine._id !== wine._id)
},
async refreshData(){
[this.wines, this.isAdmin] = await allRequestedWines() || [[], false]
}
}, },
mounted() { mounted() {
this.refreshData() this.fetchRequestedWines();
},
methods: {
filterOutDeletedWine(wine) {
this.wines = this.wines.filter(item => item.wine._id !== wine._id);
},
fetchRequestedWines() {
return fetch("/api/requests")
.then(resp => {
this.isAdmin = resp.headers.get("vinlottis-admin") == "true";
return resp;
})
.then(resp => resp.json())
.then(response => (this.wines = response.wines));
}
} }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -55,10 +65,4 @@ h1 {
color: $matte-text-color; color: $matte-text-color;
font-weight: normal; font-weight: normal;
} }
</style>
.requested-wines-container{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 2rem;
}
</style>

View File

@@ -2,10 +2,9 @@
<div class="container"> <div class="container">
<h1 class="">Alle viner</h1> <h1 class="">Alle viner</h1>
<div id="wines-container"> <div class="wines-container">
<Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id"> <Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id">
<div class="winners-container"> <div class="winners-container">
<span class="label">Vinnende lodd:</span> <span class="label">Vinnende lodd:</span>
<div class="flex row"> <div class="flex row">
<span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span> <span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span>
@@ -19,43 +18,44 @@
<ul class="names"> <ul class="names">
<li v-for="(winner, index) in wine.winners"> <li v-for="(winner, index) in wine.winners">
<router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link> <router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link>
-&nbsp; -&nbsp;
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ dateString(wine.dates[index]) }}</router-link> <router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{
dateString(wine.dates[index])
}}</router-link>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</Wine> </Wine>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Banner from "@/ui/Banner";
import Wine from "@/ui/Wine"; import Wine from "@/ui/Wine";
import { overallWineStatistics } from "@/api";
import { dateString } from "@/utils"; import { dateString } from "@/utils";
export default { export default {
components: { components: { Wine },
Banner,
Wine
},
data() { data() {
return { return {
wines: [] wines: []
}; };
}, },
mounted() {
this.overallWineStatistics();
},
methods: { methods: {
winDateUrl(date) { winDateUrl(date) {
const timestamp = new Date(date).getTime(); const timestamp = new Date(date).getTime();
return `/history/${timestamp}` return `/history/${timestamp}`;
},
overallWineStatistics() {
return fetch("/api/wines")
.then(resp => resp.json())
.then(response => (this.wines = response.wines));
}, },
dateString: dateString dateString: dateString
},
async mounted() {
this.wines = await overallWineStatistics();
} }
}; };
</script> </script>
@@ -84,18 +84,6 @@ h1 {
font-weight: 600; font-weight: 600;
} }
#wines-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 2rem;
> div {
justify-content: flex-start;
margin-bottom: 2rem;
}
}
.name-wins { .name-wins {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -5,7 +5,7 @@
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer' Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
</p> </p>
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" /> <RaffleGenerator @numberOfRaffles="val => (this.numberOfRaffles = val)" />
<Vipps class="vipps" :amount="numberOfRaffles" /> <Vipps class="vipps" :amount="numberOfRaffles" />
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> <Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
@@ -43,16 +43,16 @@ export default {
this.hardStart = true; this.hardStart = true;
}, },
track() { track() {
window.ga('send', 'pageview', '/lottery/generate'); window.ga("send", "pageview", "/lottery/generate");
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/variables.scss"; @import "@/styles/variables.scss";
@import "../styles/global.scss"; @import "@/styles/global.scss";
@import "../styles/media-queries.scss"; @import "@/styles/media-queries.scss";
h1 { h1 {
cursor: pointer; cursor: pointer;
} }
@@ -67,7 +67,9 @@ p {
} }
.vipps { .vipps {
margin: 5rem auto 2.5rem auto; display: flex;
justify-content: center;
margin-top: 4rem;
@include mobile { @include mobile {
margin-top: 2rem; margin-top: 2rem;
@@ -75,7 +77,6 @@ p {
} }
.container { .container {
margin: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

View File

@@ -12,7 +12,12 @@
<p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p> <p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p>
<ol v-if="highscore.length > 0" class="highscore-list"> <ol v-if="highscore.length > 0" class="highscore-list">
<li v-for="person in filteredResults" @click="selectWinner(person)" @keydown.enter="selectWinner(person)" tabindex="0"> <li
v-for="person in filteredResults"
@click="goToWinner(person)"
@keydown.enter="goToWinner(person)"
tabindex="0"
>
<b>{{ person.rank }}.</b>&nbsp;&nbsp;&nbsp;{{ person.name }} - {{ person.wins.length }} <b>{{ person.rank }}.</b>&nbsp;&nbsp;&nbsp;{{ person.name }} - {{ person.wins.length }}
</li> </li>
</ol> </ol>
@@ -24,8 +29,6 @@
</template> </template>
<script> <script>
import { highscoreStatistics } from "@/api";
import { humanReadableDate, daysAgo } from "@/utils"; import { humanReadableDate, daysAgo } from "@/utils";
import Wine from "@/ui/Wine"; import Wine from "@/ui/Wine";
@@ -34,18 +37,12 @@ export default {
data() { data() {
return { return {
highscore: [], highscore: [],
filterInput: '' filterInput: ""
} };
}, },
async mounted() { async mounted() {
let response = await highscoreStatistics(); const winners = await this.highscoreStatistics();
response.sort((a, b) => { this.highscore = this.generateScoreBoard(winners);
return a.wins.length > b.wins.length ? -1 : 1;
});
response = response.filter(
person => person.name != null && person.name != ""
);
this.highscore = this.generateScoreBoard(response);
}, },
computed: { computed: {
filteredResults() { filteredResults() {
@@ -53,37 +50,42 @@ export default {
let val = this.filterInput; let val = this.filterInput;
if (val.length) { if (val.length) {
val = val.toLowerCase() val = val.toLowerCase();
const nameIncludesString = (person) => person.name.toLowerCase().includes(val); const nameIncludesString = person => person.name.toLowerCase().includes(val);
highscore = highscore.filter(nameIncludesString) highscore = highscore.filter(nameIncludesString);
} }
return highscore return highscore;
} }
}, },
methods: { methods: {
generateScoreBoard(highscore=this.highscore) { highscoreStatistics() {
return fetch("/api/history/by-wins")
.then(resp => resp.json())
.then(response => response.winners);
},
generateScoreBoard(highscore = this.highscore) {
let place = 0; let place = 0;
let highestWinCount = -1; let highestWinCount = -1;
return highscore.map(win => { return highscore.map(win => {
const wins = win.wins.length const wins = win.wins.length;
if (wins != highestWinCount) { if (wins != highestWinCount) {
place += 1 place += 1;
highestWinCount = wins highestWinCount = wins;
} }
const placeString = place.toString().padStart(2, "0"); const placeString = place.toString().padStart(2, "0");
win.rank = placeString; win.rank = placeString;
return win return win;
}) });
}, },
resetFilter() { resetFilter() {
this.filterInput = ''; this.filterInput = "";
document.getElementsByTagName('input')[0].focus(); document.getElementsByTagName("input")[0].focus();
}, },
selectWinner(winner) { goToWinner(winner) {
const path = "/highscore/" + encodeURIComponent(winner.name) const path = "/highscore/" + encodeURIComponent(winner.name);
this.$router.push(path); this.$router.push(path);
}, },
humanReadableDate: humanReadableDate, humanReadableDate: humanReadableDate,
@@ -152,7 +154,8 @@ h1 {
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
&:hover, &:focus { &:hover,
&:focus {
border-color: $link-color; border-color: $link-color;
} }
} }

View File

@@ -3,42 +3,51 @@
<h1>Historie fra tidligere lotteri</h1> <h1>Historie fra tidligere lotteri</h1>
<div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries"> <div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries">
<Winners :winners="lottery.winners" :title="`Vinnere fra ${ humanReadableDate(lottery.date) }`" /> <Winners :winners="lottery.winners" :title="`Vinnere fra ${humanReadableDate(lottery.date)}`" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { historyByDate, historyAll } from '@/api' import { historyByDate, historyAll } from "@/api";
import { humanReadableDate } from "@/utils"; import { humanReadableDate } from "@/utils";
import Winners from '@/ui/Winners' import Winners from "@/ui/Winners";
export default { export default {
name: 'History page of prev lotteries', name: "History page of prev lotteries",
components: { Winners }, components: { Winners },
data() { data() {
return { return {
lotteries: [], lotteries: []
} };
},
methods: {
humanReadableDate: humanReadableDate
}, },
created() { created() {
const dateFromUrl = this.$route.params.date; const dateFromUrl = this.$route.params.date;
if (dateFromUrl !== undefined) if (dateFromUrl !== undefined) {
historyByDate(dateFromUrl) this.fetchHistoryByDate(dateFromUrl).then(history => (this.lotteries = [history]));
.then(history => this.lotteries = { "lottery": history }) } else {
else this.fetchHistory().then(history => (this.lotteries = history));
historyAll() }
.then(history => this.lotteries = history.lotteries) },
methods: {
humanReadableDate: humanReadableDate,
fetchHistory() {
return fetch("/api/history/by-date")
.then(resp => resp.json())
.then(response => response.lotteries);
},
fetchHistoryByDate(date) {
return fetch(`/api/history/by-date/${date}`)
.then(resp => resp.json())
.then(response => response);
}
} }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
h1 { h1 {
text-align: center; text-align: center;
} }
</style> </style>

View File

@@ -7,6 +7,7 @@
<input <input
type="text" type="text"
v-model="username" v-model="username"
ref="username"
placeholder="Brukernavn" placeholder="Brukernavn"
autocapitalize="none" autocapitalize="none"
@keyup.enter="submit" @keyup.enter="submit"
@@ -34,6 +35,9 @@ export default {
error: undefined error: undefined
}; };
}, },
mounted() {
this.$refs.username.focus();
},
methods: { methods: {
submit() { submit() {
login(this.username, this.password) login(this.username, this.password)

View File

@@ -14,20 +14,25 @@
<h4 class="margin-bottom-0">Vinnende farger:</h4> <h4 class="margin-bottom-0">Vinnende farger:</h4>
<div class="raffle-container el-spacing"> <div class="raffle-container el-spacing">
<div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)" :key="color"> <div
class="raffle-element"
:class="color + `-raffle`"
v-for="[color, occurences] in Object.entries(winningColors)"
:key="color"
>
{{ occurences }} {{ occurences }}
</div> </div>
</div> </div>
<h4 class="el-spacing">Flasker vunnet:</h4> <h4 class="el-spacing">Flasker vunnet:</h4>
<div v-for="win in winner.highscore" :key="win._id"> <div v-for="win in winner.wins" :key="win._id">
<router-link :to="winDateUrl(win.date)" class="days-ago"> <router-link :to="winDateUrl(win.date)" class="days-ago">
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden {{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
</router-link> </router-link>
<div class="won-wine"> <div class="won-wine" v-if="win.wine">
<img :src="smallerWineImage(win.wine.image)"> <img :src="smallerWineImage(win.wine.image)" />
<div class="won-wine-details"> <div class="won-wine-details">
<h3>{{ win.wine.name }}</h3> <h3>{{ win.wine.name }}</h3>
@@ -38,6 +43,11 @@
<div class="raffle-element small" :class="win.color + `-raffle`"></div> <div class="raffle-element small" :class="win.color + `-raffle`"></div>
</div> </div>
<div class="won-wine" v-else>
<div class="won-wine-details">
<h3>Oisann! Klarte ikke finne vin.</h3>
</div>
</div>
</div> </div>
</section> </section>
@@ -49,67 +59,71 @@
</template> </template>
<script> <script>
import { getWinnerByName } from "@/api"; import { dateString, humanReadableDate, daysAgo } from "@/utils";
import { humanReadableDate, daysAgo } from "@/utils";
export default { export default {
data() { data() {
return { return {
winner: undefined, winner: undefined,
name: undefined,
error: undefined, error: undefined,
previousRoute: { previousRoute: {
default: true, default: true,
name: "topplisten", name: "topplisten",
path: "/highscore" path: "/highscore"
} }
} };
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
next(vm => { next(vm => {
if (from.name != null) if (from.name != null) vm.previousRoute = from;
vm.previousRoute = from });
})
}, },
computed: { computed: {
numberOfWins() { numberOfWins() {
return this.winner.highscore.length return this.winner.wins.length;
} }
}, },
created() { created() {
const nameFromURL = this.$route.params.name; this.name = this.$route.params.name;
getWinnerByName(nameFromURL) this.getWinnerByName(this.name)
.then(winner => this.setWinner(winner)) .then(winner => this.setWinner(winner))
.catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`) .catch(err => (this.error = `Ingen med navn: "${nameFromURL}" funnet.`));
}, },
methods: { methods: {
getWinnerByName(name) {
return fetch(`/api/history/by-name/${name}`)
.then(resp => resp.json())
.then(response => response.winner);
},
setWinner(winner) { setWinner(winner) {
this.winner = { this.winner = {
name: winner.name, name: winner.name,
highscore: [], highscore: [],
...winner ...winner
} };
this.winningColors = this.findWinningColors() this.winningColors = this.findWinningColors();
}, },
smallerWineImage(image) { smallerWineImage(image) {
if (image && image.includes(`515x515`)) if (image && image.includes(`515x515`)) return image.replace(`515x515`, `175x175`);
return image.replace(`515x515`, `175x175`) if (image && image.includes(`500x500`)) return image.replace(`500x500`, `175x175`);
return image return image;
}, },
findWinningColors() { findWinningColors() {
const colors = this.winner.highscore.map(win => win.color) const colors = this.winner.wins.map(win => win.color);
const colorOccurences = {} const colorOccurences = {};
colors.forEach(color => { colors.forEach(color => {
if (colorOccurences[color] == undefined) { if (colorOccurences[color] == undefined) {
colorOccurences[color] = 1 colorOccurences[color] = 1;
} else { } else {
colorOccurences[color] += 1 colorOccurences[color] += 1;
} }
}) });
return colorOccurences return colorOccurences;
}, },
winDateUrl(date) { winDateUrl(date) {
const timestamp = new Date(date).getTime(); const dateParameter = dateString(new Date(date));
return `/history/${timestamp}` return `/history/${dateParameter}`;
}, },
navigateBack() { navigateBack() {
if (this.previousRoute.default) { if (this.previousRoute.default) {
@@ -121,7 +135,7 @@ export default {
humanReadableDate: humanReadableDate, humanReadableDate: humanReadableDate,
daysAgo: daysAgo daysAgo: daysAgo
} }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -142,7 +156,7 @@ $elementSpacing: 3rem;
} }
.container { .container {
width: 90vw; width: 90vw;
margin: 3rem auto; margin: 3rem auto;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 3rem; padding-bottom: 3rem;
@@ -233,7 +247,7 @@ h1 {
@include tablet { @include tablet {
width: calc(100% - 160px - 80px); width: calc(100% - 160px - 80px);
} }
& > * { & > * {
width: 100%; width: 100%;
} }
@@ -259,10 +273,9 @@ h1 {
} }
} }
.backdrop { .backdrop {
$background: rgb(244,244,244); $background: rgb(244, 244, 244);
--padding: 2rem; --padding: 2rem;
@include desktop { @include desktop {
--padding: 5rem; --padding: 5rem;
@@ -270,4 +283,4 @@ h1 {
background-color: $background; background-color: $background;
padding: var(--padding); padding: var(--padding);
} }
</style> </style>

View File

@@ -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>

View File

@@ -1,26 +1,29 @@
<template> <template>
<section class="main-container"> <section class="main-container">
<Modal <Modal
v-if="showModal" v-if="showModal"
modalText="Ønsket ditt har blitt lagt til" modalText="Ønsket ditt har blitt lagt til"
:buttons="modalButtons" :buttons="modalButtons"
@click="emitFromModalButton" @click="emitFromModalButton"
></Modal> ></Modal>
<h1> <h1>
Foreslå en vin! Foreslå en vin!
</h1> </h1>
<section class="search-container"> <section class="search-container">
<section class="search-section"> <section class="search-section">
<input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷" class="search-input-field"> <input
<button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button> type="text"
</section> v-model="searchString"
<section v-for="(wine, index) in this.wines" :key="index" class="single-result"> @keyup.enter="searchWines()"
<img placeholder="Søk etter en vin du liker her!🍷"
v-if="wine.image" class="search-input-field"
:src="wine.image"
class="wine-image"
:class="{ 'fullscreen': fullscreen }"
/> />
<button :disabled="!searchString" @click="searchWines()" class="vin-button">Søk</button>
</section>
<section v-for="(wine, index) in wines" :key="index" class="single-result">
<img v-if="wine.image" :src="wine.image" class="wine-image" :class="{ fullscreen: fullscreen }" />
<img v-else class="wine-placeholder" alt="Wine image" /> <img v-else class="wine-placeholder" alt="Wine image" />
<section class="wine-info"> <section class="wine-info">
<h2 v-if="wine.name">{{ wine.name }}</h2> <h2 v-if="wine.name">{{ wine.name }}</h2>
@@ -29,37 +32,38 @@
<span v-if="wine.rating">{{ wine.rating }}%</span> <span v-if="wine.rating">{{ wine.rating }}%</span>
<span v-if="wine.price">{{ wine.price }} NOK</span> <span v-if="wine.price">{{ wine.price }} NOK</span>
<span v-if="wine.country">{{ wine.country }}</span> <span v-if="wine.country">{{ wine.country }}</span>
<span v-if="wine.year">{{ wine.year }}</span>
</div> </div>
</section> </section>
<button class="vin-button" @click="request(wine)">Foreslå denne</button> <button class="vin-button" @click="requestWine(wine)">Foreslå denne</button>
<a <a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="wine-link">Les mer</a>
v-if="wine.vivinoLink"
:href="wine.vivinoLink"
class="wine-link"
>Les mer</a>
</section> </section>
<p v-if="this.wines && this.wines.length == 0"> <p v-if="loading == false && wines && wines.length == 0">
Fant ingen viner med det navnet! Fant ingen viner med det navnet!
</p> </p>
<p v-else-if="loading">Loading...</p>
</section> </section>
</section> </section>
</template> </template>
<script> <script>
import { searchForWine, requestNewWine } from "@/api"; import { searchForWine } from "@/api";
import Wine from "@/ui/Wine"; import Wine from "@/ui/Wine";
import Modal from "@/ui/Modal"; import Modal from "@/ui/Modal";
import RequestedWineCard from "@/ui/RequestedWineCard";
export default { export default {
components: { components: {
Wine, Wine,
Modal Modal,
RequestedWineCard
}, },
data() { data() {
return { return {
searchString: undefined, searchString: undefined,
wines: undefined, wines: undefined,
showModal: false, showModal: false,
loading: false,
modalButtons: [ modalButtons: [
{ {
text: "Legg til flere viner", text: "Legg til flere viner",
@@ -70,30 +74,59 @@ export default {
action: "move" action: "move"
} }
] ]
} };
}, },
methods: { methods: {
fetchWineFromVin(){ fetchWinesByQuery(query) {
if(this.searchString){ let url = new URL("/api/vinmonopolet/wine/search", window.location);
this.wines = [] url.searchParams.set("name", query);
let localSearchString = this.searchString.replace(/ /g,"_");
searchForWine(localSearchString) this.wines = [];
.then(res => this.wines = res) this.loading = true;
return fetch(url.href)
.then(resp => resp.json())
.then(response => (this.wines = response.wines))
.finally(wines => (this.loading = false));
},
searchWines() {
if (this.searchString) {
let localSearchString = this.searchString.replace(/ /g, "_");
this.fetchWinesByQuery(localSearchString);
} }
}, },
request(wine){ requestWine(wine) {
requestNewWine(wine) const options = {
.then(() => this.showModal = true) method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wine: wine })
};
return fetch("/api/request", options)
.then(resp => resp.json())
.then(response => {
if (response.success) {
this.showModal = true;
this.$toast.info({
title: `Vinen ${wine.name} har blitt foreslått!`
});
} else {
this.$toast.error({
title: "Obs, her oppsto det en feil! Feilen er logget.",
description: response.message
});
}
});
}, },
emitFromModalButton(action){ emitFromModalButton(action) {
if(action == "stay"){ if (action == "stay") {
this.showModal = false this.showModal = false;
} else { } else {
this.$router.push("/requested-wines"); this.$router.push("/requested-wines");
} }
} }
}, }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -101,12 +134,11 @@ export default {
@import "@/styles/global"; @import "@/styles/global";
@import "@/styles/variables"; @import "@/styles/variables";
h1 {
h1{
text-align: center; text-align: center;
} }
.main-container{ .main-container {
margin: auto; margin: auto;
max-width: 1200px; max-width: 1200px;
} }
@@ -120,66 +152,63 @@ input[type="text"] {
max-width: 90%; max-width: 90%;
} }
.search-container {
.search-container{
margin: 1rem; margin: 1rem;
} }
.search-section{ .search-section {
display: grid; display: grid;
grid: 1fr / 1fr .2fr; grid: 1fr / 1fr 0.2fr;
@include mobile{ @include mobile {
.vin-button{ .vin-button {
display: none; display: none;
} }
.search-input-field{ .search-input-field {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
} }
} }
.single-result{ .single-result {
margin-top: 1rem; margin-top: 1rem;
display: grid; display: grid;
grid: 1fr / .5fr 2fr .5fr .5fr; grid: 1fr / 0.5fr 2fr 0.5fr 0.5fr;
grid-template-areas: "picture details button-left button-right"; grid-template-areas: "picture details button-left button-right";
justify-items: center; justify-items: center;
align-items: center; align-items: center;
grid-gap: 1em; grid-gap: 1em;
padding-bottom: 1em; padding-bottom: 1em;
margin-bottom: 1em; margin-bottom: 1em;
box-shadow: 0 1px 0 0 rgba(0,0,0,0.2); box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
@include mobile{ @include mobile {
grid: 1fr 0.5fr / 0.5fr 1fr;
grid-template-areas:
"picture details"
"button-left button-right";
grid-gap: 0.5em;
grid: 1fr .5fr / .5fr 1fr; .vin-button {
grid-template-areas: "picture details"
"button-left button-right";
grid-gap: .5em;
.vin-button{
grid-area: button-right; grid-area: button-right;
padding: .5em; padding: 0.5em;
font-size: 1em; font-size: 1em;
line-height: 1em; line-height: 1em;
height: 2em; height: 2em;
} }
.wine-link{ .wine-link {
grid-area: button-left; grid-area: button-left;
} }
h2{ h2 {
font-size: 1em; font-size: 1em;
max-width: 80%; max-width: 80%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis text-overflow: ellipsis;
} }
} }
.wine-image { .wine-image {
height: 100px; height: 100px;
@@ -192,14 +221,14 @@ input[type="text"] {
grid-area: picture; grid-area: picture;
} }
.wine-info{ .wine-info {
grid-area: details; grid-area: details;
width: 100%; width: 100%;
h2{ h2 {
margin: 0; margin: 0;
} }
.details{ .details {
top: 0; top: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -216,22 +245,20 @@ input[type="text"] {
width: max-content; width: max-content;
} }
.vin-button{ .vin-button {
grid-area: button-right; grid-area: button-right;
} }
@include tablet{ @include tablet {
h2{ h2 {
font-size: 1.2em; font-size: 1.2em;
} }
} }
@include desktop{ @include desktop {
h2{ h2 {
font-size: 1.6em; font-size: 1.6em;
} }
} }
} }
</style>
</style>

View File

@@ -23,7 +23,9 @@ export default {
}; };
}, },
async mounted() { async mounted() {
prelottery().then(wines => this.wines = wines); fetch("/api/lottery/wines")
.then(resp => resp.json())
.then(response => (this.wines = response.wines));
} }
}; };
</script> </script>
@@ -42,19 +44,18 @@ h1 {
} }
.wines-container { .wines-container {
display: grid; width: 90vw;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); padding: 5vw;
grid-gap: 2rem;
gap: 2rem; @include desktop {
width: 80vw;
padding: 0 10vw;
}
@media (min-width: 1500px) { @media (min-width: 1500px) {
max-width: 1500px; max-width: 1500px;
margin: 0 auto; margin: 0 auto;
} }
@include mobile {
flex-direction: column;
}
} }
h3 { h3 {
@@ -65,23 +66,6 @@ h3 {
} }
} }
.inner-wine-container {
display: flex;
flex-direction: row;
margin: auto;
width: 500px;
font-family: Arial;
margin-bottom: 30px;
@include desktop {
justify-content: center;
}
@include mobile {
width: auto;
}
}
.right { .right {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,8 +1,6 @@
<template> <template>
<main class="main-container"> <main class="main-container">
<section class="top-container"> <section class="top-container">
<div class="want-to-win"> <div class="want-to-win">
<h1> <h1>
Vil du også vinne? Vil du også vinne?
@@ -18,8 +16,8 @@
</div> </div>
<router-link to="/lottery" class="participate-button"> <router-link to="/lottery" class="participate-button">
<i class="icon icon--arrow-right"></i> <i class="icon icon--arrow-right"></i>
<p>Trykk her for å delta</p> <p>Trykk her for å delta</p>
</router-link> </router-link>
<router-link to="/generate" class="see-details-link"> <router-link to="/generate" class="see-details-link">
@@ -38,17 +36,16 @@
<i class="icon icon--bottle"></i> <i class="icon icon--bottle"></i>
<i class="icon icon--bottle"></i> <i class="icon icon--bottle"></i>
</div> </div>
</section> </section>
<section class="content-container"> <section class="content-container">
<div class="scroll-info"> <div class="scroll-info">
<i class ="icon icon--arrow-long-right"></i> <i class="icon icon--arrow-long-right"></i>
<p>Scroll for å se vinnere og annen gøy statistikk</p> <p>Scroll for å se vinnere og annen gøy statistikk</p>
</div> </div>
<Highscore class="highscore"/> <Highscore class="highscore" />
<TotalBought class="total-bought" /> <TotalBought class="total-bought" />
<section class="chart-container"> <section class="chart-container">
@@ -56,12 +53,10 @@
<WinGraph class="win" /> <WinGraph class="win" />
</section> </section>
<Wines class="wines-container" /> <Wines class="wine-container" />
</section> </section>
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" /> <Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
</main> </main>
</template> </template>
@@ -96,11 +91,7 @@ export default {
if (!("PushManager" in window)) { if (!("PushManager" in window)) {
return false; return false;
} }
return ( return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null;
Notification.permission !== "granted" ||
!this.pushAllowed ||
localStorage.getItem("push") == null
);
} }
}, },
async mounted() { async mounted() {
@@ -120,7 +111,7 @@ export default {
this.hardStart = way; this.hardStart = way;
}, },
track() { track() {
window.ga('send', 'pageview', '/'); window.ga("send", "pageview", "/");
}, },
startCountdown() { startCountdown() {
this.hardStart = true; this.hardStart = true;
@@ -145,7 +136,7 @@ export default {
align-items: center; align-items: center;
justify-items: start; justify-items: start;
@include mobile{ @include mobile {
padding-bottom: 2em; padding-bottom: 2em;
height: 15em; height: 15em;
grid-template-rows: repeat(7, 1fr); grid-template-rows: repeat(7, 1fr);
@@ -156,13 +147,13 @@ export default {
grid-column: 2 / -1; grid-column: 2 / -1;
display: flex; display: flex;
h1{ h1 {
font-size: 2em; font-size: 2em;
font-weight: 400; font-weight: 400;
} }
@include tablet { @include tablet {
h1{ h1 {
font-size: 3em; font-size: 3em;
} }
grid-row: 2 / 4; grid-row: 2 / 4;
@@ -170,7 +161,7 @@ export default {
} }
} }
.notification-request-button{ .notification-request-button {
cursor: pointer; cursor: pointer;
} }
@@ -229,7 +220,7 @@ export default {
.icons-container { .icons-container {
grid-column: 1 / -1; grid-column: 1 / -1;
grid-row: 7 / -1; grid-row: 7 / -1;
@include mobile{ @include mobile {
margin-top: 2em; margin-top: 2em;
display: none; display: none;
} }
@@ -239,7 +230,7 @@ export default {
grid-column: 7 / -1; grid-column: 7 / -1;
} }
@include desktop{ @include desktop {
grid-row: 4 / -3; grid-row: 4 / -3;
grid-column: 7 / 11; grid-column: 7 / 11;
} }
@@ -257,30 +248,27 @@ export default {
i { i {
font-size: 5em; font-size: 5em;
&.icon--heart-sparks{ &.icon--heart-sparks {
grid-column: 2 / 4; grid-column: 2 / 4;
grid-row: 2 / 4; grid-row: 2 / 4;
align-self: center; align-self: center;
justify-self: center; justify-self: center;
} }
&.icon--face-1{ &.icon--face-1 {
grid-column: 4 / 7; grid-column: 4 / 7;
grid-row: 2 / 4; grid-row: 2 / 4;
justify-self: center; justify-self: center;
} }
&.icon--face-3{ &.icon--face-3 {
grid-column: 7 / 10; grid-column: 7 / 10;
grid-row: 1 / 4; grid-row: 1 / 4;
align-self: center; align-self: center;
} }
&.icon--ballon{ &.icon--ballon {
grid-column: 9 / 11; grid-column: 9 / 11;
grid-row: 3 / 5; grid-row: 3 / 5;
} }
&.icon--bottle{ &.icon--bottle {
grid-row: 4 / -1; grid-row: 4 / -1;
&:nth-of-type(5) { &:nth-of-type(5) {
@@ -297,14 +285,13 @@ export default {
&:nth-of-type(8) { &:nth-of-type(8) {
grid-column: 7 / 8; grid-column: 7 / 8;
} }
&:nth-of-type(9){ &:nth-of-type(9) {
grid-column: 8 / 9; grid-column: 8 / 9;
align-self: center; align-self: center;
} }
} }
} }
} }
} }
h1 { h1 {
@@ -312,12 +299,12 @@ h1 {
font-family: "knowit"; font-family: "knowit";
} }
.to-lottery{ .to-lottery {
color: #333; color: #333;
text-decoration: none; text-decoration: none;
display: block; display: block;
text-align: center; text-align: center;
margin-bottom: 0; margin-bottom: 0;
} }
.content-container { .content-container {
@@ -326,10 +313,10 @@ h1 {
row-gap: 5em; row-gap: 5em;
.scroll-info { .scroll-info {
display: flex; display: flex;
align-items: center; align-items: center;
column-gap: 10px; column-gap: 10px;
grid-column: 2 / -2; grid-column: 2 / -2;
} }
.chart-container { .chart-container {
@@ -346,8 +333,8 @@ h1 {
grid-column: 2 / -2; grid-column: 2 / -2;
} }
.wines-container { .wine-container {
grid-column: 2 / -2; grid-column: 3 / -3;
} }
.icon--arrow-long-right { .icon--arrow-long-right {
@@ -356,8 +343,7 @@ h1 {
} }
@include tablet { @include tablet {
.scroll-info {
.scroll-info{
grid-column: 3 / -3; grid-column: 3 / -3;
} }

View File

@@ -5,7 +5,10 @@
<div class="instructions"> <div class="instructions">
<h1 class="title">Virtuelt lotteri</h1> <h1 class="title">Virtuelt lotteri</h1>
<ol> <ol>
<li>Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>, eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link></li> <li>
Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>,
eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link>
</li>
<li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li> <li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li>
<li>Send gjerne melding om fargeønske også.</li> <li>Send gjerne melding om fargeønske også.</li>
</ol> </ol>
@@ -15,18 +18,16 @@
<VippsPill class="vipps-pill mobile-only" /> <VippsPill class="vipps-pill mobile-only" />
<p class="call-to-action"> <p class="call-to-action">
<span class="vin-link">Følg med utviklingen</span> og <span class="vin-link">chat om trekningen</span> <span class="vin-link" @click="scrollToContent">Følg med utviklingen</span> og
<i class="icon icon--arrow-left" @click="scrollToContent"></i></p> <span class="vin-link" @click="scrollToContent">chat om trekningen</span>
<i class="icon icon--arrow-left" @click="scrollToContent"></i>
</p>
</div> </div>
</header> </header>
<div class="container" ref="content"> <div class="container" ref="content">
<WinnerDraw <WinnerDraw :currentWinnerDrawn="currentWinnerDrawn" :currentWinner="currentWinner" :attendees="attendees" />
:currentWinnerDrawn="currentWinnerDrawn"
:currentWinner="currentWinner"
:attendees="attendees"
/>
<div class="todays-raffles"> <div class="todays-raffles">
<h2>Liste av lodd kjøpt i dag</h2> <h2>Liste av lodd kjøpt i dag</h2>
@@ -51,15 +52,16 @@
</div> </div>
</div> </div>
<div class="container wines-container"> <div class="todays-wines">
<h2>Dagens fangst ({{ wines.length }})</h2> <h2>Dagens fangst ({{ wines.length }})</h2>
<Wine :wine="wine" v-for="wine in wines" :key="wine" /> <div class="wines-container">
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { attendees, winners, prelottery } from "@/api";
import Chat from "@/ui/Chat"; import Chat from "@/ui/Chat";
import Vipps from "@/ui/Vipps"; import Vipps from "@/ui/Vipps";
import VippsPill from "@/ui/VippsPill"; import VippsPill from "@/ui/VippsPill";
@@ -74,18 +76,18 @@ export default {
data() { data() {
return { return {
attendees: [], attendees: [],
attendeesFetched: false,
winners: [], winners: [],
wines: [], wines: [],
currentWinnerDrawn: false, currentWinnerDrawn: false,
currentWinner: null, currentWinner: null,
socket: null, socket: null,
attendeesFetched: false,
wasDisconnected: false, wasDisconnected: false,
ticketsBought: { ticketsBought: {
"red": 0, red: 0,
"blue": 0, blue: 0,
"green": 0, green: 0,
"yellow": 0 yellow: 0
} }
}; };
}, },
@@ -129,42 +131,45 @@ export default {
this.socket = null; this.socket = null;
}, },
methods: { methods: {
getWinners: async function() { getWinners() {
let response = await winners(); fetch("/api/lottery/winners")
if (response) { .then(resp => resp.json())
this.winners = response; .then(response => (this.winners = response.winners));
}
}, },
getTodaysWines() { getTodaysWines() {
prelottery() fetch("/api/lottery/wines")
.then(resp => resp.json())
.then(response => response.wines)
.then(wines => { .then(wines => {
this.wines = wines; this.wines = wines;
this.todayExists = wines.length > 0; this.todayExists = wines.length > 0;
}) })
.catch(_ => this.todayExists = false) .catch(_ => (this.todayExists = false));
}, },
getAttendees: async function() { getAttendees() {
let response = await attendees(); fetch("/api/lottery/attendees")
if (response) { .then(resp => resp.json())
this.attendees = response; .then(response => {
if (this.attendees == undefined || this.attendees.length == 0) { const { attendees } = response;
this.attendeesFetched = true; this.attendees = attendees || [];
return;
}
const addValueOfListObjectByKey = (list, key) =>
list.map(object => object[key]).reduce((a, b) => a + b);
this.ticketsBought = { if (attendees == undefined || attendees.length == 0) {
red: addValueOfListObjectByKey(response, "red"), return;
blue: addValueOfListObjectByKey(response, "blue"), }
green: addValueOfListObjectByKey(response, "green"),
yellow: addValueOfListObjectByKey(response, "yellow") const addValueOfListObjectByKey = (list, key) => list.map(object => object[key]).reduce((a, b) => a + b);
};
} this.ticketsBought = {
this.attendeesFetched = true; red: addValueOfListObjectByKey(attendees, "red"),
blue: addValueOfListObjectByKey(attendees, "blue"),
green: addValueOfListObjectByKey(attendees, "green"),
yellow: addValueOfListObjectByKey(attendees, "yellow")
};
})
.finally(_ => (this.attendeesFetched = true));
}, },
scrollToContent() { scrollToContent() {
console.log(window.scrollY) console.log(window.scrollY);
const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50; const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50;
const { scrollY } = window; const { scrollY } = window;
let scrollHeight = intersectingHeaderHeight; let scrollHeight = intersectingHeaderHeight;
@@ -178,14 +183,13 @@ export default {
}); });
}, },
track() { track() {
window.ga('send', 'pageview', '/lottery/game'); window.ga("send", "pageview", "/lottery/game");
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/variables.scss"; @import "../styles/variables.scss";
@import "../styles/media-queries.scss"; @import "../styles/media-queries.scss";
@@ -201,7 +205,8 @@ export default {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
> div, > section { > div,
> section {
@include mobile { @include mobile {
grid-column: span 5; grid-column: span 5;
} }
@@ -343,6 +348,8 @@ header {
> div { > div {
padding: 1rem; padding: 1rem;
max-height: 638px;
overflow-y: scroll;
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
@@ -369,11 +376,14 @@ header {
} }
} }
.todays-wines {
width: 80vw;
padding: 0 10vw;
.wines-container { @include mobile {
display: flex; width: 90vw;
flex-wrap: wrap; padding: 0 5vw;
margin-bottom: 4rem; }
h2 { h2 {
width: 100%; width: 100%;

View File

@@ -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>

View File

@@ -1,21 +1,23 @@
<template> <template>
<div class="container"> <div>
<div v-if="!posted"> <div v-if="!posted" class="container">
<h1 v-if="name">Gratulerer {{name}}!</h1> <h1 v-if="name">Gratulerer {{ name }}!</h1>
<p v-if="name"> <p v-if="name">
Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en. Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.
</p> </p>
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
<h1 v-else-if="!turn && wines.length" class="sent-container">Finner ikke noen vinner her..</h1>
<h1 v-else-if="!turn" class="sent-container">Du vente tur..</h1> <h1 v-else-if="!turn" class="sent-container">Du vente tur..</h1>
<div class="wines-container" v-if="name"> <div class="wines-container" v-if="name">
<Wine :wine="wine" v-for="wine in wines" :key="wine"> <Wine :wine="wine" v-for="wine in wines" :key="wine">
<button <button @click="chooseWine(wine)" class="vin-button select-wine">Velg denne vinnen</button>
@click="chooseWine(wine.name)"
class="vin-button select-wine"
>Velg denne vinnen</button>
</Wine> </Wine>
</div> </div>
</div> </div>
<div v-else-if="posted" class="sent-container"> <div v-else-if="posted" class="sent-container">
<h1>Valget ditt er sendt inn!</h1> <h1>Valget ditt er sendt inn!</h1>
<p>Du får mer info om henting snarest!</p> <p>Du får mer info om henting snarest!</p>
@@ -24,15 +26,13 @@
</template> </template>
<script> <script>
import { getAmIWinner, postWineChosen, prelottery } from "@/api";
import Wine from "@/ui/Wine"; import Wine from "@/ui/Wine";
export default { export default {
components: { Wine }, components: { Wine },
data() { data() {
return { return {
id: null, id: null,
existing: false,
fetched: false,
turn: false, turn: false,
name: null, name: null,
wines: [], wines: [],
@@ -40,30 +40,43 @@ export default {
}; };
}, },
async mounted() { async mounted() {
this.id = this.$router.currentRoute.params.id; const { id } = this.$router.currentRoute.params;
let winnerObject = await getAmIWinner(this.id); this.id = id;
this.fetched = true; this.getPrizes(id);
if (!winnerObject || !winnerObject.existing) {
console.error("non existing", winnerObject);
return;
}
this.existing = true;
if (winnerObject.existing && !winnerObject.turn) {
console.error("not your turn yet", winnerObject);
return;
}
this.turn = true;
this.name = winnerObject.name;
this.wines = await prelottery();
}, },
methods: { methods: {
chooseWine: async function(name) { getPrizes(id) {
let posted = await postWineChosen(this.id, name); fetch(`/api/lottery/prize-distribution/prizes/${id}`)
console.log("response", posted); .then(resp => resp.json())
if (posted.success) { .then(response => {
this.posted = true; if (response.success) {
} this.wines = response.wines;
this.name = response.winner.name;
this.turn = true;
}
});
},
chooseWine(wine) {
const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wine })
};
fetch(`/api/lottery/prize-distribution/prize/${this.id}`, options)
.then(resp => resp.json())
.then(response => {
if (response.success) {
this.$toast.info({ title: `Valgt vin: ${wine.name}` });
this.posted = true;
} else {
this.$toast.error({
title: "Klarte ikke velge vin :(",
description: response.message
});
}
});
} }
} }
}; };
@@ -74,9 +87,19 @@ export default {
.container { .container {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 2rem; margin-top: 2rem;
padding: 2rem; padding: 2rem;
width: 80%;
margin: 0 auto;
max-width: 2000px;
} }
.wines-container {
width: 100%;
}
.sent-container { .sent-container {
width: 100%; width: 100%;
height: 90vh; height: 90vh;
@@ -90,11 +113,4 @@ export default {
.select-wine { .select-wine {
margin-top: 1rem; margin-top: 1rem;
} }
</style>
.wines-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: flex-start;
}
</style>

View 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>

View 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>

View 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>

View 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 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>

View 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>

View 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>

View 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 });
}
};
}
};

View File

@@ -20,6 +20,8 @@ body {
a { a {
text-decoration: none; text-decoration: none;
cursor: pointer;
color: inherit;
} }
.title { .title {
@@ -51,8 +53,10 @@ a {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
position: relative;
label { label {
margin-top: 0.7rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -76,6 +80,7 @@ a {
> *:not(:last-child) { > *:not(:last-child) {
margin-right: 2rem; margin-right: 2rem;
margin-bottom: 0.75rem;
} }
&.column { &.column {
@@ -95,7 +100,7 @@ a {
> *:not(:last-child) { > *:not(:last-child) {
margin-right: unset; margin-right: unset;
margin-bottom: .75rem; margin-bottom: 0.75rem;
} }
} }
} }
@@ -105,6 +110,8 @@ input,
textarea { textarea {
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
padding: 0;
margin: 0;
-webkit-appearance: none; -webkit-appearance: none;
font-size: 1.1rem; font-size: 1.1rem;
border: 1px solid rgba(#333333, 0.3); border: 1px solid rgba(#333333, 0.3);
@@ -136,6 +143,11 @@ textarea {
height: auto; height: auto;
} }
&.warning {
background-color: #f9826c;
color: white;
}
&.danger { &.danger {
background-color: $red; background-color: $red;
color: white; color: white;
@@ -151,9 +163,12 @@ textarea {
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; opacity: 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), 0 4px 8px rgba(0, 0, 0, 0.07),
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07); }
&.active {
font-weight: bold;
} }
&:hover:not(:disabled) { &:hover:not(:disabled) {
@@ -163,7 +178,7 @@ textarea {
opacity: 1; opacity: 1;
} }
} }
&:disabled{ &:disabled {
opacity: 0.25; opacity: 0.25;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -173,6 +188,21 @@ textarea {
} }
} }
.pulse-button:not(:hover) {
animation: pulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1);
}
@keyframes pulse {
from {
transform: scale(1);
}
50% {
transform: scale(1.12);
}
to {
transform: scale(1);
}
}
.cursor { .cursor {
&-pointer { &-pointer {
@@ -193,11 +223,23 @@ textarea {
text-decoration: none; text-decoration: none;
color: $matte-text-color; color: $matte-text-color;
&:focus, &:hover { &:focus,
&:hover {
border-color: $link-color; border-color: $link-color;
} }
} }
.margin {
&-md {
margin: 3rem;
}
&-sm {
margin: 1rem;
}
&-0 {
margin: 0;
}
}
.margin-top { .margin-top {
&-md { &-md {
@@ -269,14 +311,29 @@ textarea {
margin: 0 !important; margin: 0 !important;
} }
.wines-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 2rem;
}
.raffle-element { .raffle-element {
width: 45px;
height: 45px;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.75rem;
font-weight: bold;
margin: 20px 0; margin: 20px 0;
color: #333333;
-webkit-mask-image: url(/public/assets/images/lodd.svg); -webkit-mask-image: url(/public/assets/images/lodd.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
mask-image: url(/public/assets/images/lodd.svg); mask-image: url(/public/assets/images/lodd.svg);
-webkit-mask-repeat: no-repeat; -webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat; mask-repeat: no-repeat;
color: #333333;
&.green-raffle { &.green-raffle {
background-color: $light-green; background-color: $light-green;
@@ -293,11 +350,16 @@ textarea {
&.red-raffle { &.red-raffle {
background-color: $light-red; background-color: $light-red;
} }
&:not(:last-of-type) {
margin-right: 1rem;
}
} }
@mixin raffle { @mixin raffle {
padding-bottom: 50px; padding-bottom: 50px;
&::before, &::after { &::before,
&::after {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
@@ -309,11 +371,11 @@ textarea {
background-position: 0 25px; background-position: 0 25px;
background-repeat: repeat-x; background-repeat: repeat-x;
} }
&::after{ &::after {
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%); background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
background-size: 50px 50px; background-size: 50px 50px;
background-position: 25px -25px; background-position: 25px -25px;
bottom: -25px bottom: -25px;
} }
} }
@@ -327,4 +389,4 @@ textarea {
@include desktop { @include desktop {
display: none; display: none;
} }
} }

View File

@@ -1,3 +1,5 @@
@import "@/styles/media-queries.scss";
.flex { .flex {
display: flex; display: flex;
@@ -7,6 +9,10 @@
&.row { &.row {
flex-direction: row; flex-direction: row;
@include mobile {
flex-direction: column;
}
} }
&.wrap { &.wrap {
@@ -43,4 +49,4 @@
&-right { &-right {
float: right; float: right;
} }
} }

View File

@@ -1,21 +1,49 @@
$primary: #b7debd; body {
--primary: #b7debd;
$light-green: #c8f9df; --light-green: #c8f9df;
$green: #0be881; --green: #0be881;
$dark-green: #0ed277; --dark-green: #0ed277;
$light-blue: #d4f2fe; --light-blue: #d4f2fe;
$blue: #4bcffa; --blue: #4bcffa;
$dark-blue: #24acda; --dark-blue: #24acda;
$light-yellow: #fff6d6; --light-yellow: #fff6d6;
$yellow: #ffde5d; --yellow: #ffde5d;
$dark-yellow: #ecc31d; --dark-yellow: #ecc31d;
$light-red: #fbd7de; --light-red: #fbd7de;
$red: #ef5878; --red: #ef5878;
$dark-red: #ec3b61; --dark-red: #ec3b61;
$link-color: #ff5fff; --link-color: #ff5fff;
--underlinenav-text: #e1e4e8;
--underlinenav-text-active: #f9826c;
--underlinenav-text-hover: #d1d5da;
$matte-text-color: #333333; --matte-text-color: #333333;
}
$primary: var(--primary);
$light-green: var(--light-green);
$green: var(--green);
$dark-green: var(--dark-green);
$light-blue: var(--light-blue);
$blue: var(--blue);
$dark-blue: var(--dark-blue);
$light-yellow: var(--light-yellow);
$yellow: var(--yellow);
$dark-yellow: var(--dark-yellow);
$light-red: var(--light-red);
$red: var(--red);
$dark-red: var(--dark-red);
$link-color: var(--link-color);
$underlinenav-text-active: var(--underlinenav-text-active);
$matte-text-color: var(--matte-text-color);

View File

@@ -1,12 +1,45 @@
<template> <template>
<div class="attendees" v-if="attendees.length > 0"> <div v-if="attendees.length > 0" class="attendee-container">
<div class="attendees-container" ref="attendees"> <div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index"> <div class="attendee-info">
<span class="attendee-name">{{ attendee.name }}</span> <router-link class="attendee-name" :to="`/highscore/${attendee.name}`">
<div class="red-raffle raffle-element small">{{ attendee.red }}</div> {{ attendee.name }}
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div> </router-link>
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div> <div v-if="admin" class="flex column justify-center margin-top-sm">
<span>Phone: {{ attendee.phoneNumber }}</span>
<span>Has won: {{ attendee.winner }}</span>
</div>
<div class="raffle-container">
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
</div>
</div>
<div v-if="admin" class="attendee-admin">
<button class="vin-button small" @click="editingAttendee = editingAttendee == attendee ? false : attendee">
{{ editingAttendee == attendee ? "Lukk" : "Rediger" }}
</button>
</div>
<div v-if="editingAttendee == attendee" class="attendee-edit">
<div class="label-div" v-for="key in Object.keys(attendee)" :key="key">
<label>{{ key }}</label>
<input type="text" v-model="attendee[key]" :placeholder="key" />
</div>
<div v-if="editingAttendee == attendee">
<button class="vin-button small warning" @click="updateAttendee(attendee)">
Oppdater deltaker
</button>
<button class="vin-button small danger" @click="deleteAttendee(attendee)">
Slett deltaker
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -17,33 +50,79 @@ export default {
props: { props: {
attendees: { attendees: {
type: Array type: Array
},
admin: {
type: Boolean,
default: false
} }
}, },
methods: { data() {
flipList: (list) => list.slice().reverse() return {
editingAttendee: undefined
};
}, },
watch: { methods: {
attendees: { flipList: list => list.slice().reverse(),
deep: true, updateAttendee(updatedAttendee) {
handler() { const options = {
if (this.$refs && this.$refs.history) { method: "PUT",
setTimeout(() => { headers: { "Content-Type": "application/json" },
this.$refs.attendees.scrollTop = this.$refs.attendees.scrollHeight; body: JSON.stringify({ attendee: updatedAttendee })
}, 50); };
}
} fetch(`/api/lottery/attendee/${updatedAttendee._id}`, options)
.then(resp => resp.json())
.then(response => {
this.editingAttendee = null;
const { message, success } = response;
if (success) {
this.$toast.info({
title: response.message
});
} else {
this.$toast.error({
title: response.message
});
}
});
},
deleteAttendee(deletedAttendee) {
const options = {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ attendee: deletedAttendee })
};
fetch(`/api/lottery/attendee/${deletedAttendee._id}`, options)
.then(resp => resp.json())
.then(response => {
this.editingAttendee = null;
const { message, success } = response;
if (success) {
this.$toast.info({
title: response.message
});
} else {
this.$toast.error({
title: response.message
});
}
});
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/global.scss"; @import "@/styles/variables.scss";
@import "../styles/variables.scss"; @import "@/styles/media-queries.scss";
@import "../styles/media-queries.scss";
.attendee-name { .attendee-name {
width: 60%; font-size: 1.1rem;
} }
hr { hr {
@@ -51,45 +130,60 @@ hr {
width: 100%; width: 100%;
} }
.raffle-element { .attendee-container {
font-size: 0.75rem;
width: 45px;
height: 45px;
display: flex;
justify-content: center;
align-items: center; align-items: center;
font-weight: bold;
font-size: 0.75rem;
text-align: center;
&:not(:last-of-type) {
margin-right: 1rem;
}
}
.attendees {
display: flex;
flex-direction: column;
align-items: center;
height: auto;
}
.attendees-container {
width: 100%;
height: 100%; height: 100%;
overflow-y: scroll;
max-height: 550px; padding: 1rem;
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
} }
.attendee { .attendee {
padding: 0.5rem;
display: flex; display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: center;
width: 100%; @include mobile {
margin: 0 auto; align-items: center;
justify-content: center;
}
&:not(:last-of-type) { &:not(:last-of-type) {
border-bottom: 2px solid #d7d8d7; border-bottom: 2px solid #d7d8d7;
} }
&:not(:first-of-type) {
margin-top: 0.5rem;
}
button {
margin-bottom: 0.5rem;
}
&-info {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
@include mobile {
flex-direction: column;
}
}
&-edit {
button {
margin-top: 0.5rem;
}
}
.raffle-container {
display: flex;
flex-direction: row;
}
} }
</style> </style>

View File

@@ -5,14 +5,14 @@
<img src="/public/assets/images/knowit.svg" alt="knowit logo" /> <img src="/public/assets/images/knowit.svg" alt="knowit logo" />
</router-link> </router-link>
<a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'" > <a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'">
<span class="menu-toggle"></span> <span class="menu-toggle"></span>
<span class="menu-toggle"></span> <span class="menu-toggle"></span>
<span class="menu-toggle"></span> <span class="menu-toggle"></span>
</a> </a>
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'" > <nav class="menu" :class="isOpen ? 'open' : 'collapsed'">
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link" > <router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link">
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a> <a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a>
<i class="icon icon--arrow-right"></i> <i class="icon icon--arrow-right"></i>
</router-link> </router-link>
@@ -21,8 +21,9 @@
<div class="clock"> <div class="clock">
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver"> <h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
<span v-if="days > 0">{{ pad(days) }}:</span> <span v-if="days > 0">{{ pad(days) }}:</span>
<span>{{ pad(hours) }}</span>: <span>{{ pad(hours) }}</span
<span>{{ pad(minutes) }}</span>: >: <span>{{ pad(minutes) }}</span
>:
<span>{{ pad(seconds) }}</span> <span>{{ pad(seconds) }}</span>
</h2> </h2>
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2> <h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
@@ -41,7 +42,7 @@ export default {
minutes: 0, minutes: 0,
seconds: 0, seconds: 0,
distance: 0, distance: 0,
interval: null, interval: null
}; };
}, },
props: { props: {
@@ -68,7 +69,7 @@ export default {
} }
}, },
methods: { methods: {
toggleMenu(){ toggleMenu() {
this.isOpen = this.isOpen ? false : true; this.isOpen = this.isOpen ? false : true;
}, },
pad: function(num) { pad: function(num) {
@@ -91,10 +92,7 @@ export default {
let nowDate = new Date(); let nowDate = new Date();
let now = nowDate.getTime(); let now = nowDate.getTime();
if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) { if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) {
let _diff = let _diff = (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * 60 * -1;
(nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) *
60 *
-1;
nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff); nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff);
} }
this.nextLottery = nextDayOfLottery; this.nextLottery = nextDayOfLottery;
@@ -110,12 +108,8 @@ export default {
// Time calculations for days, hours, minutes and seconds // Time calculations for days, hours, minutes and seconds
this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24)); this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24));
this.hours = Math.floor( this.hours = Math.floor((this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
(this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) this.minutes = Math.floor((this.distance % (1000 * 60 * 60)) / (1000 * 60));
);
this.minutes = Math.floor(
(this.distance % (1000 * 60 * 60)) / (1000 * 60)
);
this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000); this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000);
if (this.days == 7) { if (this.days == 7) {
this.days = 0; this.days = 0;
@@ -124,7 +118,7 @@ export default {
this.initialize(); this.initialize();
} }
this.interval = setTimeout(this.countdown, 500); this.interval = setTimeout(this.countdown, 500);
}, }
} }
}; };
</script> </script>

View File

@@ -1,6 +1,9 @@
<template> <template>
<div class="chat-container"> <div class="chat-container">
<span class="logged-in-username" v-if="username">Logget inn som: <span class="username">{{ username }}</span> <button @click="removeUsername">Logg ut</button></span> <span class="logged-in-username" v-if="username"
>Logget inn som: <span class="username">{{ username }}</span>
<button @click="removeUsername">Logg ut</button></span
>
<div class="history" ref="history" v-if="chatHistory.length > 0"> <div class="history" ref="history" v-if="chatHistory.length > 0">
<div class="opaque-skirt"></div> <div class="opaque-skirt"></div>
@@ -8,7 +11,8 @@
<button @click="loadMoreHistory">Hent eldre meldinger</button> <button @click="loadMoreHistory">Hent eldre meldinger</button>
</div> </div>
<div class="history-message" <div
class="history-message"
v-for="(history, index) in chatHistory" v-for="(history, index) in chatHistory"
:key="`${history.username}-${history.timestamp}-${index}`" :key="`${history.username}-${history.timestamp}-${index}`"
> >
@@ -61,12 +65,11 @@ export default {
}; };
}, },
created() { created() {
getChatHistory(1, this.pageSize) getChatHistory(1, this.pageSize).then(resp => {
.then(resp => { this.chatHistory = resp.messages;
this.chatHistory = resp.messages; this.hasMorePages = resp.total != resp.messages.length;
this.hasMorePages = resp.total != resp.messages.length; });
}); const username = window.localStorage.getItem("username");
const username = window.localStorage.getItem('username');
if (username) { if (username) {
this.username = username; this.username = username;
this.emitUsernameOnConnect = true; this.emitUsernameOnConnect = true;
@@ -77,8 +80,7 @@ export default {
handler: function(newVal, oldVal) { handler: function(newVal, oldVal) {
if (oldVal.length == 0) { if (oldVal.length == 0) {
this.scrollToBottomOfHistory(); this.scrollToBottomOfHistory();
} } else if (newVal && newVal.length == oldVal.length) {
else if (newVal && newVal.length == oldVal.length) {
if (this.isScrollPositionAtBottom()) { if (this.isScrollPositionAtBottom()) {
this.scrollToBottomOfHistory(); this.scrollToBottomOfHistory();
} }
@@ -105,10 +107,7 @@ export default {
}); });
this.socket.on("connect", msg => { this.socket.on("connect", msg => {
if ( if (this.emitUsernameOnConnect || (this.wasDisconnected && this.username != null)) {
this.emitUsernameOnConnect ||
(this.wasDisconnected && this.username != null)
) {
this.setUsername(this.username); this.setUsername(this.username);
} }
}); });
@@ -133,12 +132,11 @@ export default {
let { page, pageSize } = this; let { page, pageSize } = this;
page = page + 1; page = page + 1;
getChatHistory(page, pageSize) getChatHistory(page, pageSize).then(resp => {
.then(resp => { this.chatHistory = resp.messages.concat(this.chatHistory);
this.chatHistory = resp.messages.concat(this.chatHistory); this.page = page;
this.page = page; this.hasMorePages = resp.total != this.chatHistory.length;
this.hasMorePages = resp.total != this.chatHistory.length; });
});
}, },
pad(num) { pad(num) {
if (num > 9) return num; if (num > 9) return num;
@@ -146,9 +144,7 @@ export default {
}, },
getTime(timestamp) { getTime(timestamp) {
let date = new Date(timestamp); let date = new Date(timestamp);
const timeString = `${this.pad(date.getHours())}:${this.pad( const timeString = `${this.pad(date.getHours())}:${this.pad(date.getMinutes())}:${this.pad(date.getSeconds())}`;
date.getMinutes()
)}:${this.pad(date.getSeconds())}`;
if (date.getDate() == new Date().getDate()) { if (date.getDate() == new Date().getDate()) {
return timeString; return timeString;
@@ -158,10 +154,10 @@ export default {
sendMessage() { sendMessage() {
const message = { message: this.message }; const message = { message: this.message };
this.socket.emit("chat", message); this.socket.emit("chat", message);
this.message = ''; this.message = "";
this.scrollToBottomOfHistory(); this.scrollToBottomOfHistory();
}, },
setUsername(username=undefined) { setUsername(username = undefined) {
if (this.temporaryUsername) { if (this.temporaryUsername) {
username = this.temporaryUsername; username = this.temporaryUsername;
} }
@@ -178,7 +174,7 @@ export default {
if (history) { if (history) {
return history.offsetHeight + history.scrollTop >= history.scrollHeight; return history.offsetHeight + history.scrollTop >= history.scrollHeight;
} }
return false return false;
}, },
scrollToBottomOfHistory() { scrollToBottomOfHistory() {
setTimeout(() => { setTimeout(() => {
@@ -189,15 +185,15 @@ export default {
scrollToMessageElement(message) { scrollToMessageElement(message) {
const elemTimestamp = this.getTime(message.timestamp); const elemTimestamp = this.getTime(message.timestamp);
const self = this; const self = this;
const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText; const getTimeStamp = elem => elem.getElementsByClassName("timestamp")[0].innerText;
const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp; const prevOldestMessageInNewList = elem => getTimeStamp(elem) == elemTimestamp;
setTimeout(() => { setTimeout(() => {
const { history } = self.$refs; const { history } = self.$refs;
const childrenElements = Array.from(history.getElementsByClassName('history-message')); const childrenElements = Array.from(history.getElementsByClassName("history-message"));
const elemInNewList = childrenElements.find(prevOldestMessageInNewList); const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
history.scrollTop = elemInNewList.offsetTop - 70 history.scrollTop = elemInNewList.offsetTop - 70;
}, 1); }, 1);
} }
} }
@@ -210,7 +206,7 @@ export default {
.chat-container { .chat-container {
position: relative; position: relative;
transform: translate3d(0,0,0); transform: translate3d(0, 0, 0);
} }
input { input {
@@ -241,7 +237,6 @@ input {
display: flex; display: flex;
} }
.history { .history {
height: 75%; height: 75%;
overflow-y: scroll; overflow-y: scroll;
@@ -276,11 +271,7 @@ input {
position: fixed; position: fixed;
height: 2rem; height: 2rem;
z-index: 1; z-index: 1;
background: linear-gradient( background: linear-gradient(to bottom, white, rgba(255, 255, 255, 0));
to bottom,
white,
rgba(255, 255, 255, 0)
);
} }
& .fetch-older-history { & .fetch-older-history {
@@ -310,7 +301,7 @@ input {
border-radius: 4px; border-radius: 4px;
&::before { &::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 2.1rem; top: 2.1rem;
left: 2rem; left: 2rem;

View File

@@ -1,60 +1,48 @@
<template> <template>
<div class="highscores" v-if="highscore.length > 0"> <div class="highscores" v-if="highscore.length > 0">
<section class="heading"> <section class="heading">
<h3> <h3>
Topp 5 vinnere Topp vinnere
</h3> </h3>
<router-link to="highscore" class=""> <router-link to="highscore" class="">
<span class="vin-link">Se alle vinnere</span> <span class="vin-link">Se alle vinnere</span>
</router-link> </router-link>
</section> </section>
<ol class="winner-list-container"> <ol class="winner-list-container">
<li v-for="(person, index) in highscore" :key="person._id" class="single-winner"> <li v-for="(person, index) in highscore" :key="person._id">
<span class="placement">{{index + 1}}.</span> <router-link :to="`/highscore/${person.name}`" class="single-winner">
<i class="icon icon--medal"></i> <span class="placement">{{ index + 1 }}.</span>
<p class="winner-name">{{ person.name }}</p> <i class="icon icon--medal"></i>
<p class="winner-name">{{ person.name }}</p>
</router-link>
</li> </li>
</ol> </ol>
</div> </div>
</template> </template>
<script> <script>
import { highscoreStatistics } from "@/api"; import { highscoreStatistics } from "@/api";
export default { export default {
data() { data() {
return { highscore: [] }; return {
highscore: [],
limit: 22
};
}, },
async mounted() { async mounted() {
let response = await highscoreStatistics(); return fetch(`/api/history/by-wins?limit=${this.limit}`)
response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1) .then(resp => resp.json())
this.highscore = this.generateScoreBoard(response.slice(0, 5)); .then(response => {
}, this.highscore = response.winners;
methods: { });
generateScoreBoard(highscore=this.highscore) {
let place = 0;
let highestWinCount = -1;
return highscore.map(win => {
const wins = win.wins.length
if (wins != highestWinCount) {
place += 1
highestWinCount = wins
}
const placeString = place.toString().padStart(2, "0");
win.rank = placeString;
return win
})
}
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/variables.scss"; @import "@/styles/variables.scss";
.heading { .heading {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -81,8 +69,8 @@ ol {
.winner-list-container { .winner-list-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr)); grid-template-columns: repeat(auto-fit, minmax(12em, 1fr));
gap: 5%; gap: 2rem;
.single-winner { .single-winner {
box-sizing: border-box; box-sizing: border-box;
@@ -91,7 +79,7 @@ ol {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
align-items: center; align-items: center;
padding: 1em; padding: 1em;
i { i {
font-size: 3em; font-size: 3em;
@@ -110,11 +98,71 @@ ol {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.winner-count {
grid-row: 3;
grid-column: 1 / -1;
margin: 0;
}
.winner-icon { .winner-icon {
grid-row: 1; grid-row: 1;
grid-column: 3; grid-column: 3;
} }
} }
// I'm sorry mama
@media (max-width: 550px) {
*:nth-child(n + 7) {
display: none;
}
}
@media (max-width: 1295px) {
*:nth-child(n + 7) {
display: none;
}
}
@media (max-width: 1630px) {
*:nth-child(n + 9) {
display: none;
}
}
@media (max-width: 1968px) {
*:nth-child(n + 11) {
display: none;
}
}
@media (max-width: 2300px) {
*:nth-child(n + 13) {
display: none;
}
}
@media (max-width: 2645px) {
*:nth-child(n + 15) {
display: none;
}
}
@media (max-width: 2975px) {
*:nth-child(n + 17) {
display: none;
}
}
@media (max-width: 3311px) {
*:nth-child(n + 19) {
display: none;
}
}
@media (max-width: 3647px) {
*:nth-child(n + 21) {
display: none;
}
}
} }
</style> </style>

View File

@@ -2,89 +2,47 @@
<div class="chart"> <div class="chart">
<canvas ref="purchase-chart" width="100" height="50"></canvas> <canvas ref="purchase-chart" width="100" height="50"></canvas>
<div ref="chartjsLegend" class="chartjsLegend"></div> <div ref="chartjsLegend" class="chartjsLegend"></div>
<div class="year-select" v-if="years.length">
<button
class="vin-button small"
v-for="year in years"
:class="{ active: yearSelected == year }"
@click="yearFilterClicked(year)"
>
{{ year }}
</button>
</div>
</div> </div>
</template> </template>
<script> <script>
import Chartjs from "chart.js"; import Chartjs from "chart.js";
import { chartPurchaseByColor } from "@/api";
export default { export default {
data() {
return {
lotteries: [],
years: [],
yearSelected: undefined,
chart: undefined
};
},
async mounted() { async mounted() {
let canvas = this.$refs["purchase-chart"].getContext("2d"); let canvas = this.$refs["purchase-chart"].getContext("2d");
let response = await chartPurchaseByColor(); this.lotteries = await this.chartPurchaseByColor();
let labels = []; if (this.lotteries?.length) this.years = [...new Set(this.lotteries.map(lot => lot.date.slice(0, 4)))];
let blue = {
label: "Blå", const dataset = this.calculateChartDatapoints();
borderColor: "#57d2fb",
backgroundColor: "#d4f2fe", let chartData = {
borderWidth: 2, labels: dataset.labels,
data: [] datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
};
let yellow = {
label: "Gul",
borderColor: "#ffde5d",
backgroundColor: "#fff6d6",
borderWidth: 2,
data: []
};
let red = {
label: "Rød",
borderColor: "#ef5878",
backgroundColor: "#fbd7de",
borderWidth: 2,
data: []
};
let green = {
label: "Grønn",
borderColor: "#10e783",
backgroundColor: "#c8f9df",
borderWidth: 2,
data: []
}; };
if (response.length == 1) { this.chart = new Chart(canvas, {
labels.push("");
blue.data.push(0);
yellow.data.push(0);
red.data.push(0);
green.data.push(0);
}
let highestNumber = 0;
for (let i = 0; i < response.length; i++) {
let thisDate = response[i];
let dateObject = new Date(thisDate.date);
labels.push(this.getPrettierDateString(dateObject));
blue.data.push(thisDate.blue);
yellow.data.push(thisDate.yellow);
red.data.push(thisDate.red);
green.data.push(thisDate.green);
if (thisDate.blue > highestNumber) {
highestNumber = thisDate.blue;
}
if (thisDate.yellow > highestNumber) {
highestNumber = thisDate.yellow;
}
if (thisDate.green > highestNumber) {
highestNumber = thisDate.green;
}
if (thisDate.red > highestNumber) {
highestNumber = thisDate.red;
}
}
let datasets = [blue, yellow, green, red];
let chartdata = {
labels: labels,
datasets: datasets
};
let chart = new Chart(canvas, {
type: "line", type: "line",
data: chartdata, data: chartData,
options: { options: {
maintainAspectRatio: false, maintainAspectRatio: false,
animation: { animation: {
@@ -110,8 +68,7 @@ export default {
yAxes: [ yAxes: [
{ {
ticks: { ticks: {
beginAtZero: true, beginAtZero: true
suggestedMax: highestNumber + 5
} }
} }
] ]
@@ -120,10 +77,82 @@ export default {
}); });
}, },
methods: { methods: {
async yearFilterClicked(year) {
this.yearSelected = this.yearSelected === year ? null : year;
this.lotteries = await this.chartPurchaseByColor();
const dataset = this.calculateChartDatapoints();
let chartData = {
labels: dataset.labels,
datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
};
this.chart.data = chartData;
this.chart.update();
},
setupDataset() {
let blue = {
label: "Blå",
borderColor: "#57d2fb",
backgroundColor: "#d4f2fe",
borderWidth: 2,
data: []
};
let yellow = {
label: "Gul",
borderColor: "#ffde5d",
backgroundColor: "#fff6d6",
borderWidth: 2,
data: []
};
let red = {
label: "Rød",
borderColor: "#ef5878",
backgroundColor: "#fbd7de",
borderWidth: 2,
data: []
};
let green = {
label: "Grønn",
borderColor: "#10e783",
backgroundColor: "#c8f9df",
borderWidth: 2,
data: []
};
return {
labels: [""],
blue,
green,
red,
yellow
};
},
calculateChartDatapoints() {
let dataset = this.setupDataset();
this.lotteries.map(lottery => {
const date = new Date(lottery.date);
dataset.labels.push(this.getPrettierDateString(date));
dataset.blue.data.push(lottery.blue);
dataset.green.data.push(lottery.green);
dataset.red.data.push(lottery.red);
dataset.yellow.data.push(lottery.yellow);
});
return dataset;
},
chartPurchaseByColor() {
const url = new URL("/api/lotteries", window.location);
if (this.yearSelected != null) url.searchParams.set("year", this.yearSelected);
return fetch(url.href)
.then(resp => resp.json())
.then(response => response.lotteries);
},
getPrettierDateString(date) { getPrettierDateString(date) {
return `${this.pad(date.getDate())}.${this.pad( return `${this.pad(date.getDate())}.${this.pad(date.getMonth() + 1)}.${this.pad(date.getYear() - 100)}`;
date.getMonth() + 1
)}.${this.pad(date.getYear() - 100)}`;
}, },
pad(num) { pad(num) {
if (num < 10) { if (num < 10) {
@@ -136,11 +165,19 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/media-queries.scss"; @import "@/styles/media-queries.scss";
.chart { .chart {
height: 40vh; height: 40vh;
max-height: 500px; max-height: 500px;
width: 100%; width: 100%;
} }
.year-select {
margin-top: 1rem;
button:not(:first-of-type) {
margin-left: 0.5rem;
}
}
</style> </style>

View File

@@ -2,28 +2,28 @@
<div class="container"> <div class="container">
<div class="input-line"> <div class="input-line">
<label for="redCheckbox"> <label for="redCheckbox">
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/> <input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors" />
<span class="border"> <span class="border">
<span class="checkmark"></span> <span class="checkmark"></span>
</span> </span>
<span class="text">Rød</span> <span class="text">Rød</span>
</label> </label>
<label for="blueCheckbox"> <label for="blueCheckbox">
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors"/> <input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors" />
<span class="border"> <span class="border">
<span class="checkmark"></span> <span class="checkmark"></span>
</span> </span>
<span class="text">Blå</span> <span class="text">Blå</span>
</label> </label>
<label for="yellowCheckbox"> <label for="yellowCheckbox">
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors"/> <input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors" />
<span class="border"> <span class="border">
<span class="checkmark"></span> <span class="checkmark"></span>
</span> </span>
<span class="text">Gul</span> <span class="text">Gul</span>
</label> </label>
<label for="greenCheckbox"> <label for="greenCheckbox">
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors"/> <input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors" />
<span class="border"> <span class="border">
<span class="checkmark"></span> <span class="checkmark"></span>
</span> </span>
@@ -31,15 +31,10 @@
</label> </label>
</div> </div>
<div class="input-line"> <div class="input-line">
<input <input type="number" placeholder="Antall lodd" @keyup.enter="generateColors" v-model="numberOfRaffles" />
type="number"
placeholder="Antall lodd"
@keyup.enter="generateColors"
v-model="numberOfRaffles"
/>
<button class="vin-button" @click="generateColors">Generer</button> <button class="vin-button" @click="generateColors">Generer</button>
</div> </div>
<div class="colors"> <div class="colors" :class="{ compact }">
<div <div
v-for="color in colors" v-for="color in colors"
:class="getColorClass(color)" :class="getColorClass(color)"
@@ -47,13 +42,6 @@
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }" :style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
></div> ></div>
</div> </div>
<div class="color-count-container" v-if="generated">
<span>Rød: {{ red }}</span>
<span>Blå: {{ blue }}</span>
<span>Gul: {{ yellow }}</span>
<span>Grønn: {{ green }}</span>
</div>
</div> </div>
</template> </template>
@@ -64,11 +52,15 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
compact: {
type: Boolean,
default: false
} }
}, },
data() { data() {
return { return {
numberOfRaffles: 4, numberOfRaffles: 6,
colors: [], colors: [],
blue: 0, blue: 0,
red: 0, red: 0,
@@ -101,18 +93,21 @@ export default {
if (time == 5) { if (time == 5) {
this.generating = false; this.generating = false;
this.generated = true; this.generated = true;
if (this.numberOfRaffles > 1 && if (
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) { this.numberOfRaffles > 1 &&
return [this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true)
.length == 1
) {
return;
} }
if (new Set(this.colors).size == 1) { if (new Set(this.colors).size == 1) {
alert("BINGO"); alert("BINGO");
} }
this.emitColors() this.emitColors();
window.ga('send', { window.ga("send", {
hitType: "event", hitType: "event",
eventCategory: "Raffles", eventCategory: "Raffles",
eventAction: "Generate", eventAction: "Generate",
@@ -147,8 +142,7 @@ export default {
} }
if (this.numberOfRaffles > 0) { if (this.numberOfRaffles > 0) {
for (let i = 0; i < this.numberOfRaffles; i++) { for (let i = 0; i < this.numberOfRaffles; i++) {
let color = let color = randomArray[Math.floor(Math.random() * randomArray.length)];
randomArray[Math.floor(Math.random() * randomArray.length)];
this.colors.push(color); this.colors.push(color);
if (color == 1) { if (color == 1) {
this.red += 1; this.red += 1;
@@ -201,12 +195,12 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/variables.scss"; @import "@/styles/variables.scss";
@import "../styles/global.scss"; @import "@/styles/global.scss";
@import "../styles/media-queries.scss"; @import "@/styles/media-queries.scss";
.container { .container {
margin: auto; // margin: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -282,6 +276,15 @@ label .text {
max-width: 1400px; max-width: 1400px;
margin: 3rem auto 0; margin: 3rem auto 0;
&.compact {
margin-top: 0.5rem;
> .color-box {
width: 100px;
height: 100px;
}
}
@include mobile { @include mobile {
margin: 1.8rem auto 0; margin: 1.8rem auto 0;
} }
@@ -309,20 +312,6 @@ label .text {
justify-content: space-around; justify-content: space-around;
} }
.color-count-container {
margin: auto;
width: 300px;
justify-content: space-around;
align-items: center;
display: flex;
font-family: Arial;
margin-top: 35px;
@include mobile {
width: 80vw;
}
}
.green { .green {
background-color: $light-green; background-color: $light-green;
} }

View File

@@ -4,7 +4,7 @@
<div class="flex justify-end"> <div class="flex justify-end">
<div class="requested-count cursor-pointer" @click="request"> <div class="requested-count cursor-pointer" @click="request">
<span>{{ requestedElement.count }}</span> <span>{{ requestedElement.count }}</span>
<i class="icon icon--heart" :class="{ 'active': locallyRequested }" /> <i class="icon icon--heart" :class="{ active: locallyRequested }" />
</div> </div>
</div> </div>
</template> </template>
@@ -17,10 +17,9 @@
<template v-slot:bottom> <template v-slot:bottom>
<div class="float-left request"> <div class="float-left request">
<i class="icon icon--heart request-icon" :class="{ 'active': locallyRequested }"></i> <i class="icon icon--heart request-icon" :class="{ active: locallyRequested }"></i>
<a aria-role="button" tabindex="0" class="link" @click="request" <a aria-role="button" tabindex="0" class="link" @click="request" :class="{ active: locallyRequested }">
:class="{ 'active': locallyRequested }"> {{ locallyRequested ? "Anbefalt" : "Anbefal" }}
{{ locallyRequested ? 'Anbefalt' : 'Anbefal' }}
</a> </a>
</div> </div>
</template> </template>
@@ -35,14 +34,14 @@ export default {
components: { components: {
Wine Wine
}, },
data(){ data() {
return { return {
wine: this.requestedElement.wine, wine: this.requestedElement.wine,
locallyRequested: false locallyRequested: false
} };
}, },
props: { props: {
requestedElement: { requestedElement: {
required: true, required: true,
type: Object type: Object
}, },
@@ -53,27 +52,26 @@ export default {
} }
}, },
methods: { methods: {
request(){ request() {
if (this.locallyRequested) if (this.locallyRequested) return;
return
console.log("requesting", this.wine) this.locallyRequested = true;
this.locallyRequested = true this.requestedElement.count = this.requestedElement.count + 1;
this.requestedElement.count = this.requestedElement.count +1 requestNewWine(this.wine);
requestNewWine(this.wine)
}, },
async deleteWine() { async deleteWine() {
const wine = this.wine const wine = this.wine;
if (window.confirm("Er du sikker på at du vil slette vinen?")) { if (window.confirm("Er du sikker på at du vil slette vinen?")) {
let response = await deleteRequestedWine(wine); let response = await deleteRequestedWine(wine);
if (response['success'] == true) { if (response["success"] == true) {
this.$emit('wineDeleted', wine); this.$emit("wineDeleted", wine);
} else { } else {
alert("Klarte ikke slette vinen"); alert("Klarte ikke slette vinen");
} }
} }
}, }
}, }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -83,7 +81,7 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: -0.5rem; margin-top: -0.5rem;
background-color: rgb(244,244,244); background-color: rgb(244, 244, 244);
border-radius: 1.1rem; border-radius: 1.1rem;
padding: 0.25rem 1rem; padding: 0.25rem 1rem;
font-size: 1.25em; font-size: 1.25em;
@@ -93,14 +91,14 @@ export default {
line-height: 1.25em; line-height: 1.25em;
} }
.icon--heart{ .icon--heart {
color: grey; color: grey;
} }
} }
.active { .active {
&.link { &.link {
border-color: $link-color border-color: $link-color;
} }
&.icon--heart { &.icon--heart {
@@ -121,4 +119,4 @@ export default {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div id="camera-stream">
<h2 v-if="errorMessage">{{ errorMessage }}</h2> <h2 v-if="errorMessage">{{ errorMessage }}</h2>
<video playsinline autoplay class="hidden"></video> <video playsinline autoplay class="hidden"></video>
</div> </div>
@@ -47,13 +47,8 @@ export default {
this.searchVideoForBarcode(this.video); this.searchVideoForBarcode(this.video);
}, },
handleError(error) { handleError(error) {
console.log( console.log("navigator.MediaDevices.getUserMedia error: ", error.message, error.name);
"navigator.MediaDevices.getUserMedia error: ", this.errorMessage = "Feil ved oppstart av kamera! Feilmelding: " + error.message;
error.message,
error.name
);
this.errorMessage =
"Feil ved oppstart av kamera! Feilmelding: " + error.message;
}, },
searchVideoForBarcode(video) { searchVideoForBarcode(video) {
const codeReader = new BrowserBarcodeReader(); const codeReader = new BrowserBarcodeReader();
@@ -84,10 +79,7 @@ export default {
this.errorMessage = "Feil! " + error.message || error; this.errorMessage = "Feil! " + error.message || error;
}, },
scrollIntoView() { scrollIntoView() {
window.scrollTo( window.scrollTo(0, document.getElementById("camera-stream").offsetTop - 10);
0,
document.getElementById("addwine-title").offsetTop - 10
);
} }
} }
}; };
@@ -112,4 +104,4 @@ h2 {
text-align: center; text-align: center;
color: $red; color: $red;
} }
</style> </style>

View File

@@ -1,51 +1,80 @@
<template> <template>
<div> <div>
<div class="tab-container"> <nav class="tab-container">
<div <a
class="tab" class="tab"
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
:key="index" :key="index"
@click="changeTab(index)" @click="changeTab(index)"
@keydown.enter="changeTab(index)"
tabindex="0"
:class="chosenTab == index ? 'active' : null" :class="chosenTab == index ? 'active' : null"
>{{ tab.name }}</div> >
</div> {{ tab.name }}
<span v-if="tab.counter" class="counter">{{ tab.counter }}</span>
</a>
</nav>
<div class="tab-elements"> <div class="tab-elements">
<component :is="tabs[chosenTab].component" /> <component :is="tabs[chosenTab].component" @counter="updateCounter" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import eventBus from "@/mixins/EventBus";
export default { export default {
props: { props: {
tabs: { tabs: {
type: Array type: Array
},
active: {
type: Number,
default: 0
} }
}, },
beforeMount() { beforeMount() {
this.chosenTab = this.active; const url = location.href;
if (url.includes("tab=")) {
const tabParameter = url.split("tab=")[1];
const matchingSlug = this.tabs.findIndex(tab => tab.slug == tabParameter);
console.log("matchingSlug:", matchingSlug);
this.chosenTab = matchingSlug;
}
}, },
data() { data() {
return { return {
chosenTab: 0 chosenTab: 0
}; };
}, },
computed: {
activeTab() {
return this.tabs[this.chosenTab];
}
},
methods: { methods: {
changeTab: function(num) { changeTab(num) {
this.chosenTab = num; this.chosenTab = num;
this.$emit("tabChange", num);
eventBus.$emit("tab-change"); let url = location.href;
const tabParameterIndex = url.indexOf("tab=");
if (tabParameterIndex > 0) {
url = url.split("tab=")[0] + `tab=${this.activeTab.slug}`;
} else {
url = url + `?tab=${this.activeTab.slug}`;
}
window.history.pushState({}, "", url);
},
updateCounter(val) {
this.activeTab.counter = val;
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/variables.scss";
@import "@/styles/media-queries.scss";
h1 { h1 {
text-align: center; text-align: center;
} }
@@ -54,28 +83,50 @@ h1 {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
margin-top: 25px; // margin-top: 25px;
border-bottom: 1px solid #333333; border-bottom: 1px solid var(--underlinenav-text);
margin-top: 2rem;
@include mobile {
flex-direction: column;
}
} }
.tab { .tab {
cursor: pointer; cursor: pointer;
font-size: 1.2rem; font-size: 1.1rem;
display: flex; padding: 8px 16px;
justify-content: center; border-bottom: 2px solid transparent;
align-items: center; color: rgba($matte-text-color, 0.9);
padding: 20px;
margin: 0 15px;
border: 1px solid #333333;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
background: #00000008;
border-bottom: 1px solid #333333;
margin-bottom: -1px;
&.active { &.active {
border-bottom: 1px solid white; color: $matte-text-color;
border-color: var(--underlinenav-text-active) !important;
background: white; background: white;
font-weight: 600;
}
&:hover,
&:focus {
border-color: var(--underlinenav-text-hover);
outline: 0;
}
& .counter {
margin-left: 4px;
box-sizing: border-box;
display: inline-block;
min-width: 20px;
padding: 0 6px;
font-size: 14px;
font-weight: 600;
line-height: 18px;
text-align: center;
background-color: rgba(209, 213, 218, 0.5);
border: 1px solid transparent;
border-radius: 2em;
} }
} }
</style> </style>

View File

@@ -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>

View File

@@ -1,37 +1,30 @@
<template> <template>
<section class="outer-bought"> <div>
<h3>Loddstatistikk</h3> <h3>Loddstatistikk</h3>
<div class="total-raffles"> <div class="total-raffles">
Totalt&nbsp; Totalt&nbsp;
<span class="total">{{ total }}</span> <span class="total">{{ total }}</span>
&nbsp;kjøpte,&nbsp; &nbsp;kjøpte,&nbsp;
<span>{{ totalWin }}&nbsp;vinn og&nbsp;</span> <span>{{ totalWin }}&nbsp;vinn og&nbsp;</span>
<span> {{ stolen }} stjålet </span> <span> {{ stolen }} stjålet </span>
</div> </div>
<div class="bought-container"> <div class="bought-container">
<div <div
v-for="color in colors" v-for="color in colors"
:class=" :class="color.name + '-container ' + color.name + '-raffle raffle-element-local'"
color.name +
'-container ' +
color.name +
'-raffle raffle-element-local'
"
:key="color.name" :key="color.name"
> >
<p class="winner-chance"> <p class="winner-chance">{{ translate(color.name) }} vinnersjanse</p>
{{translate(color.name)}} vinnersjanse
</p>
<span class="win-percentage">{{ color.totalPercentage }}% </span> <span class="win-percentage">{{ color.totalPercentage }}% </span>
<p class="total-bought-color">{{ color.total }} kjøpte</p> <p class="total-bought-color">{{ color.total }} kjøpte</p>
<p class="amount-of-wins"> {{ color.win }} vinn </p> <p class="amount-of-wins">{{ color.win }} vinn</p>
</div> </div>
</div> </div>
</section> </div>
</template> </template>
<script> <script>
import { colorStatistics } from "@/api"; import { colorStatistics } from "@/api";
@@ -45,109 +38,128 @@ export default {
green: 0, green: 0,
total: 0, total: 0,
totalWin: 0, totalWin: 0,
stolen: 0, stolen: 0
wins: 0,
redPercentage: 0,
yellowPercentage: 0,
greenPercentage: 0,
bluePercentage: 0
}; };
}, },
async mounted() { async mounted() {
let response = await colorStatistics(); this.allLotteries().then(this.computeColors);
this.red = response.red;
this.blue = response.blue;
this.green = response.green;
this.yellow = response.yellow;
this.total = response.total;
this.totalWin =
this.red.win + this.yellow.win + this.blue.win + this.green.win;
this.stolen = response.stolen;
this.redPercentage = this.round(
this.red.win == 0 ? 0 : (this.red.win / this.totalWin) * 100
);
this.greenPercentage = this.round(
this.green.win == 0 ? 0 : (this.green.win / this.totalWin) * 100
);
this.bluePercentage = this.round(
this.blue.win == 0 ? 0 : (this.blue.win / this.totalWin) * 100
);
this.yellowPercentage = this.round(
this.yellow.win == 0 ? 0 : (this.yellow.win / this.totalWin) * 100
);
this.colors.push({
name: "red",
total: this.red.total,
win: this.red.win,
totalPercentage: this.getPercentage(this.red.win, this.totalWin),
percentage: this.getPercentage(this.red.win, this.red.total)
});
this.colors.push({
name: "blue",
total: this.blue.total,
win: this.blue.win,
totalPercentage: this.getPercentage(this.blue.win, this.totalWin),
percentage: this.getPercentage(this.blue.win, this.blue.total)
});
this.colors.push({
name: "yellow",
total: this.yellow.total,
win: this.yellow.win,
totalPercentage: this.getPercentage(this.yellow.win, this.totalWin),
percentage: this.getPercentage(this.yellow.win, this.yellow.total)
});
this.colors.push({
name: "green",
total: this.green.total,
win: this.green.win,
totalPercentage: this.getPercentage(this.green.win, this.totalWin),
percentage: this.getPercentage(this.green.win, this.green.total)
});
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
}, },
methods: { methods: {
translate(color){ allLotteries() {
switch(color) { return fetch("/api/lotteries?includeWinners=true")
.then(resp => resp.json())
.then(response => response.lotteries);
},
translate(color) {
switch (color) {
case "blue": case "blue":
return "Blå" return "Blå";
break; break;
case "red": case "red":
return "Rød" return "Rød";
break; break;
case "green": case "green":
return "Grønn" return "Grønn";
break; break;
case "yellow": case "yellow":
return "Gul" return "Gul";
break;
break; break;
break;
} }
}, },
getPercentage: function(win, total) { getPercentage: function(win, total) {
return this.round(win == 0 ? 0 : (win / total) * 100); return this.round(win == 0 ? 0 : (win / total) * 100);
}, },
round: function(number) { round: function(number) {
//this can make the odds added together more than 100%, maybe rework? //this can make the odds added together more than 100%, maybe rework?
let actualPercentage = Math.round(number * 100) / 100; let actualPercentage = Math.round(number * 100) / 100;
let rounded = actualPercentage.toFixed(0); let rounded = actualPercentage.toFixed(0);
return rounded; return rounded;
},
computeColors(lotteries) {
let totalRed = 0;
let totalGreen = 0;
let totalYellow = 0;
let totalBlue = 0;
let total = 0;
let stolen = 0;
const colorAccumulatedWins = {
blue: 0,
green: 0,
red: 0,
yellow: 0
};
const accumelatedColors = (winners, colorAccumulatedWins) => {
winners.forEach(winner => {
const winnerColor = winner.color;
colorAccumulatedWins[winnerColor] += 1;
});
};
lotteries.forEach(lottery => {
totalRed += lottery.red;
totalGreen += lottery.green;
totalYellow += lottery.yellow;
totalBlue += lottery.blue;
total += lottery.bought;
stolen += lottery.stolen;
accumelatedColors(lottery.winners, colorAccumulatedWins);
});
this.red = totalRed;
this.yellow = totalYellow;
this.green = totalGreen;
this.blue = totalBlue;
this.total = total;
this.totalWin =
colorAccumulatedWins.red + colorAccumulatedWins.yellow + colorAccumulatedWins.blue + colorAccumulatedWins.green;
this.stolen = stolen;
this.colors.push({
name: "red",
total: totalRed,
win: colorAccumulatedWins.red,
totalPercentage: this.getPercentage(colorAccumulatedWins.red, this.totalWin),
percentage: this.getPercentage(colorAccumulatedWins.red, this.red.total)
});
this.colors.push({
name: "blue",
total: totalBlue,
win: colorAccumulatedWins.blue,
totalPercentage: this.getPercentage(colorAccumulatedWins.blue, this.totalWin),
percentage: this.getPercentage(colorAccumulatedWins.blue, this.blue.total)
});
this.colors.push({
name: "yellow",
total: totalYellow,
win: colorAccumulatedWins.yellow,
totalPercentage: this.getPercentage(colorAccumulatedWins.yellow, this.totalWin),
percentage: this.getPercentage(colorAccumulatedWins.yellow, this.yellow.total)
});
this.colors.push({
name: "green",
total: totalGreen,
win: colorAccumulatedWins.green,
totalPercentage: this.getPercentage(colorAccumulatedWins.green, this.totalWin),
percentage: this.getPercentage(colorAccumulatedWins.green, this.green.total)
});
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/variables.scss"; @import "@/styles/variables.scss";
@import "../styles/media-queries.scss"; @import "@/styles/media-queries.scss";
@import "../styles/global.scss"; @import "@/styles/global.scss";
@include mobile{ @include mobile {
section { section {
margin-top: 5em; margin-top: 5em;
} }
@@ -182,7 +194,7 @@ export default {
margin-top: 40px; margin-top: 40px;
} }
&.total-bought-color{ &.total-bought-color {
font-weight: bold; font-weight: bold;
margin-top: 25px; margin-top: 25px;
} }

View File

@@ -5,13 +5,17 @@
</template> </template>
<script> <script>
import { chartWinsByColor } from "@/api";
export default { export default {
methods: {
fetchWinsByColor() {
return fetch("/api/history/by-color").then(resp => resp.json());
}
},
async mounted() { async mounted() {
let canvas = this.$refs["win-chart"].getContext("2d"); let canvas = this.$refs["win-chart"].getContext("2d");
let response = await chartWinsByColor(); let response = await this.fetchWinsByColor();
const { colors } = response;
let labels = ["Vunnet"]; let labels = ["Vunnet"];
let blue = { let blue = {
label: "Blå", label: "Blå",
@@ -42,23 +46,26 @@ export default {
data: [] data: []
}; };
blue.data.push(response.blue.win); const findColorWinners = (colorSelect, colors) => {
yellow.data.push(response.yellow.win); return colors.filter(color => color.color == colorSelect)[0].count;
red.data.push(response.red.win); };
green.data.push(response.green.win);
const blueWinCount = findColorWinners("blue", colors);
const redWinCount = findColorWinners("red", colors);
const greenWinCount = findColorWinners("green", colors);
const yellowWinCount = findColorWinners("yellow", colors);
blue.data.push(blueWinCount);
red.data.push(redWinCount);
green.data.push(greenWinCount);
yellow.data.push(yellowWinCount);
let highestNumber = 0; let highestNumber = 0;
if (response.blue.win > highestNumber) { [blueWinCount, redWinCount, greenWinCount, greenWinCount].forEach(winCount => {
highestNumber = response.blue.win; if (winCount > highestNumber) {
} highestNumber = winCount;
if (response.red.win > highestNumber) { }
highestNumber = response.red.win; });
}
if (response.green.win > highestNumber) {
highestNumber = response.green.win;
}
if (response.yellow.win > highestNumber) {
highestNumber = response.yellow.win;
}
let datasets = [blue, yellow, green, red]; let datasets = [blue, yellow, green, red];
let chartdata = { let chartdata = {
@@ -102,8 +109,6 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../styles/media-queries.scss";
.chart { .chart {
height: 40vh; height: 40vh;
max-height: 500px; max-height: 500px;

View File

@@ -2,10 +2,7 @@
<div class="wine"> <div class="wine">
<slot name="top"></slot> <slot name="top"></slot>
<div class="wine-image"> <div class="wine-image">
<img <img v-if="wine.image && loadImage" :src="wine.image" />
v-if="wine.image && loadImage"
:src="wine.image"
/>
<img v-else class="wine-placeholder" alt="Wine image" /> <img v-else class="wine-placeholder" alt="Wine image" />
</div> </div>
@@ -38,7 +35,7 @@ export default {
data() { data() {
return { return {
loadImage: false loadImage: false
} };
}, },
methods: { methods: {
setImage(entries) { setImage(entries) {
@@ -53,7 +50,7 @@ export default {
this.observer = new IntersectionObserver(this.setImage, { this.observer = new IntersectionObserver(this.setImage, {
root: this.$el, root: this.$el,
threshold: 0 threshold: 0
}) });
}, },
mounted() { mounted() {
this.observer.observe(this.$el); this.observer.observe(this.$el);
@@ -66,16 +63,17 @@ export default {
@import "@/styles/variables"; @import "@/styles/variables";
.wine { .wine {
align-self: flex-start;
padding: 1rem; padding: 1rem;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15); box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
width: 100%;
@include tablet { @include tablet {
width: 250px; max-width: 280px;
height: 100%;
} }
} }
@@ -85,19 +83,18 @@ export default {
margin-top: 10px; margin-top: 10px;
img { img {
height: 250px; height: 280px;
@include mobile { @include mobile {
object-fit: cover; object-fit: cover;
max-width: 90px; max-width: 90px;
} }
} }
.wine-placeholder { .wine-placeholder {
height: 250px; height: 280px;
width: 70px; width: 70px;
} }
} }
.wine-details { .wine-details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -107,7 +104,7 @@ export default {
} }
} }
.wine-name{ .wine-name {
font-size: 20px; font-size: 20px;
margin: 1em 0; margin: 1em 0;
} }
@@ -120,6 +117,7 @@ export default {
.bottom-section { .bottom-section {
width: 100%; width: 100%;
margin-top: 1rem; margin-top: 1rem;
align-self: flex-end;
.link { .link {
color: $matte-text-color; color: $matte-text-color;
@@ -135,4 +133,4 @@ export default {
} }
} }
} }
</style> </style>

View File

@@ -2,18 +2,18 @@
<div v-if="wines.length > 0" class="wines-main-container"> <div v-if="wines.length > 0" class="wines-main-container">
<div class="info-and-link"> <div class="info-and-link">
<h3> <h3>
Topp 5 viner Topp viner
</h3> </h3>
<router-link to="viner"> <router-link to="viner">
<span class="vin-link">Se alle viner </span> <span class="vin-link">Se alle viner </span>
</router-link> </router-link>
</div> </div>
<div class="wine-container"> <div class="wines-container">
<Wine v-for="wine in wines" :key="wine" :wine="wine"> <Wine v-for="wine in wines" :key="wine" :wine="wine">
<template v-slot:top> <template v-slot:top>
<div class="flex justify-end"> <div class="flex justify-end">
<div class="requested-count cursor-pointer"> <div class="requested-count cursor-pointer">
<span> {{ wine.occurences }} </span> <span> {{ wine.occurences }} </span>
<i class="icon icon--heart" /> <i class="icon icon--heart" />
</div> </div>
</div> </div>
@@ -32,32 +32,36 @@ export default {
Wine Wine
}, },
data() { data() {
return { return {
wines: [], wines: [],
clickedWine: null, clickedWine: null,
limit: 18
}; };
}, },
async mounted() { async mounted() {
let response = await overallWineStatistics(); this.getAllWines();
response.sort();
response = response
.filter(wine => wine.name != null && wine.name != "")
.sort(
this.predicate(
{
name: "occurences",
reverse: true
},
{
name: "rating",
reverse: true
}
)
);
this.wines = response.slice(0, 5);
}, },
methods: { methods: {
getAllWines() {
return fetch(`/api/wines?limit=${this.limit}`)
.then(resp => resp.json())
.then(response => {
let { wines, success } = response;
this.wines = wines.sort(
this.predicate(
{
name: "occurences",
reverse: true
},
{
name: "rating",
reverse: true
}
)
);
});
},
predicate: function() { predicate: function() {
var fields = [], var fields = [],
n_fields = arguments.length, n_fields = arguments.length,
@@ -125,42 +129,72 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/variables.scss"; @import "@/styles/variables.scss";
@import "@/styles/global.scss"; @import "@/styles/global.scss";
@import "../styles/media-queries.scss"; @import "@/styles/media-queries.scss";
.wines-main-container { .wines-main-container {
margin-bottom: 10em; margin-bottom: 10em;
} }
.info-and-link{ .info-and-link {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.wine-container { .requested-count {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); align-items: center;
grid-gap: 2rem; margin-top: -0.5rem;
background-color: rgb(244, 244, 244);
border-radius: 1.1rem;
padding: 0.25rem 1rem;
font-size: 1.25em;
.requested-count { span {
display: flex; padding-right: 0.5rem;
align-items: center; line-height: 1.25em;
margin-top: -0.5rem; }
background-color: rgb(244,244,244); .icon--heart {
border-radius: 1.1rem; font-size: 1.5rem;
padding: 0.25rem 1rem; color: var(--link-color);
font-size: 1.25em;
span {
padding-right: 0.5rem;
line-height: 1.25em;
}
.icon--heart{
font-size: 1.5rem;
color: $link-color;
}
} }
} }
// Call for help
.wines-container {
@media (max-width: 1643px) {
*:nth-child(n + 7) {
display: none;
}
}
@media (max-width: 2066px) {
*:nth-child(n + 9) {
display: none;
}
}
@media (max-width: 2490px) {
*:nth-child(n + 11) {
display: none;
}
}
@media (max-width: 2915px) {
*:nth-child(n + 13) {
display: none;
}
}
@media (max-width: 3335px) {
*:nth-child(n + 15) {
display: none;
}
}
@media (max-width: 3758px) {
*:nth-child(n + 17) {
display: none;
}
}
}
</style> </style>

View File

@@ -85,9 +85,7 @@ export default {
this.startConfetti(this.currentName); this.startConfetti(this.currentName);
return; return;
} }
this.currentName = this.attendees[ this.currentName = this.attendees[this.nameRounds % this.attendees.length].name;
this.nameRounds % this.attendees.length
].name;
this.nameRounds += 1; this.nameRounds += 1;
clearTimeout(this.nameTimeout); clearTimeout(this.nameTimeout);
this.nameTimeout = setTimeout(() => { this.nameTimeout = setTimeout(() => {
@@ -136,8 +134,8 @@ export default {
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds. //duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
var duration = 7 * 1000; var duration = 7 * 1000;
var animationEnd = Date.now() + duration; var animationEnd = Date.now() + duration;
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20}; var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20 };
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35} var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35 };
function randomInRange(min, max) { function randomInRange(min, max) {
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
@@ -148,27 +146,27 @@ export default {
var timeLeft = animationEnd - Date.now(); var timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) { if (timeLeft <= 0) {
self.drawing = false; self.drawing = false;
console.time("drawing finished") console.time("drawing finished");
return clearInterval(interval); return clearInterval(interval);
} }
if (currentName == "Amund Brandsrud") { if (currentName == "Amund Brandsrud") {
runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135}); runCannon(uberDefaults, { x: 1, y: 1 }, { angle: 135 });
runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45}); runCannon(uberDefaults, { x: 0, y: 1 }, { angle: 45 });
runCannon(uberDefaults, {y: 1 }, {angle: 90}); runCannon(uberDefaults, { y: 1 }, { angle: 90 });
runCannon(uberDefaults, {x: 0 }, {angle: 45}); runCannon(uberDefaults, { x: 0 }, { angle: 45 });
runCannon(uberDefaults, {x: 1 }, {angle: 135}); runCannon(uberDefaults, { x: 1 }, { angle: 135 });
} else { } else {
runCannon(defaults, {x: 0 }, {angle: 45}); runCannon(defaults, { x: 0 }, { angle: 45 });
runCannon(defaults, {x: 1 }, {angle: 135}); runCannon(defaults, { x: 1 }, { angle: 135 });
runCannon(defaults, {y: 1 }, {angle: 90}); runCannon(defaults, { y: 1 }, { angle: 90 });
} }
}, 250); }, 250);
function runCannon(confettiDefaultValues, originPoint, launchAngle){ function runCannon(confettiDefaultValues, originPoint, launchAngle) {
confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle)) confetti(Object.assign({}, confettiDefaultValues, { origin: originPoint }, launchAngle));
} }
}, },
ordinalNumber(number=this.currentWinnerLocal.winnerCount) { ordinalNumber(number = this.currentWinnerLocal.winnerCount) {
const dictonary = { const dictonary = {
1: "første", 1: "første",
2: "andre", 2: "andre",
@@ -187,7 +185,6 @@ export default {
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,9 +1,9 @@
<template> <template>
<section> <section>
<h2>{{ title ? title : 'Vinnere' }}</h2> <h2>{{ title ? title : "Vinnere" }}</h2>
<div class="winning-raffles" v-if="winners.length > 0"> <div class="winning-raffles" v-if="winners.length > 0">
<div v-for="(winner, index) in winners" :key="index"> <div v-for="(winner, index) in winners" :key="index">
<router-link :to="`/highscore/${ encodeURIComponent(winner.name) }`"> <router-link :to="`/highscore/${winner.name}`">
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div> <div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
</router-link> </router-link>
</div> </div>
@@ -26,7 +26,7 @@ export default {
type: Array type: Array
}, },
drawing: { drawing: {
type: Boolean, type: Boolean
}, },
title: { title: {
type: String, type: String,

View File

@@ -1,17 +1,16 @@
const dateString = date => {
const dateString = (date) => { if (typeof date == "string") {
if (typeof(date) == "string") {
date = new Date(date); date = new Date(date);
} }
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date) const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date) const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date) const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
return `${ye}-${mo}-${da}` return `${ye}-${mo}-${da}`;
} };
function humanReadableDate(date) { function humanReadableDate(date) {
const options = { year: 'numeric', month: 'long', day: 'numeric' }; const options = { year: "numeric", month: "long", day: "numeric" };
return new Date(date).toLocaleDateString(undefined, options); return new Date(date).toLocaleDateString(undefined, options);
} }
@@ -20,8 +19,4 @@ function daysAgo(date) {
return Math.round(Math.abs((new Date() - new Date(date)) / day)); return Math.round(Math.abs((new Date() - new Date(date)) / day));
} }
export { export { dateString, humanReadableDate, daysAgo };
dateString,
humanReadableDate,
daysAgo
}

View File

@@ -3,43 +3,47 @@ import VueRouter from "vue-router";
import { routes } from "@/router.js"; import { routes } from "@/router.js";
import Vinlottis from "@/Vinlottis"; import Vinlottis from "@/Vinlottis";
import Toast from "@/plugins/Toast";
import * as Sentry from "@sentry/browser"; import * as Sentry from "@sentry/browser";
import { Vue as VueIntegration } from "@sentry/integrations"; import { Vue as VueIntegration } from "@sentry/integrations";
Vue.use(VueRouter); Vue.use(VueRouter);
// Plugins
Vue.use(Toast);
const ENV = window.location.href.includes("localhost") ? "development" : "production"; const ENV = window.location.href.includes("localhost") ? "development" : "production";
if (ENV !== "development") { if (ENV !== "development") {
Sentry.init({ Sentry.init({
dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091", dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
integrations: [ integrations: [new VueIntegration({ Vue })],
new VueIntegration({ Vue })
],
beforeSend: event => { beforeSend: event => {
console.error(event); console.error(event);
return event; return event;
} }
}) });
} }
// Add global GA variables // Add global GA variables
window.ga = window.ga || function(){ window.ga =
window.ga.q = window.ga.q || []; window.ga ||
window.ga.q.push(arguments); function() {
}; window.ga.q = window.ga.q || [];
window.ga.q.push(arguments);
};
ga.l = 1 * new Date(); ga.l = 1 * new Date();
// Initiate // Initiate
ga('create', __GA_TRACKINGID__, { ga("create", __GA_TRACKINGID__, {
'allowAnchor': false, allowAnchor: false,
'cookieExpires': __GA_COOKIELIFETIME__, // Time in seconds cookieExpires: __GA_COOKIELIFETIME__, // Time in seconds
'cookieFlags': 'SameSite=Strict; Secure' cookieFlags: "SameSite=Strict; Secure"
}); });
ga('set', 'anonymizeIp', true); // Enable IP Anonymization/IP masking ga("set", "anonymizeIp", true); // Enable IP Anonymization/IP masking
ga('send', 'pageview'); ga("send", "pageview");
if (ENV == 'development') if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true;
window[`ga-disable-${__GA_TRACKINGID__}`] = true;
const router = new VueRouter({ const router = new VueRouter({
routes: routes routes: routes

View File

@@ -6,9 +6,9 @@
"scripts": { "scripts": {
"build": "cross-env NODE_ENV=production webpack --progress", "build": "cross-env NODE_ENV=production webpack --progress",
"build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress", "build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress",
"dev": "yarn webpack serve --mode development --env development", "watch": "yarn webpack serve --mode development --env development",
"start": "node server.js", "start": "node server.js",
"start-noauth": "cross-env NODE_ENV=development node server.js", "dev": "cross-env NODE_ENV=development node server.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",

View File

@@ -18,24 +18,36 @@ const MongoStore = require("connect-mongo")(session);
// mongoose / database // mongoose / database
console.log("Trying to connect with mongodb.."); console.log("Trying to connect with mongodb..");
mongoose.promise = global.Promise; mongoose.promise = global.Promise;
mongoose.connect("mongodb://localhost/vinlottis", { mongoose
useCreateIndex: true, .connect("mongodb://localhost/vinlottis", {
useNewUrlParser: true, useCreateIndex: true,
useUnifiedTopology: true, useNewUrlParser: true,
serverSelectionTimeoutMS: 10000 // initial connection timeout useUnifiedTopology: true,
}).then(_ => console.log("Mongodb connection established!")) serverSelectionTimeoutMS: 10000 // initial connection timeout
.catch(err => { })
console.log(err); .then(_ => console.log("Mongodb connection established!"))
console.error("ERROR! Mongodb required to run."); .catch(err => {
process.exit(1); console.log(err);
}) console.error("ERROR! Mongodb required to run.");
process.exit(1);
});
mongoose.set("debug", false); mongoose.set("debug", false);
// middleware // middleware
const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS")); const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS"));
const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders")); const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders"));
app.use(setupCORS) app.use(setupCORS);
app.use(setupHeaders) app.use(setupHeaders);
if (process.env.NODE_ENV == "development") {
console.info(`NODE_ENV=development set, your are now always an authenticated user.`);
const alwaysAuthenticatedWhenLocalhost = require(path.join(
__dirname,
"/api/middleware/alwaysAuthenticatedWhenLocalhost"
));
app.use(alwaysAuthenticatedWhenLocalhost);
}
// parse application/json // parse application/json
app.use(express.json()); app.use(express.json());
@@ -52,7 +64,7 @@ app.use(
}) })
); );
app.set('socketio', io); // set io instance to key "socketio" app.set("socketio", io); // set io instance to key "socketio"
const passport = require("passport"); const passport = require("passport");
const LocalStrategy = require("passport-local"); const LocalStrategy = require("passport-local");