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

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

View File

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

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) => {
if (process.env.NODE_ENV == "development") {
console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
req.isAuthenticated = () => true;
return next();
}
if (!req.isAuthenticated()) {
return res.status(401).send({
success: false,

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

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,20 +14,25 @@
<h4 class="margin-bottom-0">Vinnende farger:</h4>
<div class="raffle-container el-spacing">
<div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)" :key="color">
<div
class="raffle-element"
:class="color + `-raffle`"
v-for="[color, occurences] in Object.entries(winningColors)"
:key="color"
>
{{ occurences }}
</div>
</div>
<h4 class="el-spacing">Flasker vunnet:</h4>
<div v-for="win in winner.highscore" :key="win._id">
<div v-for="win in winner.wins" :key="win._id">
<router-link :to="winDateUrl(win.date)" class="days-ago">
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
</router-link>
<div class="won-wine">
<img :src="smallerWineImage(win.wine.image)">
<div class="won-wine" v-if="win.wine">
<img :src="smallerWineImage(win.wine.image)" />
<div class="won-wine-details">
<h3>{{ win.wine.name }}</h3>
@@ -38,6 +43,11 @@
<div class="raffle-element small" :class="win.color + `-raffle`"></div>
</div>
<div class="won-wine" v-else>
<div class="won-wine-details">
<h3>Oisann! Klarte ikke finne vin.</h3>
</div>
</div>
</div>
</section>
@@ -49,67 +59,71 @@
</template>
<script>
import { getWinnerByName } from "@/api";
import { humanReadableDate, daysAgo } from "@/utils";
import { dateString, humanReadableDate, daysAgo } from "@/utils";
export default {
data() {
return {
winner: undefined,
name: undefined,
error: undefined,
previousRoute: {
default: true,
name: "topplisten",
path: "/highscore"
}
}
};
},
beforeRouteEnter(to, from, next) {
next(vm => {
if (from.name != null)
vm.previousRoute = from
})
if (from.name != null) vm.previousRoute = from;
});
},
computed: {
numberOfWins() {
return this.winner.highscore.length
return this.winner.wins.length;
}
},
created() {
const nameFromURL = this.$route.params.name;
getWinnerByName(nameFromURL)
this.name = this.$route.params.name;
this.getWinnerByName(this.name)
.then(winner => this.setWinner(winner))
.catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`)
.catch(err => (this.error = `Ingen med navn: "${nameFromURL}" funnet.`));
},
methods: {
getWinnerByName(name) {
return fetch(`/api/history/by-name/${name}`)
.then(resp => resp.json())
.then(response => response.winner);
},
setWinner(winner) {
this.winner = {
name: winner.name,
highscore: [],
...winner
}
this.winningColors = this.findWinningColors()
};
this.winningColors = this.findWinningColors();
},
smallerWineImage(image) {
if (image && image.includes(`515x515`))
return image.replace(`515x515`, `175x175`)
return image
if (image && image.includes(`515x515`)) return image.replace(`515x515`, `175x175`);
if (image && image.includes(`500x500`)) return image.replace(`500x500`, `175x175`);
return image;
},
findWinningColors() {
const colors = this.winner.highscore.map(win => win.color)
const colorOccurences = {}
const colors = this.winner.wins.map(win => win.color);
const colorOccurences = {};
colors.forEach(color => {
if (colorOccurences[color] == undefined) {
colorOccurences[color] = 1
colorOccurences[color] = 1;
} else {
colorOccurences[color] += 1
colorOccurences[color] += 1;
}
})
return colorOccurences
});
return colorOccurences;
},
winDateUrl(date) {
const timestamp = new Date(date).getTime();
return `/history/${timestamp}`
const dateParameter = dateString(new Date(date));
return `/history/${dateParameter}`;
},
navigateBack() {
if (this.previousRoute.default) {
@@ -121,7 +135,7 @@ export default {
humanReadableDate: humanReadableDate,
daysAgo: daysAgo
}
}
};
</script>
<style lang="scss" scoped>
@@ -142,7 +156,7 @@ $elementSpacing: 3rem;
}
.container {
width: 90vw;
width: 90vw;
margin: 3rem auto;
margin-bottom: 0;
padding-bottom: 3rem;
@@ -233,7 +247,7 @@ h1 {
@include tablet {
width: calc(100% - 160px - 80px);
}
& > * {
width: 100%;
}
@@ -259,10 +273,9 @@ h1 {
}
}
.backdrop {
$background: rgb(244,244,244);
$background: rgb(244, 244, 244);
--padding: 2rem;
@include desktop {
--padding: 5rem;
@@ -270,4 +283,4 @@ h1 {
background-color: $background;
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>
<section class="main-container">
<Modal
v-if="showModal"
modalText="Ønsket ditt har blitt lagt til"
<Modal
v-if="showModal"
modalText="Ønsket ditt har blitt lagt til"
:buttons="modalButtons"
@click="emitFromModalButton"
></Modal>
<h1>
Foreslå en vin!
</h1>
<section class="search-container">
<section class="search-section">
<input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷" class="search-input-field">
<button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button>
</section>
<section v-for="(wine, index) in this.wines" :key="index" class="single-result">
<img
v-if="wine.image"
:src="wine.image"
class="wine-image"
:class="{ 'fullscreen': fullscreen }"
<input
type="text"
v-model="searchString"
@keyup.enter="searchWines()"
placeholder="Søk etter en vin du liker her!🍷"
class="search-input-field"
/>
<button :disabled="!searchString" @click="searchWines()" class="vin-button">Søk</button>
</section>
<section v-for="(wine, index) in wines" :key="index" class="single-result">
<img v-if="wine.image" :src="wine.image" class="wine-image" :class="{ fullscreen: fullscreen }" />
<img v-else class="wine-placeholder" alt="Wine image" />
<section class="wine-info">
<h2 v-if="wine.name">{{ wine.name }}</h2>
@@ -29,37 +32,38 @@
<span v-if="wine.rating">{{ wine.rating }}%</span>
<span v-if="wine.price">{{ wine.price }} NOK</span>
<span v-if="wine.country">{{ wine.country }}</span>
<span v-if="wine.year">{{ wine.year }}</span>
</div>
</section>
<button class="vin-button" @click="request(wine)">Foreslå denne</button>
<a
v-if="wine.vivinoLink"
:href="wine.vivinoLink"
class="wine-link"
>Les mer</a>
<button class="vin-button" @click="requestWine(wine)">Foreslå denne</button>
<a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="wine-link">Les mer</a>
</section>
<p v-if="this.wines && this.wines.length == 0">
<p v-if="loading == false && wines && wines.length == 0">
Fant ingen viner med det navnet!
</p>
<p v-else-if="loading">Loading...</p>
</section>
</section>
</template>
<script>
import { searchForWine, requestNewWine } from "@/api";
import { searchForWine } from "@/api";
import Wine from "@/ui/Wine";
import Modal from "@/ui/Modal";
import RequestedWineCard from "@/ui/RequestedWineCard";
export default {
components: {
Wine,
Modal
Modal,
RequestedWineCard
},
data() {
return {
searchString: undefined,
wines: undefined,
showModal: false,
loading: false,
modalButtons: [
{
text: "Legg til flere viner",
@@ -70,30 +74,59 @@ export default {
action: "move"
}
]
}
};
},
methods: {
fetchWineFromVin(){
if(this.searchString){
this.wines = []
let localSearchString = this.searchString.replace(/ /g,"_");
searchForWine(localSearchString)
.then(res => this.wines = res)
fetchWinesByQuery(query) {
let url = new URL("/api/vinmonopolet/wine/search", window.location);
url.searchParams.set("name", query);
this.wines = [];
this.loading = true;
return fetch(url.href)
.then(resp => resp.json())
.then(response => (this.wines = response.wines))
.finally(wines => (this.loading = false));
},
searchWines() {
if (this.searchString) {
let localSearchString = this.searchString.replace(/ /g, "_");
this.fetchWinesByQuery(localSearchString);
}
},
request(wine){
requestNewWine(wine)
.then(() => this.showModal = true)
requestWine(wine) {
const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wine: wine })
};
return fetch("/api/request", options)
.then(resp => resp.json())
.then(response => {
if (response.success) {
this.showModal = true;
this.$toast.info({
title: `Vinen ${wine.name} har blitt foreslått!`
});
} else {
this.$toast.error({
title: "Obs, her oppsto det en feil! Feilen er logget.",
description: response.message
});
}
});
},
emitFromModalButton(action){
if(action == "stay"){
this.showModal = false
emitFromModalButton(action) {
if (action == "stay") {
this.showModal = false;
} else {
this.$router.push("/requested-wines");
}
}
},
}
}
};
</script>
<style lang="scss" scoped>
@@ -101,12 +134,11 @@ export default {
@import "@/styles/global";
@import "@/styles/variables";
h1{
h1 {
text-align: center;
}
.main-container{
.main-container {
margin: auto;
max-width: 1200px;
}
@@ -120,66 +152,63 @@ input[type="text"] {
max-width: 90%;
}
.search-container{
.search-container {
margin: 1rem;
}
.search-section{
.search-section {
display: grid;
grid: 1fr / 1fr .2fr;
grid: 1fr / 1fr 0.2fr;
@include mobile{
.vin-button{
@include mobile {
.vin-button {
display: none;
}
.search-input-field{
.search-input-field {
grid-column: 1 / -1;
}
}
}
.single-result{
.single-result {
margin-top: 1rem;
display: grid;
grid: 1fr / .5fr 2fr .5fr .5fr;
grid: 1fr / 0.5fr 2fr 0.5fr 0.5fr;
grid-template-areas: "picture details button-left button-right";
justify-items: center;
align-items: center;
grid-gap: 1em;
padding-bottom: 1em;
margin-bottom: 1em;
box-shadow: 0 1px 0 0 rgba(0,0,0,0.2);
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
@include mobile{
@include mobile {
grid: 1fr 0.5fr / 0.5fr 1fr;
grid-template-areas:
"picture details"
"button-left button-right";
grid-gap: 0.5em;
grid: 1fr .5fr / .5fr 1fr;
grid-template-areas: "picture details"
"button-left button-right";
grid-gap: .5em;
.vin-button{
.vin-button {
grid-area: button-right;
padding: .5em;
padding: 0.5em;
font-size: 1em;
line-height: 1em;
height: 2em;
}
.wine-link{
.wine-link {
grid-area: button-left;
}
h2{
h2 {
font-size: 1em;
max-width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis
text-overflow: ellipsis;
}
}
.wine-image {
height: 100px;
@@ -192,14 +221,14 @@ input[type="text"] {
grid-area: picture;
}
.wine-info{
.wine-info {
grid-area: details;
width: 100%;
h2{
h2 {
margin: 0;
}
.details{
.details {
top: 0;
display: flex;
flex-direction: column;
@@ -216,22 +245,20 @@ input[type="text"] {
width: max-content;
}
.vin-button{
.vin-button {
grid-area: button-right;
}
@include tablet{
h2{
@include tablet {
h2 {
font-size: 1.2em;
}
}
@include desktop{
h2{
@include desktop {
h2 {
font-size: 1.6em;
}
}
}
</style>
</style>

View File

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

View File

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

View File

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

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>
<div class="container">
<div v-if="!posted">
<h1 v-if="name">Gratulerer {{name}}!</h1>
<div>
<div v-if="!posted" class="container">
<h1 v-if="name">Gratulerer {{ name }}!</h1>
<p v-if="name">
Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.
</p>
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
<h1 v-else-if="!turn && wines.length" class="sent-container">Finner ikke noen vinner her..</h1>
<h1 v-else-if="!turn" class="sent-container">Du vente tur..</h1>
<div class="wines-container" v-if="name">
<Wine :wine="wine" v-for="wine in wines" :key="wine">
<button
@click="chooseWine(wine.name)"
class="vin-button select-wine"
>Velg denne vinnen</button>
<button @click="chooseWine(wine)" class="vin-button select-wine">Velg denne vinnen</button>
</Wine>
</div>
</div>
<div v-else-if="posted" class="sent-container">
<h1>Valget ditt er sendt inn!</h1>
<p>Du får mer info om henting snarest!</p>
@@ -24,15 +26,13 @@
</template>
<script>
import { getAmIWinner, postWineChosen, prelottery } from "@/api";
import Wine from "@/ui/Wine";
export default {
components: { Wine },
data() {
return {
id: null,
existing: false,
fetched: false,
turn: false,
name: null,
wines: [],
@@ -40,30 +40,43 @@ export default {
};
},
async mounted() {
this.id = this.$router.currentRoute.params.id;
const { id } = this.$router.currentRoute.params;
let winnerObject = await getAmIWinner(this.id);
this.fetched = true;
if (!winnerObject || !winnerObject.existing) {
console.error("non existing", winnerObject);
return;
}
this.existing = true;
if (winnerObject.existing && !winnerObject.turn) {
console.error("not your turn yet", winnerObject);
return;
}
this.turn = true;
this.name = winnerObject.name;
this.wines = await prelottery();
this.id = id;
this.getPrizes(id);
},
methods: {
chooseWine: async function(name) {
let posted = await postWineChosen(this.id, name);
console.log("response", posted);
if (posted.success) {
this.posted = true;
}
getPrizes(id) {
fetch(`/api/lottery/prize-distribution/prizes/${id}`)
.then(resp => resp.json())
.then(response => {
if (response.success) {
this.wines = response.wines;
this.name = response.winner.name;
this.turn = true;
}
});
},
chooseWine(wine) {
const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wine })
};
fetch(`/api/lottery/prize-distribution/prize/${this.id}`, options)
.then(resp => resp.json())
.then(response => {
if (response.success) {
this.$toast.info({ title: `Valgt vin: ${wine.name}` });
this.posted = true;
} else {
this.$toast.error({
title: "Klarte ikke velge vin :(",
description: response.message
});
}
});
}
}
};
@@ -74,9 +87,19 @@ export default {
.container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 2rem;
padding: 2rem;
width: 80%;
margin: 0 auto;
max-width: 2000px;
}
.wines-container {
width: 100%;
}
.sent-container {
width: 100%;
height: 90vh;
@@ -90,11 +113,4 @@ export default {
.select-wine {
margin-top: 1rem;
}
.wines-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: flex-start;
}
</style>
</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 {
text-decoration: none;
cursor: pointer;
color: inherit;
}
.title {
@@ -51,8 +53,10 @@ a {
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
label {
margin-top: 0.7rem;
margin-bottom: 0.25rem;
font-weight: 600;
text-transform: uppercase;
@@ -76,6 +80,7 @@ a {
> *:not(:last-child) {
margin-right: 2rem;
margin-bottom: 0.75rem;
}
&.column {
@@ -95,7 +100,7 @@ a {
> *:not(:last-child) {
margin-right: unset;
margin-bottom: .75rem;
margin-bottom: 0.75rem;
}
}
}
@@ -105,6 +110,8 @@ input,
textarea {
border-radius: 0;
box-shadow: none;
padding: 0;
margin: 0;
-webkit-appearance: none;
font-size: 1.1rem;
border: 1px solid rgba(#333333, 0.3);
@@ -136,6 +143,11 @@ textarea {
height: auto;
}
&.warning {
background-color: #f9826c;
color: white;
}
&.danger {
background-color: $red;
color: white;
@@ -151,9 +163,12 @@ textarea {
top: 0;
left: 0;
opacity: 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), 0 4px 8px rgba(0, 0, 0, 0.07),
0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
}
&.active {
font-weight: bold;
}
&:hover:not(:disabled) {
@@ -163,7 +178,7 @@ textarea {
opacity: 1;
}
}
&:disabled{
&:disabled {
opacity: 0.25;
cursor: not-allowed;
}
@@ -173,6 +188,21 @@ textarea {
}
}
.pulse-button:not(:hover) {
animation: pulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1);
}
@keyframes pulse {
from {
transform: scale(1);
}
50% {
transform: scale(1.12);
}
to {
transform: scale(1);
}
}
.cursor {
&-pointer {
@@ -193,11 +223,23 @@ textarea {
text-decoration: none;
color: $matte-text-color;
&:focus, &:hover {
&:focus,
&:hover {
border-color: $link-color;
}
}
.margin {
&-md {
margin: 3rem;
}
&-sm {
margin: 1rem;
}
&-0 {
margin: 0;
}
}
.margin-top {
&-md {
@@ -269,14 +311,29 @@ textarea {
margin: 0 !important;
}
.wines-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 2rem;
}
.raffle-element {
width: 45px;
height: 45px;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.75rem;
font-weight: bold;
margin: 20px 0;
color: #333333;
-webkit-mask-image: url(/public/assets/images/lodd.svg);
background-repeat: no-repeat;
mask-image: url(/public/assets/images/lodd.svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
color: #333333;
&.green-raffle {
background-color: $light-green;
@@ -293,11 +350,16 @@ textarea {
&.red-raffle {
background-color: $light-red;
}
&:not(:last-of-type) {
margin-right: 1rem;
}
}
@mixin raffle {
padding-bottom: 50px;
&::before, &::after {
&::before,
&::after {
content: "";
position: absolute;
left: 0;
@@ -309,11 +371,11 @@ textarea {
background-position: 0 25px;
background-repeat: repeat-x;
}
&::after{
&::after {
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
background-size: 50px 50px;
background-position: 25px -25px;
bottom: -25px
bottom: -25px;
}
}
@@ -327,4 +389,4 @@ textarea {
@include desktop {
display: none;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -2,10 +2,7 @@
<div class="wine">
<slot name="top"></slot>
<div class="wine-image">
<img
v-if="wine.image && loadImage"
:src="wine.image"
/>
<img v-if="wine.image && loadImage" :src="wine.image" />
<img v-else class="wine-placeholder" alt="Wine image" />
</div>
@@ -38,7 +35,7 @@ export default {
data() {
return {
loadImage: false
}
};
},
methods: {
setImage(entries) {
@@ -53,7 +50,7 @@ export default {
this.observer = new IntersectionObserver(this.setImage, {
root: this.$el,
threshold: 0
})
});
},
mounted() {
this.observer.observe(this.$el);
@@ -66,16 +63,17 @@ export default {
@import "@/styles/variables";
.wine {
align-self: flex-start;
padding: 1rem;
box-sizing: border-box;
position: relative;
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
width: 100%;
@include tablet {
width: 250px;
height: 100%;
max-width: 280px;
}
}
@@ -85,19 +83,18 @@ export default {
margin-top: 10px;
img {
height: 250px;
height: 280px;
@include mobile {
object-fit: cover;
max-width: 90px;
}
}
.wine-placeholder {
height: 250px;
height: 280px;
width: 70px;
}
}
.wine-details {
display: flex;
flex-direction: column;
@@ -107,7 +104,7 @@ export default {
}
}
.wine-name{
.wine-name {
font-size: 20px;
margin: 1em 0;
}
@@ -120,6 +117,7 @@ export default {
.bottom-section {
width: 100%;
margin-top: 1rem;
align-self: flex-end;
.link {
color: $matte-text-color;
@@ -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 class="info-and-link">
<h3>
Topp 5 viner
Topp viner
</h3>
<router-link to="viner">
<span class="vin-link">Se alle viner </span>
</router-link>
</div>
<div class="wine-container">
<div class="wines-container">
<Wine v-for="wine in wines" :key="wine" :wine="wine">
<template v-slot:top>
<div class="flex justify-end">
<div class="requested-count cursor-pointer">
<span> {{ wine.occurences }} </span>
<span> {{ wine.occurences }} </span>
<i class="icon icon--heart" />
</div>
</div>
@@ -32,32 +32,36 @@ export default {
Wine
},
data() {
return {
wines: [],
clickedWine: null,
return {
wines: [],
clickedWine: null,
limit: 18
};
},
async mounted() {
let response = await overallWineStatistics();
response.sort();
response = response
.filter(wine => wine.name != null && wine.name != "")
.sort(
this.predicate(
{
name: "occurences",
reverse: true
},
{
name: "rating",
reverse: true
}
)
);
this.wines = response.slice(0, 5);
this.getAllWines();
},
methods: {
getAllWines() {
return fetch(`/api/wines?limit=${this.limit}`)
.then(resp => resp.json())
.then(response => {
let { wines, success } = response;
this.wines = wines.sort(
this.predicate(
{
name: "occurences",
reverse: true
},
{
name: "rating",
reverse: true
}
)
);
});
},
predicate: function() {
var fields = [],
n_fields = arguments.length,
@@ -125,42 +129,72 @@ export default {
<style lang="scss" scoped>
@import "@/styles/variables.scss";
@import "@/styles/global.scss";
@import "../styles/media-queries.scss";
@import "@/styles/media-queries.scss";
.wines-main-container {
margin-bottom: 10em;
}
.info-and-link{
.info-and-link {
display: flex;
justify-content: space-between;
}
.wine-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 2rem;
.requested-count {
display: flex;
align-items: center;
margin-top: -0.5rem;
background-color: rgb(244, 244, 244);
border-radius: 1.1rem;
padding: 0.25rem 1rem;
font-size: 1.25em;
.requested-count {
display: flex;
align-items: center;
margin-top: -0.5rem;
background-color: rgb(244,244,244);
border-radius: 1.1rem;
padding: 0.25rem 1rem;
font-size: 1.25em;
span {
padding-right: 0.5rem;
line-height: 1.25em;
}
.icon--heart{
font-size: 1.5rem;
color: $link-color;
}
span {
padding-right: 0.5rem;
line-height: 1.25em;
}
.icon--heart {
font-size: 1.5rem;
color: var(--link-color);
}
}
// Call for help
.wines-container {
@media (max-width: 1643px) {
*:nth-child(n + 7) {
display: none;
}
}
@media (max-width: 2066px) {
*:nth-child(n + 9) {
display: none;
}
}
@media (max-width: 2490px) {
*:nth-child(n + 11) {
display: none;
}
}
@media (max-width: 2915px) {
*:nth-child(n + 13) {
display: none;
}
}
@media (max-width: 3335px) {
*:nth-child(n + 15) {
display: none;
}
}
@media (max-width: 3758px) {
*:nth-child(n + 17) {
display: none;
}
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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