Merge pull request #75 from KevinMidboe/feat/controllers
Feat/controllers - refactor entire backend and new admin interface
This commit is contained in:
81
api/attendee.js
Normal file
81
api/attendee.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const path = require("path");
|
||||
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const { UserNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const redactAttendeeInfoMapper = attendee => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||
red: attendee.red,
|
||||
blue: attendee.blue,
|
||||
green: attendee.green,
|
||||
yellow: attendee.yellow
|
||||
};
|
||||
};
|
||||
|
||||
const allAttendees = (isAdmin = false) => {
|
||||
if (!isAdmin) {
|
||||
return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper));
|
||||
} else {
|
||||
return Attendee.find();
|
||||
}
|
||||
};
|
||||
|
||||
const addAttendee = attendee => {
|
||||
const { name, red, blue, green, yellow, phoneNumber } = attendee;
|
||||
|
||||
let newAttendee = new Attendee({
|
||||
name,
|
||||
red,
|
||||
blue,
|
||||
green,
|
||||
yellow,
|
||||
phoneNumber,
|
||||
winner: false
|
||||
});
|
||||
|
||||
return newAttendee.save().then(_ => newAttendee);
|
||||
};
|
||||
|
||||
const updateAttendeeById = (id, updateModel) => {
|
||||
return Attendee.findOne({ _id: id }).then(attendee => {
|
||||
if (attendee == null) {
|
||||
throw new UserNotFound();
|
||||
}
|
||||
|
||||
const updatedAttendee = {
|
||||
name: updateModel.name != null ? updateModel.name : attendee.name,
|
||||
green: updateModel.green != null ? updateModel.green : attendee.green,
|
||||
red: updateModel.red != null ? updateModel.red : attendee.red,
|
||||
blue: updateModel.blue != null ? updateModel.blue : attendee.blue,
|
||||
yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow,
|
||||
phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber,
|
||||
winner: updateModel.winner != null ? updateModel.winner : attendee.winner
|
||||
};
|
||||
|
||||
return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendeeById = id => {
|
||||
return Attendee.findOne({ _id: id }).then(attendee => {
|
||||
if (attendee == null) {
|
||||
throw new UserNotFound();
|
||||
}
|
||||
|
||||
return Attendee.deleteOne({ _id: id }).then(_ => attendee);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendees = () => {
|
||||
return Attendee.deleteMany();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allAttendees,
|
||||
addAttendee,
|
||||
updateAttendeeById,
|
||||
deleteAttendeeById,
|
||||
deleteAttendees
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require("path");
|
||||
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
|
||||
const { history, clearHistory } = require(path.join(__dirname + "/../redis"));
|
||||
console.log("loading chat");
|
||||
|
||||
const getAllHistory = (req, res) => {
|
||||
let { page, limit } = req.query;
|
||||
@@ -8,19 +9,23 @@ const getAllHistory = (req, res) => {
|
||||
|
||||
return history(page, limit)
|
||||
.then(messages => res.json(messages))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
.catch(error =>
|
||||
res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const deleteHistory = (req, res) => {
|
||||
return clearHistory()
|
||||
.then(message => res.json(message))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
.catch(error =>
|
||||
res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
261
api/controllers/historyController.js
Normal file
261
api/controllers/historyController.js
Normal file
@@ -0,0 +1,261 @@
|
||||
const path = require("path");
|
||||
const historyRepository = require(path.join(__dirname, "../history"));
|
||||
|
||||
const sortOptions = ["desc", "asc"];
|
||||
const includeWinesOptions = ["true", "false"];
|
||||
|
||||
const all = (req, res) => {
|
||||
const { sort, includeWines } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.all(includeWines == "true")
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: sort !== "asc" ? winners : winners.reverse(),
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winners."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const byDate = (req, res) => {
|
||||
let { date } = req.params;
|
||||
|
||||
const regexDate = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
|
||||
if (!isNaN(date)) {
|
||||
date = new Date(new Date(parseInt(date * 1000)).setHours(0, 0, 0, 0));
|
||||
} else if (regexDate.test(date)) {
|
||||
date = new Date(date);
|
||||
} else if (date !== undefined) {
|
||||
return res.status(400).send({
|
||||
message: "Invalid date parameter, allowed epoch seconds or YYYY-MM-DD.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.byDate(date)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
date: date,
|
||||
winners: winners,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by date."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const groupByDate = (req, res) => {
|
||||
const { sort, includeWines } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.groupByDate(includeWines == "true", sort)
|
||||
.then(lotteries =>
|
||||
res.send({
|
||||
lotteries: lotteries,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by date."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const latest = (req, res) => {
|
||||
return historyRepository
|
||||
.latest()
|
||||
.then(winners =>
|
||||
res.send({
|
||||
...winners,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by date."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const byName = (req, res) => {
|
||||
const { name } = req.params;
|
||||
const { sort } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.byName(name, sort)
|
||||
.then(winner =>
|
||||
res.send({
|
||||
winner: winner,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by name."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const search = (req, res) => {
|
||||
const { name, sort } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.search(name, sort)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: winners || [],
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by name."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const groupByColor = (req, res) => {
|
||||
const { includeWines } = req.query;
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.groupByColor(includeWines == "true")
|
||||
.then(colors =>
|
||||
res.send({
|
||||
colors: colors,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winners by color."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const orderByWins = (req, res) => {
|
||||
let { includeWines, limit } = req.query;
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (limit && isNaN(limit)) {
|
||||
return res.status(400).send({
|
||||
message: "If limit query parameter is provided it must be a number",
|
||||
success: false
|
||||
});
|
||||
} else if (!!!isNaN(limit)) {
|
||||
limit = Number(limit);
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.orderByWins(includeWines == "true", limit)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: winners,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winners by color."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
all,
|
||||
byDate,
|
||||
groupByDate,
|
||||
latest,
|
||||
byName,
|
||||
search,
|
||||
groupByColor,
|
||||
orderByWins
|
||||
};
|
||||
135
api/controllers/lotteryAttendeeController.js
Normal file
135
api/controllers/lotteryAttendeeController.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const path = require("path");
|
||||
const attendeeRepository = require(path.join(__dirname, "../attendee"));
|
||||
|
||||
const allAttendees = (req, res) => {
|
||||
const isAdmin = req.isAuthenticated();
|
||||
|
||||
return attendeeRepository
|
||||
.allAttendees(isAdmin)
|
||||
.then(attendees =>
|
||||
res.send({
|
||||
attendees: attendees,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch lottery attendees."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addAttendee = (req, res) => {
|
||||
const { attendee } = req.body;
|
||||
|
||||
const requiredColors = [attendee["red"], attendee["blue"], attendee["green"], attendee["yellow"]];
|
||||
const correctColorsTypes = requiredColors.filter(color => typeof color === "number");
|
||||
if (requiredColors.length !== correctColorsTypes.length) {
|
||||
return res.status(400).send({
|
||||
message: "Incorrect or missing color, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof attendee["name"] !== "string" || typeof attendee["phoneNumber"] !== "number") {
|
||||
return res.status(400).send({
|
||||
message: "Incorrect or missing attendee keys 'name' or 'phoneNumber'.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return attendeeRepository
|
||||
.addAttendee(attendee)
|
||||
.then(savedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("new_attendee", {});
|
||||
return savedAttendee;
|
||||
})
|
||||
.then(savedAttendee =>
|
||||
res.send({
|
||||
attendee: savedAttendee,
|
||||
message: `Successfully added attendee ${attendee.name} to lottery.`,
|
||||
success: true
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateAttendeeById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { attendee } = req.body;
|
||||
|
||||
return attendeeRepository
|
||||
.updateAttendeeById(id, attendee)
|
||||
.then(updatedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
return updatedAttendee;
|
||||
})
|
||||
.then(attendee =>
|
||||
res.send({
|
||||
attendee,
|
||||
message: `Updated attendee: ${attendee.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleteing attendee by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendeeById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
return attendeeRepository
|
||||
.deleteAttendeeById(id)
|
||||
.then(removedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
return removedAttendee;
|
||||
})
|
||||
.then(attendee =>
|
||||
res.send({
|
||||
message: `Removed attendee: ${attendee.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleteing attendee by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendees = (req, res) => {
|
||||
return attendeeRepository
|
||||
.deleteAttendees()
|
||||
.then(removedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
})
|
||||
.then(_ =>
|
||||
res.send({
|
||||
message: "Removed all attendees",
|
||||
success: true
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allAttendees,
|
||||
addAttendee,
|
||||
updateAttendeeById,
|
||||
deleteAttendeeById,
|
||||
deleteAttendees
|
||||
};
|
||||
192
api/controllers/lotteryController.js
Normal file
192
api/controllers/lotteryController.js
Normal file
@@ -0,0 +1,192 @@
|
||||
const path = require("path");
|
||||
const lotteryRepository = require(path.join(__dirname, "../lottery"));
|
||||
|
||||
const drawWinner = (req, res) => {
|
||||
return lotteryRepository
|
||||
.drawWinner()
|
||||
.then(({ winner, color, winners }) => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("winner", {
|
||||
color: color,
|
||||
name: winner.name,
|
||||
winner_count: winners.length + 1
|
||||
});
|
||||
|
||||
return { winner, color, winners };
|
||||
})
|
||||
.then(({ winner, color, winners }) =>
|
||||
res.send({
|
||||
color: color,
|
||||
winner: winner,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while drawing winner.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const archiveLottery = (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
if (lottery == undefined || !lottery instanceof Object) {
|
||||
return res.status(400).send({
|
||||
message: "Missing lottery object.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
let { stolen, date, raffles, wines } = lottery;
|
||||
stolen = stolen !== undefined ? stolen : 0; // default = 0
|
||||
|
||||
const validDateFormat = new RegExp("d{4}-d{2}-d{2}");
|
||||
if (date != undefined && (!validDateFormat.test(date) || isNaN(date))) {
|
||||
return res.status(400).send({
|
||||
message: "Date must be defined as 'yyyy-mm-dd'.",
|
||||
success: false
|
||||
});
|
||||
} else if (date != undefined) {
|
||||
date = Date.parse(date, "yyyy-MM-dd");
|
||||
} else {
|
||||
date = new Date();
|
||||
}
|
||||
|
||||
return verifyLotteryPayload(raffles, stolen, wines)
|
||||
.then(_ => lotteryRepository.archive(date, raffles, stolen, wines))
|
||||
.then(_ =>
|
||||
res.send({
|
||||
message: "Successfully archive lottery",
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while submitting lottery.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const lotteryByDate = (req, res) => {
|
||||
const { epoch } = req.params;
|
||||
|
||||
if (!/^\d+$/.test(epoch)) {
|
||||
return res.status(400).send({
|
||||
message: "Last parameter must be epoch (in seconds).",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
const date = new Date(Number(epoch) * 1000);
|
||||
|
||||
return lotteryRepository
|
||||
.lotteryByDate(date)
|
||||
.then(lottery =>
|
||||
res.send({
|
||||
lottery,
|
||||
message: `Lottery for date: ${dateToDateString(date)}/${epoch}.`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while fetching lottery by date.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const sortOptions = ["desc", "asc"];
|
||||
const allLotteries = (req, res) => {
|
||||
let { includeWinners, year, sort } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
} else if (sort === undefined) {
|
||||
sort = "asc";
|
||||
}
|
||||
|
||||
let allLotteriesFunction = lotteryRepository.allLotteries;
|
||||
if (includeWinners === "true") {
|
||||
allLotteriesFunction = lotteryRepository.allLotteriesIncludingWinners;
|
||||
}
|
||||
|
||||
return allLotteriesFunction(sort, year)
|
||||
.then(lotteries =>
|
||||
res.send({
|
||||
lotteries,
|
||||
message: "All lotteries.",
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while fetching all lotteries.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function verifyLotteryPayload(raffles, stolen, wines) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (raffles == undefined || !raffles instanceof Array) {
|
||||
reject({
|
||||
message: "Raffles must be array.",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
const requiredColors = [raffles["red"], raffles["blue"], raffles["green"], raffles["yellow"]];
|
||||
const correctColorsTypes = requiredColors.filter(color => typeof color === "number");
|
||||
if (requiredColors.length !== correctColorsTypes.length) {
|
||||
reject({
|
||||
message:
|
||||
"Incorrect or missing raffle colors, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
if (stolen == undefined || (isNaN(stolen) && stolen >= 0)) {
|
||||
reject({
|
||||
message: "Number of stolen raffles must be positive integer or 0.",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
if (wines == undefined || !wines instanceof Array) {
|
||||
reject({
|
||||
message: "Wines must be array.",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function dateToDateString(date) {
|
||||
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||
|
||||
return `${ye}-${mo}-${da}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
drawWinner,
|
||||
archiveLottery,
|
||||
lotteryByDate,
|
||||
allLotteries
|
||||
};
|
||||
207
api/controllers/lotteryWineController.js
Normal file
207
api/controllers/lotteryWineController.js
Normal file
@@ -0,0 +1,207 @@
|
||||
const path = require("path");
|
||||
const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine"));
|
||||
|
||||
const allWines = (req, res) => {
|
||||
return prelotteryWineRepository
|
||||
.allWines()
|
||||
.then(wines =>
|
||||
res.send({
|
||||
wines: wines,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch lottery wines."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addWines = (req, res) => {
|
||||
let { wines } = req.body;
|
||||
|
||||
if (!(wines instanceof Array)) {
|
||||
return res.status(400).send({
|
||||
message: "Wines must be array.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
const validateAllWines = wines =>
|
||||
wines.map(wine => {
|
||||
const requiredAttributes = ["name", "vivinoLink", "image", "id", "price"];
|
||||
|
||||
return Promise.all(
|
||||
requiredAttributes.map(attr => {
|
||||
if (typeof wine[attr] === "undefined" || wine[attr] == "") {
|
||||
return Promise.reject({
|
||||
message: `Incorrect or missing attribute: ${attr}.`,
|
||||
statusCode: 400,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
).then(_ => Promise.resolve(wine));
|
||||
});
|
||||
|
||||
return Promise.all(validateAllWines(wines))
|
||||
.then(wines => prelotteryWineRepository.addWines(wines))
|
||||
.then(savedWines => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("new_wine", {});
|
||||
return true;
|
||||
})
|
||||
.then(success =>
|
||||
res.send({
|
||||
message: `Successfully added wines to lottery.`,
|
||||
success: success
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured adding wines.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const wineById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
return prelotteryWineRepository
|
||||
.wineById(id)
|
||||
.then(wine =>
|
||||
res.send({
|
||||
wine,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while fetching wine by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateWineById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { wine } = req.body;
|
||||
|
||||
if (id == null || id == "undefined") {
|
||||
return res.status(400).send({
|
||||
message: "Unable to update without id.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return prelotteryWineRepository
|
||||
.updateWineById(id, wine)
|
||||
.then(updatedWine => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
return updatedWine;
|
||||
})
|
||||
.then(wine =>
|
||||
res.send({
|
||||
wine,
|
||||
message: `Updated wine: ${wine.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleteing wine by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWineById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
return prelotteryWineRepository
|
||||
.deleteWineById(id)
|
||||
.then(removedWine => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
return removedWine;
|
||||
})
|
||||
.then(wine =>
|
||||
res.send({
|
||||
message: `Removed wine: ${wine.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleteing wine by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWines = (req, res) => {
|
||||
return prelotteryWineRepository
|
||||
.deleteWines()
|
||||
.then(_ => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
})
|
||||
.then(_ =>
|
||||
res.send({
|
||||
message: "Removed all wines.",
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleting wines",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const wineSchema = (req, res) => {
|
||||
return prelotteryWineRepository
|
||||
.wineSchema()
|
||||
.then(schema =>
|
||||
res.send({
|
||||
schema: schema,
|
||||
message: `Wine schema template.`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch wine schema template."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allWines,
|
||||
addWines,
|
||||
wineById,
|
||||
updateWineById,
|
||||
deleteWineById,
|
||||
deleteWines,
|
||||
wineSchema
|
||||
};
|
||||
195
api/controllers/lotteryWinnerController.js
Normal file
195
api/controllers/lotteryWinnerController.js
Normal file
@@ -0,0 +1,195 @@
|
||||
const path = require("path");
|
||||
const winnerRepository = require(path.join(__dirname, "../winner"));
|
||||
const { WinnerNotFound } = require(path.join(__dirname, "../vinlottisErrors"));
|
||||
const prizeDistributionRepository = require(path.join(__dirname, "../prizeDistribution"));
|
||||
|
||||
// should not be used, is done through POST /lottery/prize-distribution/prize/:id - claimPrize.
|
||||
const addWinners = (req, res) => {
|
||||
const { winners } = req.body;
|
||||
|
||||
if (!(winners instanceof Array)) {
|
||||
return res.status(400).send({
|
||||
message: "Winners must be array.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
const requiredAttributes = ["name", "color", "wine"];
|
||||
const validColors = ["red", "blue", "green", "yellow"];
|
||||
const validateAllWinners = winners =>
|
||||
winners.map(winner => {
|
||||
return Promise.all(
|
||||
requiredAttributes.map(attr => {
|
||||
if (typeof winner[attr] === "undefined") {
|
||||
return Promise.reject({
|
||||
message: `Incorrect or missing attribute: ${attr}.`,
|
||||
statusCode: 400
|
||||
});
|
||||
}
|
||||
|
||||
if (!validColors.includes(winner.color)) {
|
||||
return Promise.reject({
|
||||
message: `Missing or incorrect color value, must have one of values: ${validColors.join(", ")}.`,
|
||||
statusCode: 400
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
})
|
||||
).then(_ => Promise.resolve(winner));
|
||||
});
|
||||
|
||||
return Promise.all(validateAllWinners(winners))
|
||||
.then(winners =>
|
||||
winners.map(winner => {
|
||||
return prizeDistributionRepository.claimPrize(winner, winner.wine);
|
||||
})
|
||||
)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: winners,
|
||||
message: `Successfully added winners to lottery.`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured adding winners.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const allWinners = (req, res) => {
|
||||
const isAdmin = req.isAuthenticated();
|
||||
|
||||
return winnerRepository
|
||||
.allWinners(isAdmin)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: winners,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch lottery winners."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const winnerById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const isAdmin = req.isAuthenticated();
|
||||
|
||||
return winnerRepository
|
||||
.winnerById(id, isAdmin)
|
||||
.then(winner =>
|
||||
res.send({
|
||||
winner,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured, unable to fetch winner by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateWinnerById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { winner } = req.body;
|
||||
|
||||
if (id == null || id == "undefined") {
|
||||
return res.status(400).send({
|
||||
message: "Unable to update without id.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return winnerRepository
|
||||
.updateWinnerById(id, winner)
|
||||
.then(winner =>
|
||||
res.send({
|
||||
winner,
|
||||
message: `Updated winner: ${winner.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while updating winner by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWinnerById = (req, res) => {
|
||||
const isAdmin = req.isAuthenticated();
|
||||
const { id } = req.params;
|
||||
|
||||
return winnerRepository
|
||||
.deleteWinnerById(id, isAdmin)
|
||||
.then(removedWinner => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
return removedWinner;
|
||||
})
|
||||
.then(winner =>
|
||||
res.send({
|
||||
message: `Removed winner: ${winner.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleteing wine by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWinners = (req, res) => {
|
||||
return winnerRepository
|
||||
.deleteWinners()
|
||||
.then(_ => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
})
|
||||
.then(_ =>
|
||||
res.send({
|
||||
message: "Removed all winners.",
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleting wines",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addWinners,
|
||||
allWinners,
|
||||
winnerById,
|
||||
updateWinnerById,
|
||||
deleteWinnerById,
|
||||
deleteWinners
|
||||
};
|
||||
30
api/controllers/messageController.js
Normal file
30
api/controllers/messageController.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const path = require("path");
|
||||
const messageRepository = require(path.join(__dirname, "../message"));
|
||||
const winnerRepository = require(path.join(__dirname, "../winner"));
|
||||
|
||||
const notifyWinnerById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const isAdmin = req.isAuthenticated();
|
||||
|
||||
return winnerRepository
|
||||
.winnerById(id, isAdmin)
|
||||
.then(winner => messageRepository.sendPrizeSelectionLink(winner))
|
||||
.then(messageResponse =>
|
||||
res.send({
|
||||
messageResponse,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while sending message to winner by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
notifyWinnerById
|
||||
};
|
||||
104
api/controllers/prizeDistributionController.js
Normal file
104
api/controllers/prizeDistributionController.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const path = require("path");
|
||||
|
||||
const prizeDistribution = require(path.join(__dirname, "../prizeDistribution"));
|
||||
const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine"));
|
||||
const winnerRepository = require(path.join(__dirname, "../winner"));
|
||||
const message = require(path.join(__dirname, "../message"));
|
||||
|
||||
const start = async (req, res) => {
|
||||
const allWinners = await winnerRepository.allWinners(true);
|
||||
if (allWinners.length === 0) {
|
||||
return res.status(503).send({
|
||||
message: "No winners found to distribute prizes to.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
const laterWinners = allWinners.slice(1);
|
||||
|
||||
return prizeDistribution
|
||||
.notifyNextWinner()
|
||||
.then(_ => message.sendInitialMessageToWinners(laterWinners))
|
||||
.then(_ =>
|
||||
res.send({
|
||||
message: `Send link to first winner and notified everyone else.`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while starting prize distribution.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getPrizesForWinnerById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
return prizeDistribution
|
||||
.verifyWinnerNextInLine(id)
|
||||
.then(winner => {
|
||||
return prelotteryWineRepository.allWinesWithoutWinner().then(wines => [wines, winner]);
|
||||
})
|
||||
.then(([wines, winner]) =>
|
||||
res.send({
|
||||
wines: wines,
|
||||
winner: winner,
|
||||
message: "Wines to select from",
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while fetching prizes.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const submitPrizeForWinnerById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { wine } = req.body;
|
||||
|
||||
let prelotteryWine, winner;
|
||||
try {
|
||||
prelotteryWine = await prelotteryWineRepository.wineById(wine._id);
|
||||
winner = await winnerRepository.winnerById(id, true);
|
||||
} catch (error) {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while claiming prize.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return prizeDistribution
|
||||
.claimPrize(prelotteryWine, winner)
|
||||
.then(_ => prizeDistribution.notifyNextWinner())
|
||||
.then(_ =>
|
||||
res.send({
|
||||
message: `${winner.name} successfully claimed prize: ${prelotteryWine.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while claiming prize.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
getPrizesForWinnerById,
|
||||
submitPrizeForWinnerById
|
||||
};
|
||||
104
api/controllers/requestController.js
Normal file
104
api/controllers/requestController.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const path = require("path");
|
||||
const requestRepository = require(path.join(__dirname, "../request"));
|
||||
|
||||
function addRequest(req, res) {
|
||||
const { wine } = req.body;
|
||||
|
||||
return verifyWineValues(wine)
|
||||
.then(_ => requestRepository.addNew(wine))
|
||||
.then(wine =>
|
||||
res.json({
|
||||
message: "Successfully added new request",
|
||||
wine: wine,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { message, statusCode } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to add requested wine."
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function allRequests(req, res) {
|
||||
return requestRepository
|
||||
.getAll()
|
||||
.then(wines =>
|
||||
res.json({
|
||||
wines: wines,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { message, statusCode } = error;
|
||||
return res.status(statusCode || 500).json({
|
||||
success: false,
|
||||
message: message || "Unable to fetch all requested wines."
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRequest(req, res) {
|
||||
const { id } = req.params;
|
||||
|
||||
return requestRepository
|
||||
.deleteById(id)
|
||||
.then(_ =>
|
||||
res.json({
|
||||
message: `Slettet vin med id: ${id}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to delete requested wine."
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyWineValues(wine) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (wine == undefined) {
|
||||
reject({
|
||||
message: "No wine object found in request body.",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
if (wine.id == null) {
|
||||
reject({
|
||||
message: "Wine object missing value id.",
|
||||
status: 400
|
||||
});
|
||||
} else if (wine.name == null) {
|
||||
reject({
|
||||
message: "Wine object missing value name.",
|
||||
status: 400
|
||||
});
|
||||
} else if (wine.vivinoLink == null) {
|
||||
reject({
|
||||
message: "Wine object missing value vivinoLink.",
|
||||
status: 400
|
||||
});
|
||||
} else if (wine.image == null) {
|
||||
reject({
|
||||
message: "Wine object missing value image.",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addRequest,
|
||||
allRequests,
|
||||
deleteRequest
|
||||
};
|
||||
55
api/controllers/userController.js
Normal file
55
api/controllers/userController.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const path = require("path");
|
||||
const userRepository = require(path.join(__dirname, "../user"));
|
||||
|
||||
function register(req, res, next) {
|
||||
const { username, password } = req.body;
|
||||
|
||||
return userRepository
|
||||
.register(username, password)
|
||||
.then(user => userRepository.login(req, user))
|
||||
.then(_ =>
|
||||
res.send({
|
||||
messsage: `Bruker registrert. Velkommen ${username}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unable to sign in with given username and passowrd",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const login = (req, res, next) => {
|
||||
return userRepository
|
||||
.authenticate(req)
|
||||
.then(user => userRepository.login(req, user))
|
||||
.then(user => {
|
||||
res.send({
|
||||
message: `Velkommen ${user.username}`,
|
||||
success: true
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unable to sign in with given username and passowrd",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const logout = (req, res) => {
|
||||
req.logout();
|
||||
res.redirect("/");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
logout
|
||||
};
|
||||
85
api/controllers/vinmonopoletController.js
Normal file
85
api/controllers/vinmonopoletController.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const path = require("path");
|
||||
const vinmonopoletRepository = require(path.join(__dirname, "../vinmonopolet"));
|
||||
|
||||
function searchWines(req, res) {
|
||||
const { name, page } = req.query;
|
||||
|
||||
return vinmonopoletRepository.searchWinesByName(name, page).then(wines =>
|
||||
res.json({
|
||||
wines: wines,
|
||||
count: wines.length,
|
||||
page: page,
|
||||
success: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function wineByEAN(req, res) {
|
||||
const { ean } = req.params;
|
||||
|
||||
return vinmonopoletRepository.searchByEAN(ean).then(wines =>
|
||||
res.json({
|
||||
wines: wines,
|
||||
success: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function wineById(req, res) {
|
||||
const { id } = req.params;
|
||||
|
||||
return vinmonopoletRepository.wineById(id).then(wines =>
|
||||
res.json({
|
||||
wine: wines[0],
|
||||
success: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function allStores(req, res) {
|
||||
return vinmonopoletRepository
|
||||
.allStores()
|
||||
.then(stores =>
|
||||
res.send({
|
||||
stores,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while fetch all vinmonopolet stores.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function searchStores(req, res) {
|
||||
const { name } = req.query;
|
||||
|
||||
return vinmonopoletRepository
|
||||
.searchStoresByName(name)
|
||||
.then(stores =>
|
||||
res.send({
|
||||
stores,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while fetch all vinmonopolet stores.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
searchWines,
|
||||
wineByEAN,
|
||||
wineById,
|
||||
allStores,
|
||||
searchStores
|
||||
};
|
||||
60
api/controllers/wineController.js
Normal file
60
api/controllers/wineController.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const path = require("path");
|
||||
const wineRepository = require(path.join(__dirname, "../wine"));
|
||||
|
||||
const allWines = (req, res) => {
|
||||
// TODO add "includeWinners"
|
||||
let { limit } = req.query;
|
||||
|
||||
if (limit && isNaN(limit)) {
|
||||
return res.status(400).send({
|
||||
message: "If limit query parameter is provided it must be a number",
|
||||
success: false
|
||||
});
|
||||
} else if (!!!isNaN(limit)) {
|
||||
limit = Number(limit);
|
||||
}
|
||||
|
||||
return wineRepository
|
||||
.allWines(limit)
|
||||
.then(wines =>
|
||||
res.send({
|
||||
wines: wines,
|
||||
message: `All wines.`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch all wines."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const wineById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
return wineRepository
|
||||
.wineById(id)
|
||||
.then(wine => {
|
||||
res.send({
|
||||
wine,
|
||||
success: true
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while fetching wine by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allWines,
|
||||
wineById
|
||||
};
|
||||
349
api/history.js
Normal file
349
api/history.js
Normal file
@@ -0,0 +1,349 @@
|
||||
const path = require("path");
|
||||
|
||||
const Winner = require(path.join(__dirname, "/schemas/Highscore"));
|
||||
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||
|
||||
class HistoryByDateNotFound extends Error {
|
||||
constructor(message = "History for given date not found.") {
|
||||
super(message);
|
||||
this.name = "HistoryByDateNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryForUserNotFound extends Error {
|
||||
constructor(message = "History for given user not found.") {
|
||||
super(message);
|
||||
this.name = "HistoryForUserNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
// highscore
|
||||
const addWinnerWithWine = async (winner, wine) => {
|
||||
const exisitingWinner = await Winner.findOne({
|
||||
name: winner.name
|
||||
});
|
||||
const savedWine = await wineRepository.addWine(wine);
|
||||
|
||||
const date = new Date();
|
||||
date.setHours(5, 0, 0, 0);
|
||||
const winObject = {
|
||||
date: date,
|
||||
wine: savedWine,
|
||||
color: winner.color
|
||||
};
|
||||
|
||||
if (exisitingWinner == undefined) {
|
||||
const newWinner = new Winner({
|
||||
name: winner.name,
|
||||
wins: [winObject]
|
||||
});
|
||||
|
||||
await newWinner.save();
|
||||
} else {
|
||||
exisitingWinner.wins.push(winObject);
|
||||
exisitingWinner.markModified("wins");
|
||||
await exisitingWinner.save();
|
||||
}
|
||||
|
||||
return exisitingWinner;
|
||||
};
|
||||
|
||||
// lottery
|
||||
const all = (includeWines = false) => {
|
||||
if (includeWines === false) {
|
||||
return Winner.find().sort("-wins.date");
|
||||
} else {
|
||||
return Winner.find()
|
||||
.sort("-wins.date")
|
||||
.populate("wins.wine");
|
||||
}
|
||||
};
|
||||
|
||||
// lottery
|
||||
const byDate = date => {
|
||||
const startQueryDate = new Date(date.setHours(0, 0, 0, 0));
|
||||
const endQueryDate = new Date(date.setHours(24, 59, 59, 99));
|
||||
const query = [
|
||||
{
|
||||
$match: {
|
||||
"wins.date": {
|
||||
$gte: startQueryDate,
|
||||
$lte: endQueryDate
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $unwind: "$wins" },
|
||||
{
|
||||
$match: {
|
||||
"wins.date": {
|
||||
$gte: startQueryDate,
|
||||
$lte: endQueryDate
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wins.wine",
|
||||
foreignField: "_id",
|
||||
as: "wins.wine"
|
||||
}
|
||||
},
|
||||
{ $unwind: "$wins.wine" },
|
||||
{
|
||||
$project: {
|
||||
name: "$name",
|
||||
date: "$wins.date",
|
||||
color: "$wins.color",
|
||||
wine: "$wins.wine"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return Winner.aggregate(query).then(winners => {
|
||||
if (winners.length == 0) {
|
||||
throw new HistoryByDateNotFound();
|
||||
}
|
||||
return winners;
|
||||
});
|
||||
};
|
||||
|
||||
// highscore
|
||||
const byName = (name, sort = "desc") => {
|
||||
return Winner.findOne({ name }, ["name", "wins"])
|
||||
.sort("-wins.date")
|
||||
.populate("wins.wine")
|
||||
.then(winner => {
|
||||
if (winner) {
|
||||
winner.wins = sort !== "asc" ? winner.wins.reverse() : winner.wins;
|
||||
return winner;
|
||||
} else {
|
||||
throw new HistoryForUserNotFound();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// highscore
|
||||
const search = (query, sort = "desc") => {
|
||||
return Winner.find({ name: { $regex: query, $options: "i" } }, ["name"]).then(winners => {
|
||||
if (winners) {
|
||||
winners = sort === "desc" ? winners.reverse() : winners;
|
||||
return winners;
|
||||
} else {
|
||||
throw new HistoryForUserNotFound();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// lottery
|
||||
const latest = () => {
|
||||
const query = [
|
||||
{
|
||||
$unwind: "$wins"
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wins.wine",
|
||||
foreignField: "_id",
|
||||
as: "wins.wine"
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$wins.date",
|
||||
winners: {
|
||||
$push: {
|
||||
_id: "$_id",
|
||||
name: "$name",
|
||||
color: "$wins.color",
|
||||
wine: "$wins.wine"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
date: "$_id",
|
||||
winners: "$winners"
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
_id: -1
|
||||
}
|
||||
},
|
||||
{
|
||||
$limit: 1
|
||||
}
|
||||
];
|
||||
|
||||
return Winner.aggregate(query).then(winners => winners[0]);
|
||||
};
|
||||
|
||||
// lottery - byDate
|
||||
const groupByDate = (includeWines = false, sort = "asc") => {
|
||||
const sortDirection = sort == "asc" ? -1 : 1;
|
||||
const query = [
|
||||
{
|
||||
$unwind: "$wins"
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$wins.date",
|
||||
winners: {
|
||||
$push: {
|
||||
_id: "$_id",
|
||||
name: "$name",
|
||||
color: "$wins.color",
|
||||
wine: "$wins.wine"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
date: "$_id",
|
||||
winners: "$winners"
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
date: sortDirection
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (includeWines) {
|
||||
query.splice(1, 0, {
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wins.wine",
|
||||
foreignField: "_id",
|
||||
as: "wins.wine"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Winner.aggregate(query);
|
||||
};
|
||||
|
||||
// highscore - byColor
|
||||
const groupByColor = (includeWines = false) => {
|
||||
const query = [
|
||||
{
|
||||
$unwind: "$wins"
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$wins.color",
|
||||
winners: {
|
||||
$push: {
|
||||
_id: "$_id",
|
||||
name: "$name",
|
||||
date: "$wins.date",
|
||||
wine: "$wins.wine"
|
||||
}
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
color: "$_id",
|
||||
count: "$count",
|
||||
winners: "$winners"
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
_id: -1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (includeWines) {
|
||||
query.splice(1, 0, {
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wins.wine",
|
||||
foreignField: "_id",
|
||||
as: "wins.wine"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Winner.aggregate(query);
|
||||
};
|
||||
|
||||
// highscore - byWineOccurences
|
||||
|
||||
// highscore - byWinCount
|
||||
const orderByWins = (includeWines = false, limit = undefined) => {
|
||||
let query = [
|
||||
{
|
||||
$project: {
|
||||
name: "$name",
|
||||
wins: "$wins",
|
||||
totalWins: { $size: "$wins" }
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
totalWins: -1,
|
||||
"wins.date": -1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (includeWines) {
|
||||
const includeWinesSubQuery = [
|
||||
{
|
||||
$unwind: "$wins"
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wins.wine",
|
||||
foreignField: "_id",
|
||||
as: "wins.wine"
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: "$wins._id"
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$_id",
|
||||
name: { $first: "$name" },
|
||||
totalWins: { $first: "$totalWins" },
|
||||
wins: { $push: "$wins" }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
query = includeWinesSubQuery.concat(query);
|
||||
}
|
||||
|
||||
return Winner.aggregate(query).then(winners => {
|
||||
if (limit == null) {
|
||||
return winners;
|
||||
}
|
||||
|
||||
return winners.slice(0, limit);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addWinnerWithWine,
|
||||
all,
|
||||
byDate,
|
||||
byName,
|
||||
search,
|
||||
latest,
|
||||
groupByDate,
|
||||
groupByColor,
|
||||
orderByWins
|
||||
};
|
||||
369
api/lottery.js
369
api/lottery.js
@@ -1,132 +1,263 @@
|
||||
const path = require('path');
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const Highscore = require(path.join(__dirname, '/schemas/Highscore'));
|
||||
const Wine = require(path.join(__dirname, '/schemas/Wine'));
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
|
||||
// Utils
|
||||
const epochToDateString = date => new Date(parseInt(date)).toDateString();
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const historyRepository = require(path.join(__dirname, "/history"));
|
||||
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||
|
||||
const sortNewestFirst = (lotteries) => {
|
||||
return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1)
|
||||
}
|
||||
const {
|
||||
WinnerNotFound,
|
||||
NoMoreAttendeesToWin,
|
||||
CouldNotFindNewWinnerAfterNTries,
|
||||
LotteryByDateNotFound
|
||||
} = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const groupHighscoreByDate = async (highscore=undefined) => {
|
||||
if (highscore == undefined)
|
||||
highscore = await Highscore.find();
|
||||
const archive = (date, raffles, stolen, wines) => {
|
||||
const { blue, red, yellow, green } = raffles;
|
||||
const bought = blue + red + yellow + green;
|
||||
|
||||
const highscoreByDate = [];
|
||||
|
||||
highscore.forEach(person => {
|
||||
person.wins.map(win => {
|
||||
const epochDate = new Date(win.date).setHours(0,0,0,0);
|
||||
const winnerObject = {
|
||||
name: person.name,
|
||||
color: win.color,
|
||||
wine: win.wine,
|
||||
date: epochDate
|
||||
}
|
||||
|
||||
const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate)
|
||||
if (existingDateIndex > -1)
|
||||
highscoreByDate[existingDateIndex].winners.push(winnerObject);
|
||||
else
|
||||
highscoreByDate.push({
|
||||
date: epochDate,
|
||||
winners: [winnerObject]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return sortNewestFirst(highscoreByDate);
|
||||
}
|
||||
|
||||
const resolveWineReferences = (highscoreObject, key) => {
|
||||
const listWithWines = highscoreObject[key]
|
||||
|
||||
return Promise.all(listWithWines.map(element =>
|
||||
Wine.findById(element.wine)
|
||||
.then(wine => {
|
||||
element.wine = wine
|
||||
return element
|
||||
}))
|
||||
)
|
||||
.then(resolvedListWithWines => {
|
||||
highscoreObject[key] = resolvedListWithWines;
|
||||
return highscoreObject
|
||||
})
|
||||
}
|
||||
// end utils
|
||||
|
||||
// Routes
|
||||
const all = (req, res) => {
|
||||
return Highscore.find()
|
||||
.then(highscore => groupHighscoreByDate(highscore))
|
||||
.then(lotteries => res.send({
|
||||
message: "Lotteries by date!",
|
||||
lotteries
|
||||
}))
|
||||
}
|
||||
|
||||
const latest = (req, res) => {
|
||||
return groupHighscoreByDate()
|
||||
.then(lotteries => lotteries.shift()) // first element in list
|
||||
.then(latestLottery => resolveWineReferences(latestLottery, "winners"))
|
||||
.then(lottery => res.send({
|
||||
message: "Latest lottery!",
|
||||
winners: lottery.winners
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const byEpochDate = (req, res) => {
|
||||
let { date } = req.params;
|
||||
date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime()
|
||||
const dateString = epochToDateString(date);
|
||||
|
||||
return groupHighscoreByDate()
|
||||
.then(lotteries => {
|
||||
const lottery = lotteries.filter(lottery => lottery.date == date)
|
||||
if (lottery.length > 0) {
|
||||
return lottery[0]
|
||||
} else {
|
||||
return res.status(404).send({
|
||||
message: `No lottery found for date: ${ dateString }`
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(lottery => resolveWineReferences(lottery, "winners"))
|
||||
.then(lottery => res.send({
|
||||
message: `Lottery for date: ${ dateString}`,
|
||||
return Promise.all(wines.map(wine => wineRepository.findWine(wine))).then(resolvedWines => {
|
||||
const lottery = new Lottery({
|
||||
date,
|
||||
winners: lottery.winners
|
||||
}))
|
||||
}
|
||||
blue,
|
||||
red,
|
||||
yellow,
|
||||
green,
|
||||
bought,
|
||||
stolen,
|
||||
wines: resolvedWines
|
||||
});
|
||||
|
||||
const byName = (req, res) => {
|
||||
const { name } = req.params;
|
||||
const regexName = new RegExp(name, "i"); // lowercase regex of the name
|
||||
return lottery.save();
|
||||
});
|
||||
};
|
||||
|
||||
return Highscore.find({ name })
|
||||
.then(highscore => {
|
||||
if (highscore.length > 0) {
|
||||
return highscore[0]
|
||||
} else {
|
||||
return res.status(404).send({
|
||||
message: `Name: ${ name } not found in leaderboards.`
|
||||
})
|
||||
const lotteryByDate = date => {
|
||||
const startOfDay = new Date(date.setHours(0, 0, 0, 0));
|
||||
const endOfDay = new Date(date.setHours(24, 59, 59, 99));
|
||||
|
||||
const query = [
|
||||
{
|
||||
$match: {
|
||||
date: {
|
||||
$gte: startOfDay,
|
||||
$lte: endOfDay
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(highscore => resolveWineReferences(highscore, "wins"))
|
||||
.then(highscore => res.send({
|
||||
message: `Lottery winnings for name: ${ name }.`,
|
||||
name: highscore.name,
|
||||
highscore: sortNewestFirst(highscore.wins)
|
||||
}))
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wines",
|
||||
foreignField: "_id",
|
||||
as: "wines"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const aggregateLottery = Lottery.aggregate(query);
|
||||
return aggregateLottery.project("-_id -__v").then(lotteries => {
|
||||
if (lotteries.length == 0) {
|
||||
throw new LotteryByDateNotFound(date);
|
||||
}
|
||||
return lotteries[0];
|
||||
});
|
||||
};
|
||||
|
||||
const allLotteries = (sort = "asc", yearFilter = undefined) => {
|
||||
const sortDirection = sort == "asc" ? 1 : -1;
|
||||
|
||||
let startQueryDate = new Date("1970-01-01");
|
||||
let endQueryDate = new Date("2999-01-01");
|
||||
if (yearFilter) {
|
||||
startQueryDate = new Date(`${yearFilter}-01-01`);
|
||||
endQueryDate = new Date(`${Number(yearFilter) + 1}-01-01`);
|
||||
}
|
||||
|
||||
const query = [
|
||||
{
|
||||
$match: {
|
||||
date: {
|
||||
$gte: startQueryDate,
|
||||
$lte: endQueryDate
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
date: sortDirection
|
||||
}
|
||||
},
|
||||
{
|
||||
$unset: ["_id", "__v"]
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wines",
|
||||
foreignField: "_id",
|
||||
as: "wines"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return Lottery.aggregate(query);
|
||||
};
|
||||
|
||||
const allLotteriesIncludingWinners = async (sort = "asc", yearFilter = undefined) => {
|
||||
const lotteries = await allLotteries(sort, yearFilter);
|
||||
const allWinners = await historyRepository.groupByDate(false, sort);
|
||||
|
||||
return lotteries.map(lottery => {
|
||||
const { winners } = allWinners.pop();
|
||||
|
||||
return {
|
||||
wines: lottery.wines,
|
||||
date: lottery.date,
|
||||
blue: lottery.blue,
|
||||
green: lottery.green,
|
||||
yellow: lottery.yellow,
|
||||
red: lottery.red,
|
||||
bought: lottery.bought,
|
||||
stolen: lottery.stolen,
|
||||
winners: winners
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const drawWinner = async () => {
|
||||
let allContestants = await Attendee.find({ winner: false });
|
||||
|
||||
if (allContestants.length == 0) {
|
||||
throw new NoMoreAttendeesToWin();
|
||||
}
|
||||
|
||||
let raffleColors = [];
|
||||
for (let i = 0; i < allContestants.length; i++) {
|
||||
let currentContestant = allContestants[i];
|
||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||
raffleColors.push("blue");
|
||||
}
|
||||
for (let red = 0; red < currentContestant.red; red++) {
|
||||
raffleColors.push("red");
|
||||
}
|
||||
for (let green = 0; green < currentContestant.green; green++) {
|
||||
raffleColors.push("green");
|
||||
}
|
||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||
raffleColors.push("yellow");
|
||||
}
|
||||
}
|
||||
|
||||
raffleColors = shuffle(raffleColors);
|
||||
|
||||
let colorToChooseFrom = raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||
let findObject = { winner: false };
|
||||
|
||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||
|
||||
let tries = 0;
|
||||
const maxTries = 3;
|
||||
let contestantsToChooseFrom = undefined;
|
||||
while (contestantsToChooseFrom == undefined && tries < maxTries) {
|
||||
const hit = await Attendee.find(findObject);
|
||||
if (hit && hit.length) {
|
||||
contestantsToChooseFrom = hit;
|
||||
break;
|
||||
}
|
||||
tries++;
|
||||
}
|
||||
if (contestantsToChooseFrom == undefined) {
|
||||
throw new CouldNotFindNewWinnerAfterNTries(maxTries);
|
||||
}
|
||||
|
||||
let attendeeListDemocratic = [];
|
||||
|
||||
let currentContestant;
|
||||
for (let i = 0; i < contestantsToChooseFrom.length; i++) {
|
||||
currentContestant = contestantsToChooseFrom[i];
|
||||
for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) {
|
||||
attendeeListDemocratic.push({
|
||||
name: currentContestant.name,
|
||||
phoneNumber: currentContestant.phoneNumber,
|
||||
red: currentContestant.red,
|
||||
blue: currentContestant.blue,
|
||||
green: currentContestant.green,
|
||||
yellow: currentContestant.yellow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attendeeListDemocratic = shuffle(attendeeListDemocratic);
|
||||
|
||||
let winner = attendeeListDemocratic[Math.floor(Math.random() * attendeeListDemocratic.length)];
|
||||
|
||||
let newWinnerElement = new VirtualWinner({
|
||||
name: winner.name,
|
||||
phoneNumber: winner.phoneNumber,
|
||||
color: colorToChooseFrom,
|
||||
red: winner.red,
|
||||
blue: winner.blue,
|
||||
green: winner.green,
|
||||
yellow: winner.yellow,
|
||||
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||
timestamp_drawn: new Date().getTime()
|
||||
});
|
||||
|
||||
await newWinnerElement.save();
|
||||
await Attendee.updateOne({ name: winner.name, phoneNumber: winner.phoneNumber }, { $set: { winner: true } });
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
|
||||
return { winner, color: colorToChooseFrom, winners };
|
||||
};
|
||||
|
||||
/** - - UTILS - - **/
|
||||
const genRandomString = function(length) {
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString("hex") /** convert to hexadecimal format */
|
||||
.slice(0, length); /** return required number of characters */
|
||||
};
|
||||
|
||||
const sha512 = function(password, salt) {
|
||||
var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */
|
||||
hash.update(password);
|
||||
var value = hash.digest("hex");
|
||||
return value;
|
||||
};
|
||||
|
||||
function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
temporaryValue,
|
||||
randomIndex;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (0 !== currentIndex) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
// And swap it with the current element.
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
all,
|
||||
latest,
|
||||
byEpochDate,
|
||||
byName
|
||||
drawWinner,
|
||||
archive,
|
||||
lotteryByDate,
|
||||
allLotteries,
|
||||
allLotteriesIncludingWinners
|
||||
};
|
||||
|
||||
123
api/message.js
123
api/message.js
@@ -2,34 +2,50 @@ const https = require("https");
|
||||
const path = require("path");
|
||||
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
|
||||
|
||||
const dateString = (date) => {
|
||||
if (typeof(date) == "string") {
|
||||
const dateString = date => {
|
||||
if (typeof date == "string") {
|
||||
date = new Date(date);
|
||||
}
|
||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
||||
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||
|
||||
return `${da}-${mo}-${ye}`
|
||||
return `${da}-${mo}-${ye}`;
|
||||
};
|
||||
|
||||
async function sendInitialMessageToWinners(winners) {
|
||||
const numbers = winners.map(winner => ({ msisdn: `47${winner.phoneNumber}` }));
|
||||
|
||||
const body = {
|
||||
sender: "Vinlottis",
|
||||
message: "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
|
||||
recipients: numbers
|
||||
};
|
||||
|
||||
return gatewayRequest(body);
|
||||
}
|
||||
|
||||
async function sendWineSelectMessage(winnerObject) {
|
||||
winnerObject.timestamp_sent = new Date().getTime();
|
||||
winnerObject.timestamp_limit = new Date().getTime() * 600000;
|
||||
await winnerObject.save();
|
||||
async function sendPrizeSelectionLink(winner) {
|
||||
winner.timestamp_sent = new Date().getTime();
|
||||
winner.timestamp_limit = new Date().getTime() + 1000 * 600;
|
||||
await winner.save();
|
||||
|
||||
let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin");
|
||||
const { id, name, phoneNumber } = winner;
|
||||
const url = new URL(`/#/winner/${id}`, "https://lottis.vin");
|
||||
const message = `Gratulerer som heldig vinner av vinlotteriet ${name}! Her er linken for \
|
||||
å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst \
|
||||
i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.`;
|
||||
|
||||
return sendMessageToUser(
|
||||
winnerObject.phoneNumber,
|
||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Her er linken for å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.)`
|
||||
)
|
||||
return sendMessageToNumber(phoneNumber, message);
|
||||
}
|
||||
|
||||
async function sendWineConfirmation(winnerObject, wineObject, date) {
|
||||
date = dateString(date);
|
||||
return sendMessageToUser(winnerObject.phoneNumber,
|
||||
`Bekreftelse på din vin ${ winnerObject.name }.\nDato vunnet: ${ date }.\nVin valgt: ${ wineObject.name }.\nDu vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`)
|
||||
return sendMessageToNumber(
|
||||
winnerObject.phoneNumber,
|
||||
`Bekreftelse på din vin ${winnerObject.name}.\nDato vunnet: ${date}.\nVin valgt: ${wineObject.name}.\
|
||||
\nDu vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendLastWinnerMessage(winnerObject, wineObject) {
|
||||
@@ -38,84 +54,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) {
|
||||
winnerObject.timestamp_limit = new Date().getTime();
|
||||
await winnerObject.save();
|
||||
|
||||
return sendMessageToUser(
|
||||
return sendMessageToNumber(
|
||||
winnerObject.phoneNumber,
|
||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`
|
||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, \
|
||||
du vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendWineSelectMessageTooLate(winnerObject) {
|
||||
return sendMessageToUser(
|
||||
return sendMessageToNumber(
|
||||
winnerObject.phoneNumber,
|
||||
`Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.`
|
||||
`Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor \
|
||||
puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendMessageToUser(phoneNumber, message) {
|
||||
console.log(`Attempting to send message to ${ phoneNumber }.`)
|
||||
async function sendMessageToNumber(phoneNumber, message) {
|
||||
console.log(`Attempting to send message to ${phoneNumber}.`);
|
||||
|
||||
const body = {
|
||||
sender: "Vinlottis",
|
||||
message: message,
|
||||
recipients: [{ msisdn: `47${ phoneNumber }`}]
|
||||
recipients: [{ msisdn: `47${phoneNumber}` }]
|
||||
};
|
||||
|
||||
return gatewayRequest(body);
|
||||
}
|
||||
|
||||
|
||||
async function sendInitialMessageToWinners(winners) {
|
||||
let numbers = [];
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
numbers.push({ msisdn: `47${winners[i].phoneNumber}` });
|
||||
}
|
||||
|
||||
const body = {
|
||||
sender: "Vinlottis",
|
||||
message:
|
||||
"Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
|
||||
recipients: numbers
|
||||
}
|
||||
|
||||
return gatewayRequest(body);
|
||||
}
|
||||
|
||||
async function gatewayRequest(body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: "gatewayapi.com",
|
||||
post: 443,
|
||||
path: `/rest/mtsms?token=${ config.gatewayToken }`,
|
||||
path: `/rest/mtsms?token=${config.gatewayToken}`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
console.log(`statusCode: ${ res.statusCode }`);
|
||||
console.log(`statusMessage: ${ res.statusMessage }`);
|
||||
const req = https.request(options, res => {
|
||||
console.log(`statusCode: ${res.statusCode}`);
|
||||
console.log(`statusMessage: ${res.statusMessage}`);
|
||||
|
||||
res.setEncoding('utf8');
|
||||
res.setEncoding("utf8");
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
res.on("data", (data) => {
|
||||
console.log("Response from message gateway:", data)
|
||||
res.on("data", data => {
|
||||
console.log("Response from message gateway:", data);
|
||||
|
||||
resolve(JSON.parse(data))
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
} else {
|
||||
res.on("data", (data) => {
|
||||
res.on("data", data => {
|
||||
data = JSON.parse(data);
|
||||
return reject('Gateway error: ' + data['message'] || data)
|
||||
return reject("Gateway error: " + data["message"] || data);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
req.on("error", (error) => {
|
||||
console.error(`Error from sms service: ${ error }`);
|
||||
reject(`Error from sms service: ${ error }`);
|
||||
})
|
||||
req.on("error", error => {
|
||||
console.error(`Error from sms service: ${error}`);
|
||||
reject(`Error from sms service: ${error}`);
|
||||
});
|
||||
|
||||
req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
@@ -123,9 +124,9 @@ async function gatewayRequest(body) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendWineSelectMessage,
|
||||
sendInitialMessageToWinners,
|
||||
sendPrizeSelectionLink,
|
||||
sendWineConfirmation,
|
||||
sendLastWinnerMessage,
|
||||
sendWineSelectMessageTooLate,
|
||||
sendInitialMessageToWinners
|
||||
}
|
||||
sendWineSelectMessageTooLate
|
||||
};
|
||||
|
||||
6
api/middleware/alwaysAuthenticatedWhenLocalhost.js
Normal file
6
api/middleware/alwaysAuthenticatedWhenLocalhost.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const alwaysAuthenticatedWhenLocalhost = (req, res, next) => {
|
||||
req.isAuthenticated = () => true;
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = alwaysAuthenticatedWhenLocalhost;
|
||||
@@ -1,10 +1,4 @@
|
||||
const mustBeAuthenticated = (req, res, next) => {
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
|
||||
req.isAuthenticated = () => true;
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
|
||||
103
api/prelotteryWine.js
Normal file
103
api/prelotteryWine.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const path = require("path");
|
||||
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const allWines = () => {
|
||||
return PreLotteryWine.find().populate("winner");
|
||||
};
|
||||
|
||||
const allWinesWithoutWinner = () => {
|
||||
return PreLotteryWine.find({ winner: { $exists: false } });
|
||||
};
|
||||
|
||||
const addWines = wines => {
|
||||
const prelotteryWines = wines.map(wine => {
|
||||
let newPrelotteryWine = new PreLotteryWine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
year: wine.year,
|
||||
image: wine.image,
|
||||
price: wine.price,
|
||||
country: wine.country,
|
||||
id: wine.id
|
||||
});
|
||||
|
||||
return newPrelotteryWine.save();
|
||||
});
|
||||
|
||||
return Promise.all(prelotteryWines);
|
||||
};
|
||||
|
||||
const wineById = id => {
|
||||
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
const updateWineById = (id, updateModel) => {
|
||||
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
const updatedWine = {
|
||||
name: updateModel.name != null ? updateModel.name : wine.name,
|
||||
vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink,
|
||||
rating: updateModel.rating != null ? updateModel.rating : wine.rating,
|
||||
year: updateModel.year != null ? updateModel.year : wine.year,
|
||||
image: updateModel.image != null ? updateModel.image : wine.image,
|
||||
price: updateModel.price != null ? updateModel.price : wine.price,
|
||||
country: updateModel.country != null ? updateModel.country : wine.country,
|
||||
id: updateModel.id != null ? updateModel.id : wine.id
|
||||
};
|
||||
|
||||
return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine);
|
||||
});
|
||||
};
|
||||
|
||||
const addWinnerToWine = (wine, winner) => {
|
||||
wine.winner = winner;
|
||||
winner.prize_selected = true;
|
||||
return Promise.all([wine.save(), winner.save()]);
|
||||
};
|
||||
|
||||
const deleteWineById = id => {
|
||||
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWines = () => {
|
||||
return PreLotteryWine.deleteMany();
|
||||
};
|
||||
|
||||
const wineSchema = () => {
|
||||
let schema = { ...PreLotteryWine.schema.obj };
|
||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||
accumulator[current] = "";
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
return Promise.resolve(nulledSchema);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allWines,
|
||||
allWinesWithoutWinner,
|
||||
addWines,
|
||||
wineById,
|
||||
addWinnerToWine,
|
||||
updateWineById,
|
||||
deleteWineById,
|
||||
deleteWines,
|
||||
wineSchema
|
||||
};
|
||||
110
api/prizeDistribution.js
Normal file
110
api/prizeDistribution.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const path = require("path");
|
||||
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
|
||||
const message = require(path.join(__dirname, "/message"));
|
||||
const historyRepository = require(path.join(__dirname, "/history"));
|
||||
const winnerRepository = require(path.join(__dirname, "/winner"));
|
||||
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||
const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine"));
|
||||
|
||||
const { WinnerNotFound, WineSelectionWinnerNotNextInLine, WinnersTimelimitExpired } = require(path.join(
|
||||
__dirname,
|
||||
"/vinlottisErrors"
|
||||
));
|
||||
|
||||
const verifyWinnerNextInLine = async id => {
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
throw new WinnerNotFound();
|
||||
} else if (foundWinner.timestamp_limit < new Date().getTime()) {
|
||||
throw new WinnersTimelimitExpired();
|
||||
}
|
||||
|
||||
let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
|
||||
if (
|
||||
foundWinner.timestamp_limit == undefined ||
|
||||
foundWinner.timestamp_sent == undefined ||
|
||||
foundWinner.prize_selected == true
|
||||
) {
|
||||
throw new WineSelectionWinnerNotNextInLine();
|
||||
}
|
||||
|
||||
return Promise.resolve(foundWinner);
|
||||
};
|
||||
|
||||
const claimPrize = (wine, winner) => {
|
||||
return wineRepository
|
||||
.addWine(wine)
|
||||
.then(_ => prelotteryWineRepository.addWinnerToWine(wine, winner)) // prelotteryWine.deleteById
|
||||
.then(_ => historyRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine
|
||||
.then(_ => message.sendWineConfirmation(winner, wine));
|
||||
};
|
||||
|
||||
const notifyNextWinner = async () => {
|
||||
let nextWinner = undefined;
|
||||
|
||||
const winnersLeft = await VirtualWinner.find({ prize_selected: false }).sort({ timestamp_drawn: 1 });
|
||||
const winesLeft = await PreLotteryWine.find({ winner: { $exists: false } });
|
||||
|
||||
if (winnersLeft.length > 1) {
|
||||
console.log("multiple winners left, choose next in line");
|
||||
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
||||
console.log("one winner left, but multiple wines");
|
||||
nextWinner = winnersLeft[0]; // one winner left, but multiple wines
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
||||
console.log("one winner and one wine left, choose for user");
|
||||
nextWinner = winnersLeft[0]; // one winner and one wine left, choose for user
|
||||
wine = winesLeft[0];
|
||||
return claimPrize(wine, nextWinner);
|
||||
}
|
||||
|
||||
if (nextWinner) {
|
||||
return message.sendPrizeSelectionLink(nextWinner).then(_ => startTimeout(nextWinner.id));
|
||||
} else {
|
||||
console.info("All winners notified. Could start cleanup here.");
|
||||
return Promise.resolve({
|
||||
message: "All winners notified."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// these need to be register somewhere to cancel if something
|
||||
// goes wrong and we want to start prize distribution again
|
||||
function startTimeout(id) {
|
||||
const minute = 60000;
|
||||
const minutesForTimeout = 10;
|
||||
|
||||
console.log(`Starting timeout for user ${id}.`);
|
||||
console.log(`Timeout duration: ${minutesForTimeout * minute}`);
|
||||
setTimeout(async () => {
|
||||
let virtualWinner = await VirtualWinner.findOne({ id: id, prize_selected: false });
|
||||
if (!virtualWinner) {
|
||||
console.log(`Timeout done for user ${id}, but user has already sent data.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Timeout done for user ${id}, sending update to user.`);
|
||||
|
||||
message.sendWineSelectMessageTooLate(virtualWinner);
|
||||
|
||||
virtualWinner.timestamp_drawn = new Date().getTime();
|
||||
virtualWinner.timestamp_limit = null;
|
||||
virtualWinner.timestamp_sent = null;
|
||||
await virtualWinner.save();
|
||||
|
||||
notifyNextWinner();
|
||||
}, minutesForTimeout * minute);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyWinnerNextInLine,
|
||||
claimPrize,
|
||||
notifyNextWinner
|
||||
};
|
||||
@@ -1,41 +1,20 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const RequestedWine = require(path.join(
|
||||
__dirname, "/schemas/RequestedWine"
|
||||
));
|
||||
const Wine = require(path.join(
|
||||
__dirname, "/schemas/Wine"
|
||||
));
|
||||
const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine"));
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
|
||||
const deleteRequestedWineById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
if(id == null){
|
||||
return res.json({
|
||||
message: "Id er ikke definert",
|
||||
success: false
|
||||
})
|
||||
class RequestedWineNotFound extends Error {
|
||||
constructor(message = "Wine with this id was not found.") {
|
||||
super(message);
|
||||
this.name = "RequestedWineNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
await RequestedWine.deleteOne({wineId: id})
|
||||
return res.json({
|
||||
message: `Slettet vin med id: ${id}`,
|
||||
success: true
|
||||
});
|
||||
}
|
||||
|
||||
const getAllRequestedWines = async (req, res) => {
|
||||
const allWines = await RequestedWine.find({}).populate("wine");
|
||||
const addNew = async wine => {
|
||||
let foundWine = await Wine.findOne({ id: wine.id });
|
||||
|
||||
return res.json(allWines);
|
||||
}
|
||||
|
||||
const requestNewWine = async (req, res) => {
|
||||
const {wine} = req.body
|
||||
|
||||
let thisWineIsLOKO = await Wine.findOne({id: wine.id})
|
||||
|
||||
if(thisWineIsLOKO == undefined){
|
||||
thisWineIsLOKO = new Wine({
|
||||
if (foundWine == undefined) {
|
||||
foundWine = new Wine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: null,
|
||||
@@ -43,27 +22,47 @@ const requestNewWine = async (req, res) => {
|
||||
image: wine.image,
|
||||
id: wine.id
|
||||
});
|
||||
await thisWineIsLOKO.save()
|
||||
await foundWine.save();
|
||||
}
|
||||
|
||||
let requestedWine = await RequestedWine.findOne({ "wineId": wine.id})
|
||||
let requestedWine = await RequestedWine.findOne({ wineId: wine.id });
|
||||
|
||||
if(requestedWine == undefined){
|
||||
if (requestedWine == undefined) {
|
||||
requestedWine = new RequestedWine({
|
||||
count: 1,
|
||||
wineId: wine.id,
|
||||
wine: thisWineIsLOKO
|
||||
})
|
||||
wine: foundWine
|
||||
});
|
||||
} else {
|
||||
requestedWine.count += 1;
|
||||
}
|
||||
await requestedWine.save()
|
||||
await requestedWine.save();
|
||||
|
||||
return res.send(requestedWine);
|
||||
}
|
||||
return requestedWine;
|
||||
};
|
||||
|
||||
const getById = id => {
|
||||
return RequestedWine.findOne({ wineId: id })
|
||||
.populate("wine")
|
||||
.then(wine => {
|
||||
if (wine == null) {
|
||||
throw new RequestedWineNotFound();
|
||||
}
|
||||
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteById = id => {
|
||||
return getById(id).then(requestedWine => RequestedWine.deleteOne({ _id: requestedWine._id }));
|
||||
};
|
||||
|
||||
const getAll = () => {
|
||||
return RequestedWine.find({}).populate("wine");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
requestNewWine,
|
||||
getAllRequestedWines,
|
||||
deleteRequestedWineById
|
||||
addNew,
|
||||
getAll,
|
||||
deleteById
|
||||
};
|
||||
|
||||
154
api/retrieve.js
154
api/retrieve.js
@@ -1,154 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const Purchase = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
const prelotteryWines = async (req, res) => {
|
||||
let wines = await PreLotteryWine.find();
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
const allPurchase = async (req, res) => {
|
||||
let purchases = await Purchase.find()
|
||||
.populate("wines")
|
||||
.sort({ date: 1 });
|
||||
return res.json(purchases);
|
||||
};
|
||||
|
||||
const purchaseByColor = async (req, res) => {
|
||||
const countColor = await Purchase.find();
|
||||
let red = 0;
|
||||
let blue = 0;
|
||||
let yellow = 0;
|
||||
let green = 0;
|
||||
let stolen = 0;
|
||||
for (let i = 0; i < countColor.length; i++) {
|
||||
let element = countColor[i];
|
||||
red += element.red;
|
||||
blue += element.blue;
|
||||
yellow += element.yellow;
|
||||
green += element.green;
|
||||
if (element.stolen != undefined) {
|
||||
stolen += element.stolen;
|
||||
}
|
||||
}
|
||||
|
||||
const highscore = await Highscore.find();
|
||||
let redWin = 0;
|
||||
let blueWin = 0;
|
||||
let yellowWin = 0;
|
||||
let greenWin = 0;
|
||||
for (let i = 0; i < highscore.length; i++) {
|
||||
let element = highscore[i];
|
||||
for (let y = 0; y < element.wins.length; y++) {
|
||||
let currentWin = element.wins[y];
|
||||
switch (currentWin.color) {
|
||||
case "blue":
|
||||
blueWin += 1;
|
||||
break;
|
||||
case "red":
|
||||
redWin += 1;
|
||||
break;
|
||||
case "yellow":
|
||||
yellowWin += 1;
|
||||
break;
|
||||
case "green":
|
||||
greenWin += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = red + yellow + blue + green;
|
||||
|
||||
return res.json({
|
||||
red: {
|
||||
total: red,
|
||||
win: redWin
|
||||
},
|
||||
blue: {
|
||||
total: blue,
|
||||
win: blueWin
|
||||
},
|
||||
green: {
|
||||
total: green,
|
||||
win: greenWin
|
||||
},
|
||||
yellow: {
|
||||
total: yellow,
|
||||
win: yellowWin
|
||||
},
|
||||
stolen: stolen,
|
||||
total: total
|
||||
});
|
||||
};
|
||||
|
||||
const highscore = async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
|
||||
return res.json(highscore);
|
||||
};
|
||||
|
||||
const allWines = async (req, res) => {
|
||||
const wines = await Wine.find();
|
||||
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
const allWinesSummary = async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
let wines = {};
|
||||
|
||||
for (let i = 0; i < highscore.length; i++) {
|
||||
let person = highscore[i];
|
||||
for (let y = 0; y < person.wins.length; y++) {
|
||||
let wine = person.wins[y].wine;
|
||||
let date = person.wins[y].date;
|
||||
let color = person.wins[y].color;
|
||||
|
||||
if (wines[wine._id] == undefined) {
|
||||
wines[wine._id] = {
|
||||
name: wine.name,
|
||||
occurences: wine.occurences,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
id: wine.id,
|
||||
_id: wine._id,
|
||||
dates: [date],
|
||||
winners: [person.name],
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0
|
||||
};
|
||||
wines[wine._id][color] += 1;
|
||||
} else {
|
||||
wines[wine._id].dates.push(date);
|
||||
wines[wine._id].winners.push(person.name);
|
||||
if (wines[wine._id][color] == undefined) {
|
||||
wines[wine._id][color] = 1;
|
||||
} else {
|
||||
wines[wine._id][color] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wines = Object.values(wines).reverse()
|
||||
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
prelotteryWines,
|
||||
allPurchase,
|
||||
purchaseByColor,
|
||||
highscore,
|
||||
allWines,
|
||||
allWinesSummary
|
||||
};
|
||||
130
api/router.js
130
api/router.js
@@ -4,67 +4,97 @@ const path = require("path");
|
||||
const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated"));
|
||||
const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated"));
|
||||
|
||||
const update = require(path.join(__dirname, "/update"));
|
||||
const retrieve = require(path.join(__dirname, "/retrieve"));
|
||||
const request = require(path.join(__dirname, "/request"));
|
||||
const subscriptionApi = require(path.join(__dirname, "/subscriptions"));
|
||||
const userApi = require(path.join(__dirname, "/user"));
|
||||
const wineinfo = require(path.join(__dirname, "/wineinfo"));
|
||||
const virtualApi = require(path.join(__dirname, "/virtualLottery"));
|
||||
const virtualRegistrationApi = require(path.join(
|
||||
__dirname, "/virtualRegistration"
|
||||
));
|
||||
const lottery = require(path.join(__dirname, "/lottery"));
|
||||
const chatHistoryApi = require(path.join(__dirname, "/chatHistory"));
|
||||
const requestController = require(path.join(__dirname, "/controllers/requestController"));
|
||||
const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController"));
|
||||
const chatController = require(path.join(__dirname, "/controllers/chatController"));
|
||||
const userController = require(path.join(__dirname, "/controllers/userController"));
|
||||
const historyController = require(path.join(__dirname, "/controllers/historyController"));
|
||||
const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController"));
|
||||
const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController"));
|
||||
const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController"));
|
||||
const lotteryController = require(path.join(__dirname, "/controllers/lotteryController"));
|
||||
const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController"));
|
||||
const wineController = require(path.join(__dirname, "/controllers/wineController"));
|
||||
const messageController = require(path.join(__dirname, "/controllers/messageController"));
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/wineinfo/search", wineinfo.wineSearch);
|
||||
router.get("/vinmonopolet/wine/search", vinmonopoletController.searchWines);
|
||||
router.get("/vinmonopolet/wine/by-ean/:ean", vinmonopoletController.wineByEAN);
|
||||
router.get("/vinmonopolet/wine/by-id/:id", vinmonopoletController.wineById);
|
||||
router.get("/vinmonopolet/stores/", vinmonopoletController.allStores);
|
||||
router.get("/vinmonopolet/stores/search", vinmonopoletController.searchStores);
|
||||
|
||||
router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines);
|
||||
router.post("/request/new-wine", request.requestNewWine);
|
||||
router.delete("/request/:id", request.deleteRequestedWineById);
|
||||
router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests);
|
||||
router.post("/request", requestController.addRequest);
|
||||
router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest);
|
||||
|
||||
router.get("/wineinfo/schema", mustBeAuthenticated, update.schema);
|
||||
router.get("/wineinfo/:ean", wineinfo.byEAN);
|
||||
router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences
|
||||
router.get("/wine/:id", wineController.wineById); // sort = by-date, by-name, by-occurences
|
||||
// router.update("/wine/:id", mustBeAuthenticated, wineController.update);
|
||||
|
||||
router.post("/log/wines", mustBeAuthenticated, update.submitWines);
|
||||
router.post("/lottery", update.submitLottery);
|
||||
router.post("/lottery/wines", update.submitWinesToLottery);
|
||||
// router.delete("/lottery/wine/:id", update.deleteWineFromLottery);
|
||||
router.post("/lottery/winners", update.submitWinnersToLottery);
|
||||
router.get("/history", historyController.all);
|
||||
router.get("/history/latest", historyController.latest);
|
||||
router.get("/history/by-wins/", historyController.orderByWins);
|
||||
router.get("/history/by-color/", historyController.groupByColor);
|
||||
router.get("/history/by-date/:date", historyController.byDate);
|
||||
router.get("/history/by-name/:name", historyController.byName);
|
||||
router.get("/history/search/", historyController.search);
|
||||
router.get("/history/by-date/", historyController.groupByDate);
|
||||
|
||||
router.get("/wines/prelottery", retrieve.prelotteryWines);
|
||||
router.get("/purchase/statistics", retrieve.allPurchase);
|
||||
router.get("/purchase/statistics/color", retrieve.purchaseByColor);
|
||||
router.get("/highscore/statistics", retrieve.highscore)
|
||||
router.get("/wines/statistics", retrieve.allWines);
|
||||
router.get("/wines/statistics/overall", retrieve.allWinesSummary);
|
||||
// router.get("/purchases", purchaseController.lotteryPurchases);
|
||||
// // returns list per date and count of each colors that where bought
|
||||
// router.get("/purchases/summary", purchaseController.lotteryPurchases);
|
||||
// // returns total, wins?, stolen
|
||||
// router.get("/purchase/:date", purchaseController.lotteryPurchaseByDate);
|
||||
|
||||
router.get("/lottery/all", lottery.all);
|
||||
router.get("/lottery/latest", lottery.latest);
|
||||
router.get("/lottery/by-date/:date", lottery.byEpochDate);
|
||||
router.get("/lottery/by-name/:name", lottery.byName);
|
||||
router.get("/lottery/wines", prelotteryWineController.allWines);
|
||||
router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema);
|
||||
router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById);
|
||||
router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines);
|
||||
router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines);
|
||||
router.put("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.updateWineById);
|
||||
router.delete("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.deleteWineById);
|
||||
|
||||
router.delete('/virtual/winner/all', mustBeAuthenticated, virtualApi.deleteWinners);
|
||||
router.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees);
|
||||
router.get('/virtual/winner/draw', virtualApi.drawWinner);
|
||||
router.get('/virtual/winner/all', virtualApi.winners);
|
||||
router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure);
|
||||
router.post('/virtual/finish', mustBeAuthenticated, virtualApi.finish);
|
||||
router.get('/virtual/attendee/all', virtualApi.attendees);
|
||||
router.get('/virtual/attendee/all/secure', mustBeAuthenticated, virtualApi.attendeesSecure);
|
||||
router.post('/virtual/attendee/add', mustBeAuthenticated, virtualApi.addAttendee);
|
||||
router.get("/lottery/attendees", setAdminHeaderIfAuthenticated, attendeeController.allAttendees);
|
||||
router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees);
|
||||
router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee);
|
||||
router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById);
|
||||
router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById);
|
||||
|
||||
router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById);
|
||||
router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById);
|
||||
router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection);
|
||||
router.get("/lottery/winners", winnerController.allWinners);
|
||||
router.get("/lottery/winner/:id", winnerController.winnerById);
|
||||
router.post("/lottery/winners", mustBeAuthenticated, winnerController.addWinners);
|
||||
router.delete("/lottery/winners", mustBeAuthenticated, winnerController.deleteWinners);
|
||||
router.put("/lottery/winner/:id", mustBeAuthenticated, winnerController.updateWinnerById);
|
||||
router.delete("/lottery/winner/:id", mustBeAuthenticated, winnerController.deleteWinnerById);
|
||||
|
||||
router.get('/chat/history', chatHistoryApi.getAllHistory)
|
||||
router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory)
|
||||
router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner);
|
||||
router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery);
|
||||
router.get("/lottery/:epoch", lotteryController.lotteryByDate);
|
||||
router.get("/lotteries/", lotteryController.allLotteries);
|
||||
|
||||
router.post('/login', userApi.login);
|
||||
router.post('/register', mustBeAuthenticated, userApi.register);
|
||||
router.get('/logout', userApi.logout);
|
||||
// router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status);
|
||||
router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start);
|
||||
// router.post("/lottery/prize-distribution/stop", mustBeAuthenticated, prizeDistributionController.stop);
|
||||
router.get("/lottery/prize-distribution/prizes/:id", prizeDistributionController.getPrizesForWinnerById);
|
||||
router.post("/lottery/prize-distribution/prize/:id", prizeDistributionController.submitPrizeForWinnerById);
|
||||
|
||||
router.post("/lottery/messages/winner/:id", mustBeAuthenticated, messageController.notifyWinnerById);
|
||||
|
||||
router.get("/chat/history", chatController.getAllHistory);
|
||||
router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory);
|
||||
|
||||
router.post("/login", userController.login);
|
||||
router.post("/register", mustBeAuthenticated, userController.register);
|
||||
router.get("/logout", userController.logout);
|
||||
|
||||
// router.get("/", documentation.apiInfo);
|
||||
|
||||
// router.get("/wine/schema", mustBeAuthenticated, update.schema);
|
||||
// router.get("/purchase/statistics", retrieve.allPurchase);
|
||||
// router.get("/highscore/statistics", retrieve.highscore);
|
||||
// router.get("/wines/statistics", retrieve.allWines);
|
||||
// router.get("/wines/statistics/overall", retrieve.allWinesSummary);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -6,9 +6,14 @@ const PreLotteryWine = new Schema({
|
||||
vivinoLink: String,
|
||||
rating: Number,
|
||||
id: String,
|
||||
year: Number,
|
||||
image: String,
|
||||
price: String,
|
||||
country: String
|
||||
country: String,
|
||||
winner: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "VirtualWinner"
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);
|
||||
|
||||
@@ -10,6 +10,10 @@ const VirtualWinner = new Schema({
|
||||
red: Number,
|
||||
yellow: Number,
|
||||
id: String,
|
||||
prize_selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
timestamp_drawn: Number,
|
||||
timestamp_sent: Number,
|
||||
timestamp_limit: Number
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
const mongoose = require("mongoose");
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const Wine = new Schema({
|
||||
const WineSchema = new Schema({
|
||||
name: String,
|
||||
vivinoLink: String,
|
||||
rating: Number,
|
||||
occurences: Number,
|
||||
id: String,
|
||||
year: Number,
|
||||
image: String,
|
||||
price: String,
|
||||
country: String
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Wine", Wine);
|
||||
module.exports = mongoose.model("Wine", WineSchema);
|
||||
|
||||
142
api/update.js
142
api/update.js
@@ -1,142 +0,0 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
|
||||
const sub = require(path.join(__dirname, "/subscriptions"));
|
||||
|
||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
||||
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
|
||||
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
const submitWines = async (req, res) => {
|
||||
const wines = req.body;
|
||||
for (let i = 0; i < wines.length; i++) {
|
||||
let wine = wines[i];
|
||||
let newWonWine = new PreLotteryWine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
price: wine.price,
|
||||
country: wine.country,
|
||||
id: wine.id
|
||||
});
|
||||
await newWonWine.save();
|
||||
}
|
||||
|
||||
let subs = await Subscription.find();
|
||||
console.log("Sending new wines w/ push notification to all subscribers.")
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
let subscription = subs[i]; //get subscription from your databse here.
|
||||
|
||||
const message = JSON.stringify({
|
||||
message: "Dagens vin er lagt til, se den på lottis.vin/dagens!",
|
||||
title: "Ny vin!",
|
||||
link: "/#/dagens"
|
||||
});
|
||||
|
||||
try {
|
||||
sub.sendNotification(subscription, message);
|
||||
} catch (error) {
|
||||
console.error("Error when trying to send push notification to subscriber.");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return res.send({
|
||||
message: "Submitted and notified push subscribers of new wines!",
|
||||
success: true
|
||||
});
|
||||
};
|
||||
|
||||
const schema = async (req, res) => {
|
||||
let schema = { ...PreLotteryWine.schema.obj };
|
||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||
accumulator[current] = "";
|
||||
return accumulator
|
||||
}, {});
|
||||
|
||||
return res.send(nulledSchema);
|
||||
}
|
||||
|
||||
// TODO IMPLEMENT WITH FRONTEND (unused)
|
||||
const submitWinesToLottery = async (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
const { date, wines } = lottery;
|
||||
const wineObjects = await Promise.all(wines.map(async (wine) => await _wineFunctions.findSaveWine(wine)))
|
||||
|
||||
return Lottery.findOneAndUpdate({ date: date }, {
|
||||
date: date,
|
||||
wines: wineObjects
|
||||
}, {
|
||||
upsert: true
|
||||
}).then(_ => res.send(true))
|
||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving wine to lottery.',
|
||||
success: false,
|
||||
exception: err.message }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiParam (Request body) {Array} winners List of winners
|
||||
*/
|
||||
const submitWinnersToLottery = async (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
const { winners, date } = lottery;
|
||||
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
let currentWinner = winners[i];
|
||||
let wonWine = await _wineFunctions.findSaveWine(currentWinner.wine); // TODO rename to findAndSaveWineToLottery
|
||||
await _personFunctions.findSavePerson(currentWinner, wonWine, date); // TODO rename to findAndSaveWineToPerson
|
||||
}
|
||||
|
||||
return res.json(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiParam (Request body) {Date} date Date of lottery
|
||||
* @apiParam (Request body) {Number} blue Number of blue tickets
|
||||
* @apiParam (Request body) {Number} red Number of red tickets
|
||||
* @apiParam (Request body) {Number} green Number of green tickets
|
||||
* @apiParam (Request body) {Number} yellow Number of yellow tickets
|
||||
* @apiParam (Request body) {Number} bought Number of tickets bought
|
||||
* @apiParam (Request body) {Number} stolen Number of tickets stolen
|
||||
*/
|
||||
const submitLottery = async (req, res) => {
|
||||
const { lottery } = req.body
|
||||
|
||||
const { date,
|
||||
blue,
|
||||
red,
|
||||
yellow,
|
||||
green,
|
||||
bought,
|
||||
stolen } = lottery;
|
||||
|
||||
return Lottery.findOneAndUpdate({ date: date }, {
|
||||
date: date,
|
||||
blue: blue,
|
||||
yellow: yellow,
|
||||
red: red,
|
||||
green: green,
|
||||
bought: bought,
|
||||
stolen: stolen
|
||||
}, {
|
||||
upsert: true
|
||||
}).then(_ => res.send(true))
|
||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving lottery.',
|
||||
success: false,
|
||||
exception: err.message }));
|
||||
|
||||
return res.send(true);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
submitWines,
|
||||
schema,
|
||||
submitLottery,
|
||||
submitWinnersToLottery,
|
||||
submitWinesToLottery
|
||||
};
|
||||
111
api/user.js
111
api/user.js
@@ -1,51 +1,90 @@
|
||||
const passport = require("passport");
|
||||
const path = require("path");
|
||||
const User = require(path.join(__dirname, "/schemas/User"));
|
||||
const router = require("express").Router();
|
||||
|
||||
const register = (req, res, next) => {
|
||||
User.register(
|
||||
new User({ username: req.body.username }),
|
||||
req.body.password,
|
||||
function(err) {
|
||||
class UserExistsError extends Error {
|
||||
constructor(message = "Username already exists.") {
|
||||
super(message);
|
||||
this.name = "UserExists";
|
||||
this.statusCode = 409;
|
||||
}
|
||||
}
|
||||
|
||||
class MissingUsernameError extends Error {
|
||||
constructor(message = "No username given.") {
|
||||
super(message);
|
||||
this.name = "MissingUsernameError";
|
||||
this.statusCode = 400;
|
||||
}
|
||||
}
|
||||
|
||||
class MissingPasswordError extends Error {
|
||||
constructor(message = "No password given.") {
|
||||
super(message);
|
||||
this.name = "MissingPasswordError";
|
||||
this.statusCode = 400;
|
||||
}
|
||||
}
|
||||
|
||||
class IncorrectUserCredentialsError extends Error {
|
||||
constructor(message = "Incorrect username or password") {
|
||||
super(message);
|
||||
this.name = "IncorrectUserCredentialsError";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
function userAuthenticationErrorHandler(err) {
|
||||
if (err.name == "UserExistsError") {
|
||||
throw new UserExistsError(err.message);
|
||||
} else if (err.name == "MissingUsernameError") {
|
||||
throw new MissingUsernameError(err.message);
|
||||
} else if (err.name == "MissingPasswordError") {
|
||||
throw new MissingPasswordError(err.message);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const register = (username, password) => {
|
||||
return User.register(new User({ username: username }), password).catch(userAuthenticationErrorHandler);
|
||||
};
|
||||
|
||||
const authenticate = req => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (username == undefined) throw new MissingUsernameError();
|
||||
if (password == undefined) throw new MissingPasswordError();
|
||||
|
||||
passport.authenticate("local", function(err, user, info) {
|
||||
if (err) {
|
||||
if (err.name == "UserExistsError")
|
||||
res.status(409).send({ success: false, message: err.message })
|
||||
else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
|
||||
res.status(400).send({ success: false, message: err.message })
|
||||
return next(err);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
|
||||
}
|
||||
);
|
||||
if (!user) {
|
||||
reject(new IncorrectUserCredentialsError());
|
||||
}
|
||||
|
||||
resolve(user);
|
||||
})(req);
|
||||
});
|
||||
};
|
||||
|
||||
const login = (req, res, next) => {
|
||||
passport.authenticate("local", function(err, user, info) {
|
||||
if (err) {
|
||||
if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
|
||||
return res.status(400).send({ message: err.message, success: false })
|
||||
return next(err);
|
||||
}
|
||||
const login = (req, user) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.logIn(user, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false })
|
||||
|
||||
req.logIn(user, (err) => {
|
||||
if (err) { return next(err) }
|
||||
|
||||
return res.status(200).send({ message: "Velkommen " + user.username, success: true })
|
||||
})
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
const logout = (req, res) => {
|
||||
req.logout();
|
||||
res.redirect("/");
|
||||
resolve(user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
logout
|
||||
authenticate,
|
||||
login
|
||||
};
|
||||
|
||||
90
api/vinlottisErrors.js
Normal file
90
api/vinlottisErrors.js
Normal file
@@ -0,0 +1,90 @@
|
||||
class UserNotFound extends Error {
|
||||
constructor(message = "User not found.") {
|
||||
super(message);
|
||||
this.name = "UserNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class WineNotFound extends Error {
|
||||
constructor(message = "Wine not found.") {
|
||||
super(message);
|
||||
this.name = "WineNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class WinnerNotFound extends Error {
|
||||
constructor(message = "Winner not found.") {
|
||||
super(message);
|
||||
this.name = "WinnerNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class WinnersTimelimitExpired extends Error {
|
||||
constructor(message = "Timelimit expired, you will need to wait until it's your turn again.") {
|
||||
super(message);
|
||||
this.name = "WinnersTimelimitExpired";
|
||||
this.statusCode = 403;
|
||||
}
|
||||
}
|
||||
|
||||
class WineSelectionWinnerNotNextInLine extends Error {
|
||||
constructor(message = "Not the winner next in line!") {
|
||||
super(message);
|
||||
this.name = "WineSelectionWinnerNotNextInLine";
|
||||
this.statusCode = 403;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class NoMoreAttendeesToWin extends Error {
|
||||
constructor(message = "No more attendees left to drawn from.") {
|
||||
super(message);
|
||||
this.name = "NoMoreAttendeesToWin";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class CouldNotFindNewWinnerAfterNTries extends Error {
|
||||
constructor(tries) {
|
||||
let message = `Could not a new winner after ${tries} tries.`;
|
||||
super(message);
|
||||
this.name = "CouldNotFindNewWinnerAfterNTries";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class LotteryByDateNotFound extends Error {
|
||||
constructor(date) {
|
||||
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||
|
||||
const dateString = `${ye}-${mo}-${da}`;
|
||||
const dateUnix = date.getTime();
|
||||
const message = `Could not find lottery for date: ${dateString}.`;
|
||||
super(message);
|
||||
this.name = "LotteryByDateNotFoundError";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UserNotFound,
|
||||
WineNotFound,
|
||||
WinnerNotFound,
|
||||
WinnersTimelimitExpired,
|
||||
WineSelectionWinnerNotNextInLine,
|
||||
NoMoreAttendeesToWin,
|
||||
CouldNotFindNewWinnerAfterNTries,
|
||||
LotteryByDateNotFound
|
||||
};
|
||||
114
api/vinmonopolet.js
Normal file
114
api/vinmonopolet.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const fetch = require("node-fetch");
|
||||
const path = require("path");
|
||||
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
||||
|
||||
const convertToOurWineObject = wine => {
|
||||
if (wine.basic.ageLimit === "18") {
|
||||
return {
|
||||
name: wine.basic.productShortName,
|
||||
vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId,
|
||||
rating: wine.basic.alcoholContent,
|
||||
occurences: 0,
|
||||
id: wine.basic.productId,
|
||||
year: wine.basic.vintage,
|
||||
image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`,
|
||||
price: wine.prices[0].salesPrice.toString(),
|
||||
country: wine.origins.origin.country
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const convertToOurStoreObject = store => {
|
||||
return {
|
||||
id: store.storeId,
|
||||
name: store.storeName,
|
||||
...store.address
|
||||
};
|
||||
};
|
||||
|
||||
const searchWinesByName = async (name, page = 1) => {
|
||||
const pageSize = 15;
|
||||
let url = new URL(
|
||||
`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=gato&maxResults=15`
|
||||
);
|
||||
url.searchParams.set("maxResults", pageSize);
|
||||
url.searchParams.set("start", pageSize * (page - 1));
|
||||
url.searchParams.set("productShortNameContains", name);
|
||||
|
||||
const vinmonopoletResponse = await fetch(url, {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||
}
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(err => console.error(err));
|
||||
|
||||
if (vinmonopoletResponse.errors != null) {
|
||||
return vinmonopoletResponse.errors.map(error => {
|
||||
if (error.type == "UnknownProductError") {
|
||||
return res.status(404).json({
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
}
|
||||
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean);
|
||||
|
||||
return winesConverted;
|
||||
};
|
||||
|
||||
const wineByEAN = ean => {
|
||||
const url = `https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/${ean}`;
|
||||
return fetch(url)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.map(convertToOurWineObject));
|
||||
};
|
||||
|
||||
const wineById = id => {
|
||||
const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`;
|
||||
const options = {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||
}
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.map(convertToOurWineObject));
|
||||
};
|
||||
|
||||
const allStores = () => {
|
||||
const url = `https://apis.vinmonopolet.no/stores/v0/details`;
|
||||
const options = {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||
}
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.map(convertToOurStoreObject));
|
||||
};
|
||||
|
||||
const searchStoresByName = name => {
|
||||
const url = `https://apis.vinmonopolet.no/stores/v0/details?storeNameContains=${name}`;
|
||||
const options = {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||
}
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.map(convertToOurStoreObject));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
searchWinesByName,
|
||||
wineByEAN,
|
||||
wineById,
|
||||
allStores,
|
||||
searchStoresByName
|
||||
};
|
||||
@@ -1,281 +0,0 @@
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const config = require(path.join(__dirname, "/../config/defaults/lottery"));
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration"));
|
||||
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
|
||||
|
||||
const winners = async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
let winnersRedacted = [];
|
||||
let winner;
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
winner = winners[i];
|
||||
winnersRedacted.push({
|
||||
name: winner.name,
|
||||
color: winner.color
|
||||
});
|
||||
}
|
||||
res.json(winnersRedacted);
|
||||
};
|
||||
|
||||
const winnersSecure = async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
|
||||
return res.json(winners);
|
||||
};
|
||||
|
||||
const deleteWinners = async (req, res) => {
|
||||
await VirtualWinner.deleteMany();
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("refresh_data", {});
|
||||
return res.json(true);
|
||||
};
|
||||
|
||||
const attendees = async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
let attendeesRedacted = [];
|
||||
let attendee;
|
||||
for (let i = 0; i < attendees.length; i++) {
|
||||
attendee = attendees[i];
|
||||
attendeesRedacted.push({
|
||||
name: attendee.name,
|
||||
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||
red: attendee.red,
|
||||
blue: attendee.blue,
|
||||
green: attendee.green,
|
||||
yellow: attendee.yellow
|
||||
});
|
||||
}
|
||||
return res.json(attendeesRedacted);
|
||||
};
|
||||
|
||||
const attendeesSecure = async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
|
||||
return res.json(attendees);
|
||||
};
|
||||
|
||||
const addAttendee = async (req, res) => {
|
||||
const attendee = req.body;
|
||||
const { red, blue, yellow, green } = attendee;
|
||||
|
||||
let newAttendee = new Attendee({
|
||||
name: attendee.name,
|
||||
red,
|
||||
blue,
|
||||
green,
|
||||
yellow,
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
winner: false
|
||||
});
|
||||
await newAttendee.save();
|
||||
|
||||
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("new_attendee", {});
|
||||
|
||||
return res.send(true);
|
||||
};
|
||||
|
||||
const deleteAttendees = async (req, res) => {
|
||||
await Attendee.deleteMany();
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("refresh_data", {});
|
||||
return res.json(true);
|
||||
};
|
||||
|
||||
const drawWinner = async (req, res) => {
|
||||
let allContestants = await Attendee.find({ winner: false });
|
||||
|
||||
if (allContestants.length == 0) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No attendees left that have not won."
|
||||
});
|
||||
}
|
||||
let raffleColors = [];
|
||||
for (let i = 0; i < allContestants.length; i++) {
|
||||
let currentContestant = allContestants[i];
|
||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||
raffleColors.push("blue");
|
||||
}
|
||||
for (let red = 0; red < currentContestant.red; red++) {
|
||||
raffleColors.push("red");
|
||||
}
|
||||
for (let green = 0; green < currentContestant.green; green++) {
|
||||
raffleColors.push("green");
|
||||
}
|
||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||
raffleColors.push("yellow");
|
||||
}
|
||||
}
|
||||
|
||||
raffleColors = shuffle(raffleColors);
|
||||
|
||||
let colorToChooseFrom =
|
||||
raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||
let findObject = { winner: false };
|
||||
|
||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||
|
||||
let tries = 0;
|
||||
const maxTries = 3;
|
||||
let contestantsToChooseFrom = undefined;
|
||||
while (contestantsToChooseFrom == undefined && tries < maxTries) {
|
||||
const hit = await Attendee.find(findObject);
|
||||
if (hit && hit.length) {
|
||||
contestantsToChooseFrom = hit;
|
||||
break;
|
||||
}
|
||||
tries++;
|
||||
}
|
||||
if (contestantsToChooseFrom == undefined) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: `Klarte ikke trekke en vinner etter ${maxTries} forsøk.`
|
||||
});
|
||||
}
|
||||
|
||||
let attendeeListDemocratic = [];
|
||||
|
||||
let currentContestant;
|
||||
for (let i = 0; i < contestantsToChooseFrom.length; i++) {
|
||||
currentContestant = contestantsToChooseFrom[i];
|
||||
for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) {
|
||||
attendeeListDemocratic.push({
|
||||
name: currentContestant.name,
|
||||
phoneNumber: currentContestant.phoneNumber,
|
||||
red: currentContestant.red,
|
||||
blue: currentContestant.blue,
|
||||
green: currentContestant.green,
|
||||
yellow: currentContestant.yellow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attendeeListDemocratic = shuffle(attendeeListDemocratic);
|
||||
|
||||
let winner =
|
||||
attendeeListDemocratic[
|
||||
Math.floor(Math.random() * attendeeListDemocratic.length)
|
||||
];
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("winner", {
|
||||
color: colorToChooseFrom,
|
||||
name: winner.name,
|
||||
winner_count: winners.length + 1
|
||||
});
|
||||
|
||||
let newWinnerElement = new VirtualWinner({
|
||||
name: winner.name,
|
||||
phoneNumber: winner.phoneNumber,
|
||||
color: colorToChooseFrom,
|
||||
red: winner.red,
|
||||
blue: winner.blue,
|
||||
green: winner.green,
|
||||
yellow: winner.yellow,
|
||||
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||
timestamp_drawn: new Date().getTime()
|
||||
});
|
||||
|
||||
await Attendee.update(
|
||||
{ name: winner.name, phoneNumber: winner.phoneNumber },
|
||||
{ $set: { winner: true } }
|
||||
);
|
||||
|
||||
await newWinnerElement.save();
|
||||
return res.json({
|
||||
success: true,
|
||||
winner
|
||||
});
|
||||
};
|
||||
|
||||
const finish = async (req, res) => {
|
||||
if (!config.gatewayToken) {
|
||||
return res.json({
|
||||
message: "Missing api token for sms gateway.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
|
||||
if (winners.length == 0) {
|
||||
return res.json({
|
||||
message: "No winners to draw from.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
Message.sendInitialMessageToWinners(winners.slice(1));
|
||||
|
||||
return findAndNotifyNextWinner()
|
||||
.then(() => res.json({
|
||||
success: true,
|
||||
message: "Sent wine select message to first winner and update message to rest of winners."
|
||||
}))
|
||||
.catch(error => res.json({
|
||||
message: error["message"] || "Unable to send message to first winner.",
|
||||
success: false
|
||||
}))
|
||||
};
|
||||
|
||||
const genRandomString = function(length) {
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString("hex") /** convert to hexadecimal format */
|
||||
.slice(0, length); /** return required number of characters */
|
||||
};
|
||||
|
||||
const sha512 = function(password, salt) {
|
||||
var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */
|
||||
hash.update(password);
|
||||
var value = hash.digest("hex");
|
||||
return value;
|
||||
};
|
||||
|
||||
function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
temporaryValue,
|
||||
randomIndex;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (0 !== currentIndex) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
// And swap it with the current element.
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
winners,
|
||||
winnersSecure,
|
||||
drawWinner,
|
||||
finish,
|
||||
attendees,
|
||||
attendeesSecure,
|
||||
addAttendee
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const VirtualWinner = require(path.join(
|
||||
__dirname, "/schemas/VirtualWinner"
|
||||
));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
|
||||
const getWinesToWinnerById = async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No winner with this id.",
|
||||
existing: false,
|
||||
turn: false
|
||||
});
|
||||
}
|
||||
|
||||
let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
if (
|
||||
allWinners[0].id != foundWinner.id ||
|
||||
foundWinner.timestamp_limit == undefined ||
|
||||
foundWinner.timestamp_sent == undefined
|
||||
) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Not the winner next in line!",
|
||||
existing: true,
|
||||
turn: false
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
existing: true,
|
||||
turn: true,
|
||||
name: foundWinner.name,
|
||||
color: foundWinner.color
|
||||
});
|
||||
};
|
||||
|
||||
const registerWinnerSelection = async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let wineName = req.body.wineName;
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No winner with this id."
|
||||
})
|
||||
} else if (foundWinner.timestamp_limit < new Date().getTime()) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Timelimit expired, you will receive a wine after other users have chosen.",
|
||||
limit: true
|
||||
})
|
||||
}
|
||||
|
||||
let date = new Date();
|
||||
date.setHours(5, 0, 0, 0);
|
||||
let prelotteryWine = await PreLotteryWine.findOne({ name: wineName });
|
||||
|
||||
if (!prelotteryWine) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No wine with this name.",
|
||||
wine: false
|
||||
});
|
||||
}
|
||||
|
||||
let wonWine = await _wineFunctions.findSaveWine(prelotteryWine);
|
||||
await prelotteryWine.delete();
|
||||
await _personFunctions.findSavePerson(foundWinner, wonWine, date);
|
||||
await Message.sendWineConfirmation(foundWinner, wonWine, date);
|
||||
|
||||
await foundWinner.delete();
|
||||
console.info("Saved winners choice.");
|
||||
|
||||
return findAndNotifyNextWinner()
|
||||
.then(() => res.json({
|
||||
message: "Choice saved and next in line notified.",
|
||||
success: true
|
||||
}))
|
||||
.catch(error => res.json({
|
||||
message: error["message"] || "Error when notifing next winner.",
|
||||
success: false
|
||||
}))
|
||||
};
|
||||
|
||||
const chooseLastWineForUser = (winner, preLotteryWine) => {
|
||||
let date = new Date();
|
||||
date.setHours(5, 0, 0, 0);
|
||||
|
||||
return _wineFunctions.findSaveWine(preLotteryWine)
|
||||
.then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date))
|
||||
.then(() => preLotteryWine.delete())
|
||||
.then(() => Message.sendLastWinnerMessage(winner, preLotteryWine))
|
||||
.then(() => winner.delete())
|
||||
.catch(err => {
|
||||
console.log("Error thrown from chooseLastWineForUser: " + err);
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
|
||||
const findAndNotifyNextWinner = async () => {
|
||||
let nextWinner = undefined;
|
||||
|
||||
let winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
let winesLeft = await PreLotteryWine.find();
|
||||
|
||||
if (winnersLeft.length > 1) {
|
||||
console.log("multiple winners left, choose next in line")
|
||||
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
||||
console.log("one winner left, but multiple wines")
|
||||
nextWinner = winnersLeft[0] // one winner left, but multiple wines
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
||||
console.log("one winner and one wine left, choose for user")
|
||||
nextWinner = winnersLeft[0] // one winner and one wine left, choose for user
|
||||
wine = winesLeft[0]
|
||||
return chooseLastWineForUser(nextWinner, wine);
|
||||
}
|
||||
|
||||
if (nextWinner) {
|
||||
return Message.sendWineSelectMessage(nextWinner)
|
||||
.then(messageResponse => startTimeout(nextWinner.id))
|
||||
} else {
|
||||
console.info("All winners notified. Could start cleanup here.");
|
||||
return Promise.resolve({
|
||||
message: "All winners notified."
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotificationToWinnerById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
let winner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!winner) {
|
||||
return res.json({
|
||||
message: "No winner with this id.",
|
||||
success: false
|
||||
})
|
||||
}
|
||||
|
||||
return Message.sendWineSelectMessage(winner)
|
||||
.then(success => res.json({
|
||||
success: success,
|
||||
message: `Message sent to winner ${id} successfully!`
|
||||
}))
|
||||
.catch(err => res.json({
|
||||
success: false,
|
||||
message: "Error while trying to send sms.",
|
||||
error: err
|
||||
}))
|
||||
}
|
||||
|
||||
function startTimeout(id) {
|
||||
const minute = 60000;
|
||||
const minutesForTimeout = 10;
|
||||
|
||||
console.log(`Starting timeout for user ${id}.`);
|
||||
console.log(`Timeout duration: ${ minutesForTimeout * minute }`)
|
||||
setTimeout(async () => {
|
||||
let virtualWinner = await VirtualWinner.findOne({ id: id });
|
||||
if (!virtualWinner) {
|
||||
console.log(`Timeout done for user ${id}, but user has already sent data.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Timeout done for user ${id}, sending update to user.`);
|
||||
|
||||
Message.sendWineSelectMessageTooLate(virtualWinner);
|
||||
|
||||
virtualWinner.timestamp_drawn = new Date().getTime();
|
||||
virtualWinner.timestamp_limit = null;
|
||||
virtualWinner.timestamp_sent = null;
|
||||
await virtualWinner.save();
|
||||
|
||||
findAndNotifyNextWinner();
|
||||
}, minutesForTimeout * minute);
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWinesToWinnerById,
|
||||
registerWinnerSelection,
|
||||
findAndNotifyNextWinner,
|
||||
|
||||
sendNotificationToWinnerById
|
||||
};
|
||||
72
api/wine.js
72
api/wine.js
@@ -1,27 +1,63 @@
|
||||
const path = require("path");
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
|
||||
async function findSaveWine(prelotteryWine) {
|
||||
let wonWine = await Wine.findOne({ name: prelotteryWine.name });
|
||||
if (wonWine == undefined) {
|
||||
let newWonWine = new Wine({
|
||||
name: prelotteryWine.name,
|
||||
vivinoLink: prelotteryWine.vivinoLink,
|
||||
rating: prelotteryWine.rating,
|
||||
const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const addWine = async wine => {
|
||||
let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year });
|
||||
|
||||
if (existingWine == undefined) {
|
||||
let newWine = new Wine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
occurences: 1,
|
||||
image: prelotteryWine.image,
|
||||
id: prelotteryWine.id
|
||||
id: wine.id,
|
||||
year: wine.year,
|
||||
image: wine.image,
|
||||
price: wine.price,
|
||||
country: wine.country
|
||||
});
|
||||
await newWonWine.save();
|
||||
wonWine = newWonWine;
|
||||
await newWine.save();
|
||||
return newWine;
|
||||
} else {
|
||||
wonWine.occurences += 1;
|
||||
wonWine.image = prelotteryWine.image;
|
||||
wonWine.id = prelotteryWine.id;
|
||||
await wonWine.save();
|
||||
existingWine.occurences += 1;
|
||||
await existingWine.save();
|
||||
return existingWine;
|
||||
}
|
||||
};
|
||||
|
||||
return wonWine;
|
||||
}
|
||||
const allWines = (limit = undefined) => {
|
||||
if (limit) {
|
||||
return Wine.find().limit(limit);
|
||||
} else {
|
||||
return Wine.find();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.findSaveWine = findSaveWine;
|
||||
const wineById = id => {
|
||||
return Wine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
const findWine = wine => {
|
||||
return Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addWine,
|
||||
allWines,
|
||||
wineById,
|
||||
findWine
|
||||
};
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
const fetch = require('node-fetch')
|
||||
const path = require('path')
|
||||
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
||||
|
||||
const convertToOurWineObject = wine => {
|
||||
if(wine.basic.ageLimit === "18"){
|
||||
return {
|
||||
name: wine.basic.productShortName,
|
||||
vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId,
|
||||
rating: wine.basic.alcoholContent,
|
||||
occurences: 0,
|
||||
id: wine.basic.productId,
|
||||
image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`,
|
||||
price: wine.prices[0].salesPrice.toString(),
|
||||
country: wine.origins.origin.country
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wineSearch = async (req, res) => {
|
||||
const {query} = req.query
|
||||
let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`)
|
||||
url.searchParams.set('productShortNameContains', query)
|
||||
|
||||
const vinmonopoletResponse = await fetch(url, {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||
}
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(err => console.error(err))
|
||||
|
||||
|
||||
if (vinmonopoletResponse.errors != null) {
|
||||
return vinmonopoletResponse.errors.map(error => {
|
||||
if (error.type == "UnknownProductError") {
|
||||
return res.status(404).json({
|
||||
message: error.message
|
||||
})
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
})
|
||||
}
|
||||
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean)
|
||||
|
||||
return res.send(winesConverted);
|
||||
}
|
||||
|
||||
const byEAN = async (req, res) => {
|
||||
const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean)
|
||||
.then(resp => resp.json())
|
||||
|
||||
if (vinmonopoletResponse.errors != null) {
|
||||
return vinmonopoletResponse.errors.map(error => {
|
||||
if (error.type == "UnknownProductError") {
|
||||
return res.status(404).json({
|
||||
message: error.message
|
||||
})
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return res.send(vinmonopoletResponse);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
byEAN,
|
||||
wineSearch
|
||||
};
|
||||
95
api/winner.js
Normal file
95
api/winner.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const path = require("path");
|
||||
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
const { WinnerNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const redactWinnerInfoMapper = winner => {
|
||||
return {
|
||||
name: winner.name,
|
||||
color: winner.color
|
||||
};
|
||||
};
|
||||
|
||||
const addWinners = winners => {
|
||||
return Promise.all(
|
||||
winners.map(winner => {
|
||||
let newWinnerElement = new VirtualWinner({
|
||||
name: winner.name,
|
||||
color: winner.color,
|
||||
timestamp_drawn: new Date().getTime()
|
||||
});
|
||||
|
||||
return newWinnerElement.save();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const allWinners = (isAdmin = false) => {
|
||||
const sortQuery = { timestamp_drawn: 1 };
|
||||
|
||||
if (!isAdmin) {
|
||||
return VirtualWinner.find()
|
||||
.sort(sortQuery)
|
||||
.then(winners => winners.map(redactWinnerInfoMapper));
|
||||
} else {
|
||||
return VirtualWinner.find().sort(sortQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const winnerById = (id, isAdmin = false) => {
|
||||
return VirtualWinner.findOne({ id: id }).then(winner => {
|
||||
if (winner == null) {
|
||||
throw new WinnerNotFound();
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return redactWinnerInfoMapper(winner);
|
||||
}
|
||||
return winner;
|
||||
});
|
||||
};
|
||||
|
||||
const updateWinnerById = (id, updateModel) => {
|
||||
return VirtualWinner.findOne({ id: id }).then(winner => {
|
||||
if (winner == null) {
|
||||
throw new WinnerNotFound();
|
||||
}
|
||||
|
||||
const updatedWinner = {
|
||||
name: updateModel.name != null ? updateModel.name : winner.name,
|
||||
phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : winner.phoneNumber,
|
||||
red: updateModel.red != null ? updateModel.red : winner.red,
|
||||
green: updateModel.green != null ? updateModel.green : winner.green,
|
||||
blue: updateModel.blue != null ? updateModel.blue : winner.blue,
|
||||
yellow: updateModel.yellow != null ? updateModel.yellow : winner.yellow,
|
||||
timestamp_drawn: updateModel.timestamp_drawn != null ? updateModel.timestamp_drawn : winner.timestamp_drawn,
|
||||
timestamp_limit: updateModel.timestamp_limit != null ? updateModel.timestamp_limit : winner.timestamp_limit,
|
||||
timestamp_sent: updateModel.timestamp_sent != null ? updateModel.timestamp_sent : winner.timestamp_sent
|
||||
};
|
||||
|
||||
return VirtualWinner.updateOne({ id: id }, updatedWinner).then(_ => updatedWinner);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWinnerById = id => {
|
||||
return VirtualWinner.findOne({ id: id }).then(winner => {
|
||||
if (winner == null) {
|
||||
throw new WinnerNotFound();
|
||||
}
|
||||
|
||||
return VirtualWinner.deleteOne({ id: id }).then(_ => winner);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWinners = () => {
|
||||
return VirtualWinner.deleteMany();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addWinners,
|
||||
allWinners,
|
||||
winnerById,
|
||||
updateWinnerById,
|
||||
deleteWinnerById,
|
||||
deleteWinners
|
||||
};
|
||||
@@ -26,7 +26,8 @@ const allRequestedWines = () => {;
|
||||
return fetch("/api/request/all")
|
||||
.then(resp => {
|
||||
const isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||
return Promise.all([resp.json(), isAdmin]);
|
||||
const getWinesFromBody = (resp) => resp.json().then(body => body.wines);
|
||||
return Promise.all([getWinesFromBody(resp), isAdmin]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -109,8 +110,7 @@ const deleteRequestedWine = wineToBeDeleted => {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(wineToBeDeleted)
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch("api/request/" + wineToBeDeleted.id, options)
|
||||
@@ -148,14 +148,12 @@ const attendees = () => {
|
||||
|
||||
const requestNewWine = (wine) => {
|
||||
const options = {
|
||||
body: JSON.stringify({
|
||||
wine: wine
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "post"
|
||||
body: JSON.stringify({ wine })
|
||||
}
|
||||
|
||||
return fetch("/api/request/new-wine", options)
|
||||
|
||||
@@ -1,34 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Admin-side</h1>
|
||||
<Tabs :tabs="tabs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tabs from "@/ui/Tabs";
|
||||
import RegisterPage from "@/components/RegisterPage";
|
||||
import VirtualLotteryRegistrationPage from "@/components/VirtualLotteryRegistrationPage";
|
||||
import RegisterWinePage from "@/components/admin/RegisterWinePage";
|
||||
import archiveLotteryPage from "@/components/admin/archiveLotteryPage";
|
||||
import registerAttendeePage from "@/components/admin/registerAttendeePage";
|
||||
import DrawWinnerPage from "@/components/admin/DrawWinnerPage";
|
||||
import PushPage from "@/components/admin/PushPage";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tabs,
|
||||
RegisterPage,
|
||||
VirtualLotteryRegistrationPage
|
||||
Tabs
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
{ name: "Registrering", component: RegisterPage },
|
||||
{ name: "Virtuelt lotteri", component: VirtualLotteryRegistrationPage }
|
||||
{
|
||||
name: "Vin",
|
||||
component: RegisterWinePage,
|
||||
slug: "vin",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Legg til deltakere",
|
||||
component: registerAttendeePage,
|
||||
slug: "attendees",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Trekk vinner",
|
||||
component: DrawWinnerPage,
|
||||
slug: "draw",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Arkiver lotteri",
|
||||
component: archiveLotteryPage,
|
||||
slug: "reg",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Push meldinger",
|
||||
component: PushPage,
|
||||
slug: "push"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
<style lang="scss">
|
||||
@import "@/styles/media-queries";
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,40 +2,50 @@
|
||||
<main class="container">
|
||||
<h1>Alle foreslåtte viner</h1>
|
||||
|
||||
<section class="requested-wines-container">
|
||||
<section class="wines-container">
|
||||
<p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p>
|
||||
|
||||
<RequestedWineCard v-for="requestedEl in wines" :key="requestedEl.wine._id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/>
|
||||
<RequestedWineCard
|
||||
v-for="requestedWine in wines"
|
||||
:key="requestedWine.wine._id"
|
||||
:requestedElement="requestedWine"
|
||||
@wineDeleted="filterOutDeletedWine"
|
||||
:showDeleteButton="isAdmin"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { allRequestedWines } from "@/api";
|
||||
import RequestedWineCard from "@/ui/RequestedWineCard";
|
||||
export default {
|
||||
components: {
|
||||
RequestedWineCard
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
data() {
|
||||
return {
|
||||
wines: undefined,
|
||||
canRequest: true,
|
||||
isAdmin: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterOutDeletedWine(wine){
|
||||
this.wines = this.wines.filter(item => item.wine._id !== wine._id)
|
||||
},
|
||||
async refreshData(){
|
||||
[this.wines, this.isAdmin] = await allRequestedWines() || [[], false]
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.refreshData()
|
||||
this.fetchRequestedWines();
|
||||
},
|
||||
methods: {
|
||||
filterOutDeletedWine(wine) {
|
||||
this.wines = this.wines.filter(item => item.wine._id !== wine._id);
|
||||
},
|
||||
fetchRequestedWines() {
|
||||
return fetch("/api/requests")
|
||||
.then(resp => {
|
||||
this.isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||
return resp;
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -55,10 +65,4 @@ h1 {
|
||||
color: $matte-text-color;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.requested-wines-container{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
<div class="container">
|
||||
<h1 class="">Alle viner</h1>
|
||||
|
||||
<div id="wines-container">
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id">
|
||||
<div class="winners-container">
|
||||
|
||||
<span class="label">Vinnende lodd:</span>
|
||||
<div class="flex row">
|
||||
<span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span>
|
||||
@@ -19,43 +18,44 @@
|
||||
<ul class="names">
|
||||
<li v-for="(winner, index) in wine.winners">
|
||||
<router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link>
|
||||
-
|
||||
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ dateString(wine.dates[index]) }}</router-link>
|
||||
-
|
||||
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{
|
||||
dateString(wine.dates[index])
|
||||
}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Wine>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Banner from "@/ui/Banner";
|
||||
import Wine from "@/ui/Wine";
|
||||
import { overallWineStatistics } from "@/api";
|
||||
import { dateString } from "@/utils";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Banner,
|
||||
Wine
|
||||
},
|
||||
components: { Wine },
|
||||
data() {
|
||||
return {
|
||||
wines: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.overallWineStatistics();
|
||||
},
|
||||
methods: {
|
||||
winDateUrl(date) {
|
||||
const timestamp = new Date(date).getTime();
|
||||
return `/history/${timestamp}`
|
||||
return `/history/${timestamp}`;
|
||||
},
|
||||
overallWineStatistics() {
|
||||
return fetch("/api/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
},
|
||||
dateString: dateString
|
||||
},
|
||||
async mounted() {
|
||||
this.wines = await overallWineStatistics();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -84,18 +84,6 @@ h1 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#wines-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
|
||||
|
||||
> div {
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.name-wins {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
||||
</p>
|
||||
|
||||
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" />
|
||||
<RaffleGenerator @numberOfRaffles="val => (this.numberOfRaffles = val)" />
|
||||
|
||||
<Vipps class="vipps" :amount="numberOfRaffles" />
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
@@ -43,16 +43,16 @@ export default {
|
||||
this.hardStart = true;
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/lottery/generate');
|
||||
window.ga("send", "pageview", "/lottery/generate");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/global.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
h1 {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -67,7 +67,9 @@ p {
|
||||
}
|
||||
|
||||
.vipps {
|
||||
margin: 5rem auto 2.5rem auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 4rem;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 2rem;
|
||||
@@ -75,7 +77,6 @@ p {
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
<p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p>
|
||||
|
||||
<ol v-if="highscore.length > 0" class="highscore-list">
|
||||
<li v-for="person in filteredResults" @click="selectWinner(person)" @keydown.enter="selectWinner(person)" tabindex="0">
|
||||
<li
|
||||
v-for="person in filteredResults"
|
||||
@click="goToWinner(person)"
|
||||
@keydown.enter="goToWinner(person)"
|
||||
tabindex="0"
|
||||
>
|
||||
<b>{{ person.rank }}.</b> {{ person.name }} - {{ person.wins.length }}
|
||||
</li>
|
||||
</ol>
|
||||
@@ -24,8 +29,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highscoreStatistics } from "@/api";
|
||||
import { humanReadableDate, daysAgo } from "@/utils";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
@@ -34,18 +37,12 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
highscore: [],
|
||||
filterInput: ''
|
||||
}
|
||||
filterInput: ""
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await highscoreStatistics();
|
||||
response.sort((a, b) => {
|
||||
return a.wins.length > b.wins.length ? -1 : 1;
|
||||
});
|
||||
response = response.filter(
|
||||
person => person.name != null && person.name != ""
|
||||
);
|
||||
this.highscore = this.generateScoreBoard(response);
|
||||
const winners = await this.highscoreStatistics();
|
||||
this.highscore = this.generateScoreBoard(winners);
|
||||
},
|
||||
computed: {
|
||||
filteredResults() {
|
||||
@@ -53,37 +50,42 @@ export default {
|
||||
let val = this.filterInput;
|
||||
|
||||
if (val.length) {
|
||||
val = val.toLowerCase()
|
||||
const nameIncludesString = (person) => person.name.toLowerCase().includes(val);
|
||||
highscore = highscore.filter(nameIncludesString)
|
||||
val = val.toLowerCase();
|
||||
const nameIncludesString = person => person.name.toLowerCase().includes(val);
|
||||
highscore = highscore.filter(nameIncludesString);
|
||||
}
|
||||
|
||||
return highscore
|
||||
return highscore;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generateScoreBoard(highscore=this.highscore) {
|
||||
highscoreStatistics() {
|
||||
return fetch("/api/history/by-wins")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.winners);
|
||||
},
|
||||
generateScoreBoard(highscore = this.highscore) {
|
||||
let place = 0;
|
||||
let highestWinCount = -1;
|
||||
|
||||
return highscore.map(win => {
|
||||
const wins = win.wins.length
|
||||
const wins = win.wins.length;
|
||||
if (wins != highestWinCount) {
|
||||
place += 1
|
||||
highestWinCount = wins
|
||||
place += 1;
|
||||
highestWinCount = wins;
|
||||
}
|
||||
|
||||
const placeString = place.toString().padStart(2, "0");
|
||||
win.rank = placeString;
|
||||
return win
|
||||
})
|
||||
return win;
|
||||
});
|
||||
},
|
||||
resetFilter() {
|
||||
this.filterInput = '';
|
||||
document.getElementsByTagName('input')[0].focus();
|
||||
this.filterInput = "";
|
||||
document.getElementsByTagName("input")[0].focus();
|
||||
},
|
||||
selectWinner(winner) {
|
||||
const path = "/highscore/" + encodeURIComponent(winner.name)
|
||||
goToWinner(winner) {
|
||||
const path = "/highscore/" + encodeURIComponent(winner.name);
|
||||
this.$router.push(path);
|
||||
},
|
||||
humanReadableDate: humanReadableDate,
|
||||
@@ -152,7 +154,8 @@ h1 {
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 2px solid transparent;
|
||||
&:hover, &:focus {
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<input
|
||||
type="text"
|
||||
v-model="username"
|
||||
ref="username"
|
||||
placeholder="Brukernavn"
|
||||
autocapitalize="none"
|
||||
@keyup.enter="submit"
|
||||
@@ -34,6 +35,9 @@ export default {
|
||||
error: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.username.focus();
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
login(this.username, this.password)
|
||||
|
||||
@@ -14,20 +14,25 @@
|
||||
|
||||
<h4 class="margin-bottom-0">Vinnende farger:</h4>
|
||||
<div class="raffle-container el-spacing">
|
||||
<div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)" :key="color">
|
||||
<div
|
||||
class="raffle-element"
|
||||
:class="color + `-raffle`"
|
||||
v-for="[color, occurences] in Object.entries(winningColors)"
|
||||
:key="color"
|
||||
>
|
||||
{{ occurences }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="el-spacing">Flasker vunnet:</h4>
|
||||
|
||||
<div v-for="win in winner.highscore" :key="win._id">
|
||||
<div v-for="win in winner.wins" :key="win._id">
|
||||
<router-link :to="winDateUrl(win.date)" class="days-ago">
|
||||
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
|
||||
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
|
||||
</router-link>
|
||||
|
||||
<div class="won-wine">
|
||||
<img :src="smallerWineImage(win.wine.image)">
|
||||
|
||||
<div class="won-wine" v-if="win.wine">
|
||||
<img :src="smallerWineImage(win.wine.image)" />
|
||||
|
||||
<div class="won-wine-details">
|
||||
<h3>{{ win.wine.name }}</h3>
|
||||
@@ -38,6 +43,11 @@
|
||||
|
||||
<div class="raffle-element small" :class="win.color + `-raffle`"></div>
|
||||
</div>
|
||||
<div class="won-wine" v-else>
|
||||
<div class="won-wine-details">
|
||||
<h3>Oisann! Klarte ikke finne vin.</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -49,67 +59,71 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getWinnerByName } from "@/api";
|
||||
import { humanReadableDate, daysAgo } from "@/utils";
|
||||
import { dateString, humanReadableDate, daysAgo } from "@/utils";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
winner: undefined,
|
||||
name: undefined,
|
||||
error: undefined,
|
||||
previousRoute: {
|
||||
default: true,
|
||||
name: "topplisten",
|
||||
path: "/highscore"
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
next(vm => {
|
||||
if (from.name != null)
|
||||
vm.previousRoute = from
|
||||
})
|
||||
if (from.name != null) vm.previousRoute = from;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
numberOfWins() {
|
||||
return this.winner.highscore.length
|
||||
return this.winner.wins.length;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const nameFromURL = this.$route.params.name;
|
||||
getWinnerByName(nameFromURL)
|
||||
this.name = this.$route.params.name;
|
||||
this.getWinnerByName(this.name)
|
||||
.then(winner => this.setWinner(winner))
|
||||
.catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`)
|
||||
.catch(err => (this.error = `Ingen med navn: "${nameFromURL}" funnet.`));
|
||||
},
|
||||
methods: {
|
||||
getWinnerByName(name) {
|
||||
return fetch(`/api/history/by-name/${name}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.winner);
|
||||
},
|
||||
setWinner(winner) {
|
||||
this.winner = {
|
||||
name: winner.name,
|
||||
highscore: [],
|
||||
...winner
|
||||
}
|
||||
this.winningColors = this.findWinningColors()
|
||||
};
|
||||
this.winningColors = this.findWinningColors();
|
||||
},
|
||||
smallerWineImage(image) {
|
||||
if (image && image.includes(`515x515`))
|
||||
return image.replace(`515x515`, `175x175`)
|
||||
return image
|
||||
if (image && image.includes(`515x515`)) return image.replace(`515x515`, `175x175`);
|
||||
if (image && image.includes(`500x500`)) return image.replace(`500x500`, `175x175`);
|
||||
return image;
|
||||
},
|
||||
findWinningColors() {
|
||||
const colors = this.winner.highscore.map(win => win.color)
|
||||
const colorOccurences = {}
|
||||
const colors = this.winner.wins.map(win => win.color);
|
||||
const colorOccurences = {};
|
||||
colors.forEach(color => {
|
||||
if (colorOccurences[color] == undefined) {
|
||||
colorOccurences[color] = 1
|
||||
colorOccurences[color] = 1;
|
||||
} else {
|
||||
colorOccurences[color] += 1
|
||||
colorOccurences[color] += 1;
|
||||
}
|
||||
})
|
||||
return colorOccurences
|
||||
});
|
||||
return colorOccurences;
|
||||
},
|
||||
winDateUrl(date) {
|
||||
const timestamp = new Date(date).getTime();
|
||||
return `/history/${timestamp}`
|
||||
const dateParameter = dateString(new Date(date));
|
||||
return `/history/${dateParameter}`;
|
||||
},
|
||||
navigateBack() {
|
||||
if (this.previousRoute.default) {
|
||||
@@ -121,7 +135,7 @@ export default {
|
||||
humanReadableDate: humanReadableDate,
|
||||
daysAgo: daysAgo
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,728 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Registrering</h1>
|
||||
<br />
|
||||
<br />
|
||||
<div class="notification-element">
|
||||
<div class="label-div">
|
||||
<label for="notification">Push-melding</label>
|
||||
<textarea
|
||||
id="notification"
|
||||
type="text"
|
||||
rows="3"
|
||||
v-model="pushMessage"
|
||||
placeholder="Push meldingtekst"
|
||||
/>
|
||||
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="sendPush">Send push</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2 id="addwine-title">Prelottery</h2>
|
||||
|
||||
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
|
||||
|
||||
<div class="button-container">
|
||||
<button
|
||||
class="vin-button"
|
||||
@click="showCamera = !showCamera"
|
||||
>{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}</button>
|
||||
|
||||
<button class="vin-button" @click="addWine">Legg til en vin manuelt</button>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length > 0" class="edit-container">
|
||||
<wine v-for="wine in wines" :key="key" :wine="wine">
|
||||
<div class="edit">
|
||||
<div class="button-container row">
|
||||
<button
|
||||
class="vin-button"
|
||||
@click="editWine = amIBeingEdited(wine) ? false : wine"
|
||||
>{{ amIBeingEdited(wine) ? "Lukk" : "Rediger" }}</button>
|
||||
<button class="red vin-button" @click="deleteWine(wine)">Slett</button>
|
||||
</div>
|
||||
|
||||
<div v-if="amIBeingEdited(wine)" class="wine-edit">
|
||||
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="wine[key]" :placeholder="key" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</wine>
|
||||
</div>
|
||||
<div class="button-container" v-if="wines.length > 0">
|
||||
<button class="vin-button" @click="sendWines">Send inn viner</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Lottery</h2>
|
||||
|
||||
<h3>Legg til lodd kjøpt</h3>
|
||||
<div class="colors">
|
||||
<div v-for="color in lotteryColors" :class="color.css + ' colors-box'" :key="color">
|
||||
<div class="colors-overlay">
|
||||
<p>{{ color.name }} kjøpt</p>
|
||||
<input v-model="color.value" min="0" :placeholder="0" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="label-div">
|
||||
<label>Totalt kjøpt for:</label>
|
||||
<input v-model="payed" placeholder="NOK" type="number" :step="price || 1" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="submitLottery">Send inn lotteri</button>
|
||||
</div>
|
||||
|
||||
<h3>Vinnere</h3>
|
||||
<a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a>
|
||||
<div class="winner-container" v-if="winners.length > 0">
|
||||
<wine v-for="winner in winners" :key="winner" :wine="winner.wine">
|
||||
<div class="winner-element">
|
||||
<div class="color-selector">
|
||||
<div class="label-div">
|
||||
<label>Farge vunnet</label>
|
||||
</div>
|
||||
<button
|
||||
class="blue"
|
||||
:class="{ active: winner.color == 'blue' }"
|
||||
@click="winner.color = 'blue'"
|
||||
></button>
|
||||
<button
|
||||
class="red"
|
||||
:class="{ active: winner.color == 'red' }"
|
||||
@click="winner.color = 'red'"
|
||||
></button>
|
||||
<button
|
||||
class="green"
|
||||
:class="{ active: winner.color == 'green' }"
|
||||
@click="winner.color = 'green'"
|
||||
></button>
|
||||
<button
|
||||
class="yellow"
|
||||
:class="{ active: winner.color == 'yellow' }"
|
||||
@click="winner.color = 'yellow'"
|
||||
></button>
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="winner-name">Navn vinner</label>
|
||||
<input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="potential-winner-name">Virtuelle vinnere</label>
|
||||
<select
|
||||
id="potential-winner-name"
|
||||
type="text"
|
||||
placeholder="Navn"
|
||||
v-model="winner.potentialWinner"
|
||||
@change="potentialChange($event, winner)"
|
||||
>
|
||||
<option
|
||||
v-for="fetchedWinner in fetchedWinners"
|
||||
:value="stringify(fetchedWinner)"
|
||||
>{{fetchedWinner.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</wine>
|
||||
|
||||
<div class="button-container column">
|
||||
<button class="vin-button" @click="submitLotteryWinners">Send inn vinnere</button>
|
||||
<button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventBus from "@/mixins/EventBus";
|
||||
import { dateString } from '@/utils'
|
||||
import {
|
||||
prelottery,
|
||||
sendLotteryWinners,
|
||||
sendLottery,
|
||||
logWines,
|
||||
wineSchema,
|
||||
winnersSecure,
|
||||
attendees
|
||||
} from "@/api";
|
||||
import TextToast from "@/ui/TextToast";
|
||||
import Wine from "@/ui/Wine";
|
||||
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
||||
|
||||
export default {
|
||||
components: { TextToast, Wine, ScanToVinmonopolet },
|
||||
data() {
|
||||
return {
|
||||
payed: undefined,
|
||||
winners: [],
|
||||
fetchedWinners: [],
|
||||
wines: [],
|
||||
pushMessage: "",
|
||||
pushLink: "/",
|
||||
toastText: undefined,
|
||||
showToast: false,
|
||||
showCamera: false,
|
||||
editWine: false,
|
||||
lotteryColors: [
|
||||
{ value: null, name: "Blå", css: "blue" },
|
||||
{ value: null, name: "Rød", css: "red" },
|
||||
{ value: null, name: "Grønn", css: "green" },
|
||||
{ value: null, name: "Gul", css: "yellow" }
|
||||
],
|
||||
price: __PRICE__
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchAndAddPrelotteryWines().then(this.getWinnerdataFromStorage);
|
||||
|
||||
window.addEventListener("unload", this.setWinnerdataToStorage);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.setWinnerdataToStorage();
|
||||
eventBus.$off("tab-change", () => {
|
||||
this.fetchColorsAndWinners();
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.fetchColorsAndWinners();
|
||||
|
||||
eventBus.$on("tab-change", () => {
|
||||
this.fetchColorsAndWinners();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
stringify(json) {
|
||||
return JSON.stringify(json);
|
||||
},
|
||||
potentialChange(event, winner) {
|
||||
let data = JSON.parse(event.target.value);
|
||||
winner.name = data.name;
|
||||
winner.color = data.color;
|
||||
},
|
||||
async fetchColorsAndWinners() {
|
||||
let winners = await winnersSecure();
|
||||
let _attendees = await attendees();
|
||||
let colors = {
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0
|
||||
};
|
||||
this.payed = 0;
|
||||
for (let i = 0; i < _attendees.length; i++) {
|
||||
let attendee = _attendees[i];
|
||||
colors.red += attendee.red;
|
||||
colors.blue += attendee.blue;
|
||||
colors.green += attendee.green;
|
||||
colors.yellow += attendee.yellow;
|
||||
this.payed +=
|
||||
(attendee.red + attendee.blue + attendee.green + attendee.yellow) *
|
||||
10;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.lotteryColors.length; i++) {
|
||||
let currentColor = this.lotteryColors[i];
|
||||
switch (currentColor.css) {
|
||||
case "red":
|
||||
currentColor.value = colors.red;
|
||||
break;
|
||||
case "blue":
|
||||
currentColor.value = colors.blue;
|
||||
break;
|
||||
a;
|
||||
case "green":
|
||||
currentColor.value = colors.green;
|
||||
break;
|
||||
case "yellow":
|
||||
currentColor.value = colors.yellow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.fetchedWinners = winners;
|
||||
},
|
||||
amIBeingEdited(wine) {
|
||||
return this.editWine.id == wine.id && this.editWine.name == wine.name;
|
||||
},
|
||||
async fetchAndAddPrelotteryWines() {
|
||||
const wines = await prelottery();
|
||||
|
||||
for (let i = 0; i < wines.length; i++) {
|
||||
let wine = wines[i];
|
||||
this.winners.push({
|
||||
name: "",
|
||||
color: "",
|
||||
potentialWinner: "",
|
||||
wine: {
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
id: wine.id
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
wineFromVinmonopoletScan(wineResponse) {
|
||||
if (this.wines.map(wine => wine.name).includes(wineResponse.name)) {
|
||||
this.toastText = "Vinen er allerede lagt til.";
|
||||
this.showToast = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastText = "Fant og la til vin:<br>" + wineResponse.name;
|
||||
this.showToast = true;
|
||||
|
||||
this.wines.unshift(wineResponse);
|
||||
},
|
||||
sendPush: async function() {
|
||||
let _response = await fetch("/subscription/send-notification", {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message: this.pushMessage, link: this.pushLink })
|
||||
});
|
||||
let response = await _response.json();
|
||||
if (response) {
|
||||
alert("Sendt!");
|
||||
} else {
|
||||
alert("Noe gikk galt!");
|
||||
}
|
||||
},
|
||||
addWine: async function(event) {
|
||||
const wine = await wineSchema();
|
||||
|
||||
this.editWine = wine;
|
||||
this.wines.unshift(wine);
|
||||
},
|
||||
deleteWine(deletedWine) {
|
||||
this.wines = this.wines.filter(wine => wine.name != deletedWine.name);
|
||||
},
|
||||
sendWines: async function() {
|
||||
let response = await logWines(this.wines);
|
||||
if (response.success == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("Noe gikk galt under innsending");
|
||||
}
|
||||
},
|
||||
addWinner: function(event) {
|
||||
this.winners.push({
|
||||
name: "",
|
||||
color: "",
|
||||
wine: {
|
||||
name: "",
|
||||
vivinoLink: "",
|
||||
rating: ""
|
||||
}
|
||||
});
|
||||
},
|
||||
submitLottery: async function(event) {
|
||||
const colors = {
|
||||
red: this.lotteryColors.filter(c => c.css == "red")[0].value,
|
||||
green: this.lotteryColors.filter(c => c.css == "green")[0].value,
|
||||
blue: this.lotteryColors.filter(c => c.css == "blue")[0].value,
|
||||
yellow: this.lotteryColors.filter(c => c.css == "yellow")[0].value
|
||||
};
|
||||
|
||||
let sendObject = {
|
||||
lottery: {
|
||||
date: dateString(new Date()),
|
||||
...colors
|
||||
}
|
||||
};
|
||||
|
||||
if (sendObject.lottery.red == undefined) {
|
||||
alert("Rød må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.green == undefined) {
|
||||
alert("Grønn må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.yellow == undefined) {
|
||||
alert("Gul må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.blue == undefined) {
|
||||
alert("Blå må defineres");
|
||||
return;
|
||||
}
|
||||
|
||||
sendObject.lottery.bought =
|
||||
parseInt(colors.blue) +
|
||||
parseInt(colors.red) +
|
||||
parseInt(colors.green) +
|
||||
parseInt(colors.yellow);
|
||||
const stolen = sendObject.lottery.bought - parseInt(this.payed) / 10;
|
||||
if (isNaN(stolen) || stolen == undefined) {
|
||||
alert("Betalt må registreres");
|
||||
return;
|
||||
}
|
||||
sendObject.lottery.stolen = stolen;
|
||||
|
||||
let response = await sendLottery(sendObject);
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.message || "Noe gikk galt under innsending");
|
||||
}
|
||||
},
|
||||
submitLotteryWinners: async function(event) {
|
||||
let sendObject = {
|
||||
lottery: {
|
||||
date: dateString(new Date()),
|
||||
winners: this.winners
|
||||
}
|
||||
}
|
||||
|
||||
if (sendObject.lottery.winners.length == 0) {
|
||||
alert("Det må være med vinnere");
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < sendObject.lottery.winners.length; i++) {
|
||||
let currentWinner = sendObject.lottery.winners[i];
|
||||
|
||||
if (currentWinner.name == undefined || currentWinner.name == "") {
|
||||
alert("Navn må defineres");
|
||||
return;
|
||||
}
|
||||
if (currentWinner.color == undefined || currentWinner.color == "") {
|
||||
alert("Farge må defineres");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let response = await sendLotteryWinners(sendObject);
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.message || "Noe gikk galt under innsending");
|
||||
}
|
||||
},
|
||||
getWinnerdataFromStorage() {
|
||||
let localWinners = localStorage.getItem("winners");
|
||||
if (localWinners && this.winners.length) {
|
||||
localWinners = JSON.parse(localWinners);
|
||||
|
||||
this.winners = this.winners.map(winner => {
|
||||
const localWinnerMatch = localWinners.filter(
|
||||
localWinner =>
|
||||
localWinner.wine.name == winner.wine.name ||
|
||||
localWinner.wine.id == winner.wine.id
|
||||
);
|
||||
|
||||
if (localWinnerMatch.length > 0) {
|
||||
winner.name = localWinnerMatch[0].name || winner.name;
|
||||
winner.color = localWinnerMatch[0].color || winner.name;
|
||||
}
|
||||
|
||||
return winner;
|
||||
});
|
||||
}
|
||||
|
||||
let localColors = localStorage.getItem("colorValues");
|
||||
if (localColors) {
|
||||
localColors = localColors.split(",");
|
||||
this.lotteryColors.forEach((color, i) => {
|
||||
const localColorValue = Number(localColors[i]);
|
||||
color.value = localColorValue == 0 ? null : localColorValue;
|
||||
});
|
||||
}
|
||||
},
|
||||
setWinnerdataToStorage() {
|
||||
localStorage.setItem("winners", JSON.stringify(this.winners));
|
||||
localStorage.setItem(
|
||||
"colorValues",
|
||||
this.lotteryColors.map(color => Number(color.value))
|
||||
);
|
||||
window.removeEventListener("unload", this.setWinnerdataToStorage);
|
||||
},
|
||||
resetWinnerDataInStorage() {
|
||||
this.winners = [];
|
||||
this.fetchAndAddPrelotteryWines().then(resp => (this.winners = resp));
|
||||
this.lotteryColors.map(color => (color.value = null));
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
select {
|
||||
margin: 0 0 auto;
|
||||
height: 2rem;
|
||||
min-width: 0;
|
||||
width: 98%;
|
||||
padding: 1%;
|
||||
}
|
||||
h1 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
.wine-link {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid $link-color;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 90%;
|
||||
margin: 2rem auto;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.winner-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.button-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.edit-container {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .wine {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
.edit {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-element {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.winner-element {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.wine-element {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.generate-link {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.wine-edit {
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
label {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.color-selector {
|
||||
margin-bottom: 0.65rem;
|
||||
margin-right: 1rem;
|
||||
|
||||
@include desktop {
|
||||
min-width: 175px;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
max-width: 25vw;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 2px solid unset;
|
||||
|
||||
&.green {
|
||||
border-color: $green;
|
||||
}
|
||||
&.blue {
|
||||
border-color: $dark-blue;
|
||||
}
|
||||
&.red {
|
||||
border-color: $red;
|
||||
}
|
||||
&.yellow {
|
||||
border-color: $dark-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: 2px solid transparent;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
|
||||
// disable-dbl-tap-zoom
|
||||
touch-action: manipulation;
|
||||
|
||||
@include mobile {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #c8f9df;
|
||||
}
|
||||
&.blue {
|
||||
background: #d4f2fe;
|
||||
}
|
||||
&.red {
|
||||
background: #fbd7de;
|
||||
}
|
||||
&.yellow {
|
||||
background: #fff6d6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
margin: 3rem auto 1rem;
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
}
|
||||
|
||||
.label-div {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-box {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
@include mobile {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
position: absolute;
|
||||
top: 0.4rem;
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 70%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font-size: 3rem;
|
||||
height: unset;
|
||||
max-height: unset;
|
||||
position: absolute;
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.green,
|
||||
.green .colors-overlay > input {
|
||||
background-color: $light-green;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.blue,
|
||||
.blue .colors-overlay > input {
|
||||
background-color: $light-blue;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.yellow,
|
||||
.yellow .colors-overlay > input {
|
||||
background-color: $light-yellow;
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
.red,
|
||||
.red .colors-overlay > input {
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,29 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
@@ -23,7 +23,9 @@ export default {
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
prelottery().then(wines => this.wines = wines);
|
||||
fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -42,19 +44,18 @@ h1 {
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
gap: 2rem;
|
||||
width: 90vw;
|
||||
padding: 5vw;
|
||||
|
||||
@include desktop {
|
||||
width: 80vw;
|
||||
padding: 0 10vw;
|
||||
}
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -65,23 +66,6 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
.inner-wine-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: auto;
|
||||
width: 500px;
|
||||
font-family: Arial;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@include desktop {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<main class="main-container">
|
||||
|
||||
<section class="top-container">
|
||||
|
||||
<div class="want-to-win">
|
||||
<h1>
|
||||
Vil du også vinne?
|
||||
@@ -18,8 +16,8 @@
|
||||
</div>
|
||||
|
||||
<router-link to="/lottery" class="participate-button">
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
<p>Trykk her for å delta</p>
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
<p>Trykk her for å delta</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/generate" class="see-details-link">
|
||||
@@ -38,17 +36,16 @@
|
||||
<i class="icon icon--bottle"></i>
|
||||
<i class="icon icon--bottle"></i>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="content-container">
|
||||
|
||||
<div class="scroll-info">
|
||||
<i class ="icon icon--arrow-long-right"></i>
|
||||
<i class="icon icon--arrow-long-right"></i>
|
||||
<p>Scroll for å se vinnere og annen gøy statistikk</p>
|
||||
</div>
|
||||
|
||||
<Highscore class="highscore"/>
|
||||
<Highscore class="highscore" />
|
||||
|
||||
<TotalBought class="total-bought" />
|
||||
|
||||
<section class="chart-container">
|
||||
@@ -56,12 +53,10 @@
|
||||
<WinGraph class="win" />
|
||||
</section>
|
||||
|
||||
<Wines class="wines-container" />
|
||||
|
||||
<Wines class="wine-container" />
|
||||
</section>
|
||||
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -96,11 +91,7 @@ export default {
|
||||
if (!("PushManager" in window)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
Notification.permission !== "granted" ||
|
||||
!this.pushAllowed ||
|
||||
localStorage.getItem("push") == null
|
||||
);
|
||||
return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -120,7 +111,7 @@ export default {
|
||||
this.hardStart = way;
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/');
|
||||
window.ga("send", "pageview", "/");
|
||||
},
|
||||
startCountdown() {
|
||||
this.hardStart = true;
|
||||
@@ -145,7 +136,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
|
||||
@include mobile{
|
||||
@include mobile {
|
||||
padding-bottom: 2em;
|
||||
height: 15em;
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
@@ -156,13 +147,13 @@ export default {
|
||||
grid-column: 2 / -1;
|
||||
display: flex;
|
||||
|
||||
h1{
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
h1{
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
}
|
||||
grid-row: 2 / 4;
|
||||
@@ -170,7 +161,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.notification-request-button{
|
||||
.notification-request-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -229,7 +220,7 @@ export default {
|
||||
.icons-container {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 7 / -1;
|
||||
@include mobile{
|
||||
@include mobile {
|
||||
margin-top: 2em;
|
||||
display: none;
|
||||
}
|
||||
@@ -239,7 +230,7 @@ export default {
|
||||
grid-column: 7 / -1;
|
||||
}
|
||||
|
||||
@include desktop{
|
||||
@include desktop {
|
||||
grid-row: 4 / -3;
|
||||
grid-column: 7 / 11;
|
||||
}
|
||||
@@ -257,30 +248,27 @@ export default {
|
||||
i {
|
||||
font-size: 5em;
|
||||
|
||||
&.icon--heart-sparks{
|
||||
&.icon--heart-sparks {
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 2 / 4;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
|
||||
}
|
||||
&.icon--face-1{
|
||||
&.icon--face-1 {
|
||||
grid-column: 4 / 7;
|
||||
grid-row: 2 / 4;
|
||||
justify-self: center;
|
||||
|
||||
}
|
||||
&.icon--face-3{
|
||||
&.icon--face-3 {
|
||||
grid-column: 7 / 10;
|
||||
grid-row: 1 / 4;
|
||||
align-self: center;
|
||||
}
|
||||
&.icon--ballon{
|
||||
&.icon--ballon {
|
||||
grid-column: 9 / 11;
|
||||
grid-row: 3 / 5;
|
||||
|
||||
}
|
||||
&.icon--bottle{
|
||||
&.icon--bottle {
|
||||
grid-row: 4 / -1;
|
||||
|
||||
&:nth-of-type(5) {
|
||||
@@ -297,14 +285,13 @@ export default {
|
||||
&:nth-of-type(8) {
|
||||
grid-column: 7 / 8;
|
||||
}
|
||||
&:nth-of-type(9){
|
||||
&:nth-of-type(9) {
|
||||
grid-column: 8 / 9;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -312,12 +299,12 @@ h1 {
|
||||
font-family: "knowit";
|
||||
}
|
||||
|
||||
.to-lottery{
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
.to-lottery {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
@@ -326,10 +313,10 @@ h1 {
|
||||
row-gap: 5em;
|
||||
|
||||
.scroll-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
grid-column: 2 / -2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
@@ -346,8 +333,8 @@ h1 {
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
grid-column: 2 / -2;
|
||||
.wine-container {
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
|
||||
.icon--arrow-long-right {
|
||||
@@ -356,8 +343,7 @@ h1 {
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
|
||||
.scroll-info{
|
||||
.scroll-info {
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<div class="instructions">
|
||||
<h1 class="title">Virtuelt lotteri</h1>
|
||||
<ol>
|
||||
<li>Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>, eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link></li>
|
||||
<li>
|
||||
Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>,
|
||||
eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link>
|
||||
</li>
|
||||
<li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li>
|
||||
<li>Send gjerne melding om fargeønske også.</li>
|
||||
</ol>
|
||||
@@ -15,18 +18,16 @@
|
||||
|
||||
<VippsPill class="vipps-pill mobile-only" />
|
||||
|
||||
<p class="call-to-action">
|
||||
<span class="vin-link">Følg med på utviklingen</span> og <span class="vin-link">chat om trekningen</span>
|
||||
<i class="icon icon--arrow-left" @click="scrollToContent"></i></p>
|
||||
<p class="call-to-action">
|
||||
<span class="vin-link" @click="scrollToContent">Følg med på utviklingen</span> og
|
||||
<span class="vin-link" @click="scrollToContent">chat om trekningen</span>
|
||||
<i class="icon icon--arrow-left" @click="scrollToContent"></i>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container" ref="content">
|
||||
<WinnerDraw
|
||||
:currentWinnerDrawn="currentWinnerDrawn"
|
||||
:currentWinner="currentWinner"
|
||||
:attendees="attendees"
|
||||
/>
|
||||
<WinnerDraw :currentWinnerDrawn="currentWinnerDrawn" :currentWinner="currentWinner" :attendees="attendees" />
|
||||
|
||||
<div class="todays-raffles">
|
||||
<h2>Liste av lodd kjøpt i dag</h2>
|
||||
@@ -51,15 +52,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container wines-container">
|
||||
<div class="todays-wines">
|
||||
<h2>Dagens fangst ({{ wines.length }})</h2>
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { attendees, winners, prelottery } from "@/api";
|
||||
import Chat from "@/ui/Chat";
|
||||
import Vipps from "@/ui/Vipps";
|
||||
import VippsPill from "@/ui/VippsPill";
|
||||
@@ -74,18 +76,18 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
attendees: [],
|
||||
attendeesFetched: false,
|
||||
winners: [],
|
||||
wines: [],
|
||||
currentWinnerDrawn: false,
|
||||
currentWinner: null,
|
||||
socket: null,
|
||||
attendeesFetched: false,
|
||||
wasDisconnected: false,
|
||||
ticketsBought: {
|
||||
"red": 0,
|
||||
"blue": 0,
|
||||
"green": 0,
|
||||
"yellow": 0
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -129,42 +131,45 @@ export default {
|
||||
this.socket = null;
|
||||
},
|
||||
methods: {
|
||||
getWinners: async function() {
|
||||
let response = await winners();
|
||||
if (response) {
|
||||
this.winners = response;
|
||||
}
|
||||
getWinners() {
|
||||
fetch("/api/lottery/winners")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.winners = response.winners));
|
||||
},
|
||||
getTodaysWines() {
|
||||
prelottery()
|
||||
fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.wines)
|
||||
.then(wines => {
|
||||
this.wines = wines;
|
||||
this.todayExists = wines.length > 0;
|
||||
})
|
||||
.catch(_ => this.todayExists = false)
|
||||
.catch(_ => (this.todayExists = false));
|
||||
},
|
||||
getAttendees: async function() {
|
||||
let response = await attendees();
|
||||
if (response) {
|
||||
this.attendees = response;
|
||||
if (this.attendees == undefined || this.attendees.length == 0) {
|
||||
this.attendeesFetched = true;
|
||||
return;
|
||||
}
|
||||
const addValueOfListObjectByKey = (list, key) =>
|
||||
list.map(object => object[key]).reduce((a, b) => a + b);
|
||||
getAttendees() {
|
||||
fetch("/api/lottery/attendees")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const { attendees } = response;
|
||||
this.attendees = attendees || [];
|
||||
|
||||
this.ticketsBought = {
|
||||
red: addValueOfListObjectByKey(response, "red"),
|
||||
blue: addValueOfListObjectByKey(response, "blue"),
|
||||
green: addValueOfListObjectByKey(response, "green"),
|
||||
yellow: addValueOfListObjectByKey(response, "yellow")
|
||||
};
|
||||
}
|
||||
this.attendeesFetched = true;
|
||||
if (attendees == undefined || attendees.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addValueOfListObjectByKey = (list, key) => list.map(object => object[key]).reduce((a, b) => a + b);
|
||||
|
||||
this.ticketsBought = {
|
||||
red: addValueOfListObjectByKey(attendees, "red"),
|
||||
blue: addValueOfListObjectByKey(attendees, "blue"),
|
||||
green: addValueOfListObjectByKey(attendees, "green"),
|
||||
yellow: addValueOfListObjectByKey(attendees, "yellow")
|
||||
};
|
||||
})
|
||||
.finally(_ => (this.attendeesFetched = true));
|
||||
},
|
||||
scrollToContent() {
|
||||
console.log(window.scrollY)
|
||||
console.log(window.scrollY);
|
||||
const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50;
|
||||
const { scrollY } = window;
|
||||
let scrollHeight = intersectingHeaderHeight;
|
||||
@@ -178,14 +183,13 @@ export default {
|
||||
});
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/lottery/game');
|
||||
window.ga("send", "pageview", "/lottery/game");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
@@ -201,7 +205,8 @@ export default {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
> div, > section {
|
||||
> div,
|
||||
> section {
|
||||
@include mobile {
|
||||
grid-column: span 5;
|
||||
}
|
||||
@@ -343,6 +348,8 @@ header {
|
||||
|
||||
> div {
|
||||
padding: 1rem;
|
||||
max-height: 638px;
|
||||
overflow-y: scroll;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
@@ -369,11 +376,14 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.todays-wines {
|
||||
width: 80vw;
|
||||
padding: 0 10vw;
|
||||
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 4rem;
|
||||
@include mobile {
|
||||
width: 90vw;
|
||||
padding: 0 5vw;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1 class="title">Virtuelt lotteri registrering</h1>
|
||||
<br />
|
||||
<div class="draw-winner-container" v-if="attendees.length > 0">
|
||||
<div v-if="drawingWinner">
|
||||
<span>
|
||||
Trekker {{ currentWinners }} av {{ numberOfWinners }} vinnere.
|
||||
{{ secondsLeft }} sekunder av {{ drawTime }} igjen
|
||||
</span>
|
||||
<button class="vin-button no-margin" @click="stopDraw">Stopp trekning</button>
|
||||
</div>
|
||||
<div class="draw-container" v-if="!drawingWinner">
|
||||
<button class="vin-button no-margin" @click="drawWinner">Trekk vinnere</button>
|
||||
<input type="number" v-model="numberOfWinners" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">
|
||||
<span>{{ winner.name }}</span>
|
||||
<span>{{ winner.phoneNumber }}</span>
|
||||
<span>Rød: {{ winner.red }}</span>
|
||||
<span>Blå: {{ winner.blue }}</span>
|
||||
<span>Grønn: {{ winner.green }}</span>
|
||||
<span>Gul: {{ winner.yellow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="delete-buttons" v-if="attendees.length > 0 || winners.length > 0">
|
||||
<button
|
||||
class="vin-button"
|
||||
v-if="winners.length > 0"
|
||||
@click="deleteAllWinners"
|
||||
>Slett virtuelle vinnere</button>
|
||||
<button
|
||||
class="vin-button"
|
||||
v-if="attendees.length > 0"
|
||||
@click="deleteAllAttendees"
|
||||
>Slett virtuelle deltakere</button>
|
||||
</div>
|
||||
<div class="attendees" v-if="attendees.length > 0">
|
||||
<h2>Deltakere ({{ attendees.length }})</h2>
|
||||
<div class="attendee" v-for="(attendee, index) in attendees" :key="index">
|
||||
<div class="name-and-phone">
|
||||
<span class="name">{{ attendee.name }}</span>
|
||||
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
||||
</div>
|
||||
<div class="raffles-container">
|
||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attendee-registration-container">
|
||||
<h2>Legg til deltaker</h2>
|
||||
<div class="label-div">
|
||||
<label for="name">Navn</label>
|
||||
<input id="name" type="text" placeholder="Navn" v-model="name" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="phoneNumber">Telefonnummer</label>
|
||||
<input id="phoneNumber" type="text" placeholder="Telefonnummer" v-model="phoneNumber" />
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="randomColors">Tilfeldig farger?</label>
|
||||
<input
|
||||
id="randomColors"
|
||||
type="checkbox"
|
||||
placeholder="Tilfeldig farger"
|
||||
v-model="randomColors"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!randomColors">
|
||||
<br />
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="red">Rød</label>
|
||||
<input id="red" type="number" placeholder="Rød" v-model="red" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="blue">Blå</label>
|
||||
<input id="blue" type="number" placeholder="Blå" v-model="blue" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="green">Grønn</label>
|
||||
<input id="green" type="number" placeholder="Grønn" v-model="green" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="yellow">Gul</label>
|
||||
<input id="yellow" type="number" placeholder="Gul" v-model="yellow" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
||||
|
||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import io from "socket.io-client";
|
||||
import {
|
||||
addAttendee,
|
||||
getVirtualWinner,
|
||||
attendeesSecure,
|
||||
attendees,
|
||||
winnersSecure,
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
finishedDraw,
|
||||
prelottery
|
||||
} from "@/api";
|
||||
import TextToast from "@/ui/TextToast";
|
||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RaffleGenerator,
|
||||
TextToast
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
phoneNumber: null,
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
raffles: 0,
|
||||
randomColors: false,
|
||||
attendees: [],
|
||||
winners: [],
|
||||
drawingWinner: false,
|
||||
secondsLeft: 20,
|
||||
drawTime: 20,
|
||||
currentWinners: 1,
|
||||
numberOfWinners: 4,
|
||||
socket: null,
|
||||
toastText: undefined,
|
||||
showToast: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
|
||||
this.socket = io(`${window.location.hostname}:${window.location.port}`);
|
||||
|
||||
this.socket.on("winner", async msg => {
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
});
|
||||
|
||||
this.socket.on("refresh_data", async msg => {
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
});
|
||||
|
||||
this.socket.on("new_attendee", async msg => {
|
||||
this.getAttendees();
|
||||
});
|
||||
|
||||
window.finishedDraw = finishedDraw;
|
||||
},
|
||||
methods: {
|
||||
setWithRandomColors(colors) {
|
||||
Object.keys(colors).forEach(color => (this[color] = colors[color]));
|
||||
},
|
||||
sendAttendee: async function() {
|
||||
if (this.red == 0 && this.blue == 0 && this.green == 0 && this.yellow == 0) {
|
||||
alert('Ingen farger valgt!')
|
||||
return;
|
||||
}
|
||||
if (this.name == 0 && this.phoneNumber) {
|
||||
alert('Ingen navn eller tlf satt!')
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await addAttendee({
|
||||
name: this.name,
|
||||
phoneNumber: this.phoneNumber,
|
||||
red: this.red,
|
||||
blue: this.blue,
|
||||
green: this.green,
|
||||
yellow: this.yellow,
|
||||
raffles: this.raffles
|
||||
});
|
||||
|
||||
if (response == true) {
|
||||
this.toastText = `Sendt inn deltaker: ${this.name}`;
|
||||
this.showToast = true;
|
||||
|
||||
this.name = null;
|
||||
this.phoneNumber = null;
|
||||
this.yellow = 0;
|
||||
this.green = 0;
|
||||
this.red = 0;
|
||||
this.blue = 0;
|
||||
|
||||
this.getAttendees();
|
||||
} else {
|
||||
alert("Klarte ikke sende inn.. Er du logget inn?");
|
||||
}
|
||||
},
|
||||
getAttendees: async function() {
|
||||
let response = await attendeesSecure();
|
||||
this.attendees = response;
|
||||
},
|
||||
stopDraw: function() {
|
||||
this.drawingWinner = false;
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
drawWinner: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||
this.drawingWinner = true;
|
||||
let response = await getVirtualWinner();
|
||||
|
||||
if (response.success) {
|
||||
console.log("Winner:", response.winner);
|
||||
if (this.currentWinners < this.numberOfWinners) {
|
||||
this.countdown();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
let finished = await finishedDraw();
|
||||
if(finished) {
|
||||
alert("SMS'er er sendt ut!");
|
||||
} else {
|
||||
alert("Noe gikk galt under SMS utsendelser.. Sjekk logg og database for id'er.");
|
||||
}
|
||||
}
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
alert("Noe gikk galt under trekningen..! " + response["message"]);
|
||||
}
|
||||
}
|
||||
},
|
||||
countdown: function() {
|
||||
this.secondsLeft -= 1;
|
||||
if (!this.drawingWinner) {
|
||||
return;
|
||||
}
|
||||
if (this.secondsLeft <= 0) {
|
||||
this.secondsLeft = this.drawTime;
|
||||
this.currentWinners += 1;
|
||||
if (this.currentWinners <= this.numberOfWinners) {
|
||||
this.drawWinner();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.countdown();
|
||||
}, 1000);
|
||||
},
|
||||
deleteAllWinners: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil slette vinnere?")) {
|
||||
let response = await deleteWinners();
|
||||
if (response) {
|
||||
this.getWinners();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteAllAttendees: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
|
||||
let response = await deleteAttendees();
|
||||
if (response) {
|
||||
this.getAttendees();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
}
|
||||
},
|
||||
getWinners: async function() {
|
||||
let response = await winnersSecure();
|
||||
if (response) {
|
||||
this.winners = response;
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.draw-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.draw-winner-container,
|
||||
.delete-buttons {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.delete-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h1 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 90%;
|
||||
margin: 2rem auto;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
#randomColors {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
&:checked {
|
||||
background: green;
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
width: 140px;
|
||||
height: 150px;
|
||||
margin: 20px 0;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
|
||||
&.small {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-raffle {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-raffle {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex !important;
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.winners {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attendees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attendee {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
|
||||
& .name-and-phone,
|
||||
& .raffles-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& .name-and-phone {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .raffles-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +1,23 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div v-if="!posted">
|
||||
<h1 v-if="name">Gratulerer {{name}}!</h1>
|
||||
<div>
|
||||
<div v-if="!posted" class="container">
|
||||
<h1 v-if="name">Gratulerer {{ name }}!</h1>
|
||||
|
||||
<p v-if="name">
|
||||
Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.
|
||||
</p>
|
||||
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
|
||||
|
||||
<h1 v-else-if="!turn && wines.length" class="sent-container">Finner ikke noen vinner her..</h1>
|
||||
|
||||
<h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1>
|
||||
|
||||
<div class="wines-container" v-if="name">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine">
|
||||
<button
|
||||
@click="chooseWine(wine.name)"
|
||||
class="vin-button select-wine"
|
||||
>Velg denne vinnen</button>
|
||||
<button @click="chooseWine(wine)" class="vin-button select-wine">Velg denne vinnen</button>
|
||||
</Wine>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="posted" class="sent-container">
|
||||
<h1>Valget ditt er sendt inn!</h1>
|
||||
<p>Du får mer info om henting snarest!</p>
|
||||
@@ -24,15 +26,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAmIWinner, postWineChosen, prelottery } from "@/api";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: { Wine },
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
existing: false,
|
||||
fetched: false,
|
||||
turn: false,
|
||||
name: null,
|
||||
wines: [],
|
||||
@@ -40,30 +40,43 @@ export default {
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.id = this.$router.currentRoute.params.id;
|
||||
const { id } = this.$router.currentRoute.params;
|
||||
|
||||
let winnerObject = await getAmIWinner(this.id);
|
||||
this.fetched = true;
|
||||
if (!winnerObject || !winnerObject.existing) {
|
||||
console.error("non existing", winnerObject);
|
||||
return;
|
||||
}
|
||||
this.existing = true;
|
||||
if (winnerObject.existing && !winnerObject.turn) {
|
||||
console.error("not your turn yet", winnerObject);
|
||||
return;
|
||||
}
|
||||
this.turn = true;
|
||||
this.name = winnerObject.name;
|
||||
this.wines = await prelottery();
|
||||
this.id = id;
|
||||
this.getPrizes(id);
|
||||
},
|
||||
methods: {
|
||||
chooseWine: async function(name) {
|
||||
let posted = await postWineChosen(this.id, name);
|
||||
console.log("response", posted);
|
||||
if (posted.success) {
|
||||
this.posted = true;
|
||||
}
|
||||
getPrizes(id) {
|
||||
fetch(`/api/lottery/prize-distribution/prizes/${id}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.wines = response.wines;
|
||||
this.name = response.winner.name;
|
||||
this.turn = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
chooseWine(wine) {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wine })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/prize-distribution/prize/${this.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({ title: `Valgt vin: ${wine.name}` });
|
||||
this.posted = true;
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke velge vin :(",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -74,9 +87,19 @@ export default {
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-top: 2rem;
|
||||
padding: 2rem;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
max-width: 2000px;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sent-container {
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
@@ -90,11 +113,4 @@ export default {
|
||||
.select-wine {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Trekk vinnere</h1>
|
||||
|
||||
<div class="draw-winner-container">
|
||||
<div v-if="drawingWinner == false" class="draw-container">
|
||||
<input type="number" v-model="winnersToDraw" />
|
||||
<button class="vin-button no-margin" @click="startDrawingWinners">Trekk vinnere</button>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length" class="wines-left">
|
||||
<span>Antall vin igjen: {{ winnersToDraw }} av {{ wines.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="drawingWinner == true">
|
||||
<p>Trekker vinner {{ winners.length }} av {{ wines.length }}.</p>
|
||||
<p>Neste trekning om {{ secondsLeft }} sekunder av {{ drawTime }}</p>
|
||||
|
||||
<div class="button-container draw-winner-actions">
|
||||
<button class="vin-button danger" @click="stopDraw">
|
||||
Stopp trekning
|
||||
</button>
|
||||
<button
|
||||
class="vin-button"
|
||||
:class="{ 'pulse-button': secondsLeft == 0 }"
|
||||
:disabled="secondsLeft > 0"
|
||||
@click="drawWinner"
|
||||
>
|
||||
Trekk neste
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prize-distribution">
|
||||
<h2>Prisutdeling</h2>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="startPrizeDistribution">Start automatisk prisutdeling med SMS</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element" v-for="(winner, index) in winners" :key="index">
|
||||
<span>{{ winner.name }}</span>
|
||||
<span>Phone: {{ winner.phoneNumber }}</span>
|
||||
<span>Rød: {{ winner.red }}</span>
|
||||
<span>Blå: {{ winner.blue }}</span>
|
||||
<span>Grønn: {{ winner.green }}</span>
|
||||
<span>Gul: {{ winner.yellow }}</span>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button small" @click="editingWinner = editingWinner == winner ? false : winner">
|
||||
{{ editingWinner == winner ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="editingWinner == winner" class="edit">
|
||||
<div class="label-div" v-for="key in Object.keys(winner)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="winner[key]" :placeholder="key" />
|
||||
</div>
|
||||
|
||||
<div v-if="editingWinner == winner" class="button-container column">
|
||||
<button class="vin-button small" @click="notifyWinner(winner)">
|
||||
Send SMS
|
||||
</button>
|
||||
<button class="vin-button small warning" @click="updateWinner(winner)">
|
||||
Oppdater
|
||||
</button>
|
||||
<button class="vin-button small danger" @click="deleteWinner(winner)">
|
||||
Slett
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container margin-md" v-if="winners.length > 0">
|
||||
<button class="vin-button danger" v-if="winners.length > 0" @click="deleteAllWinners">
|
||||
Slett virtuelle vinnere
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
drawingWinner: false,
|
||||
secondsLeft: 20,
|
||||
drawTime: 20,
|
||||
winners: [],
|
||||
editingWinner: undefined
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchLotterWines();
|
||||
this.fetchLotterWinners();
|
||||
},
|
||||
computed: {
|
||||
winnersToDraw() {
|
||||
if (this.wines.length == undefined || this.winners.length == undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.wines.length - this.winners.length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
winners(val) {
|
||||
this.$emit("counter", val.length);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchLotterWines() {
|
||||
return fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
},
|
||||
fetchLotterWinners() {
|
||||
return fetch("/api/lottery/winners")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.winners = response.winners));
|
||||
},
|
||||
countdown() {
|
||||
if (this.drawingWinner == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.secondsLeft > 0) {
|
||||
this.secondsLeft -= 1;
|
||||
|
||||
setTimeout(_ => {
|
||||
this.countdown();
|
||||
}, 1000);
|
||||
} else {
|
||||
if (this.winners.length == this.wines.length) {
|
||||
this.drawingWinner = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
startDrawingWinners() {
|
||||
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||
this.drawWinner();
|
||||
}
|
||||
},
|
||||
drawWinner() {
|
||||
if (this.winnersToDraw <= 0) {
|
||||
this.$toast.error({ title: "No more wines to draw" });
|
||||
return;
|
||||
}
|
||||
this.secondsLeft = this.drawTime;
|
||||
this.drawingWinner = true;
|
||||
|
||||
fetch("/api/lottery/draw")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const { winner, color, success, message } = response;
|
||||
|
||||
if (success == false) {
|
||||
this.$toast.error({ title: message });
|
||||
return;
|
||||
}
|
||||
|
||||
winner.color = color;
|
||||
this.winners.push(winner);
|
||||
this.countdown();
|
||||
})
|
||||
.catch(error => {
|
||||
if (error) {
|
||||
this.$toast.error({ title: error.message });
|
||||
}
|
||||
this.drawingWinner = false;
|
||||
});
|
||||
},
|
||||
stopDraw() {
|
||||
this.drawingWinner = false;
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
startPrizeDistribution() {
|
||||
if (!window.confirm("Er du sikker på at du vil starte prisutdeling?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawingWinner = false;
|
||||
|
||||
const options = { method: "POST" };
|
||||
fetch(`/api/lottery/prize-distribution/start`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Startet prisutdeling. SMS'er sendt ut!`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke starte prisutdeling`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
notifyWinner(winner) {
|
||||
const options = { method: "POST" };
|
||||
|
||||
fetch(`/api/lottery/messages/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Sendte sms til vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke sende sms til vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
updateWinner(winner) {
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ winner })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Oppdaterte vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke oppdatere vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteWinner(winner) {
|
||||
if (winner._id != null && window.confirm(`Er du sikker på at du vil slette vinner ${winner.name}?`)) {
|
||||
const options = { method: "DELETE" };
|
||||
|
||||
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.winners = this.winners.filter(w => w.id != winner.id);
|
||||
|
||||
this.$toast.info({
|
||||
title: `Slettet vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke slette vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteAllWinners() {
|
||||
if (window.confirm("Er du sikker på at du vil slette alle vinnere?")) {
|
||||
const options = { method: "DELETE" };
|
||||
|
||||
fetch("/api/lottery/winners", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.winners = [];
|
||||
this.$toast.info({
|
||||
title: "Slettet alle vinnere."
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke slette vinnere",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wines-left {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.draw-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
font-size: 1.7rem;
|
||||
padding: 7px;
|
||||
margin: 0;
|
||||
width: 10rem;
|
||||
height: 3rem;
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.draw-winner-actions {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.winners {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.raffle-element {
|
||||
width: 220px;
|
||||
height: 100%;
|
||||
min-height: 250px;
|
||||
font-size: 1.1rem;
|
||||
padding: 1rem;
|
||||
font-weight: 500;
|
||||
// text-align: center;
|
||||
|
||||
-webkit-mask-size: cover;
|
||||
-moz-mask-size: cover;
|
||||
mask-size: cover;
|
||||
flex-direction: column;
|
||||
|
||||
span:first-of-type {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
span.active {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.edit {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
frontend/components/admin/PushPage.vue
Normal file
59
frontend/components/admin/PushPage.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Send push melding</h1>
|
||||
|
||||
<div class="notification-element">
|
||||
<div class="label-div">
|
||||
<label for="notification">Melding</label>
|
||||
<textarea id="notification" type="text" rows="3" v-model="pushMessage" placeholder="Push meldingtekst" />
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="notification-link">Push åpner lenke</label>
|
||||
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container margin-top-sm">
|
||||
<button class="vin-button" @click="sendPush">Send push</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
pushMessage: "",
|
||||
pushLink: "/"
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
sendPush: async function() {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: this.pushMessage,
|
||||
link: this.pushLink
|
||||
})
|
||||
};
|
||||
|
||||
return fetch("/subscription/send-notification", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: "Sendt!"
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Noe gikk galt!",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
308
frontend/components/admin/RegisterWinePage.vue
Normal file
308
frontend/components/admin/RegisterWinePage.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Register vin</h1>
|
||||
|
||||
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="showCamera = !showCamera">
|
||||
{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="manualyFillInnWine">
|
||||
Legg til en vin manuelt
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="showImportLink = !showImportLink">
|
||||
{{ showImportLink ? "Skjul importer fra link" : "Importer fra link" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showImportLink" class="import-from-link">
|
||||
<label>Importer vin fra vinmonopolet link:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Vinmonopol lenke"
|
||||
ref="vinmonopoletLinkInput"
|
||||
autocapitalize="none"
|
||||
@input="addWineByUrl"
|
||||
/>
|
||||
|
||||
<div v-if="linkError" class="error">
|
||||
{{ linkError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length > 0" class="wine-edit-container">
|
||||
<h2>Dagens registrerte viner</h2>
|
||||
|
||||
<div>
|
||||
<button class="vin-button" @click="sendWines">Send inn dagens viner</button>
|
||||
</div>
|
||||
|
||||
<div class="wines">
|
||||
<wine v-for="wine in wines" :key="wine.id" :wine="wine">
|
||||
<template v-slot:default>
|
||||
<div v-if="editingWine == wine" class="wine-edit">
|
||||
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="wine[key]" :placeholder="key" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:bottom>
|
||||
<div class="button-container row small">
|
||||
<button v-if="editingWine == wine && wine._id" class="vin-button warning" @click="updateWine(wine)">
|
||||
Oppdater vin
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="editingWine = editingWine == wine ? false : wine">
|
||||
{{ editingWine == wine ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
|
||||
<button class="danger vin-button" @click="deleteWine(wine)">
|
||||
Slett
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</wine>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container" v-if="wines.length > 0"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: { ScanToVinmonopolet, Wine },
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
editingWine: undefined,
|
||||
showCamera: false,
|
||||
showImportLink: false,
|
||||
linkError: undefined
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
wines() {
|
||||
this.$emit("counter", this.wines.length);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchLotterWines();
|
||||
},
|
||||
methods: {
|
||||
fetchLotterWines() {
|
||||
fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
},
|
||||
wineFromVinmonopoletScan(wineResponse) {
|
||||
if (this.wines.map(wine => wine.name).includes(wineResponse.name)) {
|
||||
this.toastText = "Vinen er allerede lagt til.";
|
||||
this.showToast = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastText = "Fant og la til vin:<br>" + wineResponse.name;
|
||||
this.showToast = true;
|
||||
|
||||
this.wines.unshift(wineResponse);
|
||||
},
|
||||
manualyFillInnWine() {
|
||||
fetch("/api/lottery/wine/schema")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.schema)
|
||||
.then(wineSchema => {
|
||||
this.editingWine = wineSchema;
|
||||
this.wines.unshift(wineSchema);
|
||||
});
|
||||
},
|
||||
addWineByUrl(event) {
|
||||
const url = event.target.value;
|
||||
this.linkError = null;
|
||||
|
||||
if (!url.includes("vinmonopolet.no")) {
|
||||
this.linkError = "Dette er ikke en gydlig vinmonopolet lenke.";
|
||||
return;
|
||||
}
|
||||
const id = url.split("/").pop();
|
||||
|
||||
fetch(`/api/vinmonopolet/wine/by-id/${id}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const { wine } = response;
|
||||
this.wines.unshift(wine);
|
||||
this.$refs.vinmonopoletLinkInput.value = "";
|
||||
});
|
||||
},
|
||||
sendWines() {
|
||||
const filterOutExistingWines = wine => wine["_id"] == null;
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
wines: this.wines.filter(filterOutExistingWines)
|
||||
})
|
||||
};
|
||||
|
||||
fetch("/api/lottery/wines", options).then(resp => {
|
||||
try {
|
||||
if (resp.ok == false) {
|
||||
throw resp;
|
||||
}
|
||||
|
||||
resp.json().then(response => {
|
||||
if (response.success == false) {
|
||||
throw response;
|
||||
} else {
|
||||
this.$toast.info({
|
||||
title: "Viner sendt inn!",
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.$toast.error({
|
||||
title: "Feil oppsto ved innsending!",
|
||||
description: error.message,
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
updateWine(updatedWine) {
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wine: updatedWine })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/wine/${updatedWine._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingWine = null;
|
||||
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteWine(deletedWine) {
|
||||
this.wines = this.wines.filter(wine => wine.name != deletedWine.name);
|
||||
|
||||
if (deletedWine._id == null) return;
|
||||
|
||||
const options = { method: "DELETE" };
|
||||
fetch(`/api/lottery/wine/${deletedWine._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingWine = null;
|
||||
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin: 1.5rem 0 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.import-from-link {
|
||||
width: 70%;
|
||||
max-width: 800px;
|
||||
margin: 1.5rem auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 1.5rem;
|
||||
min-height: 2rem;
|
||||
line-height: 2rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid black;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
font-size: 1.3rem;
|
||||
|
||||
@include mobile {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wine-edit-container {
|
||||
max-width: 1500px;
|
||||
padding: 2rem;
|
||||
margin: 0 auto;
|
||||
|
||||
.wines {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
> div {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-top: 0.7rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
441
frontend/components/admin/archiveLotteryPage.vue
Normal file
441
frontend/components/admin/archiveLotteryPage.vue
Normal file
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Arkiver lotteri</h1>
|
||||
|
||||
<h2>Registrer lodd kjøpt</h2>
|
||||
|
||||
<div class="colors">
|
||||
<div v-for="color in lotteryColors" :class="color.key + ' colors-box'" :key="color">
|
||||
<div class="colors-overlay">
|
||||
<p>{{ color.name }} kjøpt</p>
|
||||
<input v-model.number="color.value" min="0" :placeholder="0" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="label-div">
|
||||
<label>Penger mottatt på vipps:</label>
|
||||
<input v-model.number="payed" placeholder="NOK" type="number" :step="price || 1" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length > 0">
|
||||
<h2>Vinneres vin-valg</h2>
|
||||
|
||||
<div class="winner-container">
|
||||
<wine v-for="wine in wines" :key="wine.id" :wine="wine">
|
||||
<div class="label-div">
|
||||
<label for="potential-winner-name">Virtuelle vinnere</label>
|
||||
<select id="potential-winner-name" type="text" placeholder="Navn" v-model="wine.winner">
|
||||
<option v-for="winner in winners" :value="winner">{{ winner.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="winner-element">
|
||||
<div class="color-selector">
|
||||
<div class="label-div">
|
||||
<label>Farge vunnet</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="blue"
|
||||
:class="{ active: wine.winner.color == 'blue' }"
|
||||
@click="wine.winner.color = 'blue'"
|
||||
></button>
|
||||
<button
|
||||
class="red"
|
||||
:class="{ active: wine.winner.color == 'red' }"
|
||||
@click="wine.winner.color = 'red'"
|
||||
></button>
|
||||
<button
|
||||
class="green"
|
||||
:class="{ active: wine.winner.color == 'green' }"
|
||||
@click="wine.winner.color = 'green'"
|
||||
></button>
|
||||
<button
|
||||
class="yellow"
|
||||
:class="{ active: wine.winner.color == 'yellow' }"
|
||||
@click="wine.winner.color = 'yellow'"
|
||||
></button>
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="winner-name">Navn vinner</label>
|
||||
<input id="winner-name" type="text" placeholder="Navn" v-model="wine.winner.name" />
|
||||
</div>
|
||||
</div>
|
||||
</wine>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length > 0" class="button-container column">
|
||||
<button class="vin-button" @click="archiveLottery">Send inn og arkiver</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dateString } from "@/utils";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: { Wine },
|
||||
data() {
|
||||
return {
|
||||
payed: undefined,
|
||||
wines: [],
|
||||
winners: [],
|
||||
attendees: [],
|
||||
lotteryColors: [
|
||||
{ value: 0, name: "Blå", key: "blue" },
|
||||
{ value: 0, name: "Rød", key: "red" },
|
||||
{ value: 0, name: "Grønn", key: "green" },
|
||||
{ value: 0, name: "Gul", key: "yellow" }
|
||||
],
|
||||
price: __PRICE__ || 10
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchLotteryWines();
|
||||
this.fetchLotteryWinners();
|
||||
this.fetchLotteryAttendees();
|
||||
},
|
||||
watch: {
|
||||
lotteryColors: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.payed = this.getRaffleValue();
|
||||
}
|
||||
},
|
||||
payed(val) {
|
||||
this.$emit("counter", val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
wineWithWinnerMapper(wine) {
|
||||
if (wine.winner == undefined) {
|
||||
wine.winner = {
|
||||
name: undefined,
|
||||
color: undefined
|
||||
};
|
||||
}
|
||||
return wine;
|
||||
},
|
||||
fetchLotteryWines() {
|
||||
return fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.wines = response.wines.map(this.wineWithWinnerMapper);
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke hente viner.",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchLotteryWinners() {
|
||||
return fetch("/api/lottery/winners")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.winners = response.winners;
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke hente vinnere.",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchLotteryAttendees() {
|
||||
return fetch("/api/lottery/attendees")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success && response.attendees) {
|
||||
this.attendees = response.attendees;
|
||||
this.updateLotteryColorsWithAttendees(response.attendees)
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke hente deltakere.",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
updateLotteryColorsWithAttendees(attendees) {
|
||||
this.attendees.map(attendee => {
|
||||
this.lotteryColors.map(color => (color.value += attendee[color.key]));
|
||||
});
|
||||
},
|
||||
getRaffleValue() {
|
||||
let rafflesBought = 0;
|
||||
this.lotteryColors.map(color => rafflesBought += Number(color.value));
|
||||
|
||||
return rafflesBought * this.price;
|
||||
},
|
||||
archiveLottery: async function(event) {
|
||||
const validation = this.wines.every(wine => {
|
||||
if (wine.winner.name == undefined || wine.winner.name == "") {
|
||||
this.$toast.error({
|
||||
title: `Navn på vinner må defineres for vin: ${wine.name}`
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (wine.winner.color == undefined || wine.winner.color == "") {
|
||||
this.$toast.error({
|
||||
title: `Farge vunnet må defineres for vin: ${wine.name}`
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (validation == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rafflesPayload = {};
|
||||
this.lotteryColors.map(el => rafflesPayload.[el.key] = el.value);
|
||||
|
||||
let stolen = 0;
|
||||
const payedDiff = this.payed - this.getRaffleValue()
|
||||
if (payedDiff) {
|
||||
stolen = payedDiff / this.price;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
wines: this.wines,
|
||||
raffles: rafflesPayload,
|
||||
stolen: stolen
|
||||
};
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
lottery: payload
|
||||
})
|
||||
};
|
||||
|
||||
return fetch("/api/lottery/archive", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: "Lotteriet er sendt inn og arkivert! Du kan nå slette viner, deltakere & vinnere slettes.",
|
||||
timeout: 10000
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Noe gikk galt under innsending!",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/global.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
select {
|
||||
margin: 0 0 auto;
|
||||
height: 2rem;
|
||||
min-width: 0;
|
||||
width: 98%;
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.winner-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
|
||||
> div {
|
||||
margin: 1rem;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.winner-element {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.color-selector {
|
||||
margin-bottom: 0.65rem;
|
||||
margin-right: 1rem;
|
||||
|
||||
@include desktop {
|
||||
min-width: 175px;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
max-width: 25vw;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 2px solid unset;
|
||||
|
||||
&.green {
|
||||
border-color: $green;
|
||||
}
|
||||
&.blue {
|
||||
border-color: $dark-blue;
|
||||
}
|
||||
&.red {
|
||||
border-color: $red;
|
||||
}
|
||||
&.yellow {
|
||||
border-color: $dark-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: 2px solid transparent;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
|
||||
// disable-dbl-tap-zoom
|
||||
touch-action: manipulation;
|
||||
|
||||
@include mobile {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #c8f9df;
|
||||
}
|
||||
&.blue {
|
||||
background: #d4f2fe;
|
||||
}
|
||||
&.red {
|
||||
background: #fbd7de;
|
||||
}
|
||||
&.yellow {
|
||||
background: #fff6d6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
}
|
||||
|
||||
.label-div {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-box {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
@include mobile {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
position: absolute;
|
||||
top: 0.4rem;
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 70%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font-size: 3rem;
|
||||
height: unset;
|
||||
max-height: unset;
|
||||
position: absolute;
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.green,
|
||||
.green .colors-overlay > input {
|
||||
background-color: $light-green;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.blue,
|
||||
.blue .colors-overlay > input {
|
||||
background-color: $light-blue;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.yellow,
|
||||
.yellow .colors-overlay > input {
|
||||
background-color: $light-yellow;
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
.red,
|
||||
.red .colors-overlay > input {
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
329
frontend/components/admin/registerAttendeePage.vue
Normal file
329
frontend/components/admin/registerAttendeePage.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Legg til deltaker</h1>
|
||||
|
||||
<div class="attendee-registration-container">
|
||||
<div class="row flex">
|
||||
<div class="label-div">
|
||||
<label for="name" ref="name">Navn</label>
|
||||
<input id="name" type="text" placeholder="Navn" v-model="name" />
|
||||
|
||||
<ul class="autocomplete" v-if="autocompleteAttendees.length">
|
||||
<a
|
||||
v-for="attendee in autocompleteAttendees"
|
||||
tabindex="0"
|
||||
@keydown.enter="setName(attendee)"
|
||||
@keydown.space="setName(attendee)"
|
||||
>
|
||||
<li @click="setName(attendee)">
|
||||
{{ attendee }}
|
||||
</li>
|
||||
</a>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="label-div">
|
||||
<label for="phoneNumber">Telefonnummer</label>
|
||||
<input
|
||||
id="phoneNumber"
|
||||
ref="phone"
|
||||
type="phone"
|
||||
pattern="[0-9]"
|
||||
placeholder="Telefonnummer"
|
||||
v-model="phoneNumber"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="label-div">
|
||||
<label for="randomColors">Tilfeldig farger?</label>
|
||||
<input id="randomColors" type="checkbox" placeholder="Tilfeldig farger" v-model="randomColors" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!randomColors">
|
||||
<div class="row flex">
|
||||
<div class="label-div" v-for="color in colors">
|
||||
<label :for="color.key">{{ color.name }}</label>
|
||||
<input :id="color.key" type="number" :placeholder="color.name" v-model="color.value" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
||||
|
||||
<div v-if="randomColors">
|
||||
<RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" :compact="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Attendees :attendees="attendees" :admin="isAdmin" />
|
||||
|
||||
<div v-if="attendees.length" class="button-container" style="margin-top: 2rem;">
|
||||
<button class="vin-button danger" @click="deleteAllAttendees">
|
||||
Slett alle deltakere
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import io from "socket.io-client";
|
||||
import Attendees from "@/ui/Attendees";
|
||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Attendees,
|
||||
RaffleGenerator
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
red: {
|
||||
name: "Rød",
|
||||
key: "red",
|
||||
value: 0
|
||||
},
|
||||
blue: {
|
||||
name: "Blå",
|
||||
key: "blue",
|
||||
value: 0
|
||||
},
|
||||
green: {
|
||||
name: "Grønn",
|
||||
key: "green",
|
||||
value: 0
|
||||
},
|
||||
yellow: {
|
||||
name: "Gul",
|
||||
key: "yellow",
|
||||
value: 0
|
||||
},
|
||||
isAdmin: false,
|
||||
name: null,
|
||||
phoneNumber: null,
|
||||
raffles: 0,
|
||||
randomColors: false,
|
||||
attendees: [],
|
||||
autocompleteAttendees: [],
|
||||
socket: null,
|
||||
previousAttendees: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
attendees() {
|
||||
this.$emit("counter", this.attendees.length || 0);
|
||||
},
|
||||
randomColors(val) {
|
||||
if (val == false) {
|
||||
this.colors.map(color => (color.value = 0));
|
||||
}
|
||||
},
|
||||
name(newVal, oldVal) {
|
||||
if (newVal == "" || newVal == null) {
|
||||
this.autocompleteAttendees = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.autocompleteAttendees.includes(newVal)) {
|
||||
this.autocompleteAttendees = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.previousAttendees.length == 0) {
|
||||
fetch(`/api/history`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.previousAttendees = response.winners));
|
||||
}
|
||||
|
||||
this.autocompleteAttendees = this.previousAttendees
|
||||
.filter(attendee => attendee.name.toLowerCase().includes(newVal))
|
||||
.map(attendee => attendee.name);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getAttendees();
|
||||
},
|
||||
computed: {
|
||||
colors() {
|
||||
return [this.red, this.blue, this.green, this.yellow];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
this.$refs.phone.focus();
|
||||
},
|
||||
setWithRandomColors(colors) {
|
||||
Object.keys(colors).forEach(color => (this[color].value = colors[color]));
|
||||
},
|
||||
checkIfAdmin(resp) {
|
||||
this.isAdmin = resp.headers.get("vinlottis-admin") == "true" || false;
|
||||
return resp;
|
||||
},
|
||||
getAttendees: async function() {
|
||||
return fetch("/api/lottery/attendees")
|
||||
.then(resp => this.checkIfAdmin(resp))
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.attendees = response.attendees));
|
||||
},
|
||||
sendAttendee: async function() {
|
||||
const { red, blue, green, yellow } = this;
|
||||
|
||||
if (red.value == 0 && blue.value == 0 && green.value == 0 && yellow.value == 0) {
|
||||
this.$toast.error({ title: "Ingen farger valgt!" });
|
||||
return;
|
||||
}
|
||||
if (this.name == 0 && this.phoneNumber) {
|
||||
this.$toast.error({ title: "Ingen navn eller tlf satt!" });
|
||||
return;
|
||||
}
|
||||
|
||||
const attendee = {
|
||||
name: this.name,
|
||||
phoneNumber: Number(this.phoneNumber),
|
||||
red: Number(red.value),
|
||||
blue: Number(blue.value),
|
||||
green: Number(green.value),
|
||||
yellow: Number(yellow.value),
|
||||
raffles: Number(this.raffles)
|
||||
};
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ attendee })
|
||||
};
|
||||
|
||||
return fetch("/api/lottery/attendee", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success == true) {
|
||||
this.$toast.info({
|
||||
title: `Sendt inn deltaker: ${this.name}`,
|
||||
timeout: 4000
|
||||
});
|
||||
|
||||
this.name = "";
|
||||
this.phoneNumber = null;
|
||||
this.yellow.value = 0;
|
||||
this.green.value = 0;
|
||||
this.red.value = 0;
|
||||
this.blue.value = 0;
|
||||
this.randomColors = false;
|
||||
|
||||
this.$refs.name.focus();
|
||||
this.getAttendees();
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke sende deltaker`,
|
||||
description: response.message,
|
||||
timeout: 4000
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteAllAttendees() {
|
||||
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
|
||||
const options = { method: "DELETE" };
|
||||
|
||||
fetch("/api/lottery/attendees", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.attendees = [];
|
||||
this.$toast.info({
|
||||
title: "Slettet alle deltakere."
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke slette deltakere",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// global styling for disabling height of attendee class
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.attendee {
|
||||
max-height: unset;
|
||||
|
||||
.raffle-element {
|
||||
margin: 0;
|
||||
|
||||
@include mobile {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/global.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.attendee-registration-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.row.flex .label-div {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
border: 1px solid #e1e4e8;
|
||||
|
||||
& li {
|
||||
padding: 1rem;
|
||||
font-size: 1.1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: #e1e4e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 90%;
|
||||
margin: 2rem auto;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
#randomColors {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:checked::after {
|
||||
content: "✅";
|
||||
}
|
||||
|
||||
&::after {
|
||||
font-size: 2.1rem;
|
||||
content: "❌";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
166
frontend/plugins/Toast/Toast.vue
Normal file
166
frontend/plugins/Toast/Toast.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<div class="toast" :class="type" v-if="show" ref="toast">
|
||||
<div class="message">
|
||||
<span v-html="title"></span>
|
||||
<span class="description" v-if="description">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button @click="dismiss">Lukk</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
type: this.$root.type || "info",
|
||||
title: this.$root.title || undefined,
|
||||
description: this.$root.description || undefined,
|
||||
image: this.$root.image || undefined,
|
||||
link: this.$root.link || undefined,
|
||||
timeout: this.$root.timeout || 4500,
|
||||
show: false,
|
||||
mouseover: false,
|
||||
timedOut: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// Here we set show when mounted in-order to get the transition animation to be displayed correctly
|
||||
this.show = true;
|
||||
const timeout = setTimeout(() => {
|
||||
console.log("Your toast time is up 👋");
|
||||
|
||||
if (this.mouseover === false) {
|
||||
this.show = false;
|
||||
} else {
|
||||
this.timedOut = true;
|
||||
}
|
||||
}, this.timeout);
|
||||
|
||||
setTimeout(() => {
|
||||
const { toast } = this.$refs;
|
||||
|
||||
if (toast) {
|
||||
toast.addEventListener("mouseenter", _ => {
|
||||
this.mouseover = true;
|
||||
});
|
||||
|
||||
toast.addEventListener("mouseleave", _ => {
|
||||
this.mouseover = false;
|
||||
|
||||
if (this.timedOut === true) {
|
||||
this.show = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 10);
|
||||
},
|
||||
methods: {
|
||||
dismiss() {
|
||||
this.show = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.slide-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.slide-enter,
|
||||
.slide-leave-to {
|
||||
transform: translateY(100vh);
|
||||
opacity: 0;
|
||||
}
|
||||
.slide-leave-active {
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 1.3rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
background: #2d2d2d;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 80vw;
|
||||
|
||||
@include mobile {
|
||||
width: 85vw;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& span {
|
||||
color: white;
|
||||
|
||||
&.description {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
& .button-container {
|
||||
& button {
|
||||
color: #2d2d2d;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 0 3px;
|
||||
font-size: 0.8rem;
|
||||
height: max-content;
|
||||
border: 0;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:active {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: #5bc2a1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-left: 6px solid #f6993f;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: var(--red);
|
||||
|
||||
button {
|
||||
color: var(--dark-red);
|
||||
|
||||
&:active {
|
||||
background-color: var(--dark-red);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
frontend/plugins/Toast/index.js
Normal file
51
frontend/plugins/Toast/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import Vue from "vue";
|
||||
import ToastComponent from "./Toast";
|
||||
|
||||
const optionsDefaults = {
|
||||
data: {
|
||||
type: "info",
|
||||
show: true,
|
||||
timeout: 4500,
|
||||
|
||||
onCreate(created = null) {},
|
||||
onEdit(editted = null) {},
|
||||
onRemove(removed = null) {}
|
||||
}
|
||||
};
|
||||
|
||||
function toast(options) {
|
||||
// merge the default options with the passed options.
|
||||
const root = new Vue({
|
||||
data: {
|
||||
...optionsDefaults.data,
|
||||
...options
|
||||
},
|
||||
render: createElement => createElement(ToastComponent)
|
||||
});
|
||||
|
||||
root.$mount(document.body.appendChild(document.createElement("div")));
|
||||
}
|
||||
|
||||
export default {
|
||||
install(vue) {
|
||||
console.log("Installing toast plugin!");
|
||||
|
||||
Vue.prototype.$toast = {
|
||||
info(options) {
|
||||
toast({ type: "info", ...options });
|
||||
},
|
||||
success(options) {
|
||||
toast({ type: "success", ...options });
|
||||
},
|
||||
warning(options) {
|
||||
toast({ type: "warning", ...options });
|
||||
},
|
||||
error(options) {
|
||||
toast({ type: "error", ...options });
|
||||
},
|
||||
simple(options) {
|
||||
toast({ type: "simple", ...options });
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -20,6 +20,8 @@ body {
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -51,8 +53,10 @@ a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
margin-top: 0.7rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -76,6 +80,7 @@ a {
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
&.column {
|
||||
@@ -95,7 +100,7 @@ a {
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: unset;
|
||||
margin-bottom: .75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +110,8 @@ input,
|
||||
textarea {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
font-size: 1.1rem;
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
@@ -136,6 +143,11 @@ textarea {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: #f9826c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: $red;
|
||||
color: white;
|
||||
@@ -151,9 +163,12 @@ textarea {
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
|
||||
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
|
||||
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), 0 4px 8px rgba(0, 0, 0, 0.07),
|
||||
0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@@ -163,7 +178,7 @@ textarea {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:disabled{
|
||||
&:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -173,6 +188,21 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-button:not(:hover) {
|
||||
animation: pulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
@@ -193,11 +223,23 @@ textarea {
|
||||
text-decoration: none;
|
||||
color: $matte-text-color;
|
||||
|
||||
&:focus, &:hover {
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.margin {
|
||||
&-md {
|
||||
margin: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.margin-top {
|
||||
&-md {
|
||||
@@ -269,14 +311,29 @@ textarea {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
|
||||
margin: 20px 0;
|
||||
color: #333333;
|
||||
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
@@ -293,11 +350,16 @@ textarea {
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin raffle {
|
||||
padding-bottom: 50px;
|
||||
&::before, &::after {
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -309,11 +371,11 @@ textarea {
|
||||
background-position: 0 25px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
&::after{
|
||||
&::after {
|
||||
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
|
||||
background-size: 50px 50px;
|
||||
background-position: 25px -25px;
|
||||
bottom: -25px
|
||||
bottom: -25px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,4 +389,4 @@ textarea {
|
||||
@include desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,49 @@
|
||||
$primary: #b7debd;
|
||||
body {
|
||||
--primary: #b7debd;
|
||||
|
||||
$light-green: #c8f9df;
|
||||
$green: #0be881;
|
||||
$dark-green: #0ed277;
|
||||
--light-green: #c8f9df;
|
||||
--green: #0be881;
|
||||
--dark-green: #0ed277;
|
||||
|
||||
$light-blue: #d4f2fe;
|
||||
$blue: #4bcffa;
|
||||
$dark-blue: #24acda;
|
||||
--light-blue: #d4f2fe;
|
||||
--blue: #4bcffa;
|
||||
--dark-blue: #24acda;
|
||||
|
||||
$light-yellow: #fff6d6;
|
||||
$yellow: #ffde5d;
|
||||
$dark-yellow: #ecc31d;
|
||||
--light-yellow: #fff6d6;
|
||||
--yellow: #ffde5d;
|
||||
--dark-yellow: #ecc31d;
|
||||
|
||||
$light-red: #fbd7de;
|
||||
$red: #ef5878;
|
||||
$dark-red: #ec3b61;
|
||||
--light-red: #fbd7de;
|
||||
--red: #ef5878;
|
||||
--dark-red: #ec3b61;
|
||||
|
||||
$link-color: #ff5fff;
|
||||
--link-color: #ff5fff;
|
||||
--underlinenav-text: #e1e4e8;
|
||||
--underlinenav-text-active: #f9826c;
|
||||
--underlinenav-text-hover: #d1d5da;
|
||||
|
||||
$matte-text-color: #333333;
|
||||
--matte-text-color: #333333;
|
||||
}
|
||||
|
||||
$primary: var(--primary);
|
||||
|
||||
$light-green: var(--light-green);
|
||||
$green: var(--green);
|
||||
$dark-green: var(--dark-green);
|
||||
|
||||
$light-blue: var(--light-blue);
|
||||
$blue: var(--blue);
|
||||
$dark-blue: var(--dark-blue);
|
||||
|
||||
$light-yellow: var(--light-yellow);
|
||||
$yellow: var(--yellow);
|
||||
$dark-yellow: var(--dark-yellow);
|
||||
|
||||
$light-red: var(--light-red);
|
||||
$red: var(--red);
|
||||
$dark-red: var(--dark-red);
|
||||
|
||||
$link-color: var(--link-color);
|
||||
$underlinenav-text-active: var(--underlinenav-text-active);
|
||||
|
||||
$matte-text-color: var(--matte-text-color);
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
<template>
|
||||
<div class="attendees" v-if="attendees.length > 0">
|
||||
<div class="attendees-container" ref="attendees">
|
||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||
<span class="attendee-name">{{ attendee.name }}</span>
|
||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||
<div v-if="attendees.length > 0" class="attendee-container">
|
||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||
<div class="attendee-info">
|
||||
<router-link class="attendee-name" :to="`/highscore/${attendee.name}`">
|
||||
{{ attendee.name }}
|
||||
</router-link>
|
||||
|
||||
<div v-if="admin" class="flex column justify-center margin-top-sm">
|
||||
<span>Phone: {{ attendee.phoneNumber }}</span>
|
||||
<span>Has won: {{ attendee.winner }}</span>
|
||||
</div>
|
||||
|
||||
<div class="raffle-container">
|
||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="admin" class="attendee-admin">
|
||||
<button class="vin-button small" @click="editingAttendee = editingAttendee == attendee ? false : attendee">
|
||||
{{ editingAttendee == attendee ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="editingAttendee == attendee" class="attendee-edit">
|
||||
<div class="label-div" v-for="key in Object.keys(attendee)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="attendee[key]" :placeholder="key" />
|
||||
</div>
|
||||
|
||||
<div v-if="editingAttendee == attendee">
|
||||
<button class="vin-button small warning" @click="updateAttendee(attendee)">
|
||||
Oppdater deltaker
|
||||
</button>
|
||||
|
||||
<button class="vin-button small danger" @click="deleteAttendee(attendee)">
|
||||
Slett deltaker
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,33 +50,79 @@ export default {
|
||||
props: {
|
||||
attendees: {
|
||||
type: Array
|
||||
},
|
||||
admin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
flipList: (list) => list.slice().reverse()
|
||||
data() {
|
||||
return {
|
||||
editingAttendee: undefined
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
attendees: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.$refs && this.$refs.history) {
|
||||
setTimeout(() => {
|
||||
this.$refs.attendees.scrollTop = this.$refs.attendees.scrollHeight;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
methods: {
|
||||
flipList: list => list.slice().reverse(),
|
||||
updateAttendee(updatedAttendee) {
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ attendee: updatedAttendee })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/attendee/${updatedAttendee._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingAttendee = null;
|
||||
|
||||
const { message, success } = response;
|
||||
|
||||
if (success) {
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteAttendee(deletedAttendee) {
|
||||
const options = {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ attendee: deletedAttendee })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/attendee/${deletedAttendee._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingAttendee = null;
|
||||
|
||||
const { message, success } = response;
|
||||
|
||||
if (success) {
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.attendee-name {
|
||||
width: 60%;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
@@ -51,45 +130,60 @@ hr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
font-size: 0.75rem;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.attendee-container {
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.attendees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.attendees-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
max-height: 550px;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.attendee {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@include mobile {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 2px solid #d7d8d7;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&-edit {
|
||||
button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
||||
</router-link>
|
||||
|
||||
<a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'" >
|
||||
<a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'">
|
||||
<span class="menu-toggle"></span>
|
||||
<span class="menu-toggle"></span>
|
||||
<span class="menu-toggle"></span>
|
||||
</a>
|
||||
|
||||
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'" >
|
||||
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link" >
|
||||
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'">
|
||||
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link">
|
||||
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a>
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
</router-link>
|
||||
@@ -21,8 +21,9 @@
|
||||
<div class="clock">
|
||||
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
|
||||
<span v-if="days > 0">{{ pad(days) }}:</span>
|
||||
<span>{{ pad(hours) }}</span>:
|
||||
<span>{{ pad(minutes) }}</span>:
|
||||
<span>{{ pad(hours) }}</span
|
||||
>: <span>{{ pad(minutes) }}</span
|
||||
>:
|
||||
<span>{{ pad(seconds) }}</span>
|
||||
</h2>
|
||||
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
||||
@@ -41,7 +42,7 @@ export default {
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
distance: 0,
|
||||
interval: null,
|
||||
interval: null
|
||||
};
|
||||
},
|
||||
props: {
|
||||
@@ -68,7 +69,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMenu(){
|
||||
toggleMenu() {
|
||||
this.isOpen = this.isOpen ? false : true;
|
||||
},
|
||||
pad: function(num) {
|
||||
@@ -91,10 +92,7 @@ export default {
|
||||
let nowDate = new Date();
|
||||
let now = nowDate.getTime();
|
||||
if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) {
|
||||
let _diff =
|
||||
(nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) *
|
||||
60 *
|
||||
-1;
|
||||
let _diff = (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * 60 * -1;
|
||||
nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff);
|
||||
}
|
||||
this.nextLottery = nextDayOfLottery;
|
||||
@@ -110,12 +108,8 @@ export default {
|
||||
|
||||
// Time calculations for days, hours, minutes and seconds
|
||||
this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24));
|
||||
this.hours = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
||||
);
|
||||
this.minutes = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60)) / (1000 * 60)
|
||||
);
|
||||
this.hours = Math.floor((this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
this.minutes = Math.floor((this.distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000);
|
||||
if (this.days == 7) {
|
||||
this.days = 0;
|
||||
@@ -124,7 +118,7 @@ export default {
|
||||
this.initialize();
|
||||
}
|
||||
this.interval = setTimeout(this.countdown, 500);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<span class="logged-in-username" v-if="username">Logget inn som: <span class="username">{{ username }}</span> <button @click="removeUsername">Logg ut</button></span>
|
||||
<span class="logged-in-username" v-if="username"
|
||||
>Logget inn som: <span class="username">{{ username }}</span>
|
||||
<button @click="removeUsername">Logg ut</button></span
|
||||
>
|
||||
|
||||
<div class="history" ref="history" v-if="chatHistory.length > 0">
|
||||
<div class="opaque-skirt"></div>
|
||||
@@ -8,7 +11,8 @@
|
||||
<button @click="loadMoreHistory">Hent eldre meldinger</button>
|
||||
</div>
|
||||
|
||||
<div class="history-message"
|
||||
<div
|
||||
class="history-message"
|
||||
v-for="(history, index) in chatHistory"
|
||||
:key="`${history.username}-${history.timestamp}-${index}`"
|
||||
>
|
||||
@@ -61,12 +65,11 @@ export default {
|
||||
};
|
||||
},
|
||||
created() {
|
||||
getChatHistory(1, this.pageSize)
|
||||
.then(resp => {
|
||||
this.chatHistory = resp.messages;
|
||||
this.hasMorePages = resp.total != resp.messages.length;
|
||||
});
|
||||
const username = window.localStorage.getItem('username');
|
||||
getChatHistory(1, this.pageSize).then(resp => {
|
||||
this.chatHistory = resp.messages;
|
||||
this.hasMorePages = resp.total != resp.messages.length;
|
||||
});
|
||||
const username = window.localStorage.getItem("username");
|
||||
if (username) {
|
||||
this.username = username;
|
||||
this.emitUsernameOnConnect = true;
|
||||
@@ -77,8 +80,7 @@ export default {
|
||||
handler: function(newVal, oldVal) {
|
||||
if (oldVal.length == 0) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
else if (newVal && newVal.length == oldVal.length) {
|
||||
} else if (newVal && newVal.length == oldVal.length) {
|
||||
if (this.isScrollPositionAtBottom()) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
@@ -105,10 +107,7 @@ export default {
|
||||
});
|
||||
|
||||
this.socket.on("connect", msg => {
|
||||
if (
|
||||
this.emitUsernameOnConnect ||
|
||||
(this.wasDisconnected && this.username != null)
|
||||
) {
|
||||
if (this.emitUsernameOnConnect || (this.wasDisconnected && this.username != null)) {
|
||||
this.setUsername(this.username);
|
||||
}
|
||||
});
|
||||
@@ -133,12 +132,11 @@ export default {
|
||||
let { page, pageSize } = this;
|
||||
page = page + 1;
|
||||
|
||||
getChatHistory(page, pageSize)
|
||||
.then(resp => {
|
||||
this.chatHistory = resp.messages.concat(this.chatHistory);
|
||||
this.page = page;
|
||||
this.hasMorePages = resp.total != this.chatHistory.length;
|
||||
});
|
||||
getChatHistory(page, pageSize).then(resp => {
|
||||
this.chatHistory = resp.messages.concat(this.chatHistory);
|
||||
this.page = page;
|
||||
this.hasMorePages = resp.total != this.chatHistory.length;
|
||||
});
|
||||
},
|
||||
pad(num) {
|
||||
if (num > 9) return num;
|
||||
@@ -146,9 +144,7 @@ export default {
|
||||
},
|
||||
getTime(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
||||
date.getMinutes()
|
||||
)}:${this.pad(date.getSeconds())}`;
|
||||
const timeString = `${this.pad(date.getHours())}:${this.pad(date.getMinutes())}:${this.pad(date.getSeconds())}`;
|
||||
|
||||
if (date.getDate() == new Date().getDate()) {
|
||||
return timeString;
|
||||
@@ -158,10 +154,10 @@ export default {
|
||||
sendMessage() {
|
||||
const message = { message: this.message };
|
||||
this.socket.emit("chat", message);
|
||||
this.message = '';
|
||||
this.message = "";
|
||||
this.scrollToBottomOfHistory();
|
||||
},
|
||||
setUsername(username=undefined) {
|
||||
setUsername(username = undefined) {
|
||||
if (this.temporaryUsername) {
|
||||
username = this.temporaryUsername;
|
||||
}
|
||||
@@ -178,7 +174,7 @@ export default {
|
||||
if (history) {
|
||||
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
},
|
||||
scrollToBottomOfHistory() {
|
||||
setTimeout(() => {
|
||||
@@ -189,15 +185,15 @@ export default {
|
||||
scrollToMessageElement(message) {
|
||||
const elemTimestamp = this.getTime(message.timestamp);
|
||||
const self = this;
|
||||
const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText;
|
||||
const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp;
|
||||
const getTimeStamp = elem => elem.getElementsByClassName("timestamp")[0].innerText;
|
||||
const prevOldestMessageInNewList = elem => getTimeStamp(elem) == elemTimestamp;
|
||||
|
||||
setTimeout(() => {
|
||||
const { history } = self.$refs;
|
||||
const childrenElements = Array.from(history.getElementsByClassName('history-message'));
|
||||
const childrenElements = Array.from(history.getElementsByClassName("history-message"));
|
||||
|
||||
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
|
||||
history.scrollTop = elemInNewList.offsetTop - 70
|
||||
history.scrollTop = elemInNewList.offsetTop - 70;
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +206,7 @@ export default {
|
||||
|
||||
.chat-container {
|
||||
position: relative;
|
||||
transform: translate3d(0,0,0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -241,7 +237,6 @@ input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.history {
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
@@ -276,11 +271,7 @@ input {
|
||||
position: fixed;
|
||||
height: 2rem;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
white,
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
background: linear-gradient(to bottom, white, rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
& .fetch-older-history {
|
||||
@@ -310,7 +301,7 @@ input {
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2.1rem;
|
||||
left: 2rem;
|
||||
|
||||
@@ -1,60 +1,48 @@
|
||||
<template>
|
||||
<div class="highscores" v-if="highscore.length > 0">
|
||||
|
||||
<section class="heading">
|
||||
<h3>
|
||||
Topp 5 vinnere
|
||||
Topp vinnere
|
||||
</h3>
|
||||
<router-link to="highscore" class="">
|
||||
<span class="vin-link">Se alle vinnere</span>
|
||||
</router-link>
|
||||
</section>
|
||||
|
||||
<ol class="winner-list-container">
|
||||
<li v-for="(person, index) in highscore" :key="person._id" class="single-winner">
|
||||
<span class="placement">{{index + 1}}.</span>
|
||||
<i class="icon icon--medal"></i>
|
||||
<p class="winner-name">{{ person.name }}</p>
|
||||
<li v-for="(person, index) in highscore" :key="person._id">
|
||||
<router-link :to="`/highscore/${person.name}`" class="single-winner">
|
||||
<span class="placement">{{ index + 1 }}.</span>
|
||||
<i class="icon icon--medal"></i>
|
||||
<p class="winner-name">{{ person.name }}</p>
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highscoreStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { highscore: [] };
|
||||
return {
|
||||
highscore: [],
|
||||
limit: 22
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await highscoreStatistics();
|
||||
response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1)
|
||||
this.highscore = this.generateScoreBoard(response.slice(0, 5));
|
||||
},
|
||||
methods: {
|
||||
generateScoreBoard(highscore=this.highscore) {
|
||||
let place = 0;
|
||||
let highestWinCount = -1;
|
||||
|
||||
return highscore.map(win => {
|
||||
const wins = win.wins.length
|
||||
if (wins != highestWinCount) {
|
||||
place += 1
|
||||
highestWinCount = wins
|
||||
}
|
||||
|
||||
const placeString = place.toString().padStart(2, "0");
|
||||
win.rank = placeString;
|
||||
return win
|
||||
})
|
||||
}
|
||||
return fetch(`/api/history/by-wins?limit=${this.limit}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.highscore = response.winners;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -81,8 +69,8 @@ ol {
|
||||
|
||||
.winner-list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr));
|
||||
gap: 5%;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12em, 1fr));
|
||||
gap: 2rem;
|
||||
|
||||
.single-winner {
|
||||
box-sizing: border-box;
|
||||
@@ -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>
|
||||
|
||||
@@ -2,89 +2,47 @@
|
||||
<div class="chart">
|
||||
<canvas ref="purchase-chart" width="100" height="50"></canvas>
|
||||
<div ref="chartjsLegend" class="chartjsLegend"></div>
|
||||
<div class="year-select" v-if="years.length">
|
||||
<button
|
||||
class="vin-button small"
|
||||
v-for="year in years"
|
||||
:class="{ active: yearSelected == year }"
|
||||
@click="yearFilterClicked(year)"
|
||||
>
|
||||
{{ year }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chartjs from "chart.js";
|
||||
import { chartPurchaseByColor } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
lotteries: [],
|
||||
years: [],
|
||||
yearSelected: undefined,
|
||||
chart: undefined
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let canvas = this.$refs["purchase-chart"].getContext("2d");
|
||||
|
||||
let response = await chartPurchaseByColor();
|
||||
let labels = [];
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
borderColor: "#57d2fb",
|
||||
backgroundColor: "#d4f2fe",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let yellow = {
|
||||
label: "Gul",
|
||||
borderColor: "#ffde5d",
|
||||
backgroundColor: "#fff6d6",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let red = {
|
||||
label: "Rød",
|
||||
borderColor: "#ef5878",
|
||||
backgroundColor: "#fbd7de",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let green = {
|
||||
label: "Grønn",
|
||||
borderColor: "#10e783",
|
||||
backgroundColor: "#c8f9df",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
this.lotteries = await this.chartPurchaseByColor();
|
||||
if (this.lotteries?.length) this.years = [...new Set(this.lotteries.map(lot => lot.date.slice(0, 4)))];
|
||||
|
||||
const dataset = this.calculateChartDatapoints();
|
||||
|
||||
let chartData = {
|
||||
labels: dataset.labels,
|
||||
datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
|
||||
};
|
||||
|
||||
if (response.length == 1) {
|
||||
labels.push("");
|
||||
blue.data.push(0);
|
||||
yellow.data.push(0);
|
||||
red.data.push(0);
|
||||
green.data.push(0);
|
||||
}
|
||||
|
||||
let highestNumber = 0;
|
||||
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
let thisDate = response[i];
|
||||
let dateObject = new Date(thisDate.date);
|
||||
labels.push(this.getPrettierDateString(dateObject));
|
||||
|
||||
blue.data.push(thisDate.blue);
|
||||
yellow.data.push(thisDate.yellow);
|
||||
red.data.push(thisDate.red);
|
||||
green.data.push(thisDate.green);
|
||||
|
||||
if (thisDate.blue > highestNumber) {
|
||||
highestNumber = thisDate.blue;
|
||||
}
|
||||
if (thisDate.yellow > highestNumber) {
|
||||
highestNumber = thisDate.yellow;
|
||||
}
|
||||
if (thisDate.green > highestNumber) {
|
||||
highestNumber = thisDate.green;
|
||||
}
|
||||
if (thisDate.red > highestNumber) {
|
||||
highestNumber = thisDate.red;
|
||||
}
|
||||
}
|
||||
let datasets = [blue, yellow, green, red];
|
||||
let chartdata = {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
let chart = new Chart(canvas, {
|
||||
this.chart = new Chart(canvas, {
|
||||
type: "line",
|
||||
data: chartdata,
|
||||
data: chartData,
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
@@ -110,8 +68,7 @@ export default {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
suggestedMax: highestNumber + 5
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -120,10 +77,82 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async yearFilterClicked(year) {
|
||||
this.yearSelected = this.yearSelected === year ? null : year;
|
||||
|
||||
this.lotteries = await this.chartPurchaseByColor();
|
||||
const dataset = this.calculateChartDatapoints();
|
||||
let chartData = {
|
||||
labels: dataset.labels,
|
||||
datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
|
||||
};
|
||||
|
||||
this.chart.data = chartData;
|
||||
this.chart.update();
|
||||
},
|
||||
setupDataset() {
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
borderColor: "#57d2fb",
|
||||
backgroundColor: "#d4f2fe",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let yellow = {
|
||||
label: "Gul",
|
||||
borderColor: "#ffde5d",
|
||||
backgroundColor: "#fff6d6",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let red = {
|
||||
label: "Rød",
|
||||
borderColor: "#ef5878",
|
||||
backgroundColor: "#fbd7de",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let green = {
|
||||
label: "Grønn",
|
||||
borderColor: "#10e783",
|
||||
backgroundColor: "#c8f9df",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
|
||||
return {
|
||||
labels: [""],
|
||||
blue,
|
||||
green,
|
||||
red,
|
||||
yellow
|
||||
};
|
||||
},
|
||||
calculateChartDatapoints() {
|
||||
let dataset = this.setupDataset();
|
||||
|
||||
this.lotteries.map(lottery => {
|
||||
const date = new Date(lottery.date);
|
||||
dataset.labels.push(this.getPrettierDateString(date));
|
||||
|
||||
dataset.blue.data.push(lottery.blue);
|
||||
dataset.green.data.push(lottery.green);
|
||||
dataset.red.data.push(lottery.red);
|
||||
dataset.yellow.data.push(lottery.yellow);
|
||||
});
|
||||
|
||||
return dataset;
|
||||
},
|
||||
chartPurchaseByColor() {
|
||||
const url = new URL("/api/lotteries", window.location);
|
||||
if (this.yearSelected != null) url.searchParams.set("year", this.yearSelected);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.lotteries);
|
||||
},
|
||||
getPrettierDateString(date) {
|
||||
return `${this.pad(date.getDate())}.${this.pad(
|
||||
date.getMonth() + 1
|
||||
)}.${this.pad(date.getYear() - 100)}`;
|
||||
return `${this.pad(date.getDate())}.${this.pad(date.getMonth() + 1)}.${this.pad(date.getYear() - 100)}`;
|
||||
},
|
||||
pad(num) {
|
||||
if (num < 10) {
|
||||
@@ -136,11 +165,19 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.year-select {
|
||||
margin-top: 1rem;
|
||||
|
||||
button:not(:first-of-type) {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
<div class="container">
|
||||
<div class="input-line">
|
||||
<label for="redCheckbox">
|
||||
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Rød</span>
|
||||
</label>
|
||||
<label for="blueCheckbox">
|
||||
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Blå</span>
|
||||
</label>
|
||||
<label for="yellowCheckbox">
|
||||
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Gul</span>
|
||||
</label>
|
||||
<label for="greenCheckbox">
|
||||
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
@@ -31,15 +31,10 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Antall lodd"
|
||||
@keyup.enter="generateColors"
|
||||
v-model="numberOfRaffles"
|
||||
/>
|
||||
<input type="number" placeholder="Antall lodd" @keyup.enter="generateColors" v-model="numberOfRaffles" />
|
||||
<button class="vin-button" @click="generateColors">Generer</button>
|
||||
</div>
|
||||
<div class="colors">
|
||||
<div class="colors" :class="{ compact }">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:class="getColorClass(color)"
|
||||
@@ -47,13 +42,6 @@
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="color-count-container" v-if="generated">
|
||||
<span>Rød: {{ red }}</span>
|
||||
<span>Blå: {{ blue }}</span>
|
||||
<span>Gul: {{ yellow }}</span>
|
||||
<span>Grønn: {{ green }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -64,11 +52,15 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
numberOfRaffles: 4,
|
||||
numberOfRaffles: 6,
|
||||
colors: [],
|
||||
blue: 0,
|
||||
red: 0,
|
||||
@@ -101,18 +93,21 @@ export default {
|
||||
if (time == 5) {
|
||||
this.generating = false;
|
||||
this.generated = true;
|
||||
if (this.numberOfRaffles > 1 &&
|
||||
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) {
|
||||
return
|
||||
if (
|
||||
this.numberOfRaffles > 1 &&
|
||||
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true)
|
||||
.length == 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new Set(this.colors).size == 1) {
|
||||
alert("BINGO");
|
||||
}
|
||||
|
||||
this.emitColors()
|
||||
this.emitColors();
|
||||
|
||||
window.ga('send', {
|
||||
window.ga("send", {
|
||||
hitType: "event",
|
||||
eventCategory: "Raffles",
|
||||
eventAction: "Generate",
|
||||
@@ -147,8 +142,7 @@ export default {
|
||||
}
|
||||
if (this.numberOfRaffles > 0) {
|
||||
for (let i = 0; i < this.numberOfRaffles; i++) {
|
||||
let color =
|
||||
randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||
let color = randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||
this.colors.push(color);
|
||||
if (color == 1) {
|
||||
this.red += 1;
|
||||
@@ -201,12 +195,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/global.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.container {
|
||||
margin: auto;
|
||||
// margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -282,6 +276,15 @@ label .text {
|
||||
max-width: 1400px;
|
||||
margin: 3rem auto 0;
|
||||
|
||||
&.compact {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
> .color-box {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
}
|
||||
@@ -309,20 +312,6 @@ label .text {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.color-count-container {
|
||||
margin: auto;
|
||||
width: 300px;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-family: Arial;
|
||||
margin-top: 35px;
|
||||
|
||||
@include mobile {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex justify-end">
|
||||
<div class="requested-count cursor-pointer" @click="request">
|
||||
<span>{{ requestedElement.count }}</span>
|
||||
<i class="icon icon--heart" :class="{ 'active': locallyRequested }" />
|
||||
<i class="icon icon--heart" :class="{ active: locallyRequested }" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,10 +17,9 @@
|
||||
|
||||
<template v-slot:bottom>
|
||||
<div class="float-left request">
|
||||
<i class="icon icon--heart request-icon" :class="{ 'active': locallyRequested }"></i>
|
||||
<a aria-role="button" tabindex="0" class="link" @click="request"
|
||||
:class="{ 'active': locallyRequested }">
|
||||
{{ locallyRequested ? 'Anbefalt' : 'Anbefal' }}
|
||||
<i class="icon icon--heart request-icon" :class="{ active: locallyRequested }"></i>
|
||||
<a aria-role="button" tabindex="0" class="link" @click="request" :class="{ active: locallyRequested }">
|
||||
{{ locallyRequested ? "Anbefalt" : "Anbefal" }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,14 +34,14 @@ export default {
|
||||
components: {
|
||||
Wine
|
||||
},
|
||||
data(){
|
||||
data() {
|
||||
return {
|
||||
wine: this.requestedElement.wine,
|
||||
locallyRequested: false
|
||||
}
|
||||
};
|
||||
},
|
||||
props: {
|
||||
requestedElement: {
|
||||
requestedElement: {
|
||||
required: true,
|
||||
type: Object
|
||||
},
|
||||
@@ -53,27 +52,26 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
request(){
|
||||
if (this.locallyRequested)
|
||||
return
|
||||
console.log("requesting", this.wine)
|
||||
this.locallyRequested = true
|
||||
this.requestedElement.count = this.requestedElement.count +1
|
||||
requestNewWine(this.wine)
|
||||
request() {
|
||||
if (this.locallyRequested) return;
|
||||
|
||||
this.locallyRequested = true;
|
||||
this.requestedElement.count = this.requestedElement.count + 1;
|
||||
requestNewWine(this.wine);
|
||||
},
|
||||
async deleteWine() {
|
||||
const wine = this.wine
|
||||
const wine = this.wine;
|
||||
if (window.confirm("Er du sikker på at du vil slette vinen?")) {
|
||||
let response = await deleteRequestedWine(wine);
|
||||
if (response['success'] == true) {
|
||||
this.$emit('wineDeleted', wine);
|
||||
if (response["success"] == true) {
|
||||
this.$emit("wineDeleted", wine);
|
||||
} else {
|
||||
alert("Klarte ikke slette vinen");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -83,7 +81,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
background-color: rgb(244,244,244);
|
||||
background-color: rgb(244, 244, 244);
|
||||
border-radius: 1.1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
font-size: 1.25em;
|
||||
@@ -93,14 +91,14 @@ export default {
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.icon--heart{
|
||||
.icon--heart {
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
&.link {
|
||||
border-color: $link-color
|
||||
border-color: $link-color;
|
||||
}
|
||||
|
||||
&.icon--heart {
|
||||
@@ -121,4 +119,4 @@ export default {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,51 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tab-container">
|
||||
<div
|
||||
<nav class="tab-container">
|
||||
<a
|
||||
class="tab"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="changeTab(index)"
|
||||
@keydown.enter="changeTab(index)"
|
||||
tabindex="0"
|
||||
:class="chosenTab == index ? 'active' : null"
|
||||
>{{ tab.name }}</div>
|
||||
</div>
|
||||
>
|
||||
{{ tab.name }}
|
||||
|
||||
<span v-if="tab.counter" class="counter">{{ tab.counter }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="tab-elements">
|
||||
<component :is="tabs[chosenTab].component" />
|
||||
<component :is="tabs[chosenTab].component" @counter="updateCounter" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventBus from "@/mixins/EventBus";
|
||||
export default {
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array
|
||||
},
|
||||
active: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.chosenTab = this.active;
|
||||
const url = location.href;
|
||||
|
||||
if (url.includes("tab=")) {
|
||||
const tabParameter = url.split("tab=")[1];
|
||||
const matchingSlug = this.tabs.findIndex(tab => tab.slug == tabParameter);
|
||||
console.log("matchingSlug:", matchingSlug);
|
||||
this.chosenTab = matchingSlug;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chosenTab: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeTab() {
|
||||
return this.tabs[this.chosenTab];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeTab: function(num) {
|
||||
changeTab(num) {
|
||||
this.chosenTab = num;
|
||||
this.$emit("tabChange", num);
|
||||
eventBus.$emit("tab-change");
|
||||
|
||||
let url = location.href;
|
||||
const tabParameterIndex = url.indexOf("tab=");
|
||||
|
||||
if (tabParameterIndex > 0) {
|
||||
url = url.split("tab=")[0] + `tab=${this.activeTab.slug}`;
|
||||
} else {
|
||||
url = url + `?tab=${this.activeTab.slug}`;
|
||||
}
|
||||
|
||||
window.history.pushState({}, "", url);
|
||||
},
|
||||
updateCounter(val) {
|
||||
this.activeTab.counter = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -54,28 +83,50 @@ h1 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 25px;
|
||||
border-bottom: 1px solid #333333;
|
||||
// margin-top: 25px;
|
||||
border-bottom: 1px solid var(--underlinenav-text);
|
||||
|
||||
margin-top: 2rem;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin: 0 15px;
|
||||
border: 1px solid #333333;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
background: #00000008;
|
||||
border-bottom: 1px solid #333333;
|
||||
margin-bottom: -1px;
|
||||
font-size: 1.1rem;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: rgba($matte-text-color, 0.9);
|
||||
|
||||
&.active {
|
||||
border-bottom: 1px solid white;
|
||||
color: $matte-text-color;
|
||||
border-color: var(--underlinenav-text-active) !important;
|
||||
background: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--underlinenav-text-hover);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
& .counter {
|
||||
margin-left: 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
padding: 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
background-color: rgba(209, 213, 218, 0.5);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="update-toast" :class="showClass">
|
||||
<span v-html="text"></span>
|
||||
<div class="button-container">
|
||||
<button @click="closeToast">Lukk</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: { type: String, required: true },
|
||||
refreshButton: { type: Boolean, required: false }
|
||||
},
|
||||
data() {
|
||||
return { showClass: null };
|
||||
},
|
||||
created() {
|
||||
this.showClass = "show";
|
||||
},
|
||||
mounted() {
|
||||
if (this.refreshButton) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$emit("closeToast");
|
||||
}, 5000);
|
||||
},
|
||||
methods: {
|
||||
refresh: function() {
|
||||
location.reload();
|
||||
},
|
||||
closeToast: function() {
|
||||
this.$emit("closeToast");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.update-toast {
|
||||
position: fixed;
|
||||
bottom: 1.3rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
background: #2d2d2d;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 80vw;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.show {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
-moz-transition: opacity 0.5s ease-in-out;
|
||||
-ms-transition: opacity 0.5s ease-in-out;
|
||||
-o-transition: opacity 0.5s ease-in-out;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
@include mobile {
|
||||
width: 85vw;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
& span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
& .button-container {
|
||||
& button {
|
||||
color: #2d2d2d;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 0 3px;
|
||||
font-size: 0.8rem;
|
||||
height: max-content;
|
||||
|
||||
&:active {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +1,30 @@
|
||||
<template>
|
||||
<section class="outer-bought">
|
||||
<div>
|
||||
<h3>Loddstatistikk</h3>
|
||||
|
||||
<div class="total-raffles">
|
||||
Totalt
|
||||
<span class="total">{{ total }}</span>
|
||||
kjøpte,
|
||||
<span>{{ totalWin }} vinn og </span>
|
||||
<span> {{ stolen }} stjålet </span>
|
||||
Totalt
|
||||
<span class="total">{{ total }}</span>
|
||||
kjøpte,
|
||||
<span>{{ totalWin }} vinn og </span>
|
||||
<span> {{ stolen }} stjålet </span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bought-container">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:class="
|
||||
color.name +
|
||||
'-container ' +
|
||||
color.name +
|
||||
'-raffle raffle-element-local'
|
||||
"
|
||||
:class="color.name + '-container ' + color.name + '-raffle raffle-element-local'"
|
||||
:key="color.name"
|
||||
>
|
||||
<p class="winner-chance">
|
||||
{{translate(color.name)}} vinnersjanse
|
||||
</p>
|
||||
>
|
||||
<p class="winner-chance">{{ translate(color.name) }} vinnersjanse</p>
|
||||
<span class="win-percentage">{{ color.totalPercentage }}% </span>
|
||||
<p class="total-bought-color">{{ color.total }} kjøpte</p>
|
||||
<p class="amount-of-wins"> {{ color.win }} vinn </p>
|
||||
<p class="amount-of-wins">{{ color.win }} vinn</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { colorStatistics } from "@/api";
|
||||
|
||||
@@ -45,109 +38,128 @@ export default {
|
||||
green: 0,
|
||||
total: 0,
|
||||
totalWin: 0,
|
||||
stolen: 0,
|
||||
wins: 0,
|
||||
redPercentage: 0,
|
||||
yellowPercentage: 0,
|
||||
greenPercentage: 0,
|
||||
bluePercentage: 0
|
||||
stolen: 0
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await colorStatistics();
|
||||
|
||||
this.red = response.red;
|
||||
this.blue = response.blue;
|
||||
this.green = response.green;
|
||||
this.yellow = response.yellow;
|
||||
this.total = response.total;
|
||||
|
||||
this.totalWin =
|
||||
this.red.win + this.yellow.win + this.blue.win + this.green.win;
|
||||
this.stolen = response.stolen;
|
||||
|
||||
this.redPercentage = this.round(
|
||||
this.red.win == 0 ? 0 : (this.red.win / this.totalWin) * 100
|
||||
);
|
||||
this.greenPercentage = this.round(
|
||||
this.green.win == 0 ? 0 : (this.green.win / this.totalWin) * 100
|
||||
);
|
||||
this.bluePercentage = this.round(
|
||||
this.blue.win == 0 ? 0 : (this.blue.win / this.totalWin) * 100
|
||||
);
|
||||
this.yellowPercentage = this.round(
|
||||
this.yellow.win == 0 ? 0 : (this.yellow.win / this.totalWin) * 100
|
||||
);
|
||||
|
||||
this.colors.push({
|
||||
name: "red",
|
||||
total: this.red.total,
|
||||
win: this.red.win,
|
||||
totalPercentage: this.getPercentage(this.red.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.red.win, this.red.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "blue",
|
||||
total: this.blue.total,
|
||||
win: this.blue.win,
|
||||
totalPercentage: this.getPercentage(this.blue.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.blue.win, this.blue.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "yellow",
|
||||
total: this.yellow.total,
|
||||
win: this.yellow.win,
|
||||
totalPercentage: this.getPercentage(this.yellow.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.yellow.win, this.yellow.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "green",
|
||||
total: this.green.total,
|
||||
win: this.green.win,
|
||||
totalPercentage: this.getPercentage(this.green.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.green.win, this.green.total)
|
||||
});
|
||||
|
||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||
this.allLotteries().then(this.computeColors);
|
||||
},
|
||||
methods: {
|
||||
translate(color){
|
||||
switch(color) {
|
||||
allLotteries() {
|
||||
return fetch("/api/lotteries?includeWinners=true")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.lotteries);
|
||||
},
|
||||
translate(color) {
|
||||
switch (color) {
|
||||
case "blue":
|
||||
return "Blå"
|
||||
return "Blå";
|
||||
break;
|
||||
case "red":
|
||||
return "Rød"
|
||||
return "Rød";
|
||||
break;
|
||||
case "green":
|
||||
return "Grønn"
|
||||
return "Grønn";
|
||||
break;
|
||||
case "yellow":
|
||||
return "Gul"
|
||||
return "Gul";
|
||||
break;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
},
|
||||
getPercentage: function(win, total) {
|
||||
return this.round(win == 0 ? 0 : (win / total) * 100);
|
||||
},
|
||||
round: function(number) {
|
||||
|
||||
//this can make the odds added together more than 100%, maybe rework?
|
||||
let actualPercentage = Math.round(number * 100) / 100;
|
||||
let rounded = actualPercentage.toFixed(0);
|
||||
return rounded;
|
||||
},
|
||||
computeColors(lotteries) {
|
||||
let totalRed = 0;
|
||||
let totalGreen = 0;
|
||||
let totalYellow = 0;
|
||||
let totalBlue = 0;
|
||||
let total = 0;
|
||||
let stolen = 0;
|
||||
|
||||
const colorAccumulatedWins = {
|
||||
blue: 0,
|
||||
green: 0,
|
||||
red: 0,
|
||||
yellow: 0
|
||||
};
|
||||
|
||||
const accumelatedColors = (winners, colorAccumulatedWins) => {
|
||||
winners.forEach(winner => {
|
||||
const winnerColor = winner.color;
|
||||
colorAccumulatedWins[winnerColor] += 1;
|
||||
});
|
||||
};
|
||||
|
||||
lotteries.forEach(lottery => {
|
||||
totalRed += lottery.red;
|
||||
totalGreen += lottery.green;
|
||||
totalYellow += lottery.yellow;
|
||||
totalBlue += lottery.blue;
|
||||
total += lottery.bought;
|
||||
stolen += lottery.stolen;
|
||||
|
||||
accumelatedColors(lottery.winners, colorAccumulatedWins);
|
||||
});
|
||||
|
||||
this.red = totalRed;
|
||||
this.yellow = totalYellow;
|
||||
this.green = totalGreen;
|
||||
this.blue = totalBlue;
|
||||
this.total = total;
|
||||
|
||||
this.totalWin =
|
||||
colorAccumulatedWins.red + colorAccumulatedWins.yellow + colorAccumulatedWins.blue + colorAccumulatedWins.green;
|
||||
this.stolen = stolen;
|
||||
|
||||
this.colors.push({
|
||||
name: "red",
|
||||
total: totalRed,
|
||||
win: colorAccumulatedWins.red,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.red, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.red, this.red.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "blue",
|
||||
total: totalBlue,
|
||||
win: colorAccumulatedWins.blue,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.blue, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.blue, this.blue.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "yellow",
|
||||
total: totalYellow,
|
||||
win: colorAccumulatedWins.yellow,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.yellow, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.yellow, this.yellow.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "green",
|
||||
total: totalGreen,
|
||||
win: colorAccumulatedWins.green,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.green, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.green, this.green.total)
|
||||
});
|
||||
|
||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "../styles/global.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/global.scss";
|
||||
|
||||
@include mobile{
|
||||
@include mobile {
|
||||
section {
|
||||
margin-top: 5em;
|
||||
}
|
||||
@@ -182,7 +194,7 @@ export default {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&.total-bought-color{
|
||||
&.total-bought-color {
|
||||
font-weight: bold;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { chartWinsByColor } from "@/api";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
fetchWinsByColor() {
|
||||
return fetch("/api/history/by-color").then(resp => resp.json());
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
let canvas = this.$refs["win-chart"].getContext("2d");
|
||||
|
||||
let response = await chartWinsByColor();
|
||||
let response = await this.fetchWinsByColor();
|
||||
const { colors } = response;
|
||||
let labels = ["Vunnet"];
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
@@ -42,23 +46,26 @@ export default {
|
||||
data: []
|
||||
};
|
||||
|
||||
blue.data.push(response.blue.win);
|
||||
yellow.data.push(response.yellow.win);
|
||||
red.data.push(response.red.win);
|
||||
green.data.push(response.green.win);
|
||||
const findColorWinners = (colorSelect, colors) => {
|
||||
return colors.filter(color => color.color == colorSelect)[0].count;
|
||||
};
|
||||
|
||||
const blueWinCount = findColorWinners("blue", colors);
|
||||
const redWinCount = findColorWinners("red", colors);
|
||||
const greenWinCount = findColorWinners("green", colors);
|
||||
const yellowWinCount = findColorWinners("yellow", colors);
|
||||
|
||||
blue.data.push(blueWinCount);
|
||||
red.data.push(redWinCount);
|
||||
green.data.push(greenWinCount);
|
||||
yellow.data.push(yellowWinCount);
|
||||
|
||||
let highestNumber = 0;
|
||||
if (response.blue.win > highestNumber) {
|
||||
highestNumber = response.blue.win;
|
||||
}
|
||||
if (response.red.win > highestNumber) {
|
||||
highestNumber = response.red.win;
|
||||
}
|
||||
if (response.green.win > highestNumber) {
|
||||
highestNumber = response.green.win;
|
||||
}
|
||||
if (response.yellow.win > highestNumber) {
|
||||
highestNumber = response.yellow.win;
|
||||
}
|
||||
[blueWinCount, redWinCount, greenWinCount, greenWinCount].forEach(winCount => {
|
||||
if (winCount > highestNumber) {
|
||||
highestNumber = winCount;
|
||||
}
|
||||
});
|
||||
|
||||
let datasets = [blue, yellow, green, red];
|
||||
let chartdata = {
|
||||
@@ -102,8 +109,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
<div class="wine">
|
||||
<slot name="top"></slot>
|
||||
<div class="wine-image">
|
||||
<img
|
||||
v-if="wine.image && loadImage"
|
||||
:src="wine.image"
|
||||
/>
|
||||
<img v-if="wine.image && loadImage" :src="wine.image" />
|
||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||
</div>
|
||||
|
||||
@@ -38,7 +35,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loadImage: false
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setImage(entries) {
|
||||
@@ -53,7 +50,7 @@ export default {
|
||||
this.observer = new IntersectionObserver(this.setImage, {
|
||||
root: this.$el,
|
||||
threshold: 0
|
||||
})
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.observer.observe(this.$el);
|
||||
@@ -66,16 +63,17 @@ export default {
|
||||
@import "@/styles/variables";
|
||||
|
||||
.wine {
|
||||
align-self: flex-start;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
|
||||
@include tablet {
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,19 +83,18 @@ export default {
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
height: 250px;
|
||||
height: 280px;
|
||||
@include mobile {
|
||||
object-fit: cover;
|
||||
max-width: 90px;
|
||||
}
|
||||
}
|
||||
.wine-placeholder {
|
||||
height: 250px;
|
||||
height: 280px;
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.wine-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -107,7 +104,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.wine-name{
|
||||
.wine-name {
|
||||
font-size: 20px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
@@ -120,6 +117,7 @@ export default {
|
||||
.bottom-section {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
align-self: flex-end;
|
||||
|
||||
.link {
|
||||
color: $matte-text-color;
|
||||
@@ -135,4 +133,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -85,9 +85,7 @@ export default {
|
||||
this.startConfetti(this.currentName);
|
||||
return;
|
||||
}
|
||||
this.currentName = this.attendees[
|
||||
this.nameRounds % this.attendees.length
|
||||
].name;
|
||||
this.currentName = this.attendees[this.nameRounds % this.attendees.length].name;
|
||||
this.nameRounds += 1;
|
||||
clearTimeout(this.nameTimeout);
|
||||
this.nameTimeout = setTimeout(() => {
|
||||
@@ -136,8 +134,8 @@ export default {
|
||||
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
|
||||
var duration = 7 * 1000;
|
||||
var animationEnd = Date.now() + duration;
|
||||
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20};
|
||||
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35}
|
||||
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20 };
|
||||
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35 };
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
@@ -148,27 +146,27 @@ export default {
|
||||
var timeLeft = animationEnd - Date.now();
|
||||
if (timeLeft <= 0) {
|
||||
self.drawing = false;
|
||||
console.time("drawing finished")
|
||||
console.time("drawing finished");
|
||||
return clearInterval(interval);
|
||||
}
|
||||
if (currentName == "Amund Brandsrud") {
|
||||
runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135});
|
||||
runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45});
|
||||
runCannon(uberDefaults, {y: 1 }, {angle: 90});
|
||||
runCannon(uberDefaults, {x: 0 }, {angle: 45});
|
||||
runCannon(uberDefaults, {x: 1 }, {angle: 135});
|
||||
runCannon(uberDefaults, { x: 1, y: 1 }, { angle: 135 });
|
||||
runCannon(uberDefaults, { x: 0, y: 1 }, { angle: 45 });
|
||||
runCannon(uberDefaults, { y: 1 }, { angle: 90 });
|
||||
runCannon(uberDefaults, { x: 0 }, { angle: 45 });
|
||||
runCannon(uberDefaults, { x: 1 }, { angle: 135 });
|
||||
} else {
|
||||
runCannon(defaults, {x: 0 }, {angle: 45});
|
||||
runCannon(defaults, {x: 1 }, {angle: 135});
|
||||
runCannon(defaults, {y: 1 }, {angle: 90});
|
||||
runCannon(defaults, { x: 0 }, { angle: 45 });
|
||||
runCannon(defaults, { x: 1 }, { angle: 135 });
|
||||
runCannon(defaults, { y: 1 }, { angle: 90 });
|
||||
}
|
||||
}, 250);
|
||||
|
||||
function runCannon(confettiDefaultValues, originPoint, launchAngle){
|
||||
confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle))
|
||||
function runCannon(confettiDefaultValues, originPoint, launchAngle) {
|
||||
confetti(Object.assign({}, confettiDefaultValues, { origin: originPoint }, launchAngle));
|
||||
}
|
||||
},
|
||||
ordinalNumber(number=this.currentWinnerLocal.winnerCount) {
|
||||
ordinalNumber(number = this.currentWinnerLocal.winnerCount) {
|
||||
const dictonary = {
|
||||
1: "første",
|
||||
2: "andre",
|
||||
@@ -187,7 +185,6 @@ export default {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<section>
|
||||
<h2>{{ title ? title : 'Vinnere' }}</h2>
|
||||
<h2>{{ title ? title : "Vinnere" }}</h2>
|
||||
<div class="winning-raffles" v-if="winners.length > 0">
|
||||
<div v-for="(winner, index) in winners" :key="index">
|
||||
<router-link :to="`/highscore/${ encodeURIComponent(winner.name) }`">
|
||||
<router-link :to="`/highscore/${winner.name}`">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
type: Array
|
||||
},
|
||||
drawing: {
|
||||
type: Boolean,
|
||||
type: Boolean
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
|
||||
const dateString = (date) => {
|
||||
if (typeof(date) == "string") {
|
||||
const dateString = date => {
|
||||
if (typeof date == "string") {
|
||||
date = new Date(date);
|
||||
}
|
||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
||||
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||
|
||||
return `${ye}-${mo}-${da}`
|
||||
}
|
||||
return `${ye}-${mo}-${da}`;
|
||||
};
|
||||
|
||||
function humanReadableDate(date) {
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const options = { year: "numeric", month: "long", day: "numeric" };
|
||||
return new Date(date).toLocaleDateString(undefined, options);
|
||||
}
|
||||
|
||||
@@ -20,8 +19,4 @@ function daysAgo(date) {
|
||||
return Math.round(Math.abs((new Date() - new Date(date)) / day));
|
||||
}
|
||||
|
||||
export {
|
||||
dateString,
|
||||
humanReadableDate,
|
||||
daysAgo
|
||||
}
|
||||
export { dateString, humanReadableDate, daysAgo };
|
||||
|
||||
@@ -3,43 +3,47 @@ import VueRouter from "vue-router";
|
||||
import { routes } from "@/router.js";
|
||||
import Vinlottis from "@/Vinlottis";
|
||||
|
||||
import Toast from "@/plugins/Toast";
|
||||
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { Vue as VueIntegration } from "@sentry/integrations";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
// Plugins
|
||||
Vue.use(Toast);
|
||||
|
||||
const ENV = window.location.href.includes("localhost") ? "development" : "production";
|
||||
if (ENV !== "development") {
|
||||
Sentry.init({
|
||||
dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
|
||||
integrations: [
|
||||
new VueIntegration({ Vue })
|
||||
],
|
||||
integrations: [new VueIntegration({ Vue })],
|
||||
beforeSend: event => {
|
||||
console.error(event);
|
||||
return event;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Add global GA variables
|
||||
window.ga = window.ga || function(){
|
||||
window.ga.q = window.ga.q || [];
|
||||
window.ga.q.push(arguments);
|
||||
};
|
||||
window.ga =
|
||||
window.ga ||
|
||||
function() {
|
||||
window.ga.q = window.ga.q || [];
|
||||
window.ga.q.push(arguments);
|
||||
};
|
||||
ga.l = 1 * new Date();
|
||||
|
||||
// Initiate
|
||||
ga('create', __GA_TRACKINGID__, {
|
||||
'allowAnchor': false,
|
||||
'cookieExpires': __GA_COOKIELIFETIME__, // Time in seconds
|
||||
'cookieFlags': 'SameSite=Strict; Secure'
|
||||
ga("create", __GA_TRACKINGID__, {
|
||||
allowAnchor: false,
|
||||
cookieExpires: __GA_COOKIELIFETIME__, // Time in seconds
|
||||
cookieFlags: "SameSite=Strict; Secure"
|
||||
});
|
||||
ga('set', 'anonymizeIp', true); // Enable IP Anonymization/IP masking
|
||||
ga('send', 'pageview');
|
||||
ga("set", "anonymizeIp", true); // Enable IP Anonymization/IP masking
|
||||
ga("send", "pageview");
|
||||
|
||||
if (ENV == 'development')
|
||||
window[`ga-disable-${__GA_TRACKINGID__}`] = true;
|
||||
if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true;
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: routes
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --progress",
|
||||
"build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress",
|
||||
"dev": "yarn webpack serve --mode development --env development",
|
||||
"watch": "yarn webpack serve --mode development --env development",
|
||||
"start": "node server.js",
|
||||
"start-noauth": "cross-env NODE_ENV=development node server.js",
|
||||
"dev": "cross-env NODE_ENV=development node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
|
||||
40
server.js
40
server.js
@@ -18,24 +18,36 @@ const MongoStore = require("connect-mongo")(session);
|
||||
// mongoose / database
|
||||
console.log("Trying to connect with mongodb..");
|
||||
mongoose.promise = global.Promise;
|
||||
mongoose.connect("mongodb://localhost/vinlottis", {
|
||||
useCreateIndex: true,
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
serverSelectionTimeoutMS: 10000 // initial connection timeout
|
||||
}).then(_ => console.log("Mongodb connection established!"))
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
console.error("ERROR! Mongodb required to run.");
|
||||
process.exit(1);
|
||||
})
|
||||
mongoose
|
||||
.connect("mongodb://localhost/vinlottis", {
|
||||
useCreateIndex: true,
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
serverSelectionTimeoutMS: 10000 // initial connection timeout
|
||||
})
|
||||
.then(_ => console.log("Mongodb connection established!"))
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
console.error("ERROR! Mongodb required to run.");
|
||||
process.exit(1);
|
||||
});
|
||||
mongoose.set("debug", false);
|
||||
|
||||
// middleware
|
||||
const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS"));
|
||||
const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders"));
|
||||
app.use(setupCORS)
|
||||
app.use(setupHeaders)
|
||||
app.use(setupCORS);
|
||||
app.use(setupHeaders);
|
||||
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
console.info(`NODE_ENV=development set, your are now always an authenticated user.`);
|
||||
const alwaysAuthenticatedWhenLocalhost = require(path.join(
|
||||
__dirname,
|
||||
"/api/middleware/alwaysAuthenticatedWhenLocalhost"
|
||||
));
|
||||
|
||||
app.use(alwaysAuthenticatedWhenLocalhost);
|
||||
}
|
||||
|
||||
// parse application/json
|
||||
app.use(express.json());
|
||||
@@ -52,7 +64,7 @@ app.use(
|
||||
})
|
||||
);
|
||||
|
||||
app.set('socketio', io); // set io instance to key "socketio"
|
||||
app.set("socketio", io); // set io instance to key "socketio"
|
||||
|
||||
const passport = require("passport");
|
||||
const LocalStrategy = require("passport-local");
|
||||
|
||||
Reference in New Issue
Block a user