Feat/controllers - refactor entire backend and new admin interface #75
81
api/attendee.js
Normal file
81
api/attendee.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||||
|
const { UserNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||||
|
|
||||||
|
const redactAttendeeInfoMapper = attendee => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||||
|
red: attendee.red,
|
||||||
|
blue: attendee.blue,
|
||||||
|
green: attendee.green,
|
||||||
|
yellow: attendee.yellow
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const allAttendees = (isAdmin = false) => {
|
||||||
|
if (!isAdmin) {
|
||||||
|
return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper));
|
||||||
|
} else {
|
||||||
|
return Attendee.find();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAttendee = attendee => {
|
||||||
|
const { name, red, blue, green, yellow, phoneNumber } = attendee;
|
||||||
|
|
||||||
|
let newAttendee = new Attendee({
|
||||||
|
name,
|
||||||
|
red,
|
||||||
|
blue,
|
||||||
|
green,
|
||||||
|
yellow,
|
||||||
|
phoneNumber,
|
||||||
|
winner: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return newAttendee.save().then(_ => newAttendee);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAttendeeById = (id, updateModel) => {
|
||||||
|
return Attendee.findOne({ _id: id }).then(attendee => {
|
||||||
|
if (attendee == null) {
|
||||||
|
throw new UserNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAttendee = {
|
||||||
|
name: updateModel.name != null ? updateModel.name : attendee.name,
|
||||||
|
green: updateModel.green != null ? updateModel.green : attendee.green,
|
||||||
|
red: updateModel.red != null ? updateModel.red : attendee.red,
|
||||||
|
blue: updateModel.blue != null ? updateModel.blue : attendee.blue,
|
||||||
|
yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow,
|
||||||
|
phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber,
|
||||||
|
winner: updateModel.winner != null ? updateModel.winner : attendee.winner
|
||||||
|
};
|
||||||
|
|
||||||
|
return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAttendeeById = id => {
|
||||||
|
return Attendee.findOne({ _id: id }).then(attendee => {
|
||||||
|
if (attendee == null) {
|
||||||
|
throw new UserNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Attendee.deleteOne({ _id: id }).then(_ => attendee);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAttendees = () => {
|
||||||
|
return Attendee.deleteMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allAttendees,
|
||||||
|
addAttendee,
|
||||||
|
updateAttendeeById,
|
||||||
|
deleteAttendeeById,
|
||||||
|
deleteAttendees
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
|
const { history, clearHistory } = require(path.join(__dirname + "/../redis"));
|
||||||
|
console.log("loading chat");
|
||||||
|
|
||||||
const getAllHistory = (req, res) => {
|
const getAllHistory = (req, res) => {
|
||||||
let { page, limit } = req.query;
|
let { page, limit } = req.query;
|
||||||
@@ -8,19 +9,23 @@ const getAllHistory = (req, res) => {
|
|||||||
|
|
||||||
return history(page, limit)
|
return history(page, limit)
|
||||||
.then(messages => res.json(messages))
|
.then(messages => res.json(messages))
|
||||||
.catch(error => res.status(500).json({
|
.catch(error =>
|
||||||
message: error.message,
|
res.status(500).json({
|
||||||
success: false
|
message: error.message,
|
||||||
}));
|
success: false
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteHistory = (req, res) => {
|
const deleteHistory = (req, res) => {
|
||||||
return clearHistory()
|
return clearHistory()
|
||||||
.then(message => res.json(message))
|
.then(message => res.json(message))
|
||||||
.catch(error => res.status(500).json({
|
.catch(error =>
|
||||||
message: error.message,
|
res.status(500).json({
|
||||||
success: false
|
message: error.message,
|
||||||
}));
|
success: false
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
261
api/controllers/historyController.js
Normal file
261
api/controllers/historyController.js
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const historyRepository = require(path.join(__dirname, "../history"));
|
||||||
|
|
||||||
|
const sortOptions = ["desc", "asc"];
|
||||||
|
const includeWinesOptions = ["true", "false"];
|
||||||
|
|
||||||
|
const all = (req, res) => {
|
||||||
|
const { sort, includeWines } = req.query;
|
||||||
|
|
||||||
|
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyRepository
|
||||||
|
.all(includeWines == "true")
|
||||||
|
.then(winners =>
|
||||||
|
res.send({
|
||||||
|
winners: sort !== "asc" ? winners : winners.reverse(),
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winners."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const byDate = (req, res) => {
|
||||||
|
let { date } = req.params;
|
||||||
|
|
||||||
|
const regexDate = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
|
||||||
|
if (!isNaN(date)) {
|
||||||
|
date = new Date(new Date(parseInt(date * 1000)).setHours(0, 0, 0, 0));
|
||||||
|
} else if (regexDate.test(date)) {
|
||||||
|
date = new Date(date);
|
||||||
|
} else if (date !== undefined) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Invalid date parameter, allowed epoch seconds or YYYY-MM-DD.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyRepository
|
||||||
|
.byDate(date)
|
||||||
|
.then(winners =>
|
||||||
|
res.send({
|
||||||
|
date: date,
|
||||||
|
winners: winners,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winner by date."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupByDate = (req, res) => {
|
||||||
|
const { sort, includeWines } = req.query;
|
||||||
|
|
||||||
|
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyRepository
|
||||||
|
.groupByDate(includeWines == "true", sort)
|
||||||
|
.then(lotteries =>
|
||||||
|
res.send({
|
||||||
|
lotteries: lotteries,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winner by date."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const latest = (req, res) => {
|
||||||
|
return historyRepository
|
||||||
|
.latest()
|
||||||
|
.then(winners =>
|
||||||
|
res.send({
|
||||||
|
...winners,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winner by date."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const byName = (req, res) => {
|
||||||
|
const { name } = req.params;
|
||||||
|
const { sort } = req.query;
|
||||||
|
|
||||||
|
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyRepository
|
||||||
|
.byName(name, sort)
|
||||||
|
.then(winner =>
|
||||||
|
res.send({
|
||||||
|
winner: winner,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winner by name."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = (req, res) => {
|
||||||
|
const { name, sort } = req.query;
|
||||||
|
|
||||||
|
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyRepository
|
||||||
|
.search(name, sort)
|
||||||
|
.then(winners =>
|
||||||
|
res.send({
|
||||||
|
winners: winners || [],
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winner by name."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupByColor = (req, res) => {
|
||||||
|
const { includeWines } = req.query;
|
||||||
|
|
||||||
|
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyRepository
|
||||||
|
.groupByColor(includeWines == "true")
|
||||||
|
.then(colors =>
|
||||||
|
res.send({
|
||||||
|
colors: colors,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winners by color."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderByWins = (req, res) => {
|
||||||
|
let { includeWines, limit } = req.query;
|
||||||
|
|
||||||
|
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit && isNaN(limit)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "If limit query parameter is provided it must be a number",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
} else if (!!!isNaN(limit)) {
|
||||||
|
limit = Number(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyRepository
|
||||||
|
.orderByWins(includeWines == "true", limit)
|
||||||
|
.then(winners =>
|
||||||
|
res.send({
|
||||||
|
winners: winners,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch winners by color."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
all,
|
||||||
|
byDate,
|
||||||
|
groupByDate,
|
||||||
|
latest,
|
||||||
|
byName,
|
||||||
|
search,
|
||||||
|
groupByColor,
|
||||||
|
orderByWins
|
||||||
|
};
|
||||||
135
api/controllers/lotteryAttendeeController.js
Normal file
135
api/controllers/lotteryAttendeeController.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const attendeeRepository = require(path.join(__dirname, "../attendee"));
|
||||||
|
|
||||||
|
const allAttendees = (req, res) => {
|
||||||
|
const isAdmin = req.isAuthenticated();
|
||||||
|
|
||||||
|
return attendeeRepository
|
||||||
|
.allAttendees(isAdmin)
|
||||||
|
.then(attendees =>
|
||||||
|
res.send({
|
||||||
|
attendees: attendees,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch lottery attendees."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAttendee = (req, res) => {
|
||||||
|
const { attendee } = req.body;
|
||||||
|
|
||||||
|
const requiredColors = [attendee["red"], attendee["blue"], attendee["green"], attendee["yellow"]];
|
||||||
|
const correctColorsTypes = requiredColors.filter(color => typeof color === "number");
|
||||||
|
if (requiredColors.length !== correctColorsTypes.length) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Incorrect or missing color, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof attendee["name"] !== "string" || typeof attendee["phoneNumber"] !== "number") {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Incorrect or missing attendee keys 'name' or 'phoneNumber'.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attendeeRepository
|
||||||
|
.addAttendee(attendee)
|
||||||
|
.then(savedAttendee => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("new_attendee", {});
|
||||||
|
return savedAttendee;
|
||||||
|
})
|
||||||
|
.then(savedAttendee =>
|
||||||
|
res.send({
|
||||||
|
attendee: savedAttendee,
|
||||||
|
message: `Successfully added attendee ${attendee.name} to lottery.`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAttendeeById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { attendee } = req.body;
|
||||||
|
|
||||||
|
return attendeeRepository
|
||||||
|
.updateAttendeeById(id, attendee)
|
||||||
|
.then(updatedAttendee => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
return updatedAttendee;
|
||||||
|
})
|
||||||
|
.then(attendee =>
|
||||||
|
res.send({
|
||||||
|
attendee,
|
||||||
|
message: `Updated attendee: ${attendee.name}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while deleteing attendee by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAttendeeById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return attendeeRepository
|
||||||
|
.deleteAttendeeById(id)
|
||||||
|
.then(removedAttendee => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
return removedAttendee;
|
||||||
|
})
|
||||||
|
.then(attendee =>
|
||||||
|
res.send({
|
||||||
|
message: `Removed attendee: ${attendee.name}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while deleteing attendee by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAttendees = (req, res) => {
|
||||||
|
return attendeeRepository
|
||||||
|
.deleteAttendees()
|
||||||
|
.then(removedAttendee => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
})
|
||||||
|
.then(_ =>
|
||||||
|
res.send({
|
||||||
|
message: "Removed all attendees",
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allAttendees,
|
||||||
|
addAttendee,
|
||||||
|
updateAttendeeById,
|
||||||
|
deleteAttendeeById,
|
||||||
|
deleteAttendees
|
||||||
|
};
|
||||||
192
api/controllers/lotteryController.js
Normal file
192
api/controllers/lotteryController.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const lotteryRepository = require(path.join(__dirname, "../lottery"));
|
||||||
|
|
||||||
|
const drawWinner = (req, res) => {
|
||||||
|
return lotteryRepository
|
||||||
|
.drawWinner()
|
||||||
|
.then(({ winner, color, winners }) => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("winner", {
|
||||||
|
color: color,
|
||||||
|
name: winner.name,
|
||||||
|
winner_count: winners.length + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return { winner, color, winners };
|
||||||
|
})
|
||||||
|
.then(({ winner, color, winners }) =>
|
||||||
|
res.send({
|
||||||
|
color: color,
|
||||||
|
winner: winner,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while drawing winner.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const archiveLottery = (req, res) => {
|
||||||
|
const { lottery } = req.body;
|
||||||
|
if (lottery == undefined || !lottery instanceof Object) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Missing lottery object.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let { stolen, date, raffles, wines } = lottery;
|
||||||
|
stolen = stolen !== undefined ? stolen : 0; // default = 0
|
||||||
|
|
||||||
|
const validDateFormat = new RegExp("d{4}-d{2}-d{2}");
|
||||||
|
if (date != undefined && (!validDateFormat.test(date) || isNaN(date))) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Date must be defined as 'yyyy-mm-dd'.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
} else if (date != undefined) {
|
||||||
|
date = Date.parse(date, "yyyy-MM-dd");
|
||||||
|
} else {
|
||||||
|
date = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyLotteryPayload(raffles, stolen, wines)
|
||||||
|
.then(_ => lotteryRepository.archive(date, raffles, stolen, wines))
|
||||||
|
.then(_ =>
|
||||||
|
res.send({
|
||||||
|
message: "Successfully archive lottery",
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while submitting lottery.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const lotteryByDate = (req, res) => {
|
||||||
|
const { epoch } = req.params;
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(epoch)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Last parameter must be epoch (in seconds).",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const date = new Date(Number(epoch) * 1000);
|
||||||
|
|
||||||
|
return lotteryRepository
|
||||||
|
.lotteryByDate(date)
|
||||||
|
.then(lottery =>
|
||||||
|
res.send({
|
||||||
|
lottery,
|
||||||
|
message: `Lottery for date: ${dateToDateString(date)}/${epoch}.`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while fetching lottery by date.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortOptions = ["desc", "asc"];
|
||||||
|
const allLotteries = (req, res) => {
|
||||||
|
let { includeWinners, year, sort } = req.query;
|
||||||
|
|
||||||
|
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
} else if (sort === undefined) {
|
||||||
|
sort = "asc";
|
||||||
|
}
|
||||||
|
|
||||||
|
let allLotteriesFunction = lotteryRepository.allLotteries;
|
||||||
|
if (includeWinners === "true") {
|
||||||
|
allLotteriesFunction = lotteryRepository.allLotteriesIncludingWinners;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allLotteriesFunction(sort, year)
|
||||||
|
.then(lotteries =>
|
||||||
|
res.send({
|
||||||
|
lotteries,
|
||||||
|
message: "All lotteries.",
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while fetching all lotteries.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function verifyLotteryPayload(raffles, stolen, wines) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (raffles == undefined || !raffles instanceof Array) {
|
||||||
|
reject({
|
||||||
|
message: "Raffles must be array.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredColors = [raffles["red"], raffles["blue"], raffles["green"], raffles["yellow"]];
|
||||||
|
const correctColorsTypes = requiredColors.filter(color => typeof color === "number");
|
||||||
|
if (requiredColors.length !== correctColorsTypes.length) {
|
||||||
|
reject({
|
||||||
|
message:
|
||||||
|
"Incorrect or missing raffle colors, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stolen == undefined || (isNaN(stolen) && stolen >= 0)) {
|
||||||
|
reject({
|
||||||
|
message: "Number of stolen raffles must be positive integer or 0.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wines == undefined || !wines instanceof Array) {
|
||||||
|
reject({
|
||||||
|
message: "Wines must be array.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateToDateString(date) {
|
||||||
|
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||||
|
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||||
|
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||||
|
|
||||||
|
return `${ye}-${mo}-${da}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
drawWinner,
|
||||||
|
archiveLottery,
|
||||||
|
lotteryByDate,
|
||||||
|
allLotteries
|
||||||
|
};
|
||||||
207
api/controllers/lotteryWineController.js
Normal file
207
api/controllers/lotteryWineController.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine"));
|
||||||
|
|
||||||
|
const allWines = (req, res) => {
|
||||||
|
return prelotteryWineRepository
|
||||||
|
.allWines()
|
||||||
|
.then(wines =>
|
||||||
|
res.send({
|
||||||
|
wines: wines,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch lottery wines."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWines = (req, res) => {
|
||||||
|
let { wines } = req.body;
|
||||||
|
|
||||||
|
if (!(wines instanceof Array)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Wines must be array.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAllWines = wines =>
|
||||||
|
wines.map(wine => {
|
||||||
|
const requiredAttributes = ["name", "vivinoLink", "image", "id", "price"];
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
requiredAttributes.map(attr => {
|
||||||
|
if (typeof wine[attr] === "undefined" || wine[attr] == "") {
|
||||||
|
return Promise.reject({
|
||||||
|
message: `Incorrect or missing attribute: ${attr}.`,
|
||||||
|
statusCode: 400,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
).then(_ => Promise.resolve(wine));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(validateAllWines(wines))
|
||||||
|
.then(wines => prelotteryWineRepository.addWines(wines))
|
||||||
|
.then(savedWines => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("new_wine", {});
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.then(success =>
|
||||||
|
res.send({
|
||||||
|
message: `Successfully added wines to lottery.`,
|
||||||
|
success: success
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured adding wines.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const wineById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return prelotteryWineRepository
|
||||||
|
.wineById(id)
|
||||||
|
.then(wine =>
|
||||||
|
res.send({
|
||||||
|
wine,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while fetching wine by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateWineById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { wine } = req.body;
|
||||||
|
|
||||||
|
if (id == null || id == "undefined") {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Unable to update without id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return prelotteryWineRepository
|
||||||
|
.updateWineById(id, wine)
|
||||||
|
.then(updatedWine => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
return updatedWine;
|
||||||
|
})
|
||||||
|
.then(wine =>
|
||||||
|
res.send({
|
||||||
|
wine,
|
||||||
|
message: `Updated wine: ${wine.name}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while deleteing wine by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWineById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return prelotteryWineRepository
|
||||||
|
.deleteWineById(id)
|
||||||
|
.then(removedWine => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
return removedWine;
|
||||||
|
})
|
||||||
|
.then(wine =>
|
||||||
|
res.send({
|
||||||
|
message: `Removed wine: ${wine.name}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while deleteing wine by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWines = (req, res) => {
|
||||||
|
return prelotteryWineRepository
|
||||||
|
.deleteWines()
|
||||||
|
.then(_ => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
})
|
||||||
|
.then(_ =>
|
||||||
|
res.send({
|
||||||
|
message: "Removed all wines.",
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while deleting wines",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const wineSchema = (req, res) => {
|
||||||
|
return prelotteryWineRepository
|
||||||
|
.wineSchema()
|
||||||
|
.then(schema =>
|
||||||
|
res.send({
|
||||||
|
schema: schema,
|
||||||
|
message: `Wine schema template.`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch wine schema template."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allWines,
|
||||||
|
addWines,
|
||||||
|
wineById,
|
||||||
|
updateWineById,
|
||||||
|
deleteWineById,
|
||||||
|
deleteWines,
|
||||||
|
wineSchema
|
||||||
|
};
|
||||||
195
api/controllers/lotteryWinnerController.js
Normal file
195
api/controllers/lotteryWinnerController.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const winnerRepository = require(path.join(__dirname, "../winner"));
|
||||||
|
const { WinnerNotFound } = require(path.join(__dirname, "../vinlottisErrors"));
|
||||||
|
const prizeDistributionRepository = require(path.join(__dirname, "../prizeDistribution"));
|
||||||
|
|
||||||
|
// should not be used, is done through POST /lottery/prize-distribution/prize/:id - claimPrize.
|
||||||
|
const addWinners = (req, res) => {
|
||||||
|
const { winners } = req.body;
|
||||||
|
|
||||||
|
if (!(winners instanceof Array)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Winners must be array.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredAttributes = ["name", "color", "wine"];
|
||||||
|
const validColors = ["red", "blue", "green", "yellow"];
|
||||||
|
const validateAllWinners = winners =>
|
||||||
|
winners.map(winner => {
|
||||||
|
return Promise.all(
|
||||||
|
requiredAttributes.map(attr => {
|
||||||
|
if (typeof winner[attr] === "undefined") {
|
||||||
|
return Promise.reject({
|
||||||
|
message: `Incorrect or missing attribute: ${attr}.`,
|
||||||
|
statusCode: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validColors.includes(winner.color)) {
|
||||||
|
return Promise.reject({
|
||||||
|
message: `Missing or incorrect color value, must have one of values: ${validColors.join(", ")}.`,
|
||||||
|
statusCode: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
).then(_ => Promise.resolve(winner));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(validateAllWinners(winners))
|
||||||
|
.then(winners =>
|
||||||
|
winners.map(winner => {
|
||||||
|
return prizeDistributionRepository.claimPrize(winner, winner.wine);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(winners =>
|
||||||
|
res.send({
|
||||||
|
winners: winners,
|
||||||
|
message: `Successfully added winners to lottery.`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured adding winners.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const allWinners = (req, res) => {
|
||||||
|
const isAdmin = req.isAuthenticated();
|
||||||
|
|
||||||
|
return winnerRepository
|
||||||
|
.allWinners(isAdmin)
|
||||||
|
.then(winners =>
|
||||||
|
res.send({
|
||||||
|
winners: winners,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch lottery winners."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const winnerById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const isAdmin = req.isAuthenticated();
|
||||||
|
|
||||||
|
return winnerRepository
|
||||||
|
.winnerById(id, isAdmin)
|
||||||
|
.then(winner =>
|
||||||
|
res.send({
|
||||||
|
winner,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured, unable to fetch winner by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateWinnerById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { winner } = req.body;
|
||||||
|
|
||||||
|
if (id == null || id == "undefined") {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Unable to update without id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return winnerRepository
|
||||||
|
.updateWinnerById(id, winner)
|
||||||
|
.then(winner =>
|
||||||
|
res.send({
|
||||||
|
winner,
|
||||||
|
message: `Updated winner: ${winner.name}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while updating winner by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWinnerById = (req, res) => {
|
||||||
|
const isAdmin = req.isAuthenticated();
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return winnerRepository
|
||||||
|
.deleteWinnerById(id, isAdmin)
|
||||||
|
.then(removedWinner => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
return removedWinner;
|
||||||
|
})
|
||||||
|
.then(winner =>
|
||||||
|
res.send({
|
||||||
|
message: `Removed winner: ${winner.name}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while deleteing wine by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWinners = (req, res) => {
|
||||||
|
return winnerRepository
|
||||||
|
.deleteWinners()
|
||||||
|
.then(_ => {
|
||||||
|
var io = req.app.get("socketio");
|
||||||
|
io.emit("refresh_data", {});
|
||||||
|
})
|
||||||
|
.then(_ =>
|
||||||
|
res.send({
|
||||||
|
message: "Removed all winners.",
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while deleting wines",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addWinners,
|
||||||
|
allWinners,
|
||||||
|
winnerById,
|
||||||
|
updateWinnerById,
|
||||||
|
deleteWinnerById,
|
||||||
|
deleteWinners
|
||||||
|
};
|
||||||
30
api/controllers/messageController.js
Normal file
30
api/controllers/messageController.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const messageRepository = require(path.join(__dirname, "../message"));
|
||||||
|
const winnerRepository = require(path.join(__dirname, "../winner"));
|
||||||
|
|
||||||
|
const notifyWinnerById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const isAdmin = req.isAuthenticated();
|
||||||
|
|
||||||
|
return winnerRepository
|
||||||
|
.winnerById(id, isAdmin)
|
||||||
|
.then(winner => messageRepository.sendPrizeSelectionLink(winner))
|
||||||
|
.then(messageResponse =>
|
||||||
|
res.send({
|
||||||
|
messageResponse,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while sending message to winner by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notifyWinnerById
|
||||||
|
};
|
||||||
104
api/controllers/prizeDistributionController.js
Normal file
104
api/controllers/prizeDistributionController.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const prizeDistribution = require(path.join(__dirname, "../prizeDistribution"));
|
||||||
|
const prelotteryWineRepository = require(path.join(__dirname, "../prelotteryWine"));
|
||||||
|
const winnerRepository = require(path.join(__dirname, "../winner"));
|
||||||
|
const message = require(path.join(__dirname, "../message"));
|
||||||
|
|
||||||
|
const start = async (req, res) => {
|
||||||
|
const allWinners = await winnerRepository.allWinners(true);
|
||||||
|
if (allWinners.length === 0) {
|
||||||
|
return res.status(503).send({
|
||||||
|
message: "No winners found to distribute prizes to.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const laterWinners = allWinners.slice(1);
|
||||||
|
|
||||||
|
return prizeDistribution
|
||||||
|
.notifyNextWinner()
|
||||||
|
.then(_ => message.sendInitialMessageToWinners(laterWinners))
|
||||||
|
.then(_ =>
|
||||||
|
res.send({
|
||||||
|
message: `Send link to first winner and notified everyone else.`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while starting prize distribution.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrizesForWinnerById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return prizeDistribution
|
||||||
|
.verifyWinnerNextInLine(id)
|
||||||
|
.then(winner => {
|
||||||
|
return prelotteryWineRepository.allWinesWithoutWinner().then(wines => [wines, winner]);
|
||||||
|
})
|
||||||
|
.then(([wines, winner]) =>
|
||||||
|
res.send({
|
||||||
|
wines: wines,
|
||||||
|
winner: winner,
|
||||||
|
message: "Wines to select from",
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while fetching prizes.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPrizeForWinnerById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { wine } = req.body;
|
||||||
|
|
||||||
|
let prelotteryWine, winner;
|
||||||
|
try {
|
||||||
|
prelotteryWine = await prelotteryWineRepository.wineById(wine._id);
|
||||||
|
winner = await winnerRepository.winnerById(id, true);
|
||||||
|
} catch (error) {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while claiming prize.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return prizeDistribution
|
||||||
|
.claimPrize(prelotteryWine, winner)
|
||||||
|
.then(_ => prizeDistribution.notifyNextWinner())
|
||||||
|
.then(_ =>
|
||||||
|
res.send({
|
||||||
|
message: `${winner.name} successfully claimed prize: ${prelotteryWine.name}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while claiming prize.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
start,
|
||||||
|
getPrizesForWinnerById,
|
||||||
|
submitPrizeForWinnerById
|
||||||
|
};
|
||||||
104
api/controllers/requestController.js
Normal file
104
api/controllers/requestController.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const requestRepository = require(path.join(__dirname, "../request"));
|
||||||
|
|
||||||
|
function addRequest(req, res) {
|
||||||
|
const { wine } = req.body;
|
||||||
|
|
||||||
|
return verifyWineValues(wine)
|
||||||
|
.then(_ => requestRepository.addNew(wine))
|
||||||
|
.then(wine =>
|
||||||
|
res.json({
|
||||||
|
message: "Successfully added new request",
|
||||||
|
wine: wine,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { message, statusCode } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to add requested wine."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function allRequests(req, res) {
|
||||||
|
return requestRepository
|
||||||
|
.getAll()
|
||||||
|
.then(wines =>
|
||||||
|
res.json({
|
||||||
|
wines: wines,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { message, statusCode } = error;
|
||||||
|
return res.status(statusCode || 500).json({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch all requested wines."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRequest(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return requestRepository
|
||||||
|
.deleteById(id)
|
||||||
|
.then(_ =>
|
||||||
|
res.json({
|
||||||
|
message: `Slettet vin med id: ${id}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to delete requested wine."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyWineValues(wine) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (wine == undefined) {
|
||||||
|
reject({
|
||||||
|
message: "No wine object found in request body.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wine.id == null) {
|
||||||
|
reject({
|
||||||
|
message: "Wine object missing value id.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
} else if (wine.name == null) {
|
||||||
|
reject({
|
||||||
|
message: "Wine object missing value name.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
} else if (wine.vivinoLink == null) {
|
||||||
|
reject({
|
||||||
|
message: "Wine object missing value vivinoLink.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
} else if (wine.image == null) {
|
||||||
|
reject({
|
||||||
|
message: "Wine object missing value image.",
|
||||||
|
status: 400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addRequest,
|
||||||
|
allRequests,
|
||||||
|
deleteRequest
|
||||||
|
};
|
||||||
55
api/controllers/userController.js
Normal file
55
api/controllers/userController.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const userRepository = require(path.join(__dirname, "../user"));
|
||||||
|
|
||||||
|
function register(req, res, next) {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
return userRepository
|
||||||
|
.register(username, password)
|
||||||
|
.then(user => userRepository.login(req, user))
|
||||||
|
.then(_ =>
|
||||||
|
res.send({
|
||||||
|
messsage: `Bruker registrert. Velkommen ${username}`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unable to sign in with given username and passowrd",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = (req, res, next) => {
|
||||||
|
return userRepository
|
||||||
|
.authenticate(req)
|
||||||
|
.then(user => userRepository.login(req, user))
|
||||||
|
.then(user => {
|
||||||
|
res.send({
|
||||||
|
message: `Velkommen ${user.username}`,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unable to sign in with given username and passowrd",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = (req, res) => {
|
||||||
|
req.logout();
|
||||||
|
res.redirect("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register,
|
||||||
|
login,
|
||||||
|
logout
|
||||||
|
};
|
||||||
85
api/controllers/vinmonopoletController.js
Normal file
85
api/controllers/vinmonopoletController.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const vinmonopoletRepository = require(path.join(__dirname, "../vinmonopolet"));
|
||||||
|
|
||||||
|
function searchWines(req, res) {
|
||||||
|
const { name, page } = req.query;
|
||||||
|
|
||||||
|
return vinmonopoletRepository.searchWinesByName(name, page).then(wines =>
|
||||||
|
res.json({
|
||||||
|
wines: wines,
|
||||||
|
count: wines.length,
|
||||||
|
page: page,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wineByEAN(req, res) {
|
||||||
|
const { ean } = req.params;
|
||||||
|
|
||||||
|
return vinmonopoletRepository.searchByEAN(ean).then(wines =>
|
||||||
|
res.json({
|
||||||
|
wines: wines,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wineById(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return vinmonopoletRepository.wineById(id).then(wines =>
|
||||||
|
res.json({
|
||||||
|
wine: wines[0],
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function allStores(req, res) {
|
||||||
|
return vinmonopoletRepository
|
||||||
|
.allStores()
|
||||||
|
.then(stores =>
|
||||||
|
res.send({
|
||||||
|
stores,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while fetch all vinmonopolet stores.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchStores(req, res) {
|
||||||
|
const { name } = req.query;
|
||||||
|
|
||||||
|
return vinmonopoletRepository
|
||||||
|
.searchStoresByName(name)
|
||||||
|
.then(stores =>
|
||||||
|
res.send({
|
||||||
|
stores,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while fetch all vinmonopolet stores.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
searchWines,
|
||||||
|
wineByEAN,
|
||||||
|
wineById,
|
||||||
|
allStores,
|
||||||
|
searchStores
|
||||||
|
};
|
||||||
60
api/controllers/wineController.js
Normal file
60
api/controllers/wineController.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const wineRepository = require(path.join(__dirname, "../wine"));
|
||||||
|
|
||||||
|
const allWines = (req, res) => {
|
||||||
|
// TODO add "includeWinners"
|
||||||
|
let { limit } = req.query;
|
||||||
|
|
||||||
|
if (limit && isNaN(limit)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "If limit query parameter is provided it must be a number",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
} else if (!!!isNaN(limit)) {
|
||||||
|
limit = Number(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wineRepository
|
||||||
|
.allWines(limit)
|
||||||
|
.then(wines =>
|
||||||
|
res.send({
|
||||||
|
wines: wines,
|
||||||
|
message: `All wines.`,
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
success: false,
|
||||||
|
message: message || "Unable to fetch all wines."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const wineById = (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
return wineRepository
|
||||||
|
.wineById(id)
|
||||||
|
.then(wine => {
|
||||||
|
res.send({
|
||||||
|
wine,
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const { statusCode, message } = error;
|
||||||
|
|
||||||
|
return res.status(statusCode || 500).send({
|
||||||
|
message: message || "Unexpected error occured while fetching wine by id.",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allWines,
|
||||||
|
wineById
|
||||||
|
};
|
||||||
349
api/history.js
Normal file
349
api/history.js
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const Winner = require(path.join(__dirname, "/schemas/Highscore"));
|
||||||
|
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||||
|
|
||||||
|
class HistoryByDateNotFound extends Error {
|
||||||
|
constructor(message = "History for given date not found.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "HistoryByDateNotFound";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryForUserNotFound extends Error {
|
||||||
|
constructor(message = "History for given user not found.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "HistoryForUserNotFound";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// highscore
|
||||||
|
const addWinnerWithWine = async (winner, wine) => {
|
||||||
|
const exisitingWinner = await Winner.findOne({
|
||||||
|
name: winner.name
|
||||||
|
});
|
||||||
|
const savedWine = await wineRepository.addWine(wine);
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
date.setHours(5, 0, 0, 0);
|
||||||
|
const winObject = {
|
||||||
|
date: date,
|
||||||
|
wine: savedWine,
|
||||||
|
color: winner.color
|
||||||
|
};
|
||||||
|
|
||||||
|
if (exisitingWinner == undefined) {
|
||||||
|
const newWinner = new Winner({
|
||||||
|
name: winner.name,
|
||||||
|
wins: [winObject]
|
||||||
|
});
|
||||||
|
|
||||||
|
await newWinner.save();
|
||||||
|
} else {
|
||||||
|
exisitingWinner.wins.push(winObject);
|
||||||
|
exisitingWinner.markModified("wins");
|
||||||
|
await exisitingWinner.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return exisitingWinner;
|
||||||
|
};
|
||||||
|
|
||||||
|
// lottery
|
||||||
|
const all = (includeWines = false) => {
|
||||||
|
if (includeWines === false) {
|
||||||
|
return Winner.find().sort("-wins.date");
|
||||||
|
} else {
|
||||||
|
return Winner.find()
|
||||||
|
.sort("-wins.date")
|
||||||
|
.populate("wins.wine");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// lottery
|
||||||
|
const byDate = date => {
|
||||||
|
const startQueryDate = new Date(date.setHours(0, 0, 0, 0));
|
||||||
|
const endQueryDate = new Date(date.setHours(24, 59, 59, 99));
|
||||||
|
const query = [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"wins.date": {
|
||||||
|
$gte: startQueryDate,
|
||||||
|
$lte: endQueryDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ $unwind: "$wins" },
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"wins.date": {
|
||||||
|
$gte: startQueryDate,
|
||||||
|
$lte: endQueryDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "wines",
|
||||||
|
localField: "wins.wine",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "wins.wine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ $unwind: "$wins.wine" },
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
name: "$name",
|
||||||
|
date: "$wins.date",
|
||||||
|
color: "$wins.color",
|
||||||
|
wine: "$wins.wine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return Winner.aggregate(query).then(winners => {
|
||||||
|
if (winners.length == 0) {
|
||||||
|
throw new HistoryByDateNotFound();
|
||||||
|
}
|
||||||
|
return winners;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// highscore
|
||||||
|
const byName = (name, sort = "desc") => {
|
||||||
|
return Winner.findOne({ name }, ["name", "wins"])
|
||||||
|
.sort("-wins.date")
|
||||||
|
.populate("wins.wine")
|
||||||
|
.then(winner => {
|
||||||
|
if (winner) {
|
||||||
|
winner.wins = sort !== "asc" ? winner.wins.reverse() : winner.wins;
|
||||||
|
return winner;
|
||||||
|
} else {
|
||||||
|
throw new HistoryForUserNotFound();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// highscore
|
||||||
|
const search = (query, sort = "desc") => {
|
||||||
|
return Winner.find({ name: { $regex: query, $options: "i" } }, ["name"]).then(winners => {
|
||||||
|
if (winners) {
|
||||||
|
winners = sort === "desc" ? winners.reverse() : winners;
|
||||||
|
return winners;
|
||||||
|
} else {
|
||||||
|
throw new HistoryForUserNotFound();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// lottery
|
||||||
|
const latest = () => {
|
||||||
|
const query = [
|
||||||
|
{
|
||||||
|
$unwind: "$wins"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "wines",
|
||||||
|
localField: "wins.wine",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "wins.wine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$wins.date",
|
||||||
|
winners: {
|
||||||
|
$push: {
|
||||||
|
_id: "$_id",
|
||||||
|
name: "$name",
|
||||||
|
color: "$wins.color",
|
||||||
|
wine: "$wins.wine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
date: "$_id",
|
||||||
|
winners: "$winners"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
_id: -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$limit: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return Winner.aggregate(query).then(winners => winners[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// lottery - byDate
|
||||||
|
const groupByDate = (includeWines = false, sort = "asc") => {
|
||||||
|
const sortDirection = sort == "asc" ? -1 : 1;
|
||||||
|
const query = [
|
||||||
|
{
|
||||||
|
$unwind: "$wins"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$wins.date",
|
||||||
|
winners: {
|
||||||
|
$push: {
|
||||||
|
_id: "$_id",
|
||||||
|
name: "$name",
|
||||||
|
color: "$wins.color",
|
||||||
|
wine: "$wins.wine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
date: "$_id",
|
||||||
|
winners: "$winners"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
date: sortDirection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (includeWines) {
|
||||||
|
query.splice(1, 0, {
|
||||||
|
$lookup: {
|
||||||
|
from: "wines",
|
||||||
|
localField: "wins.wine",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "wins.wine"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Winner.aggregate(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
// highscore - byColor
|
||||||
|
const groupByColor = (includeWines = false) => {
|
||||||
|
const query = [
|
||||||
|
{
|
||||||
|
$unwind: "$wins"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$wins.color",
|
||||||
|
winners: {
|
||||||
|
$push: {
|
||||||
|
_id: "$_id",
|
||||||
|
name: "$name",
|
||||||
|
date: "$wins.date",
|
||||||
|
wine: "$wins.wine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
count: { $sum: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
color: "$_id",
|
||||||
|
count: "$count",
|
||||||
|
winners: "$winners"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
_id: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (includeWines) {
|
||||||
|
query.splice(1, 0, {
|
||||||
|
$lookup: {
|
||||||
|
from: "wines",
|
||||||
|
localField: "wins.wine",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "wins.wine"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Winner.aggregate(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
// highscore - byWineOccurences
|
||||||
|
|
||||||
|
// highscore - byWinCount
|
||||||
|
const orderByWins = (includeWines = false, limit = undefined) => {
|
||||||
|
let query = [
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
name: "$name",
|
||||||
|
wins: "$wins",
|
||||||
|
totalWins: { $size: "$wins" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
totalWins: -1,
|
||||||
|
"wins.date": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (includeWines) {
|
||||||
|
const includeWinesSubQuery = [
|
||||||
|
{
|
||||||
|
$unwind: "$wins"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "wines",
|
||||||
|
localField: "wins.wine",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "wins.wine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unwind: "$wins._id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$_id",
|
||||||
|
name: { $first: "$name" },
|
||||||
|
totalWins: { $first: "$totalWins" },
|
||||||
|
wins: { $push: "$wins" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
query = includeWinesSubQuery.concat(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Winner.aggregate(query).then(winners => {
|
||||||
|
if (limit == null) {
|
||||||
|
return winners;
|
||||||
|
}
|
||||||
|
|
||||||
|
return winners.slice(0, limit);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addWinnerWithWine,
|
||||||
|
all,
|
||||||
|
byDate,
|
||||||
|
byName,
|
||||||
|
search,
|
||||||
|
latest,
|
||||||
|
groupByDate,
|
||||||
|
groupByColor,
|
||||||
|
orderByWins
|
||||||
|
};
|
||||||
369
api/lottery.js
369
api/lottery.js
@@ -1,132 +1,263 @@
|
|||||||
const path = require('path');
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
const Highscore = require(path.join(__dirname, '/schemas/Highscore'));
|
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||||
const Wine = require(path.join(__dirname, '/schemas/Wine'));
|
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||||
|
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||||
|
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
||||||
|
|
||||||
// Utils
|
const Message = require(path.join(__dirname, "/message"));
|
||||||
const epochToDateString = date => new Date(parseInt(date)).toDateString();
|
const historyRepository = require(path.join(__dirname, "/history"));
|
||||||
|
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||||
|
|
||||||
const sortNewestFirst = (lotteries) => {
|
const {
|
||||||
return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1)
|
WinnerNotFound,
|
||||||
}
|
NoMoreAttendeesToWin,
|
||||||
|
CouldNotFindNewWinnerAfterNTries,
|
||||||
|
LotteryByDateNotFound
|
||||||
|
} = require(path.join(__dirname, "/vinlottisErrors"));
|
||||||
|
|
||||||
const groupHighscoreByDate = async (highscore=undefined) => {
|
const archive = (date, raffles, stolen, wines) => {
|
||||||
if (highscore == undefined)
|
const { blue, red, yellow, green } = raffles;
|
||||||
highscore = await Highscore.find();
|
const bought = blue + red + yellow + green;
|
||||||
|
|
||||||
const highscoreByDate = [];
|
return Promise.all(wines.map(wine => wineRepository.findWine(wine))).then(resolvedWines => {
|
||||||
|
const lottery = new Lottery({
|
||||||
highscore.forEach(person => {
|
|
||||||
person.wins.map(win => {
|
|
||||||
const epochDate = new Date(win.date).setHours(0,0,0,0);
|
|
||||||
const winnerObject = {
|
|
||||||
name: person.name,
|
|
||||||
color: win.color,
|
|
||||||
wine: win.wine,
|
|
||||||
date: epochDate
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate)
|
|
||||||
if (existingDateIndex > -1)
|
|
||||||
highscoreByDate[existingDateIndex].winners.push(winnerObject);
|
|
||||||
else
|
|
||||||
highscoreByDate.push({
|
|
||||||
date: epochDate,
|
|
||||||
winners: [winnerObject]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return sortNewestFirst(highscoreByDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveWineReferences = (highscoreObject, key) => {
|
|
||||||
const listWithWines = highscoreObject[key]
|
|
||||||
|
|
||||||
return Promise.all(listWithWines.map(element =>
|
|
||||||
Wine.findById(element.wine)
|
|
||||||
.then(wine => {
|
|
||||||
element.wine = wine
|
|
||||||
return element
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
.then(resolvedListWithWines => {
|
|
||||||
highscoreObject[key] = resolvedListWithWines;
|
|
||||||
return highscoreObject
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// end utils
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
const all = (req, res) => {
|
|
||||||
return Highscore.find()
|
|
||||||
.then(highscore => groupHighscoreByDate(highscore))
|
|
||||||
.then(lotteries => res.send({
|
|
||||||
message: "Lotteries by date!",
|
|
||||||
lotteries
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const latest = (req, res) => {
|
|
||||||
return groupHighscoreByDate()
|
|
||||||
.then(lotteries => lotteries.shift()) // first element in list
|
|
||||||
.then(latestLottery => resolveWineReferences(latestLottery, "winners"))
|
|
||||||
.then(lottery => res.send({
|
|
||||||
message: "Latest lottery!",
|
|
||||||
winners: lottery.winners
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const byEpochDate = (req, res) => {
|
|
||||||
let { date } = req.params;
|
|
||||||
date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime()
|
|
||||||
const dateString = epochToDateString(date);
|
|
||||||
|
|
||||||
return groupHighscoreByDate()
|
|
||||||
.then(lotteries => {
|
|
||||||
const lottery = lotteries.filter(lottery => lottery.date == date)
|
|
||||||
if (lottery.length > 0) {
|
|
||||||
return lottery[0]
|
|
||||||
} else {
|
|
||||||
return res.status(404).send({
|
|
||||||
message: `No lottery found for date: ${ dateString }`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(lottery => resolveWineReferences(lottery, "winners"))
|
|
||||||
.then(lottery => res.send({
|
|
||||||
message: `Lottery for date: ${ dateString}`,
|
|
||||||
date,
|
date,
|
||||||
winners: lottery.winners
|
blue,
|
||||||
}))
|
red,
|
||||||
}
|
yellow,
|
||||||
|
green,
|
||||||
|
bought,
|
||||||
|
stolen,
|
||||||
|
wines: resolvedWines
|
||||||
|
});
|
||||||
|
|
||||||
const byName = (req, res) => {
|
return lottery.save();
|
||||||
const { name } = req.params;
|
});
|
||||||
const regexName = new RegExp(name, "i"); // lowercase regex of the name
|
};
|
||||||
|
|
||||||
return Highscore.find({ name })
|
const lotteryByDate = date => {
|
||||||
.then(highscore => {
|
const startOfDay = new Date(date.setHours(0, 0, 0, 0));
|
||||||
if (highscore.length > 0) {
|
const endOfDay = new Date(date.setHours(24, 59, 59, 99));
|
||||||
return highscore[0]
|
|
||||||
} else {
|
const query = [
|
||||||
return res.status(404).send({
|
{
|
||||||
message: `Name: ${ name } not found in leaderboards.`
|
$match: {
|
||||||
})
|
date: {
|
||||||
|
$gte: startOfDay,
|
||||||
|
$lte: endOfDay
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.then(highscore => resolveWineReferences(highscore, "wins"))
|
{
|
||||||
.then(highscore => res.send({
|
$lookup: {
|
||||||
message: `Lottery winnings for name: ${ name }.`,
|
from: "wines",
|
||||||
name: highscore.name,
|
localField: "wines",
|
||||||
highscore: sortNewestFirst(highscore.wins)
|
foreignField: "_id",
|
||||||
}))
|
as: "wines"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const aggregateLottery = Lottery.aggregate(query);
|
||||||
|
return aggregateLottery.project("-_id -__v").then(lotteries => {
|
||||||
|
if (lotteries.length == 0) {
|
||||||
|
throw new LotteryByDateNotFound(date);
|
||||||
|
}
|
||||||
|
return lotteries[0];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const allLotteries = (sort = "asc", yearFilter = undefined) => {
|
||||||
|
const sortDirection = sort == "asc" ? 1 : -1;
|
||||||
|
|
||||||
|
let startQueryDate = new Date("1970-01-01");
|
||||||
|
let endQueryDate = new Date("2999-01-01");
|
||||||
|
if (yearFilter) {
|
||||||
|
startQueryDate = new Date(`${yearFilter}-01-01`);
|
||||||
|
endQueryDate = new Date(`${Number(yearFilter) + 1}-01-01`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
date: {
|
||||||
|
$gte: startQueryDate,
|
||||||
|
$lte: endQueryDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
date: sortDirection
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unset: ["_id", "__v"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "wines",
|
||||||
|
localField: "wines",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "wines"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return Lottery.aggregate(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const allLotteriesIncludingWinners = async (sort = "asc", yearFilter = undefined) => {
|
||||||
|
const lotteries = await allLotteries(sort, yearFilter);
|
||||||
|
const allWinners = await historyRepository.groupByDate(false, sort);
|
||||||
|
|
||||||
|
return lotteries.map(lottery => {
|
||||||
|
const { winners } = allWinners.pop();
|
||||||
|
|
||||||
|
return {
|
||||||
|
wines: lottery.wines,
|
||||||
|
date: lottery.date,
|
||||||
|
blue: lottery.blue,
|
||||||
|
green: lottery.green,
|
||||||
|
yellow: lottery.yellow,
|
||||||
|
red: lottery.red,
|
||||||
|
bought: lottery.bought,
|
||||||
|
stolen: lottery.stolen,
|
||||||
|
winners: winners
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawWinner = async () => {
|
||||||
|
let allContestants = await Attendee.find({ winner: false });
|
||||||
|
|
||||||
|
if (allContestants.length == 0) {
|
||||||
|
throw new NoMoreAttendeesToWin();
|
||||||
|
}
|
||||||
|
|
||||||
|
let raffleColors = [];
|
||||||
|
for (let i = 0; i < allContestants.length; i++) {
|
||||||
|
let currentContestant = allContestants[i];
|
||||||
|
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||||
|
raffleColors.push("blue");
|
||||||
|
}
|
||||||
|
for (let red = 0; red < currentContestant.red; red++) {
|
||||||
|
raffleColors.push("red");
|
||||||
|
}
|
||||||
|
for (let green = 0; green < currentContestant.green; green++) {
|
||||||
|
raffleColors.push("green");
|
||||||
|
}
|
||||||
|
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||||
|
raffleColors.push("yellow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raffleColors = shuffle(raffleColors);
|
||||||
|
|
||||||
|
let colorToChooseFrom = raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||||
|
let findObject = { winner: false };
|
||||||
|
|
||||||
|
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||||
|
|
||||||
|
let tries = 0;
|
||||||
|
const maxTries = 3;
|
||||||
|
let contestantsToChooseFrom = undefined;
|
||||||
|
while (contestantsToChooseFrom == undefined && tries < maxTries) {
|
||||||
|
const hit = await Attendee.find(findObject);
|
||||||
|
if (hit && hit.length) {
|
||||||
|
contestantsToChooseFrom = hit;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tries++;
|
||||||
|
}
|
||||||
|
if (contestantsToChooseFrom == undefined) {
|
||||||
|
throw new CouldNotFindNewWinnerAfterNTries(maxTries);
|
||||||
|
}
|
||||||
|
|
||||||
|
let attendeeListDemocratic = [];
|
||||||
|
|
||||||
|
let currentContestant;
|
||||||
|
for (let i = 0; i < contestantsToChooseFrom.length; i++) {
|
||||||
|
currentContestant = contestantsToChooseFrom[i];
|
||||||
|
for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) {
|
||||||
|
attendeeListDemocratic.push({
|
||||||
|
name: currentContestant.name,
|
||||||
|
phoneNumber: currentContestant.phoneNumber,
|
||||||
|
red: currentContestant.red,
|
||||||
|
blue: currentContestant.blue,
|
||||||
|
green: currentContestant.green,
|
||||||
|
yellow: currentContestant.yellow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attendeeListDemocratic = shuffle(attendeeListDemocratic);
|
||||||
|
|
||||||
|
let winner = attendeeListDemocratic[Math.floor(Math.random() * attendeeListDemocratic.length)];
|
||||||
|
|
||||||
|
let newWinnerElement = new VirtualWinner({
|
||||||
|
name: winner.name,
|
||||||
|
phoneNumber: winner.phoneNumber,
|
||||||
|
color: colorToChooseFrom,
|
||||||
|
red: winner.red,
|
||||||
|
blue: winner.blue,
|
||||||
|
green: winner.green,
|
||||||
|
yellow: winner.yellow,
|
||||||
|
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||||
|
timestamp_drawn: new Date().getTime()
|
||||||
|
});
|
||||||
|
|
||||||
|
await newWinnerElement.save();
|
||||||
|
await Attendee.updateOne({ name: winner.name, phoneNumber: winner.phoneNumber }, { $set: { winner: true } });
|
||||||
|
|
||||||
|
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||||
|
timestamp_drawn: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return { winner, color: colorToChooseFrom, winners };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** - - UTILS - - **/
|
||||||
|
const genRandomString = function(length) {
|
||||||
|
return crypto
|
||||||
|
.randomBytes(Math.ceil(length / 2))
|
||||||
|
.toString("hex") /** convert to hexadecimal format */
|
||||||
|
.slice(0, length); /** return required number of characters */
|
||||||
|
};
|
||||||
|
|
||||||
|
const sha512 = function(password, salt) {
|
||||||
|
var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */
|
||||||
|
hash.update(password);
|
||||||
|
var value = hash.digest("hex");
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
function shuffle(array) {
|
||||||
|
let currentIndex = array.length,
|
||||||
|
temporaryValue,
|
||||||
|
randomIndex;
|
||||||
|
|
||||||
|
// While there remain elements to shuffle...
|
||||||
|
while (0 !== currentIndex) {
|
||||||
|
// Pick a remaining element...
|
||||||
|
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||||
|
currentIndex -= 1;
|
||||||
|
|
||||||
|
// And swap it with the current element.
|
||||||
|
temporaryValue = array[currentIndex];
|
||||||
|
array[currentIndex] = array[randomIndex];
|
||||||
|
array[randomIndex] = temporaryValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
all,
|
drawWinner,
|
||||||
latest,
|
archive,
|
||||||
byEpochDate,
|
lotteryByDate,
|
||||||
byName
|
allLotteries,
|
||||||
|
allLotteriesIncludingWinners
|
||||||
};
|
};
|
||||||
|
|||||||
123
api/message.js
123
api/message.js
@@ -2,34 +2,50 @@ const https = require("https");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
|
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
|
||||||
|
|
||||||
const dateString = (date) => {
|
const dateString = date => {
|
||||||
if (typeof(date) == "string") {
|
if (typeof date == "string") {
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
}
|
}
|
||||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||||
|
|
||||||
return `${da}-${mo}-${ye}`
|
return `${da}-${mo}-${ye}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function sendInitialMessageToWinners(winners) {
|
||||||
|
const numbers = winners.map(winner => ({ msisdn: `47${winner.phoneNumber}` }));
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
sender: "Vinlottis",
|
||||||
|
message: "Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
|
||||||
|
recipients: numbers
|
||||||
|
};
|
||||||
|
|
||||||
|
return gatewayRequest(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendWineSelectMessage(winnerObject) {
|
async function sendPrizeSelectionLink(winner) {
|
||||||
winnerObject.timestamp_sent = new Date().getTime();
|
winner.timestamp_sent = new Date().getTime();
|
||||||
winnerObject.timestamp_limit = new Date().getTime() * 600000;
|
winner.timestamp_limit = new Date().getTime() + 1000 * 600;
|
||||||
await winnerObject.save();
|
await winner.save();
|
||||||
|
|
||||||
let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin");
|
const { id, name, phoneNumber } = winner;
|
||||||
|
const url = new URL(`/#/winner/${id}`, "https://lottis.vin");
|
||||||
|
const message = `Gratulerer som heldig vinner av vinlotteriet ${name}! Her er linken for \
|
||||||
|
å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst \
|
||||||
|
i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.`;
|
||||||
|
|
||||||
return sendMessageToUser(
|
return sendMessageToNumber(phoneNumber, message);
|
||||||
winnerObject.phoneNumber,
|
|
||||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Her er linken for å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.)`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendWineConfirmation(winnerObject, wineObject, date) {
|
async function sendWineConfirmation(winnerObject, wineObject, date) {
|
||||||
date = dateString(date);
|
date = dateString(date);
|
||||||
return sendMessageToUser(winnerObject.phoneNumber,
|
return sendMessageToNumber(
|
||||||
`Bekreftelse på din vin ${ winnerObject.name }.\nDato vunnet: ${ date }.\nVin valgt: ${ wineObject.name }.\nDu vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`)
|
winnerObject.phoneNumber,
|
||||||
|
`Bekreftelse på din vin ${winnerObject.name}.\nDato vunnet: ${date}.\nVin valgt: ${wineObject.name}.\
|
||||||
|
\nDu vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendLastWinnerMessage(winnerObject, wineObject) {
|
async function sendLastWinnerMessage(winnerObject, wineObject) {
|
||||||
@@ -38,84 +54,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) {
|
|||||||
winnerObject.timestamp_limit = new Date().getTime();
|
winnerObject.timestamp_limit = new Date().getTime();
|
||||||
await winnerObject.save();
|
await winnerObject.save();
|
||||||
|
|
||||||
return sendMessageToUser(
|
return sendMessageToNumber(
|
||||||
winnerObject.phoneNumber,
|
winnerObject.phoneNumber,
|
||||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`
|
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, \
|
||||||
|
du vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendWineSelectMessageTooLate(winnerObject) {
|
async function sendWineSelectMessageTooLate(winnerObject) {
|
||||||
return sendMessageToUser(
|
return sendMessageToNumber(
|
||||||
winnerObject.phoneNumber,
|
winnerObject.phoneNumber,
|
||||||
`Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.`
|
`Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor \
|
||||||
|
puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessageToUser(phoneNumber, message) {
|
async function sendMessageToNumber(phoneNumber, message) {
|
||||||
console.log(`Attempting to send message to ${ phoneNumber }.`)
|
console.log(`Attempting to send message to ${phoneNumber}.`);
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
sender: "Vinlottis",
|
sender: "Vinlottis",
|
||||||
message: message,
|
message: message,
|
||||||
recipients: [{ msisdn: `47${ phoneNumber }`}]
|
recipients: [{ msisdn: `47${phoneNumber}` }]
|
||||||
};
|
};
|
||||||
|
|
||||||
return gatewayRequest(body);
|
return gatewayRequest(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function sendInitialMessageToWinners(winners) {
|
|
||||||
let numbers = [];
|
|
||||||
for (let i = 0; i < winners.length; i++) {
|
|
||||||
numbers.push({ msisdn: `47${winners[i].phoneNumber}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
sender: "Vinlottis",
|
|
||||||
message:
|
|
||||||
"Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
|
|
||||||
recipients: numbers
|
|
||||||
}
|
|
||||||
|
|
||||||
return gatewayRequest(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function gatewayRequest(body) {
|
async function gatewayRequest(body) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: "gatewayapi.com",
|
hostname: "gatewayapi.com",
|
||||||
post: 443,
|
post: 443,
|
||||||
path: `/rest/mtsms?token=${ config.gatewayToken }`,
|
path: `/rest/mtsms?token=${config.gatewayToken}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const req = https.request(options, (res) => {
|
const req = https.request(options, res => {
|
||||||
console.log(`statusCode: ${ res.statusCode }`);
|
console.log(`statusCode: ${res.statusCode}`);
|
||||||
console.log(`statusMessage: ${ res.statusMessage }`);
|
console.log(`statusMessage: ${res.statusMessage}`);
|
||||||
|
|
||||||
res.setEncoding('utf8');
|
res.setEncoding("utf8");
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
res.on("data", (data) => {
|
res.on("data", data => {
|
||||||
console.log("Response from message gateway:", data)
|
console.log("Response from message gateway:", data);
|
||||||
|
|
||||||
resolve(JSON.parse(data))
|
resolve(JSON.parse(data));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.on("data", (data) => {
|
res.on("data", data => {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
return reject('Gateway error: ' + data['message'] || data)
|
return reject("Gateway error: " + data["message"] || data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
req.on("error", (error) => {
|
req.on("error", error => {
|
||||||
console.error(`Error from sms service: ${ error }`);
|
console.error(`Error from sms service: ${error}`);
|
||||||
reject(`Error from sms service: ${ error }`);
|
reject(`Error from sms service: ${error}`);
|
||||||
})
|
});
|
||||||
|
|
||||||
req.write(JSON.stringify(body));
|
req.write(JSON.stringify(body));
|
||||||
req.end();
|
req.end();
|
||||||
@@ -123,9 +124,9 @@ async function gatewayRequest(body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendWineSelectMessage,
|
sendInitialMessageToWinners,
|
||||||
|
sendPrizeSelectionLink,
|
||||||
sendWineConfirmation,
|
sendWineConfirmation,
|
||||||
sendLastWinnerMessage,
|
sendLastWinnerMessage,
|
||||||
sendWineSelectMessageTooLate,
|
sendWineSelectMessageTooLate
|
||||||
sendInitialMessageToWinners
|
};
|
||||||
}
|
|
||||||
|
|||||||
6
api/middleware/alwaysAuthenticatedWhenLocalhost.js
Normal file
6
api/middleware/alwaysAuthenticatedWhenLocalhost.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const alwaysAuthenticatedWhenLocalhost = (req, res, next) => {
|
||||||
|
req.isAuthenticated = () => true;
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = alwaysAuthenticatedWhenLocalhost;
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
const mustBeAuthenticated = (req, res, next) => {
|
const mustBeAuthenticated = (req, res, next) => {
|
||||||
if (process.env.NODE_ENV == "development") {
|
|
||||||
console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
|
|
||||||
req.isAuthenticated = () => true;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.isAuthenticated()) {
|
if (!req.isAuthenticated()) {
|
||||||
return res.status(401).send({
|
return res.status(401).send({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
103
api/prelotteryWine.js
Normal file
103
api/prelotteryWine.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||||
|
const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||||
|
|
||||||
|
const allWines = () => {
|
||||||
|
return PreLotteryWine.find().populate("winner");
|
||||||
|
};
|
||||||
|
|
||||||
|
const allWinesWithoutWinner = () => {
|
||||||
|
return PreLotteryWine.find({ winner: { $exists: false } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWines = wines => {
|
||||||
|
const prelotteryWines = wines.map(wine => {
|
||||||
|
let newPrelotteryWine = new PreLotteryWine({
|
||||||
|
name: wine.name,
|
||||||
|
vivinoLink: wine.vivinoLink,
|
||||||
|
rating: wine.rating,
|
||||||
|
year: wine.year,
|
||||||
|
image: wine.image,
|
||||||
|
price: wine.price,
|
||||||
|
country: wine.country,
|
||||||
|
id: wine.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return newPrelotteryWine.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(prelotteryWines);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wineById = id => {
|
||||||
|
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||||
|
if (wine == null) {
|
||||||
|
throw new WineNotFound();
|
||||||
|
}
|
||||||
|
return wine;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateWineById = (id, updateModel) => {
|
||||||
|
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||||
|
if (wine == null) {
|
||||||
|
throw new WineNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedWine = {
|
||||||
|
name: updateModel.name != null ? updateModel.name : wine.name,
|
||||||
|
vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink,
|
||||||
|
rating: updateModel.rating != null ? updateModel.rating : wine.rating,
|
||||||
|
year: updateModel.year != null ? updateModel.year : wine.year,
|
||||||
|
image: updateModel.image != null ? updateModel.image : wine.image,
|
||||||
|
price: updateModel.price != null ? updateModel.price : wine.price,
|
||||||
|
country: updateModel.country != null ? updateModel.country : wine.country,
|
||||||
|
id: updateModel.id != null ? updateModel.id : wine.id
|
||||||
|
};
|
||||||
|
|
||||||
|
return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWinnerToWine = (wine, winner) => {
|
||||||
|
wine.winner = winner;
|
||||||
|
winner.prize_selected = true;
|
||||||
|
return Promise.all([wine.save(), winner.save()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWineById = id => {
|
||||||
|
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||||
|
if (wine == null) {
|
||||||
|
throw new WineNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWines = () => {
|
||||||
|
return PreLotteryWine.deleteMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
const wineSchema = () => {
|
||||||
|
let schema = { ...PreLotteryWine.schema.obj };
|
||||||
|
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||||
|
accumulator[current] = "";
|
||||||
|
return accumulator;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return Promise.resolve(nulledSchema);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allWines,
|
||||||
|
allWinesWithoutWinner,
|
||||||
|
addWines,
|
||||||
|
wineById,
|
||||||
|
addWinnerToWine,
|
||||||
|
updateWineById,
|
||||||
|
deleteWineById,
|
||||||
|
deleteWines,
|
||||||
|
wineSchema
|
||||||
|
};
|
||||||
110
api/prizeDistribution.js
Normal file
110
api/prizeDistribution.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||||
|
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||||
|
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||||
|
|
||||||
|
const message = require(path.join(__dirname, "/message"));
|
||||||
|
const historyRepository = require(path.join(__dirname, "/history"));
|
||||||
|
const winnerRepository = require(path.join(__dirname, "/winner"));
|
||||||
|
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||||
|
const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine"));
|
||||||
|
|
||||||
|
const { WinnerNotFound, WineSelectionWinnerNotNextInLine, WinnersTimelimitExpired } = require(path.join(
|
||||||
|
__dirname,
|
||||||
|
"/vinlottisErrors"
|
||||||
|
));
|
||||||
|
|
||||||
|
const verifyWinnerNextInLine = async id => {
|
||||||
|
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||||
|
|
||||||
|
if (!foundWinner) {
|
||||||
|
throw new WinnerNotFound();
|
||||||
|
} else if (foundWinner.timestamp_limit < new Date().getTime()) {
|
||||||
|
throw new WinnersTimelimitExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||||
|
|
||||||
|
if (
|
||||||
|
foundWinner.timestamp_limit == undefined ||
|
||||||
|
foundWinner.timestamp_sent == undefined ||
|
||||||
|
foundWinner.prize_selected == true
|
||||||
|
) {
|
||||||
|
throw new WineSelectionWinnerNotNextInLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(foundWinner);
|
||||||
|
};
|
||||||
|
|
||||||
|
const claimPrize = (wine, winner) => {
|
||||||
|
return wineRepository
|
||||||
|
.addWine(wine)
|
||||||
|
.then(_ => prelotteryWineRepository.addWinnerToWine(wine, winner)) // prelotteryWine.deleteById
|
||||||
|
.then(_ => historyRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine
|
||||||
|
.then(_ => message.sendWineConfirmation(winner, wine));
|
||||||
|
};
|
||||||
|
|
||||||
|
const notifyNextWinner = async () => {
|
||||||
|
let nextWinner = undefined;
|
||||||
|
|
||||||
|
const winnersLeft = await VirtualWinner.find({ prize_selected: false }).sort({ timestamp_drawn: 1 });
|
||||||
|
const winesLeft = await PreLotteryWine.find({ winner: { $exists: false } });
|
||||||
|
|
||||||
|
if (winnersLeft.length > 1) {
|
||||||
|
console.log("multiple winners left, choose next in line");
|
||||||
|
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
||||||
|
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
||||||
|
console.log("one winner left, but multiple wines");
|
||||||
|
nextWinner = winnersLeft[0]; // one winner left, but multiple wines
|
||||||
|
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
||||||
|
console.log("one winner and one wine left, choose for user");
|
||||||
|
nextWinner = winnersLeft[0]; // one winner and one wine left, choose for user
|
||||||
|
wine = winesLeft[0];
|
||||||
|
return claimPrize(wine, nextWinner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextWinner) {
|
||||||
|
return message.sendPrizeSelectionLink(nextWinner).then(_ => startTimeout(nextWinner.id));
|
||||||
|
} else {
|
||||||
|
console.info("All winners notified. Could start cleanup here.");
|
||||||
|
return Promise.resolve({
|
||||||
|
message: "All winners notified."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// these need to be register somewhere to cancel if something
|
||||||
|
// goes wrong and we want to start prize distribution again
|
||||||
|
function startTimeout(id) {
|
||||||
|
const minute = 60000;
|
||||||
|
const minutesForTimeout = 10;
|
||||||
|
|
||||||
|
console.log(`Starting timeout for user ${id}.`);
|
||||||
|
console.log(`Timeout duration: ${minutesForTimeout * minute}`);
|
||||||
|
setTimeout(async () => {
|
||||||
|
let virtualWinner = await VirtualWinner.findOne({ id: id, prize_selected: false });
|
||||||
|
if (!virtualWinner) {
|
||||||
|
console.log(`Timeout done for user ${id}, but user has already sent data.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Timeout done for user ${id}, sending update to user.`);
|
||||||
|
|
||||||
|
message.sendWineSelectMessageTooLate(virtualWinner);
|
||||||
|
|
||||||
|
virtualWinner.timestamp_drawn = new Date().getTime();
|
||||||
|
virtualWinner.timestamp_limit = null;
|
||||||
|
virtualWinner.timestamp_sent = null;
|
||||||
|
await virtualWinner.save();
|
||||||
|
|
||||||
|
notifyNextWinner();
|
||||||
|
}, minutesForTimeout * minute);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
verifyWinnerNextInLine,
|
||||||
|
claimPrize,
|
||||||
|
notifyNextWinner
|
||||||
|
};
|
||||||
@@ -1,41 +1,20 @@
|
|||||||
const express = require("express");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const RequestedWine = require(path.join(
|
const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine"));
|
||||||
__dirname, "/schemas/RequestedWine"
|
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||||
));
|
|
||||||
const Wine = require(path.join(
|
|
||||||
__dirname, "/schemas/Wine"
|
|
||||||
));
|
|
||||||
|
|
||||||
const deleteRequestedWineById = async (req, res) => {
|
class RequestedWineNotFound extends Error {
|
||||||
const { id } = req.params;
|
constructor(message = "Wine with this id was not found.") {
|
||||||
if(id == null){
|
super(message);
|
||||||
return res.json({
|
this.name = "RequestedWineNotFound";
|
||||||
message: "Id er ikke definert",
|
this.statusCode = 404;
|
||||||
success: false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await RequestedWine.deleteOne({wineId: id})
|
|
||||||
return res.json({
|
|
||||||
message: `Slettet vin med id: ${id}`,
|
|
||||||
success: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllRequestedWines = async (req, res) => {
|
const addNew = async wine => {
|
||||||
const allWines = await RequestedWine.find({}).populate("wine");
|
let foundWine = await Wine.findOne({ id: wine.id });
|
||||||
|
|
||||||
return res.json(allWines);
|
if (foundWine == undefined) {
|
||||||
}
|
foundWine = new Wine({
|
||||||
|
|
||||||
const requestNewWine = async (req, res) => {
|
|
||||||
const {wine} = req.body
|
|
||||||
|
|
||||||
let thisWineIsLOKO = await Wine.findOne({id: wine.id})
|
|
||||||
|
|
||||||
if(thisWineIsLOKO == undefined){
|
|
||||||
thisWineIsLOKO = new Wine({
|
|
||||||
name: wine.name,
|
name: wine.name,
|
||||||
vivinoLink: wine.vivinoLink,
|
vivinoLink: wine.vivinoLink,
|
||||||
rating: null,
|
rating: null,
|
||||||
@@ -43,27 +22,47 @@ const requestNewWine = async (req, res) => {
|
|||||||
image: wine.image,
|
image: wine.image,
|
||||||
id: wine.id
|
id: wine.id
|
||||||
});
|
});
|
||||||
await thisWineIsLOKO.save()
|
await foundWine.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestedWine = await RequestedWine.findOne({ "wineId": wine.id})
|
let requestedWine = await RequestedWine.findOne({ wineId: wine.id });
|
||||||
|
|
||||||
if(requestedWine == undefined){
|
if (requestedWine == undefined) {
|
||||||
requestedWine = new RequestedWine({
|
requestedWine = new RequestedWine({
|
||||||
count: 1,
|
count: 1,
|
||||||
wineId: wine.id,
|
wineId: wine.id,
|
||||||
wine: thisWineIsLOKO
|
wine: foundWine
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
requestedWine.count += 1;
|
requestedWine.count += 1;
|
||||||
}
|
}
|
||||||
await requestedWine.save()
|
await requestedWine.save();
|
||||||
|
|
||||||
return res.send(requestedWine);
|
return requestedWine;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const getById = id => {
|
||||||
|
return RequestedWine.findOne({ wineId: id })
|
||||||
|
.populate("wine")
|
||||||
|
.then(wine => {
|
||||||
|
if (wine == null) {
|
||||||
|
throw new RequestedWineNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return wine;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteById = id => {
|
||||||
|
return getById(id).then(requestedWine => RequestedWine.deleteOne({ _id: requestedWine._id }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAll = () => {
|
||||||
|
return RequestedWine.find({}).populate("wine");
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
requestNewWine,
|
addNew,
|
||||||
getAllRequestedWines,
|
getAll,
|
||||||
deleteRequestedWineById
|
deleteById
|
||||||
};
|
};
|
||||||
|
|||||||
154
api/retrieve.js
154
api/retrieve.js
@@ -1,154 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
const Purchase = require(path.join(__dirname, "/schemas/Purchase"));
|
|
||||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
|
||||||
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
|
|
||||||
const PreLotteryWine = require(path.join(
|
|
||||||
__dirname, "/schemas/PreLotteryWine"
|
|
||||||
));
|
|
||||||
|
|
||||||
const prelotteryWines = async (req, res) => {
|
|
||||||
let wines = await PreLotteryWine.find();
|
|
||||||
return res.json(wines);
|
|
||||||
};
|
|
||||||
|
|
||||||
const allPurchase = async (req, res) => {
|
|
||||||
let purchases = await Purchase.find()
|
|
||||||
.populate("wines")
|
|
||||||
.sort({ date: 1 });
|
|
||||||
return res.json(purchases);
|
|
||||||
};
|
|
||||||
|
|
||||||
const purchaseByColor = async (req, res) => {
|
|
||||||
const countColor = await Purchase.find();
|
|
||||||
let red = 0;
|
|
||||||
let blue = 0;
|
|
||||||
let yellow = 0;
|
|
||||||
let green = 0;
|
|
||||||
let stolen = 0;
|
|
||||||
for (let i = 0; i < countColor.length; i++) {
|
|
||||||
let element = countColor[i];
|
|
||||||
red += element.red;
|
|
||||||
blue += element.blue;
|
|
||||||
yellow += element.yellow;
|
|
||||||
green += element.green;
|
|
||||||
if (element.stolen != undefined) {
|
|
||||||
stolen += element.stolen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const highscore = await Highscore.find();
|
|
||||||
let redWin = 0;
|
|
||||||
let blueWin = 0;
|
|
||||||
let yellowWin = 0;
|
|
||||||
let greenWin = 0;
|
|
||||||
for (let i = 0; i < highscore.length; i++) {
|
|
||||||
let element = highscore[i];
|
|
||||||
for (let y = 0; y < element.wins.length; y++) {
|
|
||||||
let currentWin = element.wins[y];
|
|
||||||
switch (currentWin.color) {
|
|
||||||
case "blue":
|
|
||||||
blueWin += 1;
|
|
||||||
break;
|
|
||||||
case "red":
|
|
||||||
redWin += 1;
|
|
||||||
break;
|
|
||||||
case "yellow":
|
|
||||||
yellowWin += 1;
|
|
||||||
break;
|
|
||||||
case "green":
|
|
||||||
greenWin += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = red + yellow + blue + green;
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
red: {
|
|
||||||
total: red,
|
|
||||||
win: redWin
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
total: blue,
|
|
||||||
win: blueWin
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
total: green,
|
|
||||||
win: greenWin
|
|
||||||
},
|
|
||||||
yellow: {
|
|
||||||
total: yellow,
|
|
||||||
win: yellowWin
|
|
||||||
},
|
|
||||||
stolen: stolen,
|
|
||||||
total: total
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const highscore = async (req, res) => {
|
|
||||||
const highscore = await Highscore.find().populate("wins.wine");
|
|
||||||
|
|
||||||
return res.json(highscore);
|
|
||||||
};
|
|
||||||
|
|
||||||
const allWines = async (req, res) => {
|
|
||||||
const wines = await Wine.find();
|
|
||||||
|
|
||||||
return res.json(wines);
|
|
||||||
};
|
|
||||||
|
|
||||||
const allWinesSummary = async (req, res) => {
|
|
||||||
const highscore = await Highscore.find().populate("wins.wine");
|
|
||||||
let wines = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < highscore.length; i++) {
|
|
||||||
let person = highscore[i];
|
|
||||||
for (let y = 0; y < person.wins.length; y++) {
|
|
||||||
let wine = person.wins[y].wine;
|
|
||||||
let date = person.wins[y].date;
|
|
||||||
let color = person.wins[y].color;
|
|
||||||
|
|
||||||
if (wines[wine._id] == undefined) {
|
|
||||||
wines[wine._id] = {
|
|
||||||
name: wine.name,
|
|
||||||
occurences: wine.occurences,
|
|
||||||
vivinoLink: wine.vivinoLink,
|
|
||||||
rating: wine.rating,
|
|
||||||
image: wine.image,
|
|
||||||
id: wine.id,
|
|
||||||
_id: wine._id,
|
|
||||||
dates: [date],
|
|
||||||
winners: [person.name],
|
|
||||||
red: 0,
|
|
||||||
blue: 0,
|
|
||||||
green: 0,
|
|
||||||
yellow: 0
|
|
||||||
};
|
|
||||||
wines[wine._id][color] += 1;
|
|
||||||
} else {
|
|
||||||
wines[wine._id].dates.push(date);
|
|
||||||
wines[wine._id].winners.push(person.name);
|
|
||||||
if (wines[wine._id][color] == undefined) {
|
|
||||||
wines[wine._id][color] = 1;
|
|
||||||
} else {
|
|
||||||
wines[wine._id][color] += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wines = Object.values(wines).reverse()
|
|
||||||
|
|
||||||
return res.json(wines);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
prelotteryWines,
|
|
||||||
allPurchase,
|
|
||||||
purchaseByColor,
|
|
||||||
highscore,
|
|
||||||
allWines,
|
|
||||||
allWinesSummary
|
|
||||||
};
|
|
||||||
130
api/router.js
130
api/router.js
@@ -4,67 +4,97 @@ const path = require("path");
|
|||||||
const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated"));
|
const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated"));
|
||||||
const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated"));
|
const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated"));
|
||||||
|
|
||||||
const update = require(path.join(__dirname, "/update"));
|
const requestController = require(path.join(__dirname, "/controllers/requestController"));
|
||||||
const retrieve = require(path.join(__dirname, "/retrieve"));
|
const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController"));
|
||||||
const request = require(path.join(__dirname, "/request"));
|
const chatController = require(path.join(__dirname, "/controllers/chatController"));
|
||||||
const subscriptionApi = require(path.join(__dirname, "/subscriptions"));
|
const userController = require(path.join(__dirname, "/controllers/userController"));
|
||||||
const userApi = require(path.join(__dirname, "/user"));
|
const historyController = require(path.join(__dirname, "/controllers/historyController"));
|
||||||
const wineinfo = require(path.join(__dirname, "/wineinfo"));
|
const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController"));
|
||||||
const virtualApi = require(path.join(__dirname, "/virtualLottery"));
|
const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController"));
|
||||||
const virtualRegistrationApi = require(path.join(
|
const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController"));
|
||||||
__dirname, "/virtualRegistration"
|
const lotteryController = require(path.join(__dirname, "/controllers/lotteryController"));
|
||||||
));
|
const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController"));
|
||||||
const lottery = require(path.join(__dirname, "/lottery"));
|
const wineController = require(path.join(__dirname, "/controllers/wineController"));
|
||||||
const chatHistoryApi = require(path.join(__dirname, "/chatHistory"));
|
const messageController = require(path.join(__dirname, "/controllers/messageController"));
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get("/wineinfo/search", wineinfo.wineSearch);
|
router.get("/vinmonopolet/wine/search", vinmonopoletController.searchWines);
|
||||||
|
router.get("/vinmonopolet/wine/by-ean/:ean", vinmonopoletController.wineByEAN);
|
||||||
|
router.get("/vinmonopolet/wine/by-id/:id", vinmonopoletController.wineById);
|
||||||
|
router.get("/vinmonopolet/stores/", vinmonopoletController.allStores);
|
||||||
|
router.get("/vinmonopolet/stores/search", vinmonopoletController.searchStores);
|
||||||
|
|
||||||
router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines);
|
router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests);
|
||||||
router.post("/request/new-wine", request.requestNewWine);
|
router.post("/request", requestController.addRequest);
|
||||||
router.delete("/request/:id", request.deleteRequestedWineById);
|
router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest);
|
||||||
|
|
||||||
router.get("/wineinfo/schema", mustBeAuthenticated, update.schema);
|
router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences
|
||||||
router.get("/wineinfo/:ean", wineinfo.byEAN);
|
router.get("/wine/:id", wineController.wineById); // sort = by-date, by-name, by-occurences
|
||||||
|
// router.update("/wine/:id", mustBeAuthenticated, wineController.update);
|
||||||
|
|
||||||
router.post("/log/wines", mustBeAuthenticated, update.submitWines);
|
router.get("/history", historyController.all);
|
||||||
router.post("/lottery", update.submitLottery);
|
router.get("/history/latest", historyController.latest);
|
||||||
router.post("/lottery/wines", update.submitWinesToLottery);
|
router.get("/history/by-wins/", historyController.orderByWins);
|
||||||
// router.delete("/lottery/wine/:id", update.deleteWineFromLottery);
|
router.get("/history/by-color/", historyController.groupByColor);
|
||||||
router.post("/lottery/winners", update.submitWinnersToLottery);
|
router.get("/history/by-date/:date", historyController.byDate);
|
||||||
|
router.get("/history/by-name/:name", historyController.byName);
|
||||||
|
router.get("/history/search/", historyController.search);
|
||||||
|
router.get("/history/by-date/", historyController.groupByDate);
|
||||||
|
|
||||||
router.get("/wines/prelottery", retrieve.prelotteryWines);
|
// router.get("/purchases", purchaseController.lotteryPurchases);
|
||||||
router.get("/purchase/statistics", retrieve.allPurchase);
|
// // returns list per date and count of each colors that where bought
|
||||||
router.get("/purchase/statistics/color", retrieve.purchaseByColor);
|
// router.get("/purchases/summary", purchaseController.lotteryPurchases);
|
||||||
router.get("/highscore/statistics", retrieve.highscore)
|
// // returns total, wins?, stolen
|
||||||
router.get("/wines/statistics", retrieve.allWines);
|
// router.get("/purchase/:date", purchaseController.lotteryPurchaseByDate);
|
||||||
router.get("/wines/statistics/overall", retrieve.allWinesSummary);
|
|
||||||
|
|
||||||
router.get("/lottery/all", lottery.all);
|
router.get("/lottery/wines", prelotteryWineController.allWines);
|
||||||
router.get("/lottery/latest", lottery.latest);
|
router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema);
|
||||||
router.get("/lottery/by-date/:date", lottery.byEpochDate);
|
router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById);
|
||||||
router.get("/lottery/by-name/:name", lottery.byName);
|
router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines);
|
||||||
|
router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines);
|
||||||
|
router.put("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.updateWineById);
|
||||||
|
router.delete("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.deleteWineById);
|
||||||
|
|
||||||
router.delete('/virtual/winner/all', mustBeAuthenticated, virtualApi.deleteWinners);
|
router.get("/lottery/attendees", setAdminHeaderIfAuthenticated, attendeeController.allAttendees);
|
||||||
router.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees);
|
router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees);
|
||||||
router.get('/virtual/winner/draw', virtualApi.drawWinner);
|
router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee);
|
||||||
router.get('/virtual/winner/all', virtualApi.winners);
|
router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById);
|
||||||
router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure);
|
router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById);
|
||||||
router.post('/virtual/finish', mustBeAuthenticated, virtualApi.finish);
|
|
||||||
router.get('/virtual/attendee/all', virtualApi.attendees);
|
|
||||||
router.get('/virtual/attendee/all/secure', mustBeAuthenticated, virtualApi.attendeesSecure);
|
|
||||||
router.post('/virtual/attendee/add', mustBeAuthenticated, virtualApi.addAttendee);
|
|
||||||
|
|
||||||
router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById);
|
router.get("/lottery/winners", winnerController.allWinners);
|
||||||
router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById);
|
router.get("/lottery/winner/:id", winnerController.winnerById);
|
||||||
router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection);
|
router.post("/lottery/winners", mustBeAuthenticated, winnerController.addWinners);
|
||||||
|
router.delete("/lottery/winners", mustBeAuthenticated, winnerController.deleteWinners);
|
||||||
|
router.put("/lottery/winner/:id", mustBeAuthenticated, winnerController.updateWinnerById);
|
||||||
|
router.delete("/lottery/winner/:id", mustBeAuthenticated, winnerController.deleteWinnerById);
|
||||||
|
|
||||||
router.get('/chat/history', chatHistoryApi.getAllHistory)
|
router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner);
|
||||||
router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory)
|
router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery);
|
||||||
|
router.get("/lottery/:epoch", lotteryController.lotteryByDate);
|
||||||
|
router.get("/lotteries/", lotteryController.allLotteries);
|
||||||
|
|
||||||
router.post('/login', userApi.login);
|
// router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status);
|
||||||
router.post('/register', mustBeAuthenticated, userApi.register);
|
router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start);
|
||||||
router.get('/logout', userApi.logout);
|
// router.post("/lottery/prize-distribution/stop", mustBeAuthenticated, prizeDistributionController.stop);
|
||||||
|
router.get("/lottery/prize-distribution/prizes/:id", prizeDistributionController.getPrizesForWinnerById);
|
||||||
|
router.post("/lottery/prize-distribution/prize/:id", prizeDistributionController.submitPrizeForWinnerById);
|
||||||
|
|
||||||
|
router.post("/lottery/messages/winner/:id", mustBeAuthenticated, messageController.notifyWinnerById);
|
||||||
|
|
||||||
|
router.get("/chat/history", chatController.getAllHistory);
|
||||||
|
router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory);
|
||||||
|
|
||||||
|
router.post("/login", userController.login);
|
||||||
|
router.post("/register", mustBeAuthenticated, userController.register);
|
||||||
|
router.get("/logout", userController.logout);
|
||||||
|
|
||||||
|
// router.get("/", documentation.apiInfo);
|
||||||
|
|
||||||
|
// router.get("/wine/schema", mustBeAuthenticated, update.schema);
|
||||||
|
// router.get("/purchase/statistics", retrieve.allPurchase);
|
||||||
|
// router.get("/highscore/statistics", retrieve.highscore);
|
||||||
|
// router.get("/wines/statistics", retrieve.allWines);
|
||||||
|
// router.get("/wines/statistics/overall", retrieve.allWinesSummary);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ const PreLotteryWine = new Schema({
|
|||||||
vivinoLink: String,
|
vivinoLink: String,
|
||||||
rating: Number,
|
rating: Number,
|
||||||
id: String,
|
id: String,
|
||||||
|
year: Number,
|
||||||
image: String,
|
image: String,
|
||||||
price: String,
|
price: String,
|
||||||
country: String
|
country: String,
|
||||||
|
winner: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "VirtualWinner"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);
|
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const VirtualWinner = new Schema({
|
|||||||
red: Number,
|
red: Number,
|
||||||
yellow: Number,
|
yellow: Number,
|
||||||
id: String,
|
id: String,
|
||||||
|
prize_selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
timestamp_drawn: Number,
|
timestamp_drawn: Number,
|
||||||
timestamp_sent: Number,
|
timestamp_sent: Number,
|
||||||
timestamp_limit: Number
|
timestamp_limit: Number
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
const mongoose = require("mongoose");
|
const mongoose = require("mongoose");
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
const Wine = new Schema({
|
const WineSchema = new Schema({
|
||||||
name: String,
|
name: String,
|
||||||
vivinoLink: String,
|
vivinoLink: String,
|
||||||
rating: Number,
|
rating: Number,
|
||||||
occurences: Number,
|
occurences: Number,
|
||||||
id: String,
|
id: String,
|
||||||
|
year: Number,
|
||||||
image: String,
|
image: String,
|
||||||
price: String,
|
price: String,
|
||||||
country: String
|
country: String
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = mongoose.model("Wine", Wine);
|
module.exports = mongoose.model("Wine", WineSchema);
|
||||||
|
|||||||
142
api/update.js
142
api/update.js
@@ -1,142 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const sub = require(path.join(__dirname, "/subscriptions"));
|
|
||||||
|
|
||||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
|
||||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
|
||||||
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
|
|
||||||
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
|
||||||
const PreLotteryWine = require(path.join(
|
|
||||||
__dirname, "/schemas/PreLotteryWine"
|
|
||||||
));
|
|
||||||
|
|
||||||
const submitWines = async (req, res) => {
|
|
||||||
const wines = req.body;
|
|
||||||
for (let i = 0; i < wines.length; i++) {
|
|
||||||
let wine = wines[i];
|
|
||||||
let newWonWine = new PreLotteryWine({
|
|
||||||
name: wine.name,
|
|
||||||
vivinoLink: wine.vivinoLink,
|
|
||||||
rating: wine.rating,
|
|
||||||
image: wine.image,
|
|
||||||
price: wine.price,
|
|
||||||
country: wine.country,
|
|
||||||
id: wine.id
|
|
||||||
});
|
|
||||||
await newWonWine.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
let subs = await Subscription.find();
|
|
||||||
console.log("Sending new wines w/ push notification to all subscribers.")
|
|
||||||
for (let i = 0; i < subs.length; i++) {
|
|
||||||
let subscription = subs[i]; //get subscription from your databse here.
|
|
||||||
|
|
||||||
const message = JSON.stringify({
|
|
||||||
message: "Dagens vin er lagt til, se den på lottis.vin/dagens!",
|
|
||||||
title: "Ny vin!",
|
|
||||||
link: "/#/dagens"
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
sub.sendNotification(subscription, message);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error when trying to send push notification to subscriber.");
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
message: "Submitted and notified push subscribers of new wines!",
|
|
||||||
success: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const schema = async (req, res) => {
|
|
||||||
let schema = { ...PreLotteryWine.schema.obj };
|
|
||||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
|
||||||
accumulator[current] = "";
|
|
||||||
return accumulator
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return res.send(nulledSchema);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO IMPLEMENT WITH FRONTEND (unused)
|
|
||||||
const submitWinesToLottery = async (req, res) => {
|
|
||||||
const { lottery } = req.body;
|
|
||||||
const { date, wines } = lottery;
|
|
||||||
const wineObjects = await Promise.all(wines.map(async (wine) => await _wineFunctions.findSaveWine(wine)))
|
|
||||||
|
|
||||||
return Lottery.findOneAndUpdate({ date: date }, {
|
|
||||||
date: date,
|
|
||||||
wines: wineObjects
|
|
||||||
}, {
|
|
||||||
upsert: true
|
|
||||||
}).then(_ => res.send(true))
|
|
||||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving wine to lottery.',
|
|
||||||
success: false,
|
|
||||||
exception: err.message }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @apiParam (Request body) {Array} winners List of winners
|
|
||||||
*/
|
|
||||||
const submitWinnersToLottery = async (req, res) => {
|
|
||||||
const { lottery } = req.body;
|
|
||||||
const { winners, date } = lottery;
|
|
||||||
|
|
||||||
for (let i = 0; i < winners.length; i++) {
|
|
||||||
let currentWinner = winners[i];
|
|
||||||
let wonWine = await _wineFunctions.findSaveWine(currentWinner.wine); // TODO rename to findAndSaveWineToLottery
|
|
||||||
await _personFunctions.findSavePerson(currentWinner, wonWine, date); // TODO rename to findAndSaveWineToPerson
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @apiParam (Request body) {Date} date Date of lottery
|
|
||||||
* @apiParam (Request body) {Number} blue Number of blue tickets
|
|
||||||
* @apiParam (Request body) {Number} red Number of red tickets
|
|
||||||
* @apiParam (Request body) {Number} green Number of green tickets
|
|
||||||
* @apiParam (Request body) {Number} yellow Number of yellow tickets
|
|
||||||
* @apiParam (Request body) {Number} bought Number of tickets bought
|
|
||||||
* @apiParam (Request body) {Number} stolen Number of tickets stolen
|
|
||||||
*/
|
|
||||||
const submitLottery = async (req, res) => {
|
|
||||||
const { lottery } = req.body
|
|
||||||
|
|
||||||
const { date,
|
|
||||||
blue,
|
|
||||||
red,
|
|
||||||
yellow,
|
|
||||||
green,
|
|
||||||
bought,
|
|
||||||
stolen } = lottery;
|
|
||||||
|
|
||||||
return Lottery.findOneAndUpdate({ date: date }, {
|
|
||||||
date: date,
|
|
||||||
blue: blue,
|
|
||||||
yellow: yellow,
|
|
||||||
red: red,
|
|
||||||
green: green,
|
|
||||||
bought: bought,
|
|
||||||
stolen: stolen
|
|
||||||
}, {
|
|
||||||
upsert: true
|
|
||||||
}).then(_ => res.send(true))
|
|
||||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving lottery.',
|
|
||||||
success: false,
|
|
||||||
exception: err.message }));
|
|
||||||
|
|
||||||
return res.send(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
submitWines,
|
|
||||||
schema,
|
|
||||||
submitLottery,
|
|
||||||
submitWinnersToLottery,
|
|
||||||
submitWinesToLottery
|
|
||||||
};
|
|
||||||
111
api/user.js
111
api/user.js
@@ -1,51 +1,90 @@
|
|||||||
const passport = require("passport");
|
const passport = require("passport");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const User = require(path.join(__dirname, "/schemas/User"));
|
const User = require(path.join(__dirname, "/schemas/User"));
|
||||||
const router = require("express").Router();
|
|
||||||
|
|
||||||
const register = (req, res, next) => {
|
class UserExistsError extends Error {
|
||||||
User.register(
|
constructor(message = "Username already exists.") {
|
||||||
new User({ username: req.body.username }),
|
super(message);
|
||||||
req.body.password,
|
this.name = "UserExists";
|
||||||
function(err) {
|
this.statusCode = 409;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MissingUsernameError extends Error {
|
||||||
|
constructor(message = "No username given.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "MissingUsernameError";
|
||||||
|
this.statusCode = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MissingPasswordError extends Error {
|
||||||
|
constructor(message = "No password given.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "MissingPasswordError";
|
||||||
|
this.statusCode = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IncorrectUserCredentialsError extends Error {
|
||||||
|
constructor(message = "Incorrect username or password") {
|
||||||
|
super(message);
|
||||||
|
this.name = "IncorrectUserCredentialsError";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userAuthenticationErrorHandler(err) {
|
||||||
|
if (err.name == "UserExistsError") {
|
||||||
|
throw new UserExistsError(err.message);
|
||||||
|
} else if (err.name == "MissingUsernameError") {
|
||||||
|
throw new MissingUsernameError(err.message);
|
||||||
|
} else if (err.name == "MissingPasswordError") {
|
||||||
|
throw new MissingPasswordError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = (username, password) => {
|
||||||
|
return User.register(new User({ username: username }), password).catch(userAuthenticationErrorHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticate = req => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (username == undefined) throw new MissingUsernameError();
|
||||||
|
if (password == undefined) throw new MissingPasswordError();
|
||||||
|
|
||||||
|
passport.authenticate("local", function(err, user, info) {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.name == "UserExistsError")
|
reject(err);
|
||||||
res.status(409).send({ success: false, message: err.message })
|
|
||||||
else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
|
|
||||||
res.status(400).send({ success: false, message: err.message })
|
|
||||||
return next(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
|
if (!user) {
|
||||||
}
|
reject(new IncorrectUserCredentialsError());
|
||||||
);
|
}
|
||||||
|
|
||||||
|
resolve(user);
|
||||||
|
})(req);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = (req, res, next) => {
|
const login = (req, user) => {
|
||||||
passport.authenticate("local", function(err, user, info) {
|
return new Promise((resolve, reject) => {
|
||||||
if (err) {
|
req.logIn(user, err => {
|
||||||
if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
|
if (err) {
|
||||||
return res.status(400).send({ message: err.message, success: false })
|
reject(err);
|
||||||
return next(err);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false })
|
resolve(user);
|
||||||
|
});
|
||||||
req.logIn(user, (err) => {
|
});
|
||||||
if (err) { return next(err) }
|
|
||||||
|
|
||||||
return res.status(200).send({ message: "Velkommen " + user.username, success: true })
|
|
||||||
})
|
|
||||||
})(req, res, next);
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = (req, res) => {
|
|
||||||
req.logout();
|
|
||||||
res.redirect("/");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
register,
|
register,
|
||||||
login,
|
authenticate,
|
||||||
logout
|
login
|
||||||
};
|
};
|
||||||
|
|||||||
90
api/vinlottisErrors.js
Normal file
90
api/vinlottisErrors.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
class UserNotFound extends Error {
|
||||||
|
constructor(message = "User not found.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "UserNotFound";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO log missing user
|
||||||
|
}
|
||||||
|
|
||||||
|
class WineNotFound extends Error {
|
||||||
|
constructor(message = "Wine not found.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "WineNotFound";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO log missing user
|
||||||
|
}
|
||||||
|
|
||||||
|
class WinnerNotFound extends Error {
|
||||||
|
constructor(message = "Winner not found.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "WinnerNotFound";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO log missing user
|
||||||
|
}
|
||||||
|
|
||||||
|
class WinnersTimelimitExpired extends Error {
|
||||||
|
constructor(message = "Timelimit expired, you will need to wait until it's your turn again.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "WinnersTimelimitExpired";
|
||||||
|
this.statusCode = 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WineSelectionWinnerNotNextInLine extends Error {
|
||||||
|
constructor(message = "Not the winner next in line!") {
|
||||||
|
super(message);
|
||||||
|
this.name = "WineSelectionWinnerNotNextInLine";
|
||||||
|
this.statusCode = 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO log missing user
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoMoreAttendeesToWin extends Error {
|
||||||
|
constructor(message = "No more attendees left to drawn from.") {
|
||||||
|
super(message);
|
||||||
|
this.name = "NoMoreAttendeesToWin";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CouldNotFindNewWinnerAfterNTries extends Error {
|
||||||
|
constructor(tries) {
|
||||||
|
let message = `Could not a new winner after ${tries} tries.`;
|
||||||
|
super(message);
|
||||||
|
this.name = "CouldNotFindNewWinnerAfterNTries";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LotteryByDateNotFound extends Error {
|
||||||
|
constructor(date) {
|
||||||
|
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||||
|
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||||
|
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||||
|
|
||||||
|
const dateString = `${ye}-${mo}-${da}`;
|
||||||
|
const dateUnix = date.getTime();
|
||||||
|
const message = `Could not find lottery for date: ${dateString}.`;
|
||||||
|
super(message);
|
||||||
|
this.name = "LotteryByDateNotFoundError";
|
||||||
|
this.statusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
UserNotFound,
|
||||||
|
WineNotFound,
|
||||||
|
WinnerNotFound,
|
||||||
|
WinnersTimelimitExpired,
|
||||||
|
WineSelectionWinnerNotNextInLine,
|
||||||
|
NoMoreAttendeesToWin,
|
||||||
|
CouldNotFindNewWinnerAfterNTries,
|
||||||
|
LotteryByDateNotFound
|
||||||
|
};
|
||||||
114
api/vinmonopolet.js
Normal file
114
api/vinmonopolet.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
const fetch = require("node-fetch");
|
||||||
|
const path = require("path");
|
||||||
|
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
||||||
|
|
||||||
|
const convertToOurWineObject = wine => {
|
||||||
|
if (wine.basic.ageLimit === "18") {
|
||||||
|
return {
|
||||||
|
name: wine.basic.productShortName,
|
||||||
|
vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId,
|
||||||
|
rating: wine.basic.alcoholContent,
|
||||||
|
occurences: 0,
|
||||||
|
id: wine.basic.productId,
|
||||||
|
year: wine.basic.vintage,
|
||||||
|
image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`,
|
||||||
|
price: wine.prices[0].salesPrice.toString(),
|
||||||
|
country: wine.origins.origin.country
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToOurStoreObject = store => {
|
||||||
|
return {
|
||||||
|
id: store.storeId,
|
||||||
|
name: store.storeName,
|
||||||
|
...store.address
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchWinesByName = async (name, page = 1) => {
|
||||||
|
const pageSize = 15;
|
||||||
|
let url = new URL(
|
||||||
|
`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=gato&maxResults=15`
|
||||||
|
);
|
||||||
|
url.searchParams.set("maxResults", pageSize);
|
||||||
|
url.searchParams.set("start", pageSize * (page - 1));
|
||||||
|
url.searchParams.set("productShortNameContains", name);
|
||||||
|
|
||||||
|
const vinmonopoletResponse = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
|
||||||
|
if (vinmonopoletResponse.errors != null) {
|
||||||
|
return vinmonopoletResponse.errors.map(error => {
|
||||||
|
if (error.type == "UnknownProductError") {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: error.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean);
|
||||||
|
|
||||||
|
return winesConverted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wineByEAN = ean => {
|
||||||
|
const url = `https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/${ean}`;
|
||||||
|
return fetch(url)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.map(convertToOurWineObject));
|
||||||
|
};
|
||||||
|
|
||||||
|
const wineById = id => {
|
||||||
|
const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`;
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.map(convertToOurWineObject));
|
||||||
|
};
|
||||||
|
|
||||||
|
const allStores = () => {
|
||||||
|
const url = `https://apis.vinmonopolet.no/stores/v0/details`;
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.map(convertToOurStoreObject));
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchStoresByName = name => {
|
||||||
|
const url = `https://apis.vinmonopolet.no/stores/v0/details?storeNameContains=${name}`;
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.map(convertToOurStoreObject));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
searchWinesByName,
|
||||||
|
wineByEAN,
|
||||||
|
wineById,
|
||||||
|
allStores,
|
||||||
|
searchStoresByName
|
||||||
|
};
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
|
|
||||||
const config = require(path.join(__dirname, "/../config/defaults/lottery"));
|
|
||||||
const Message = require(path.join(__dirname, "/message"));
|
|
||||||
const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration"));
|
|
||||||
|
|
||||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
|
||||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
|
||||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
|
||||||
|
|
||||||
|
|
||||||
const winners = async (req, res) => {
|
|
||||||
let winners = await VirtualWinner.find();
|
|
||||||
let winnersRedacted = [];
|
|
||||||
let winner;
|
|
||||||
for (let i = 0; i < winners.length; i++) {
|
|
||||||
winner = winners[i];
|
|
||||||
winnersRedacted.push({
|
|
||||||
name: winner.name,
|
|
||||||
color: winner.color
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.json(winnersRedacted);
|
|
||||||
};
|
|
||||||
|
|
||||||
const winnersSecure = async (req, res) => {
|
|
||||||
let winners = await VirtualWinner.find();
|
|
||||||
|
|
||||||
return res.json(winners);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteWinners = async (req, res) => {
|
|
||||||
await VirtualWinner.deleteMany();
|
|
||||||
var io = req.app.get('socketio');
|
|
||||||
io.emit("refresh_data", {});
|
|
||||||
return res.json(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const attendees = async (req, res) => {
|
|
||||||
let attendees = await Attendee.find();
|
|
||||||
let attendeesRedacted = [];
|
|
||||||
let attendee;
|
|
||||||
for (let i = 0; i < attendees.length; i++) {
|
|
||||||
attendee = attendees[i];
|
|
||||||
attendeesRedacted.push({
|
|
||||||
name: attendee.name,
|
|
||||||
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
|
||||||
red: attendee.red,
|
|
||||||
blue: attendee.blue,
|
|
||||||
green: attendee.green,
|
|
||||||
yellow: attendee.yellow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.json(attendeesRedacted);
|
|
||||||
};
|
|
||||||
|
|
||||||
const attendeesSecure = async (req, res) => {
|
|
||||||
let attendees = await Attendee.find();
|
|
||||||
|
|
||||||
return res.json(attendees);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAttendee = async (req, res) => {
|
|
||||||
const attendee = req.body;
|
|
||||||
const { red, blue, yellow, green } = attendee;
|
|
||||||
|
|
||||||
let newAttendee = new Attendee({
|
|
||||||
name: attendee.name,
|
|
||||||
red,
|
|
||||||
blue,
|
|
||||||
green,
|
|
||||||
yellow,
|
|
||||||
phoneNumber: attendee.phoneNumber,
|
|
||||||
winner: false
|
|
||||||
});
|
|
||||||
await newAttendee.save();
|
|
||||||
|
|
||||||
|
|
||||||
var io = req.app.get('socketio');
|
|
||||||
io.emit("new_attendee", {});
|
|
||||||
|
|
||||||
return res.send(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAttendees = async (req, res) => {
|
|
||||||
await Attendee.deleteMany();
|
|
||||||
var io = req.app.get('socketio');
|
|
||||||
io.emit("refresh_data", {});
|
|
||||||
return res.json(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const drawWinner = async (req, res) => {
|
|
||||||
let allContestants = await Attendee.find({ winner: false });
|
|
||||||
|
|
||||||
if (allContestants.length == 0) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
message: "No attendees left that have not won."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let raffleColors = [];
|
|
||||||
for (let i = 0; i < allContestants.length; i++) {
|
|
||||||
let currentContestant = allContestants[i];
|
|
||||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
|
||||||
raffleColors.push("blue");
|
|
||||||
}
|
|
||||||
for (let red = 0; red < currentContestant.red; red++) {
|
|
||||||
raffleColors.push("red");
|
|
||||||
}
|
|
||||||
for (let green = 0; green < currentContestant.green; green++) {
|
|
||||||
raffleColors.push("green");
|
|
||||||
}
|
|
||||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
|
||||||
raffleColors.push("yellow");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
raffleColors = shuffle(raffleColors);
|
|
||||||
|
|
||||||
let colorToChooseFrom =
|
|
||||||
raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
|
||||||
let findObject = { winner: false };
|
|
||||||
|
|
||||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
|
||||||
|
|
||||||
let tries = 0;
|
|
||||||
const maxTries = 3;
|
|
||||||
let contestantsToChooseFrom = undefined;
|
|
||||||
while (contestantsToChooseFrom == undefined && tries < maxTries) {
|
|
||||||
const hit = await Attendee.find(findObject);
|
|
||||||
if (hit && hit.length) {
|
|
||||||
contestantsToChooseFrom = hit;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tries++;
|
|
||||||
}
|
|
||||||
if (contestantsToChooseFrom == undefined) {
|
|
||||||
return res.status(404).send({
|
|
||||||
success: false,
|
|
||||||
message: `Klarte ikke trekke en vinner etter ${maxTries} forsøk.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let attendeeListDemocratic = [];
|
|
||||||
|
|
||||||
let currentContestant;
|
|
||||||
for (let i = 0; i < contestantsToChooseFrom.length; i++) {
|
|
||||||
currentContestant = contestantsToChooseFrom[i];
|
|
||||||
for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) {
|
|
||||||
attendeeListDemocratic.push({
|
|
||||||
name: currentContestant.name,
|
|
||||||
phoneNumber: currentContestant.phoneNumber,
|
|
||||||
red: currentContestant.red,
|
|
||||||
blue: currentContestant.blue,
|
|
||||||
green: currentContestant.green,
|
|
||||||
yellow: currentContestant.yellow
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attendeeListDemocratic = shuffle(attendeeListDemocratic);
|
|
||||||
|
|
||||||
let winner =
|
|
||||||
attendeeListDemocratic[
|
|
||||||
Math.floor(Math.random() * attendeeListDemocratic.length)
|
|
||||||
];
|
|
||||||
|
|
||||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
|
||||||
timestamp_drawn: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
var io = req.app.get('socketio');
|
|
||||||
io.emit("winner", {
|
|
||||||
color: colorToChooseFrom,
|
|
||||||
name: winner.name,
|
|
||||||
winner_count: winners.length + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
let newWinnerElement = new VirtualWinner({
|
|
||||||
name: winner.name,
|
|
||||||
phoneNumber: winner.phoneNumber,
|
|
||||||
color: colorToChooseFrom,
|
|
||||||
red: winner.red,
|
|
||||||
blue: winner.blue,
|
|
||||||
green: winner.green,
|
|
||||||
yellow: winner.yellow,
|
|
||||||
id: sha512(winner.phoneNumber, genRandomString(10)),
|
|
||||||
timestamp_drawn: new Date().getTime()
|
|
||||||
});
|
|
||||||
|
|
||||||
await Attendee.update(
|
|
||||||
{ name: winner.name, phoneNumber: winner.phoneNumber },
|
|
||||||
{ $set: { winner: true } }
|
|
||||||
);
|
|
||||||
|
|
||||||
await newWinnerElement.save();
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
winner
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const finish = async (req, res) => {
|
|
||||||
if (!config.gatewayToken) {
|
|
||||||
return res.json({
|
|
||||||
message: "Missing api token for sms gateway.",
|
|
||||||
success: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
|
||||||
timestamp_drawn: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
if (winners.length == 0) {
|
|
||||||
return res.json({
|
|
||||||
message: "No winners to draw from.",
|
|
||||||
success: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Message.sendInitialMessageToWinners(winners.slice(1));
|
|
||||||
|
|
||||||
return findAndNotifyNextWinner()
|
|
||||||
.then(() => res.json({
|
|
||||||
success: true,
|
|
||||||
message: "Sent wine select message to first winner and update message to rest of winners."
|
|
||||||
}))
|
|
||||||
.catch(error => res.json({
|
|
||||||
message: error["message"] || "Unable to send message to first winner.",
|
|
||||||
success: false
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
const genRandomString = function(length) {
|
|
||||||
return crypto
|
|
||||||
.randomBytes(Math.ceil(length / 2))
|
|
||||||
.toString("hex") /** convert to hexadecimal format */
|
|
||||||
.slice(0, length); /** return required number of characters */
|
|
||||||
};
|
|
||||||
|
|
||||||
const sha512 = function(password, salt) {
|
|
||||||
var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */
|
|
||||||
hash.update(password);
|
|
||||||
var value = hash.digest("hex");
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
function shuffle(array) {
|
|
||||||
let currentIndex = array.length,
|
|
||||||
temporaryValue,
|
|
||||||
randomIndex;
|
|
||||||
|
|
||||||
// While there remain elements to shuffle...
|
|
||||||
while (0 !== currentIndex) {
|
|
||||||
// Pick a remaining element...
|
|
||||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
||||||
currentIndex -= 1;
|
|
||||||
|
|
||||||
// And swap it with the current element.
|
|
||||||
temporaryValue = array[currentIndex];
|
|
||||||
array[currentIndex] = array[randomIndex];
|
|
||||||
array[randomIndex] = temporaryValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
deleteWinners,
|
|
||||||
deleteAttendees,
|
|
||||||
winners,
|
|
||||||
winnersSecure,
|
|
||||||
drawWinner,
|
|
||||||
finish,
|
|
||||||
attendees,
|
|
||||||
attendeesSecure,
|
|
||||||
addAttendee
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
|
||||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
|
||||||
const Message = require(path.join(__dirname, "/message"));
|
|
||||||
const VirtualWinner = require(path.join(
|
|
||||||
__dirname, "/schemas/VirtualWinner"
|
|
||||||
));
|
|
||||||
const PreLotteryWine = require(path.join(
|
|
||||||
__dirname, "/schemas/PreLotteryWine"
|
|
||||||
));
|
|
||||||
|
|
||||||
|
|
||||||
const getWinesToWinnerById = async (req, res) => {
|
|
||||||
let id = req.params.id;
|
|
||||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
|
||||||
|
|
||||||
if (!foundWinner) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
message: "No winner with this id.",
|
|
||||||
existing: false,
|
|
||||||
turn: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
|
||||||
if (
|
|
||||||
allWinners[0].id != foundWinner.id ||
|
|
||||||
foundWinner.timestamp_limit == undefined ||
|
|
||||||
foundWinner.timestamp_sent == undefined
|
|
||||||
) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
message: "Not the winner next in line!",
|
|
||||||
existing: true,
|
|
||||||
turn: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
existing: true,
|
|
||||||
turn: true,
|
|
||||||
name: foundWinner.name,
|
|
||||||
color: foundWinner.color
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerWinnerSelection = async (req, res) => {
|
|
||||||
let id = req.params.id;
|
|
||||||
let wineName = req.body.wineName;
|
|
||||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
|
||||||
|
|
||||||
if (!foundWinner) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
message: "No winner with this id."
|
|
||||||
})
|
|
||||||
} else if (foundWinner.timestamp_limit < new Date().getTime()) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
message: "Timelimit expired, you will receive a wine after other users have chosen.",
|
|
||||||
limit: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let date = new Date();
|
|
||||||
date.setHours(5, 0, 0, 0);
|
|
||||||
let prelotteryWine = await PreLotteryWine.findOne({ name: wineName });
|
|
||||||
|
|
||||||
if (!prelotteryWine) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
message: "No wine with this name.",
|
|
||||||
wine: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let wonWine = await _wineFunctions.findSaveWine(prelotteryWine);
|
|
||||||
await prelotteryWine.delete();
|
|
||||||
await _personFunctions.findSavePerson(foundWinner, wonWine, date);
|
|
||||||
await Message.sendWineConfirmation(foundWinner, wonWine, date);
|
|
||||||
|
|
||||||
await foundWinner.delete();
|
|
||||||
console.info("Saved winners choice.");
|
|
||||||
|
|
||||||
return findAndNotifyNextWinner()
|
|
||||||
.then(() => res.json({
|
|
||||||
message: "Choice saved and next in line notified.",
|
|
||||||
success: true
|
|
||||||
}))
|
|
||||||
.catch(error => res.json({
|
|
||||||
message: error["message"] || "Error when notifing next winner.",
|
|
||||||
success: false
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
const chooseLastWineForUser = (winner, preLotteryWine) => {
|
|
||||||
let date = new Date();
|
|
||||||
date.setHours(5, 0, 0, 0);
|
|
||||||
|
|
||||||
return _wineFunctions.findSaveWine(preLotteryWine)
|
|
||||||
.then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date))
|
|
||||||
.then(() => preLotteryWine.delete())
|
|
||||||
.then(() => Message.sendLastWinnerMessage(winner, preLotteryWine))
|
|
||||||
.then(() => winner.delete())
|
|
||||||
.catch(err => {
|
|
||||||
console.log("Error thrown from chooseLastWineForUser: " + err);
|
|
||||||
throw err;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const findAndNotifyNextWinner = async () => {
|
|
||||||
let nextWinner = undefined;
|
|
||||||
|
|
||||||
let winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
|
||||||
let winesLeft = await PreLotteryWine.find();
|
|
||||||
|
|
||||||
if (winnersLeft.length > 1) {
|
|
||||||
console.log("multiple winners left, choose next in line")
|
|
||||||
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
|
||||||
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
|
||||||
console.log("one winner left, but multiple wines")
|
|
||||||
nextWinner = winnersLeft[0] // one winner left, but multiple wines
|
|
||||||
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
|
||||||
console.log("one winner and one wine left, choose for user")
|
|
||||||
nextWinner = winnersLeft[0] // one winner and one wine left, choose for user
|
|
||||||
wine = winesLeft[0]
|
|
||||||
return chooseLastWineForUser(nextWinner, wine);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextWinner) {
|
|
||||||
return Message.sendWineSelectMessage(nextWinner)
|
|
||||||
.then(messageResponse => startTimeout(nextWinner.id))
|
|
||||||
} else {
|
|
||||||
console.info("All winners notified. Could start cleanup here.");
|
|
||||||
return Promise.resolve({
|
|
||||||
message: "All winners notified."
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendNotificationToWinnerById = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
let winner = await VirtualWinner.findOne({ id: id });
|
|
||||||
|
|
||||||
if (!winner) {
|
|
||||||
return res.json({
|
|
||||||
message: "No winner with this id.",
|
|
||||||
success: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Message.sendWineSelectMessage(winner)
|
|
||||||
.then(success => res.json({
|
|
||||||
success: success,
|
|
||||||
message: `Message sent to winner ${id} successfully!`
|
|
||||||
}))
|
|
||||||
.catch(err => res.json({
|
|
||||||
success: false,
|
|
||||||
message: "Error while trying to send sms.",
|
|
||||||
error: err
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function startTimeout(id) {
|
|
||||||
const minute = 60000;
|
|
||||||
const minutesForTimeout = 10;
|
|
||||||
|
|
||||||
console.log(`Starting timeout for user ${id}.`);
|
|
||||||
console.log(`Timeout duration: ${ minutesForTimeout * minute }`)
|
|
||||||
setTimeout(async () => {
|
|
||||||
let virtualWinner = await VirtualWinner.findOne({ id: id });
|
|
||||||
if (!virtualWinner) {
|
|
||||||
console.log(`Timeout done for user ${id}, but user has already sent data.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(`Timeout done for user ${id}, sending update to user.`);
|
|
||||||
|
|
||||||
Message.sendWineSelectMessageTooLate(virtualWinner);
|
|
||||||
|
|
||||||
virtualWinner.timestamp_drawn = new Date().getTime();
|
|
||||||
virtualWinner.timestamp_limit = null;
|
|
||||||
virtualWinner.timestamp_sent = null;
|
|
||||||
await virtualWinner.save();
|
|
||||||
|
|
||||||
findAndNotifyNextWinner();
|
|
||||||
}, minutesForTimeout * minute);
|
|
||||||
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getWinesToWinnerById,
|
|
||||||
registerWinnerSelection,
|
|
||||||
findAndNotifyNextWinner,
|
|
||||||
|
|
||||||
sendNotificationToWinnerById
|
|
||||||
};
|
|
||||||
72
api/wine.js
72
api/wine.js
@@ -1,27 +1,63 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||||
|
|
||||||
async function findSaveWine(prelotteryWine) {
|
const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||||
let wonWine = await Wine.findOne({ name: prelotteryWine.name });
|
|
||||||
if (wonWine == undefined) {
|
const addWine = async wine => {
|
||||||
let newWonWine = new Wine({
|
let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year });
|
||||||
name: prelotteryWine.name,
|
|
||||||
vivinoLink: prelotteryWine.vivinoLink,
|
if (existingWine == undefined) {
|
||||||
rating: prelotteryWine.rating,
|
let newWine = new Wine({
|
||||||
|
name: wine.name,
|
||||||
|
vivinoLink: wine.vivinoLink,
|
||||||
|
rating: wine.rating,
|
||||||
occurences: 1,
|
occurences: 1,
|
||||||
image: prelotteryWine.image,
|
id: wine.id,
|
||||||
id: prelotteryWine.id
|
year: wine.year,
|
||||||
|
image: wine.image,
|
||||||
|
price: wine.price,
|
||||||
|
country: wine.country
|
||||||
});
|
});
|
||||||
await newWonWine.save();
|
await newWine.save();
|
||||||
wonWine = newWonWine;
|
return newWine;
|
||||||
} else {
|
} else {
|
||||||
wonWine.occurences += 1;
|
existingWine.occurences += 1;
|
||||||
wonWine.image = prelotteryWine.image;
|
await existingWine.save();
|
||||||
wonWine.id = prelotteryWine.id;
|
return existingWine;
|
||||||
await wonWine.save();
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return wonWine;
|
const allWines = (limit = undefined) => {
|
||||||
}
|
if (limit) {
|
||||||
|
return Wine.find().limit(limit);
|
||||||
|
} else {
|
||||||
|
return Wine.find();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.findSaveWine = findSaveWine;
|
const wineById = id => {
|
||||||
|
return Wine.findOne({ _id: id }).then(wine => {
|
||||||
|
if (wine == null) {
|
||||||
|
throw new WineNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return wine;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWine = wine => {
|
||||||
|
return Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }).then(wine => {
|
||||||
|
if (wine == null) {
|
||||||
|
throw new WineNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return wine;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addWine,
|
||||||
|
allWines,
|
||||||
|
wineById,
|
||||||
|
findWine
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
const fetch = require('node-fetch')
|
|
||||||
const path = require('path')
|
|
||||||
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
|
||||||
|
|
||||||
const convertToOurWineObject = wine => {
|
|
||||||
if(wine.basic.ageLimit === "18"){
|
|
||||||
return {
|
|
||||||
name: wine.basic.productShortName,
|
|
||||||
vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId,
|
|
||||||
rating: wine.basic.alcoholContent,
|
|
||||||
occurences: 0,
|
|
||||||
id: wine.basic.productId,
|
|
||||||
image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`,
|
|
||||||
price: wine.prices[0].salesPrice.toString(),
|
|
||||||
country: wine.origins.origin.country
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const wineSearch = async (req, res) => {
|
|
||||||
const {query} = req.query
|
|
||||||
let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`)
|
|
||||||
url.searchParams.set('productShortNameContains', query)
|
|
||||||
|
|
||||||
const vinmonopoletResponse = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(resp => resp.json())
|
|
||||||
.catch(err => console.error(err))
|
|
||||||
|
|
||||||
|
|
||||||
if (vinmonopoletResponse.errors != null) {
|
|
||||||
return vinmonopoletResponse.errors.map(error => {
|
|
||||||
if (error.type == "UnknownProductError") {
|
|
||||||
return res.status(404).json({
|
|
||||||
message: error.message
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean)
|
|
||||||
|
|
||||||
return res.send(winesConverted);
|
|
||||||
}
|
|
||||||
|
|
||||||
const byEAN = async (req, res) => {
|
|
||||||
const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean)
|
|
||||||
.then(resp => resp.json())
|
|
||||||
|
|
||||||
if (vinmonopoletResponse.errors != null) {
|
|
||||||
return vinmonopoletResponse.errors.map(error => {
|
|
||||||
if (error.type == "UnknownProductError") {
|
|
||||||
return res.status(404).json({
|
|
||||||
message: error.message
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send(vinmonopoletResponse);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
byEAN,
|
|
||||||
wineSearch
|
|
||||||
};
|
|
||||||
95
api/winner.js
Normal file
95
api/winner.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||||
|
const { WinnerNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||||
|
|
||||||
|
const redactWinnerInfoMapper = winner => {
|
||||||
|
return {
|
||||||
|
name: winner.name,
|
||||||
|
color: winner.color
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWinners = winners => {
|
||||||
|
return Promise.all(
|
||||||
|
winners.map(winner => {
|
||||||
|
let newWinnerElement = new VirtualWinner({
|
||||||
|
name: winner.name,
|
||||||
|
color: winner.color,
|
||||||
|
timestamp_drawn: new Date().getTime()
|
||||||
|
});
|
||||||
|
|
||||||
|
return newWinnerElement.save();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const allWinners = (isAdmin = false) => {
|
||||||
|
const sortQuery = { timestamp_drawn: 1 };
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return VirtualWinner.find()
|
||||||
|
.sort(sortQuery)
|
||||||
|
.then(winners => winners.map(redactWinnerInfoMapper));
|
||||||
|
} else {
|
||||||
|
return VirtualWinner.find().sort(sortQuery);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const winnerById = (id, isAdmin = false) => {
|
||||||
|
return VirtualWinner.findOne({ id: id }).then(winner => {
|
||||||
|
if (winner == null) {
|
||||||
|
throw new WinnerNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return redactWinnerInfoMapper(winner);
|
||||||
|
}
|
||||||
|
return winner;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateWinnerById = (id, updateModel) => {
|
||||||
|
return VirtualWinner.findOne({ id: id }).then(winner => {
|
||||||
|
if (winner == null) {
|
||||||
|
throw new WinnerNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedWinner = {
|
||||||
|
name: updateModel.name != null ? updateModel.name : winner.name,
|
||||||
|
phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : winner.phoneNumber,
|
||||||
|
red: updateModel.red != null ? updateModel.red : winner.red,
|
||||||
|
green: updateModel.green != null ? updateModel.green : winner.green,
|
||||||
|
blue: updateModel.blue != null ? updateModel.blue : winner.blue,
|
||||||
|
yellow: updateModel.yellow != null ? updateModel.yellow : winner.yellow,
|
||||||
|
timestamp_drawn: updateModel.timestamp_drawn != null ? updateModel.timestamp_drawn : winner.timestamp_drawn,
|
||||||
|
timestamp_limit: updateModel.timestamp_limit != null ? updateModel.timestamp_limit : winner.timestamp_limit,
|
||||||
|
timestamp_sent: updateModel.timestamp_sent != null ? updateModel.timestamp_sent : winner.timestamp_sent
|
||||||
|
};
|
||||||
|
|
||||||
|
return VirtualWinner.updateOne({ id: id }, updatedWinner).then(_ => updatedWinner);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWinnerById = id => {
|
||||||
|
return VirtualWinner.findOne({ id: id }).then(winner => {
|
||||||
|
if (winner == null) {
|
||||||
|
throw new WinnerNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return VirtualWinner.deleteOne({ id: id }).then(_ => winner);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWinners = () => {
|
||||||
|
return VirtualWinner.deleteMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addWinners,
|
||||||
|
allWinners,
|
||||||
|
winnerById,
|
||||||
|
updateWinnerById,
|
||||||
|
deleteWinnerById,
|
||||||
|
deleteWinners
|
||||||
|
};
|
||||||
@@ -26,7 +26,8 @@ const allRequestedWines = () => {;
|
|||||||
return fetch("/api/request/all")
|
return fetch("/api/request/all")
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
const isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
const isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||||
return Promise.all([resp.json(), isAdmin]);
|
const getWinesFromBody = (resp) => resp.json().then(body => body.wines);
|
||||||
|
return Promise.all([getWinesFromBody(resp), isAdmin]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,8 +110,7 @@ const deleteRequestedWine = wineToBeDeleted => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
method: "DELETE",
|
method: "DELETE"
|
||||||
body: JSON.stringify(wineToBeDeleted)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch("api/request/" + wineToBeDeleted.id, options)
|
return fetch("api/request/" + wineToBeDeleted.id, options)
|
||||||
@@ -148,14 +148,12 @@ const attendees = () => {
|
|||||||
|
|
||||||
const requestNewWine = (wine) => {
|
const requestNewWine = (wine) => {
|
||||||
const options = {
|
const options = {
|
||||||
body: JSON.stringify({
|
method: "POST",
|
||||||
wine: wine
|
|
||||||
}),
|
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
method: "post"
|
body: JSON.stringify({ wine })
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch("/api/request/new-wine", options)
|
return fetch("/api/request/new-wine", options)
|
||||||
|
|||||||
@@ -1,34 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Admin-side</h1>
|
|
||||||
<Tabs :tabs="tabs" />
|
<Tabs :tabs="tabs" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Tabs from "@/ui/Tabs";
|
import Tabs from "@/ui/Tabs";
|
||||||
import RegisterPage from "@/components/RegisterPage";
|
import RegisterWinePage from "@/components/admin/RegisterWinePage";
|
||||||
import VirtualLotteryRegistrationPage from "@/components/VirtualLotteryRegistrationPage";
|
import archiveLotteryPage from "@/components/admin/archiveLotteryPage";
|
||||||
|
import registerAttendeePage from "@/components/admin/registerAttendeePage";
|
||||||
|
import DrawWinnerPage from "@/components/admin/DrawWinnerPage";
|
||||||
|
import PushPage from "@/components/admin/PushPage";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Tabs,
|
Tabs
|
||||||
RegisterPage,
|
|
||||||
VirtualLotteryRegistrationPage
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ name: "Registrering", component: RegisterPage },
|
{
|
||||||
{ name: "Virtuelt lotteri", component: VirtualLotteryRegistrationPage }
|
name: "Vin",
|
||||||
|
component: RegisterWinePage,
|
||||||
|
slug: "vin",
|
||||||
|
counter: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Legg til deltakere",
|
||||||
|
component: registerAttendeePage,
|
||||||
|
slug: "attendees",
|
||||||
|
counter: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Trekk vinner",
|
||||||
|
component: DrawWinnerPage,
|
||||||
|
slug: "draw",
|
||||||
|
counter: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Arkiver lotteri",
|
||||||
|
component: archiveLotteryPage,
|
||||||
|
slug: "reg",
|
||||||
|
counter: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Push meldinger",
|
||||||
|
component: PushPage,
|
||||||
|
slug: "push"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
h1 {
|
@import "@/styles/media-queries";
|
||||||
text-align: center;
|
|
||||||
|
.page-container {
|
||||||
|
padding: 0 1.5rem 3rem;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
max-width: 60vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,40 +2,50 @@
|
|||||||
<main class="container">
|
<main class="container">
|
||||||
<h1>Alle foreslåtte viner</h1>
|
<h1>Alle foreslåtte viner</h1>
|
||||||
|
|
||||||
<section class="requested-wines-container">
|
<section class="wines-container">
|
||||||
<p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p>
|
<p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p>
|
||||||
|
|
||||||
<RequestedWineCard v-for="requestedEl in wines" :key="requestedEl.wine._id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/>
|
<RequestedWineCard
|
||||||
|
v-for="requestedWine in wines"
|
||||||
|
:key="requestedWine.wine._id"
|
||||||
|
:requestedElement="requestedWine"
|
||||||
|
@wineDeleted="filterOutDeletedWine"
|
||||||
|
:showDeleteButton="isAdmin"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { allRequestedWines } from "@/api";
|
|
||||||
import RequestedWineCard from "@/ui/RequestedWineCard";
|
import RequestedWineCard from "@/ui/RequestedWineCard";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
RequestedWineCard
|
RequestedWineCard
|
||||||
},
|
},
|
||||||
data(){
|
data() {
|
||||||
return{
|
return {
|
||||||
wines: undefined,
|
wines: undefined,
|
||||||
canRequest: true,
|
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
};
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
filterOutDeletedWine(wine){
|
|
||||||
this.wines = this.wines.filter(item => item.wine._id !== wine._id)
|
|
||||||
},
|
|
||||||
async refreshData(){
|
|
||||||
[this.wines, this.isAdmin] = await allRequestedWines() || [[], false]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.refreshData()
|
this.fetchRequestedWines();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
filterOutDeletedWine(wine) {
|
||||||
|
this.wines = this.wines.filter(item => item.wine._id !== wine._id);
|
||||||
|
},
|
||||||
|
fetchRequestedWines() {
|
||||||
|
return fetch("/api/requests")
|
||||||
|
.then(resp => {
|
||||||
|
this.isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.wines = response.wines));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -55,10 +65,4 @@ h1 {
|
|||||||
color: $matte-text-color;
|
color: $matte-text-color;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.requested-wines-container{
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
grid-gap: 2rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="">Alle viner</h1>
|
<h1 class="">Alle viner</h1>
|
||||||
|
|
||||||
<div id="wines-container">
|
<div class="wines-container">
|
||||||
<Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id">
|
<Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id">
|
||||||
<div class="winners-container">
|
<div class="winners-container">
|
||||||
|
|
||||||
<span class="label">Vinnende lodd:</span>
|
<span class="label">Vinnende lodd:</span>
|
||||||
<div class="flex row">
|
<div class="flex row">
|
||||||
<span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span>
|
<span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span>
|
||||||
@@ -19,43 +18,44 @@
|
|||||||
<ul class="names">
|
<ul class="names">
|
||||||
<li v-for="(winner, index) in wine.winners">
|
<li v-for="(winner, index) in wine.winners">
|
||||||
<router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link>
|
<router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link>
|
||||||
-
|
-
|
||||||
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ dateString(wine.dates[index]) }}</router-link>
|
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{
|
||||||
|
dateString(wine.dates[index])
|
||||||
|
}}</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Wine>
|
</Wine>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Banner from "@/ui/Banner";
|
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
import { overallWineStatistics } from "@/api";
|
|
||||||
import { dateString } from "@/utils";
|
import { dateString } from "@/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: { Wine },
|
||||||
Banner,
|
|
||||||
Wine
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
wines: []
|
wines: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.overallWineStatistics();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
winDateUrl(date) {
|
winDateUrl(date) {
|
||||||
const timestamp = new Date(date).getTime();
|
const timestamp = new Date(date).getTime();
|
||||||
return `/history/${timestamp}`
|
return `/history/${timestamp}`;
|
||||||
|
},
|
||||||
|
overallWineStatistics() {
|
||||||
|
return fetch("/api/wines")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.wines = response.wines));
|
||||||
},
|
},
|
||||||
dateString: dateString
|
dateString: dateString
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.wines = await overallWineStatistics();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -84,18 +84,6 @@ h1 {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
#wines-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
grid-gap: 2rem;
|
|
||||||
|
|
||||||
|
|
||||||
> div {
|
|
||||||
justify-content: flex-start;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-wins {
|
.name-wins {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" />
|
<RaffleGenerator @numberOfRaffles="val => (this.numberOfRaffles = val)" />
|
||||||
|
|
||||||
<Vipps class="vipps" :amount="numberOfRaffles" />
|
<Vipps class="vipps" :amount="numberOfRaffles" />
|
||||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||||
@@ -43,16 +43,16 @@ export default {
|
|||||||
this.hardStart = true;
|
this.hardStart = true;
|
||||||
},
|
},
|
||||||
track() {
|
track() {
|
||||||
window.ga('send', 'pageview', '/lottery/generate');
|
window.ga("send", "pageview", "/lottery/generate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/variables.scss";
|
@import "@/styles/variables.scss";
|
||||||
@import "../styles/global.scss";
|
@import "@/styles/global.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "@/styles/media-queries.scss";
|
||||||
h1 {
|
h1 {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,9 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vipps {
|
.vipps {
|
||||||
margin: 5rem auto 2.5rem auto;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 4rem;
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
@@ -75,7 +77,6 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,12 @@
|
|||||||
<p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p>
|
<p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p>
|
||||||
|
|
||||||
<ol v-if="highscore.length > 0" class="highscore-list">
|
<ol v-if="highscore.length > 0" class="highscore-list">
|
||||||
<li v-for="person in filteredResults" @click="selectWinner(person)" @keydown.enter="selectWinner(person)" tabindex="0">
|
<li
|
||||||
|
v-for="person in filteredResults"
|
||||||
|
@click="goToWinner(person)"
|
||||||
|
@keydown.enter="goToWinner(person)"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
<b>{{ person.rank }}.</b> {{ person.name }} - {{ person.wins.length }}
|
<b>{{ person.rank }}.</b> {{ person.name }} - {{ person.wins.length }}
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -24,8 +29,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { highscoreStatistics } from "@/api";
|
|
||||||
import { humanReadableDate, daysAgo } from "@/utils";
|
import { humanReadableDate, daysAgo } from "@/utils";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
|
|
||||||
@@ -34,18 +37,12 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
highscore: [],
|
highscore: [],
|
||||||
filterInput: ''
|
filterInput: ""
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let response = await highscoreStatistics();
|
const winners = await this.highscoreStatistics();
|
||||||
response.sort((a, b) => {
|
this.highscore = this.generateScoreBoard(winners);
|
||||||
return a.wins.length > b.wins.length ? -1 : 1;
|
|
||||||
});
|
|
||||||
response = response.filter(
|
|
||||||
person => person.name != null && person.name != ""
|
|
||||||
);
|
|
||||||
this.highscore = this.generateScoreBoard(response);
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredResults() {
|
filteredResults() {
|
||||||
@@ -53,37 +50,42 @@ export default {
|
|||||||
let val = this.filterInput;
|
let val = this.filterInput;
|
||||||
|
|
||||||
if (val.length) {
|
if (val.length) {
|
||||||
val = val.toLowerCase()
|
val = val.toLowerCase();
|
||||||
const nameIncludesString = (person) => person.name.toLowerCase().includes(val);
|
const nameIncludesString = person => person.name.toLowerCase().includes(val);
|
||||||
highscore = highscore.filter(nameIncludesString)
|
highscore = highscore.filter(nameIncludesString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return highscore
|
return highscore;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generateScoreBoard(highscore=this.highscore) {
|
highscoreStatistics() {
|
||||||
|
return fetch("/api/history/by-wins")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.winners);
|
||||||
|
},
|
||||||
|
generateScoreBoard(highscore = this.highscore) {
|
||||||
let place = 0;
|
let place = 0;
|
||||||
let highestWinCount = -1;
|
let highestWinCount = -1;
|
||||||
|
|
||||||
return highscore.map(win => {
|
return highscore.map(win => {
|
||||||
const wins = win.wins.length
|
const wins = win.wins.length;
|
||||||
if (wins != highestWinCount) {
|
if (wins != highestWinCount) {
|
||||||
place += 1
|
place += 1;
|
||||||
highestWinCount = wins
|
highestWinCount = wins;
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeString = place.toString().padStart(2, "0");
|
const placeString = place.toString().padStart(2, "0");
|
||||||
win.rank = placeString;
|
win.rank = placeString;
|
||||||
return win
|
return win;
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
resetFilter() {
|
resetFilter() {
|
||||||
this.filterInput = '';
|
this.filterInput = "";
|
||||||
document.getElementsByTagName('input')[0].focus();
|
document.getElementsByTagName("input")[0].focus();
|
||||||
},
|
},
|
||||||
selectWinner(winner) {
|
goToWinner(winner) {
|
||||||
const path = "/highscore/" + encodeURIComponent(winner.name)
|
const path = "/highscore/" + encodeURIComponent(winner.name);
|
||||||
this.$router.push(path);
|
this.$router.push(path);
|
||||||
},
|
},
|
||||||
humanReadableDate: humanReadableDate,
|
humanReadableDate: humanReadableDate,
|
||||||
@@ -152,7 +154,8 @@ h1 {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
&:hover, &:focus {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
border-color: $link-color;
|
border-color: $link-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,42 +3,51 @@
|
|||||||
<h1>Historie fra tidligere lotteri</h1>
|
<h1>Historie fra tidligere lotteri</h1>
|
||||||
|
|
||||||
<div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries">
|
<div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries">
|
||||||
<Winners :winners="lottery.winners" :title="`Vinnere fra ${ humanReadableDate(lottery.date) }`" />
|
<Winners :winners="lottery.winners" :title="`Vinnere fra ${humanReadableDate(lottery.date)}`" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { historyByDate, historyAll } from '@/api'
|
import { historyByDate, historyAll } from "@/api";
|
||||||
import { humanReadableDate } from "@/utils";
|
import { humanReadableDate } from "@/utils";
|
||||||
import Winners from '@/ui/Winners'
|
import Winners from "@/ui/Winners";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'History page of prev lotteries',
|
name: "History page of prev lotteries",
|
||||||
components: { Winners },
|
components: { Winners },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lotteries: [],
|
lotteries: []
|
||||||
}
|
};
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
humanReadableDate: humanReadableDate
|
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
const dateFromUrl = this.$route.params.date;
|
const dateFromUrl = this.$route.params.date;
|
||||||
|
|
||||||
if (dateFromUrl !== undefined)
|
if (dateFromUrl !== undefined) {
|
||||||
historyByDate(dateFromUrl)
|
this.fetchHistoryByDate(dateFromUrl).then(history => (this.lotteries = [history]));
|
||||||
.then(history => this.lotteries = { "lottery": history })
|
} else {
|
||||||
else
|
this.fetchHistory().then(history => (this.lotteries = history));
|
||||||
historyAll()
|
}
|
||||||
.then(history => this.lotteries = history.lotteries)
|
},
|
||||||
|
methods: {
|
||||||
|
humanReadableDate: humanReadableDate,
|
||||||
|
fetchHistory() {
|
||||||
|
return fetch("/api/history/by-date")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.lotteries);
|
||||||
|
},
|
||||||
|
fetchHistoryByDate(date) {
|
||||||
|
return fetch(`/api/history/by-date/${date}`)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
|
ref="username"
|
||||||
placeholder="Brukernavn"
|
placeholder="Brukernavn"
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
@@ -34,6 +35,9 @@ export default {
|
|||||||
error: undefined
|
error: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.username.focus();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
login(this.username, this.password)
|
login(this.username, this.password)
|
||||||
|
|||||||
@@ -14,20 +14,25 @@
|
|||||||
|
|
||||||
<h4 class="margin-bottom-0">Vinnende farger:</h4>
|
<h4 class="margin-bottom-0">Vinnende farger:</h4>
|
||||||
<div class="raffle-container el-spacing">
|
<div class="raffle-container el-spacing">
|
||||||
<div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)" :key="color">
|
<div
|
||||||
|
class="raffle-element"
|
||||||
|
:class="color + `-raffle`"
|
||||||
|
v-for="[color, occurences] in Object.entries(winningColors)"
|
||||||
|
:key="color"
|
||||||
|
>
|
||||||
{{ occurences }}
|
{{ occurences }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="el-spacing">Flasker vunnet:</h4>
|
<h4 class="el-spacing">Flasker vunnet:</h4>
|
||||||
|
|
||||||
<div v-for="win in winner.highscore" :key="win._id">
|
<div v-for="win in winner.wins" :key="win._id">
|
||||||
<router-link :to="winDateUrl(win.date)" class="days-ago">
|
<router-link :to="winDateUrl(win.date)" class="days-ago">
|
||||||
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
|
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div class="won-wine">
|
<div class="won-wine" v-if="win.wine">
|
||||||
<img :src="smallerWineImage(win.wine.image)">
|
<img :src="smallerWineImage(win.wine.image)" />
|
||||||
|
|
||||||
<div class="won-wine-details">
|
<div class="won-wine-details">
|
||||||
<h3>{{ win.wine.name }}</h3>
|
<h3>{{ win.wine.name }}</h3>
|
||||||
@@ -38,6 +43,11 @@
|
|||||||
|
|
||||||
<div class="raffle-element small" :class="win.color + `-raffle`"></div>
|
<div class="raffle-element small" :class="win.color + `-raffle`"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="won-wine" v-else>
|
||||||
|
<div class="won-wine-details">
|
||||||
|
<h3>Oisann! Klarte ikke finne vin.</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -49,67 +59,71 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getWinnerByName } from "@/api";
|
import { dateString, humanReadableDate, daysAgo } from "@/utils";
|
||||||
import { humanReadableDate, daysAgo } from "@/utils";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
winner: undefined,
|
winner: undefined,
|
||||||
|
name: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
previousRoute: {
|
previousRoute: {
|
||||||
default: true,
|
default: true,
|
||||||
name: "topplisten",
|
name: "topplisten",
|
||||||
path: "/highscore"
|
path: "/highscore"
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
beforeRouteEnter(to, from, next) {
|
beforeRouteEnter(to, from, next) {
|
||||||
next(vm => {
|
next(vm => {
|
||||||
if (from.name != null)
|
if (from.name != null) vm.previousRoute = from;
|
||||||
vm.previousRoute = from
|
});
|
||||||
})
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
numberOfWins() {
|
numberOfWins() {
|
||||||
return this.winner.highscore.length
|
return this.winner.wins.length;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
const nameFromURL = this.$route.params.name;
|
this.name = this.$route.params.name;
|
||||||
getWinnerByName(nameFromURL)
|
this.getWinnerByName(this.name)
|
||||||
.then(winner => this.setWinner(winner))
|
.then(winner => this.setWinner(winner))
|
||||||
.catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`)
|
.catch(err => (this.error = `Ingen med navn: "${nameFromURL}" funnet.`));
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getWinnerByName(name) {
|
||||||
|
return fetch(`/api/history/by-name/${name}`)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.winner);
|
||||||
|
},
|
||||||
setWinner(winner) {
|
setWinner(winner) {
|
||||||
this.winner = {
|
this.winner = {
|
||||||
name: winner.name,
|
name: winner.name,
|
||||||
highscore: [],
|
highscore: [],
|
||||||
...winner
|
...winner
|
||||||
}
|
};
|
||||||
this.winningColors = this.findWinningColors()
|
this.winningColors = this.findWinningColors();
|
||||||
},
|
},
|
||||||
smallerWineImage(image) {
|
smallerWineImage(image) {
|
||||||
if (image && image.includes(`515x515`))
|
if (image && image.includes(`515x515`)) return image.replace(`515x515`, `175x175`);
|
||||||
return image.replace(`515x515`, `175x175`)
|
if (image && image.includes(`500x500`)) return image.replace(`500x500`, `175x175`);
|
||||||
return image
|
return image;
|
||||||
},
|
},
|
||||||
findWinningColors() {
|
findWinningColors() {
|
||||||
const colors = this.winner.highscore.map(win => win.color)
|
const colors = this.winner.wins.map(win => win.color);
|
||||||
const colorOccurences = {}
|
const colorOccurences = {};
|
||||||
colors.forEach(color => {
|
colors.forEach(color => {
|
||||||
if (colorOccurences[color] == undefined) {
|
if (colorOccurences[color] == undefined) {
|
||||||
colorOccurences[color] = 1
|
colorOccurences[color] = 1;
|
||||||
} else {
|
} else {
|
||||||
colorOccurences[color] += 1
|
colorOccurences[color] += 1;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return colorOccurences
|
return colorOccurences;
|
||||||
},
|
},
|
||||||
winDateUrl(date) {
|
winDateUrl(date) {
|
||||||
const timestamp = new Date(date).getTime();
|
const dateParameter = dateString(new Date(date));
|
||||||
return `/history/${timestamp}`
|
return `/history/${dateParameter}`;
|
||||||
},
|
},
|
||||||
navigateBack() {
|
navigateBack() {
|
||||||
if (this.previousRoute.default) {
|
if (this.previousRoute.default) {
|
||||||
@@ -121,7 +135,7 @@ export default {
|
|||||||
humanReadableDate: humanReadableDate,
|
humanReadableDate: humanReadableDate,
|
||||||
daysAgo: daysAgo
|
daysAgo: daysAgo
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -142,7 +156,7 @@ $elementSpacing: 3rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
margin: 3rem auto;
|
margin: 3rem auto;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
@@ -233,7 +247,7 @@ h1 {
|
|||||||
@include tablet {
|
@include tablet {
|
||||||
width: calc(100% - 160px - 80px);
|
width: calc(100% - 160px - 80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -259,10 +273,9 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
$background: rgb(244,244,244);
|
$background: rgb(244, 244, 244);
|
||||||
|
|
||||||
--padding: 2rem;
|
--padding: 2rem;
|
||||||
@include desktop {
|
@include desktop {
|
||||||
--padding: 5rem;
|
--padding: 5rem;
|
||||||
@@ -270,4 +283,4 @@ h1 {
|
|||||||
background-color: $background;
|
background-color: $background;
|
||||||
padding: var(--padding);
|
padding: var(--padding);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,728 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="page-container">
|
|
||||||
<h1>Registrering</h1>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<div class="notification-element">
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="notification">Push-melding</label>
|
|
||||||
<textarea
|
|
||||||
id="notification"
|
|
||||||
type="text"
|
|
||||||
rows="3"
|
|
||||||
v-model="pushMessage"
|
|
||||||
placeholder="Push meldingtekst"
|
|
||||||
/>
|
|
||||||
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="button-container">
|
|
||||||
<button class="vin-button" @click="sendPush">Send push</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h2 id="addwine-title">Prelottery</h2>
|
|
||||||
|
|
||||||
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
|
|
||||||
|
|
||||||
<div class="button-container">
|
|
||||||
<button
|
|
||||||
class="vin-button"
|
|
||||||
@click="showCamera = !showCamera"
|
|
||||||
>{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}</button>
|
|
||||||
|
|
||||||
<button class="vin-button" @click="addWine">Legg til en vin manuelt</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="wines.length > 0" class="edit-container">
|
|
||||||
<wine v-for="wine in wines" :key="key" :wine="wine">
|
|
||||||
<div class="edit">
|
|
||||||
<div class="button-container row">
|
|
||||||
<button
|
|
||||||
class="vin-button"
|
|
||||||
@click="editWine = amIBeingEdited(wine) ? false : wine"
|
|
||||||
>{{ amIBeingEdited(wine) ? "Lukk" : "Rediger" }}</button>
|
|
||||||
<button class="red vin-button" @click="deleteWine(wine)">Slett</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="amIBeingEdited(wine)" class="wine-edit">
|
|
||||||
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
|
|
||||||
<label>{{ key }}</label>
|
|
||||||
<input type="text" v-model="wine[key]" :placeholder="key" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</wine>
|
|
||||||
</div>
|
|
||||||
<div class="button-container" v-if="wines.length > 0">
|
|
||||||
<button class="vin-button" @click="sendWines">Send inn viner</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h2>Lottery</h2>
|
|
||||||
|
|
||||||
<h3>Legg til lodd kjøpt</h3>
|
|
||||||
<div class="colors">
|
|
||||||
<div v-for="color in lotteryColors" :class="color.css + ' colors-box'" :key="color">
|
|
||||||
<div class="colors-overlay">
|
|
||||||
<p>{{ color.name }} kjøpt</p>
|
|
||||||
<input v-model="color.value" min="0" :placeholder="0" type="number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="label-div">
|
|
||||||
<label>Totalt kjøpt for:</label>
|
|
||||||
<input v-model="payed" placeholder="NOK" type="number" :step="price || 1" min="0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-container">
|
|
||||||
<button class="vin-button" @click="submitLottery">Send inn lotteri</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Vinnere</h3>
|
|
||||||
<a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a>
|
|
||||||
<div class="winner-container" v-if="winners.length > 0">
|
|
||||||
<wine v-for="winner in winners" :key="winner" :wine="winner.wine">
|
|
||||||
<div class="winner-element">
|
|
||||||
<div class="color-selector">
|
|
||||||
<div class="label-div">
|
|
||||||
<label>Farge vunnet</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="blue"
|
|
||||||
:class="{ active: winner.color == 'blue' }"
|
|
||||||
@click="winner.color = 'blue'"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
class="red"
|
|
||||||
:class="{ active: winner.color == 'red' }"
|
|
||||||
@click="winner.color = 'red'"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
class="green"
|
|
||||||
:class="{ active: winner.color == 'green' }"
|
|
||||||
@click="winner.color = 'green'"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
class="yellow"
|
|
||||||
:class="{ active: winner.color == 'yellow' }"
|
|
||||||
@click="winner.color = 'yellow'"
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="winner-name">Navn vinner</label>
|
|
||||||
<input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="potential-winner-name">Virtuelle vinnere</label>
|
|
||||||
<select
|
|
||||||
id="potential-winner-name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Navn"
|
|
||||||
v-model="winner.potentialWinner"
|
|
||||||
@change="potentialChange($event, winner)"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="fetchedWinner in fetchedWinners"
|
|
||||||
:value="stringify(fetchedWinner)"
|
|
||||||
>{{fetchedWinner.name}}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</wine>
|
|
||||||
|
|
||||||
<div class="button-container column">
|
|
||||||
<button class="vin-button" @click="submitLotteryWinners">Send inn vinnere</button>
|
|
||||||
<button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import eventBus from "@/mixins/EventBus";
|
|
||||||
import { dateString } from '@/utils'
|
|
||||||
import {
|
|
||||||
prelottery,
|
|
||||||
sendLotteryWinners,
|
|
||||||
sendLottery,
|
|
||||||
logWines,
|
|
||||||
wineSchema,
|
|
||||||
winnersSecure,
|
|
||||||
attendees
|
|
||||||
} from "@/api";
|
|
||||||
import TextToast from "@/ui/TextToast";
|
|
||||||
import Wine from "@/ui/Wine";
|
|
||||||
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { TextToast, Wine, ScanToVinmonopolet },
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
payed: undefined,
|
|
||||||
winners: [],
|
|
||||||
fetchedWinners: [],
|
|
||||||
wines: [],
|
|
||||||
pushMessage: "",
|
|
||||||
pushLink: "/",
|
|
||||||
toastText: undefined,
|
|
||||||
showToast: false,
|
|
||||||
showCamera: false,
|
|
||||||
editWine: false,
|
|
||||||
lotteryColors: [
|
|
||||||
{ value: null, name: "Blå", css: "blue" },
|
|
||||||
{ value: null, name: "Rød", css: "red" },
|
|
||||||
{ value: null, name: "Grønn", css: "green" },
|
|
||||||
{ value: null, name: "Gul", css: "yellow" }
|
|
||||||
],
|
|
||||||
price: __PRICE__
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.fetchAndAddPrelotteryWines().then(this.getWinnerdataFromStorage);
|
|
||||||
|
|
||||||
window.addEventListener("unload", this.setWinnerdataToStorage);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.setWinnerdataToStorage();
|
|
||||||
eventBus.$off("tab-change", () => {
|
|
||||||
this.fetchColorsAndWinners();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchColorsAndWinners();
|
|
||||||
|
|
||||||
eventBus.$on("tab-change", () => {
|
|
||||||
this.fetchColorsAndWinners();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
stringify(json) {
|
|
||||||
return JSON.stringify(json);
|
|
||||||
},
|
|
||||||
potentialChange(event, winner) {
|
|
||||||
let data = JSON.parse(event.target.value);
|
|
||||||
winner.name = data.name;
|
|
||||||
winner.color = data.color;
|
|
||||||
},
|
|
||||||
async fetchColorsAndWinners() {
|
|
||||||
let winners = await winnersSecure();
|
|
||||||
let _attendees = await attendees();
|
|
||||||
let colors = {
|
|
||||||
red: 0,
|
|
||||||
blue: 0,
|
|
||||||
green: 0,
|
|
||||||
yellow: 0
|
|
||||||
};
|
|
||||||
this.payed = 0;
|
|
||||||
for (let i = 0; i < _attendees.length; i++) {
|
|
||||||
let attendee = _attendees[i];
|
|
||||||
colors.red += attendee.red;
|
|
||||||
colors.blue += attendee.blue;
|
|
||||||
colors.green += attendee.green;
|
|
||||||
colors.yellow += attendee.yellow;
|
|
||||||
this.payed +=
|
|
||||||
(attendee.red + attendee.blue + attendee.green + attendee.yellow) *
|
|
||||||
10;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < this.lotteryColors.length; i++) {
|
|
||||||
let currentColor = this.lotteryColors[i];
|
|
||||||
switch (currentColor.css) {
|
|
||||||
case "red":
|
|
||||||
currentColor.value = colors.red;
|
|
||||||
break;
|
|
||||||
case "blue":
|
|
||||||
currentColor.value = colors.blue;
|
|
||||||
break;
|
|
||||||
a;
|
|
||||||
case "green":
|
|
||||||
currentColor.value = colors.green;
|
|
||||||
break;
|
|
||||||
case "yellow":
|
|
||||||
currentColor.value = colors.yellow;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.fetchedWinners = winners;
|
|
||||||
},
|
|
||||||
amIBeingEdited(wine) {
|
|
||||||
return this.editWine.id == wine.id && this.editWine.name == wine.name;
|
|
||||||
},
|
|
||||||
async fetchAndAddPrelotteryWines() {
|
|
||||||
const wines = await prelottery();
|
|
||||||
|
|
||||||
for (let i = 0; i < wines.length; i++) {
|
|
||||||
let wine = wines[i];
|
|
||||||
this.winners.push({
|
|
||||||
name: "",
|
|
||||||
color: "",
|
|
||||||
potentialWinner: "",
|
|
||||||
wine: {
|
|
||||||
name: wine.name,
|
|
||||||
vivinoLink: wine.vivinoLink,
|
|
||||||
rating: wine.rating,
|
|
||||||
image: wine.image,
|
|
||||||
id: wine.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wineFromVinmonopoletScan(wineResponse) {
|
|
||||||
if (this.wines.map(wine => wine.name).includes(wineResponse.name)) {
|
|
||||||
this.toastText = "Vinen er allerede lagt til.";
|
|
||||||
this.showToast = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toastText = "Fant og la til vin:<br>" + wineResponse.name;
|
|
||||||
this.showToast = true;
|
|
||||||
|
|
||||||
this.wines.unshift(wineResponse);
|
|
||||||
},
|
|
||||||
sendPush: async function() {
|
|
||||||
let _response = await fetch("/subscription/send-notification", {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ message: this.pushMessage, link: this.pushLink })
|
|
||||||
});
|
|
||||||
let response = await _response.json();
|
|
||||||
if (response) {
|
|
||||||
alert("Sendt!");
|
|
||||||
} else {
|
|
||||||
alert("Noe gikk galt!");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addWine: async function(event) {
|
|
||||||
const wine = await wineSchema();
|
|
||||||
|
|
||||||
this.editWine = wine;
|
|
||||||
this.wines.unshift(wine);
|
|
||||||
},
|
|
||||||
deleteWine(deletedWine) {
|
|
||||||
this.wines = this.wines.filter(wine => wine.name != deletedWine.name);
|
|
||||||
},
|
|
||||||
sendWines: async function() {
|
|
||||||
let response = await logWines(this.wines);
|
|
||||||
if (response.success == true) {
|
|
||||||
alert("Sendt!");
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert("Noe gikk galt under innsending");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addWinner: function(event) {
|
|
||||||
this.winners.push({
|
|
||||||
name: "",
|
|
||||||
color: "",
|
|
||||||
wine: {
|
|
||||||
name: "",
|
|
||||||
vivinoLink: "",
|
|
||||||
rating: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
submitLottery: async function(event) {
|
|
||||||
const colors = {
|
|
||||||
red: this.lotteryColors.filter(c => c.css == "red")[0].value,
|
|
||||||
green: this.lotteryColors.filter(c => c.css == "green")[0].value,
|
|
||||||
blue: this.lotteryColors.filter(c => c.css == "blue")[0].value,
|
|
||||||
yellow: this.lotteryColors.filter(c => c.css == "yellow")[0].value
|
|
||||||
};
|
|
||||||
|
|
||||||
let sendObject = {
|
|
||||||
lottery: {
|
|
||||||
date: dateString(new Date()),
|
|
||||||
...colors
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sendObject.lottery.red == undefined) {
|
|
||||||
alert("Rød må defineres");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sendObject.lottery.green == undefined) {
|
|
||||||
alert("Grønn må defineres");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sendObject.lottery.yellow == undefined) {
|
|
||||||
alert("Gul må defineres");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sendObject.lottery.blue == undefined) {
|
|
||||||
alert("Blå må defineres");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendObject.lottery.bought =
|
|
||||||
parseInt(colors.blue) +
|
|
||||||
parseInt(colors.red) +
|
|
||||||
parseInt(colors.green) +
|
|
||||||
parseInt(colors.yellow);
|
|
||||||
const stolen = sendObject.lottery.bought - parseInt(this.payed) / 10;
|
|
||||||
if (isNaN(stolen) || stolen == undefined) {
|
|
||||||
alert("Betalt må registreres");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendObject.lottery.stolen = stolen;
|
|
||||||
|
|
||||||
let response = await sendLottery(sendObject);
|
|
||||||
if (response == true) {
|
|
||||||
alert("Sendt!");
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert(response.message || "Noe gikk galt under innsending");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitLotteryWinners: async function(event) {
|
|
||||||
let sendObject = {
|
|
||||||
lottery: {
|
|
||||||
date: dateString(new Date()),
|
|
||||||
winners: this.winners
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendObject.lottery.winners.length == 0) {
|
|
||||||
alert("Det må være med vinnere");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < sendObject.lottery.winners.length; i++) {
|
|
||||||
let currentWinner = sendObject.lottery.winners[i];
|
|
||||||
|
|
||||||
if (currentWinner.name == undefined || currentWinner.name == "") {
|
|
||||||
alert("Navn må defineres");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentWinner.color == undefined || currentWinner.color == "") {
|
|
||||||
alert("Farge må defineres");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = await sendLotteryWinners(sendObject);
|
|
||||||
if (response == true) {
|
|
||||||
alert("Sendt!");
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert(response.message || "Noe gikk galt under innsending");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getWinnerdataFromStorage() {
|
|
||||||
let localWinners = localStorage.getItem("winners");
|
|
||||||
if (localWinners && this.winners.length) {
|
|
||||||
localWinners = JSON.parse(localWinners);
|
|
||||||
|
|
||||||
this.winners = this.winners.map(winner => {
|
|
||||||
const localWinnerMatch = localWinners.filter(
|
|
||||||
localWinner =>
|
|
||||||
localWinner.wine.name == winner.wine.name ||
|
|
||||||
localWinner.wine.id == winner.wine.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (localWinnerMatch.length > 0) {
|
|
||||||
winner.name = localWinnerMatch[0].name || winner.name;
|
|
||||||
winner.color = localWinnerMatch[0].color || winner.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return winner;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let localColors = localStorage.getItem("colorValues");
|
|
||||||
if (localColors) {
|
|
||||||
localColors = localColors.split(",");
|
|
||||||
this.lotteryColors.forEach((color, i) => {
|
|
||||||
const localColorValue = Number(localColors[i]);
|
|
||||||
color.value = localColorValue == 0 ? null : localColorValue;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setWinnerdataToStorage() {
|
|
||||||
localStorage.setItem("winners", JSON.stringify(this.winners));
|
|
||||||
localStorage.setItem(
|
|
||||||
"colorValues",
|
|
||||||
this.lotteryColors.map(color => Number(color.value))
|
|
||||||
);
|
|
||||||
window.removeEventListener("unload", this.setWinnerdataToStorage);
|
|
||||||
},
|
|
||||||
resetWinnerDataInStorage() {
|
|
||||||
this.winners = [];
|
|
||||||
this.fetchAndAddPrelotteryWines().then(resp => (this.winners = resp));
|
|
||||||
this.lotteryColors.map(color => (color.value = null));
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../styles/global.scss";
|
|
||||||
@import "../styles/media-queries.scss";
|
|
||||||
select {
|
|
||||||
margin: 0 0 auto;
|
|
||||||
height: 2rem;
|
|
||||||
min-width: 0;
|
|
||||||
width: 98%;
|
|
||||||
padding: 1%;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-family: knowit, Arial;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-family: knowit, Arial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wine-link {
|
|
||||||
color: #333333;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid $link-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
width: 90%;
|
|
||||||
margin: 2rem auto;
|
|
||||||
color: grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
padding: 0 1.5rem 3rem;
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
max-width: 60vw;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.winner-container {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edit-container {
|
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
> .wine {
|
|
||||||
margin-right: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edit {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-element {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.winner-element {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.wine-element {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-link {
|
|
||||||
color: #333333;
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wine-edit {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-selector {
|
|
||||||
margin-bottom: 0.65rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
min-width: 175px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
max-width: 25vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
border: 2px solid unset;
|
|
||||||
|
|
||||||
&.green {
|
|
||||||
border-color: $green;
|
|
||||||
}
|
|
||||||
&.blue {
|
|
||||||
border-color: $dark-blue;
|
|
||||||
}
|
|
||||||
&.red {
|
|
||||||
border-color: $red;
|
|
||||||
}
|
|
||||||
&.yellow {
|
|
||||||
border-color: $dark-yellow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border: 2px solid transparent;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 2.5rem;
|
|
||||||
width: 2.5rem;
|
|
||||||
|
|
||||||
// disable-dbl-tap-zoom
|
|
||||||
touch-action: manipulation;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.green {
|
|
||||||
background: #c8f9df;
|
|
||||||
}
|
|
||||||
&.blue {
|
|
||||||
background: #d4f2fe;
|
|
||||||
}
|
|
||||||
&.red {
|
|
||||||
background: #fbd7de;
|
|
||||||
}
|
|
||||||
&.yellow {
|
|
||||||
background: #fff6d6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.colors {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 3rem auto 1rem;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
margin: 1.8rem auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-div {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.colors-box {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
margin: 20px;
|
|
||||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
mask-image: url(/public/assets/images/lodd.svg);
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.colors-overlay {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
position: absolute;
|
|
||||||
top: 0.4rem;
|
|
||||||
left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 70%;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 3rem;
|
|
||||||
height: unset;
|
|
||||||
max-height: unset;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.green,
|
|
||||||
.green .colors-overlay > input {
|
|
||||||
background-color: $light-green;
|
|
||||||
color: $green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue,
|
|
||||||
.blue .colors-overlay > input {
|
|
||||||
background-color: $light-blue;
|
|
||||||
color: $blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yellow,
|
|
||||||
.yellow .colors-overlay > input {
|
|
||||||
background-color: $light-yellow;
|
|
||||||
color: $yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.red,
|
|
||||||
.red .colors-overlay > input {
|
|
||||||
background-color: $light-red;
|
|
||||||
color: $red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,26 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="main-container">
|
<section class="main-container">
|
||||||
<Modal
|
<Modal
|
||||||
v-if="showModal"
|
v-if="showModal"
|
||||||
modalText="Ønsket ditt har blitt lagt til"
|
modalText="Ønsket ditt har blitt lagt til"
|
||||||
:buttons="modalButtons"
|
:buttons="modalButtons"
|
||||||
@click="emitFromModalButton"
|
@click="emitFromModalButton"
|
||||||
></Modal>
|
></Modal>
|
||||||
<h1>
|
<h1>
|
||||||
Foreslå en vin!
|
Foreslå en vin!
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<section class="search-container">
|
<section class="search-container">
|
||||||
<section class="search-section">
|
<section class="search-section">
|
||||||
<input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷" class="search-input-field">
|
<input
|
||||||
<button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button>
|
type="text"
|
||||||
</section>
|
v-model="searchString"
|
||||||
<section v-for="(wine, index) in this.wines" :key="index" class="single-result">
|
@keyup.enter="searchWines()"
|
||||||
<img
|
placeholder="Søk etter en vin du liker her!🍷"
|
||||||
v-if="wine.image"
|
class="search-input-field"
|
||||||
:src="wine.image"
|
|
||||||
class="wine-image"
|
|
||||||
:class="{ 'fullscreen': fullscreen }"
|
|
||||||
/>
|
/>
|
||||||
|
<button :disabled="!searchString" @click="searchWines()" class="vin-button">Søk</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-for="(wine, index) in wines" :key="index" class="single-result">
|
||||||
|
<img v-if="wine.image" :src="wine.image" class="wine-image" :class="{ fullscreen: fullscreen }" />
|
||||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||||
<section class="wine-info">
|
<section class="wine-info">
|
||||||
<h2 v-if="wine.name">{{ wine.name }}</h2>
|
<h2 v-if="wine.name">{{ wine.name }}</h2>
|
||||||
@@ -29,37 +32,38 @@
|
|||||||
<span v-if="wine.rating">{{ wine.rating }}%</span>
|
<span v-if="wine.rating">{{ wine.rating }}%</span>
|
||||||
<span v-if="wine.price">{{ wine.price }} NOK</span>
|
<span v-if="wine.price">{{ wine.price }} NOK</span>
|
||||||
<span v-if="wine.country">{{ wine.country }}</span>
|
<span v-if="wine.country">{{ wine.country }}</span>
|
||||||
|
<span v-if="wine.year">{{ wine.year }}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<button class="vin-button" @click="request(wine)">Foreslå denne</button>
|
<button class="vin-button" @click="requestWine(wine)">Foreslå denne</button>
|
||||||
<a
|
<a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="wine-link">Les mer</a>
|
||||||
v-if="wine.vivinoLink"
|
|
||||||
:href="wine.vivinoLink"
|
|
||||||
class="wine-link"
|
|
||||||
>Les mer</a>
|
|
||||||
</section>
|
</section>
|
||||||
<p v-if="this.wines && this.wines.length == 0">
|
<p v-if="loading == false && wines && wines.length == 0">
|
||||||
Fant ingen viner med det navnet!
|
Fant ingen viner med det navnet!
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else-if="loading">Loading...</p>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { searchForWine, requestNewWine } from "@/api";
|
import { searchForWine } from "@/api";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
import Modal from "@/ui/Modal";
|
import Modal from "@/ui/Modal";
|
||||||
|
import RequestedWineCard from "@/ui/RequestedWineCard";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Wine,
|
Wine,
|
||||||
Modal
|
Modal,
|
||||||
|
RequestedWineCard
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchString: undefined,
|
searchString: undefined,
|
||||||
wines: undefined,
|
wines: undefined,
|
||||||
showModal: false,
|
showModal: false,
|
||||||
|
loading: false,
|
||||||
modalButtons: [
|
modalButtons: [
|
||||||
{
|
{
|
||||||
text: "Legg til flere viner",
|
text: "Legg til flere viner",
|
||||||
@@ -70,30 +74,59 @@ export default {
|
|||||||
action: "move"
|
action: "move"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchWineFromVin(){
|
fetchWinesByQuery(query) {
|
||||||
if(this.searchString){
|
let url = new URL("/api/vinmonopolet/wine/search", window.location);
|
||||||
this.wines = []
|
url.searchParams.set("name", query);
|
||||||
let localSearchString = this.searchString.replace(/ /g,"_");
|
|
||||||
searchForWine(localSearchString)
|
this.wines = [];
|
||||||
.then(res => this.wines = res)
|
this.loading = true;
|
||||||
|
|
||||||
|
return fetch(url.href)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.wines = response.wines))
|
||||||
|
.finally(wines => (this.loading = false));
|
||||||
|
},
|
||||||
|
searchWines() {
|
||||||
|
if (this.searchString) {
|
||||||
|
let localSearchString = this.searchString.replace(/ /g, "_");
|
||||||
|
this.fetchWinesByQuery(localSearchString);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
request(wine){
|
requestWine(wine) {
|
||||||
requestNewWine(wine)
|
const options = {
|
||||||
.then(() => this.showModal = true)
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ wine: wine })
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch("/api/request", options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.showModal = true;
|
||||||
|
this.$toast.info({
|
||||||
|
title: `Vinen ${wine.name} har blitt foreslått!`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Obs, her oppsto det en feil! Feilen er logget.",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
emitFromModalButton(action){
|
emitFromModalButton(action) {
|
||||||
if(action == "stay"){
|
if (action == "stay") {
|
||||||
this.showModal = false
|
this.showModal = false;
|
||||||
} else {
|
} else {
|
||||||
this.$router.push("/requested-wines");
|
this.$router.push("/requested-wines");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -101,12 +134,11 @@ export default {
|
|||||||
@import "@/styles/global";
|
@import "@/styles/global";
|
||||||
@import "@/styles/variables";
|
@import "@/styles/variables";
|
||||||
|
|
||||||
|
h1 {
|
||||||
h1{
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-container{
|
.main-container {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
}
|
}
|
||||||
@@ -120,66 +152,63 @@ input[type="text"] {
|
|||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
.search-container{
|
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-section{
|
.search-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid: 1fr / 1fr .2fr;
|
grid: 1fr / 1fr 0.2fr;
|
||||||
|
|
||||||
@include mobile{
|
@include mobile {
|
||||||
.vin-button{
|
.vin-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.search-input-field{
|
.search-input-field {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-result{
|
.single-result {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid: 1fr / .5fr 2fr .5fr .5fr;
|
grid: 1fr / 0.5fr 2fr 0.5fr 0.5fr;
|
||||||
grid-template-areas: "picture details button-left button-right";
|
grid-template-areas: "picture details button-left button-right";
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid-gap: 1em;
|
grid-gap: 1em;
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
box-shadow: 0 1px 0 0 rgba(0,0,0,0.2);
|
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
@include mobile{
|
@include mobile {
|
||||||
|
grid: 1fr 0.5fr / 0.5fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"picture details"
|
||||||
|
"button-left button-right";
|
||||||
|
grid-gap: 0.5em;
|
||||||
|
|
||||||
grid: 1fr .5fr / .5fr 1fr;
|
.vin-button {
|
||||||
grid-template-areas: "picture details"
|
|
||||||
"button-left button-right";
|
|
||||||
grid-gap: .5em;
|
|
||||||
|
|
||||||
.vin-button{
|
|
||||||
grid-area: button-right;
|
grid-area: button-right;
|
||||||
padding: .5em;
|
padding: 0.5em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wine-link{
|
.wine-link {
|
||||||
grid-area: button-left;
|
grid-area: button-left;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2{
|
h2 {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.wine-image {
|
.wine-image {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
@@ -192,14 +221,14 @@ input[type="text"] {
|
|||||||
grid-area: picture;
|
grid-area: picture;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wine-info{
|
.wine-info {
|
||||||
grid-area: details;
|
grid-area: details;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
h2{
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.details{
|
.details {
|
||||||
top: 0;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -216,22 +245,20 @@ input[type="text"] {
|
|||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vin-button{
|
.vin-button {
|
||||||
grid-area: button-right;
|
grid-area: button-right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include tablet{
|
@include tablet {
|
||||||
h2{
|
h2 {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include desktop{
|
@include desktop {
|
||||||
h2{
|
h2 {
|
||||||
font-size: 1.6em;
|
font-size: 1.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
prelottery().then(wines => this.wines = wines);
|
fetch("/api/lottery/wines")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.wines = response.wines));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -42,19 +44,18 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wines-container {
|
.wines-container {
|
||||||
display: grid;
|
width: 90vw;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
padding: 5vw;
|
||||||
grid-gap: 2rem;
|
|
||||||
gap: 2rem;
|
@include desktop {
|
||||||
|
width: 80vw;
|
||||||
|
padding: 0 10vw;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 1500px) {
|
@media (min-width: 1500px) {
|
||||||
max-width: 1500px;
|
max-width: 1500px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@@ -65,23 +66,6 @@ h3 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-wine-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: auto;
|
|
||||||
width: 500px;
|
|
||||||
font-family: Arial;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="main-container">
|
<main class="main-container">
|
||||||
|
|
||||||
<section class="top-container">
|
<section class="top-container">
|
||||||
|
|
||||||
<div class="want-to-win">
|
<div class="want-to-win">
|
||||||
<h1>
|
<h1>
|
||||||
Vil du også vinne?
|
Vil du også vinne?
|
||||||
@@ -18,8 +16,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link to="/lottery" class="participate-button">
|
<router-link to="/lottery" class="participate-button">
|
||||||
<i class="icon icon--arrow-right"></i>
|
<i class="icon icon--arrow-right"></i>
|
||||||
<p>Trykk her for å delta</p>
|
<p>Trykk her for å delta</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/generate" class="see-details-link">
|
<router-link to="/generate" class="see-details-link">
|
||||||
@@ -38,17 +36,16 @@
|
|||||||
<i class="icon icon--bottle"></i>
|
<i class="icon icon--bottle"></i>
|
||||||
<i class="icon icon--bottle"></i>
|
<i class="icon icon--bottle"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content-container">
|
<section class="content-container">
|
||||||
|
|
||||||
<div class="scroll-info">
|
<div class="scroll-info">
|
||||||
<i class ="icon icon--arrow-long-right"></i>
|
<i class="icon icon--arrow-long-right"></i>
|
||||||
<p>Scroll for å se vinnere og annen gøy statistikk</p>
|
<p>Scroll for å se vinnere og annen gøy statistikk</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Highscore class="highscore"/>
|
<Highscore class="highscore" />
|
||||||
|
|
||||||
<TotalBought class="total-bought" />
|
<TotalBought class="total-bought" />
|
||||||
|
|
||||||
<section class="chart-container">
|
<section class="chart-container">
|
||||||
@@ -56,12 +53,10 @@
|
|||||||
<WinGraph class="win" />
|
<WinGraph class="win" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Wines class="wines-container" />
|
<Wines class="wine-container" />
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -96,11 +91,7 @@ export default {
|
|||||||
if (!("PushManager" in window)) {
|
if (!("PushManager" in window)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (
|
return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null;
|
||||||
Notification.permission !== "granted" ||
|
|
||||||
!this.pushAllowed ||
|
|
||||||
localStorage.getItem("push") == null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -120,7 +111,7 @@ export default {
|
|||||||
this.hardStart = way;
|
this.hardStart = way;
|
||||||
},
|
},
|
||||||
track() {
|
track() {
|
||||||
window.ga('send', 'pageview', '/');
|
window.ga("send", "pageview", "/");
|
||||||
},
|
},
|
||||||
startCountdown() {
|
startCountdown() {
|
||||||
this.hardStart = true;
|
this.hardStart = true;
|
||||||
@@ -145,7 +136,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: start;
|
justify-items: start;
|
||||||
|
|
||||||
@include mobile{
|
@include mobile {
|
||||||
padding-bottom: 2em;
|
padding-bottom: 2em;
|
||||||
height: 15em;
|
height: 15em;
|
||||||
grid-template-rows: repeat(7, 1fr);
|
grid-template-rows: repeat(7, 1fr);
|
||||||
@@ -156,13 +147,13 @@ export default {
|
|||||||
grid-column: 2 / -1;
|
grid-column: 2 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
h1{
|
h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include tablet {
|
@include tablet {
|
||||||
h1{
|
h1 {
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
}
|
}
|
||||||
grid-row: 2 / 4;
|
grid-row: 2 / 4;
|
||||||
@@ -170,7 +161,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-request-button{
|
.notification-request-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +220,7 @@ export default {
|
|||||||
.icons-container {
|
.icons-container {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
grid-row: 7 / -1;
|
grid-row: 7 / -1;
|
||||||
@include mobile{
|
@include mobile {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -239,7 +230,7 @@ export default {
|
|||||||
grid-column: 7 / -1;
|
grid-column: 7 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include desktop{
|
@include desktop {
|
||||||
grid-row: 4 / -3;
|
grid-row: 4 / -3;
|
||||||
grid-column: 7 / 11;
|
grid-column: 7 / 11;
|
||||||
}
|
}
|
||||||
@@ -257,30 +248,27 @@ export default {
|
|||||||
i {
|
i {
|
||||||
font-size: 5em;
|
font-size: 5em;
|
||||||
|
|
||||||
&.icon--heart-sparks{
|
&.icon--heart-sparks {
|
||||||
grid-column: 2 / 4;
|
grid-column: 2 / 4;
|
||||||
grid-row: 2 / 4;
|
grid-row: 2 / 4;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
&.icon--face-1{
|
&.icon--face-1 {
|
||||||
grid-column: 4 / 7;
|
grid-column: 4 / 7;
|
||||||
grid-row: 2 / 4;
|
grid-row: 2 / 4;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
&.icon--face-3{
|
&.icon--face-3 {
|
||||||
grid-column: 7 / 10;
|
grid-column: 7 / 10;
|
||||||
grid-row: 1 / 4;
|
grid-row: 1 / 4;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
&.icon--ballon{
|
&.icon--ballon {
|
||||||
grid-column: 9 / 11;
|
grid-column: 9 / 11;
|
||||||
grid-row: 3 / 5;
|
grid-row: 3 / 5;
|
||||||
|
|
||||||
}
|
}
|
||||||
&.icon--bottle{
|
&.icon--bottle {
|
||||||
grid-row: 4 / -1;
|
grid-row: 4 / -1;
|
||||||
|
|
||||||
&:nth-of-type(5) {
|
&:nth-of-type(5) {
|
||||||
@@ -297,14 +285,13 @@ export default {
|
|||||||
&:nth-of-type(8) {
|
&:nth-of-type(8) {
|
||||||
grid-column: 7 / 8;
|
grid-column: 7 / 8;
|
||||||
}
|
}
|
||||||
&:nth-of-type(9){
|
&:nth-of-type(9) {
|
||||||
grid-column: 8 / 9;
|
grid-column: 8 / 9;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@@ -312,12 +299,12 @@ h1 {
|
|||||||
font-family: "knowit";
|
font-family: "knowit";
|
||||||
}
|
}
|
||||||
|
|
||||||
.to-lottery{
|
.to-lottery {
|
||||||
color: #333;
|
color: #333;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
@@ -326,10 +313,10 @@ h1 {
|
|||||||
row-gap: 5em;
|
row-gap: 5em;
|
||||||
|
|
||||||
.scroll-info {
|
.scroll-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 10px;
|
column-gap: 10px;
|
||||||
grid-column: 2 / -2;
|
grid-column: 2 / -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
@@ -346,8 +333,8 @@ h1 {
|
|||||||
grid-column: 2 / -2;
|
grid-column: 2 / -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wines-container {
|
.wine-container {
|
||||||
grid-column: 2 / -2;
|
grid-column: 3 / -3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon--arrow-long-right {
|
.icon--arrow-long-right {
|
||||||
@@ -356,8 +343,7 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include tablet {
|
@include tablet {
|
||||||
|
.scroll-info {
|
||||||
.scroll-info{
|
|
||||||
grid-column: 3 / -3;
|
grid-column: 3 / -3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
<div class="instructions">
|
<div class="instructions">
|
||||||
<h1 class="title">Virtuelt lotteri</h1>
|
<h1 class="title">Virtuelt lotteri</h1>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>, eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link></li>
|
<li>
|
||||||
|
Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>,
|
||||||
|
eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link>
|
||||||
|
</li>
|
||||||
<li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li>
|
<li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li>
|
||||||
<li>Send gjerne melding om fargeønske også.</li>
|
<li>Send gjerne melding om fargeønske også.</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -15,18 +18,16 @@
|
|||||||
|
|
||||||
<VippsPill class="vipps-pill mobile-only" />
|
<VippsPill class="vipps-pill mobile-only" />
|
||||||
|
|
||||||
<p class="call-to-action">
|
<p class="call-to-action">
|
||||||
<span class="vin-link">Følg med på utviklingen</span> og <span class="vin-link">chat om trekningen</span>
|
<span class="vin-link" @click="scrollToContent">Følg med på utviklingen</span> og
|
||||||
<i class="icon icon--arrow-left" @click="scrollToContent"></i></p>
|
<span class="vin-link" @click="scrollToContent">chat om trekningen</span>
|
||||||
|
<i class="icon icon--arrow-left" @click="scrollToContent"></i>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container" ref="content">
|
<div class="container" ref="content">
|
||||||
<WinnerDraw
|
<WinnerDraw :currentWinnerDrawn="currentWinnerDrawn" :currentWinner="currentWinner" :attendees="attendees" />
|
||||||
:currentWinnerDrawn="currentWinnerDrawn"
|
|
||||||
:currentWinner="currentWinner"
|
|
||||||
:attendees="attendees"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="todays-raffles">
|
<div class="todays-raffles">
|
||||||
<h2>Liste av lodd kjøpt i dag</h2>
|
<h2>Liste av lodd kjøpt i dag</h2>
|
||||||
@@ -51,15 +52,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container wines-container">
|
<div class="todays-wines">
|
||||||
<h2>Dagens fangst ({{ wines.length }})</h2>
|
<h2>Dagens fangst ({{ wines.length }})</h2>
|
||||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
<div class="wines-container">
|
||||||
|
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { attendees, winners, prelottery } from "@/api";
|
|
||||||
import Chat from "@/ui/Chat";
|
import Chat from "@/ui/Chat";
|
||||||
import Vipps from "@/ui/Vipps";
|
import Vipps from "@/ui/Vipps";
|
||||||
import VippsPill from "@/ui/VippsPill";
|
import VippsPill from "@/ui/VippsPill";
|
||||||
@@ -74,18 +76,18 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
attendees: [],
|
attendees: [],
|
||||||
|
attendeesFetched: false,
|
||||||
winners: [],
|
winners: [],
|
||||||
wines: [],
|
wines: [],
|
||||||
currentWinnerDrawn: false,
|
currentWinnerDrawn: false,
|
||||||
currentWinner: null,
|
currentWinner: null,
|
||||||
socket: null,
|
socket: null,
|
||||||
attendeesFetched: false,
|
|
||||||
wasDisconnected: false,
|
wasDisconnected: false,
|
||||||
ticketsBought: {
|
ticketsBought: {
|
||||||
"red": 0,
|
red: 0,
|
||||||
"blue": 0,
|
blue: 0,
|
||||||
"green": 0,
|
green: 0,
|
||||||
"yellow": 0
|
yellow: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -129,42 +131,45 @@ export default {
|
|||||||
this.socket = null;
|
this.socket = null;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getWinners: async function() {
|
getWinners() {
|
||||||
let response = await winners();
|
fetch("/api/lottery/winners")
|
||||||
if (response) {
|
.then(resp => resp.json())
|
||||||
this.winners = response;
|
.then(response => (this.winners = response.winners));
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getTodaysWines() {
|
getTodaysWines() {
|
||||||
prelottery()
|
fetch("/api/lottery/wines")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.wines)
|
||||||
.then(wines => {
|
.then(wines => {
|
||||||
this.wines = wines;
|
this.wines = wines;
|
||||||
this.todayExists = wines.length > 0;
|
this.todayExists = wines.length > 0;
|
||||||
})
|
})
|
||||||
.catch(_ => this.todayExists = false)
|
.catch(_ => (this.todayExists = false));
|
||||||
},
|
},
|
||||||
getAttendees: async function() {
|
getAttendees() {
|
||||||
let response = await attendees();
|
fetch("/api/lottery/attendees")
|
||||||
if (response) {
|
.then(resp => resp.json())
|
||||||
this.attendees = response;
|
.then(response => {
|
||||||
if (this.attendees == undefined || this.attendees.length == 0) {
|
const { attendees } = response;
|
||||||
this.attendeesFetched = true;
|
this.attendees = attendees || [];
|
||||||
return;
|
|
||||||
}
|
|
||||||
const addValueOfListObjectByKey = (list, key) =>
|
|
||||||
list.map(object => object[key]).reduce((a, b) => a + b);
|
|
||||||
|
|
||||||
this.ticketsBought = {
|
if (attendees == undefined || attendees.length == 0) {
|
||||||
red: addValueOfListObjectByKey(response, "red"),
|
return;
|
||||||
blue: addValueOfListObjectByKey(response, "blue"),
|
}
|
||||||
green: addValueOfListObjectByKey(response, "green"),
|
|
||||||
yellow: addValueOfListObjectByKey(response, "yellow")
|
const addValueOfListObjectByKey = (list, key) => list.map(object => object[key]).reduce((a, b) => a + b);
|
||||||
};
|
|
||||||
}
|
this.ticketsBought = {
|
||||||
this.attendeesFetched = true;
|
red: addValueOfListObjectByKey(attendees, "red"),
|
||||||
|
blue: addValueOfListObjectByKey(attendees, "blue"),
|
||||||
|
green: addValueOfListObjectByKey(attendees, "green"),
|
||||||
|
yellow: addValueOfListObjectByKey(attendees, "yellow")
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.finally(_ => (this.attendeesFetched = true));
|
||||||
},
|
},
|
||||||
scrollToContent() {
|
scrollToContent() {
|
||||||
console.log(window.scrollY)
|
console.log(window.scrollY);
|
||||||
const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50;
|
const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50;
|
||||||
const { scrollY } = window;
|
const { scrollY } = window;
|
||||||
let scrollHeight = intersectingHeaderHeight;
|
let scrollHeight = intersectingHeaderHeight;
|
||||||
@@ -178,14 +183,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
track() {
|
track() {
|
||||||
window.ga('send', 'pageview', '/lottery/game');
|
window.ga("send", "pageview", "/lottery/game");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
@import "../styles/variables.scss";
|
@import "../styles/variables.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "../styles/media-queries.scss";
|
||||||
|
|
||||||
@@ -201,7 +205,8 @@ export default {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
|
||||||
> div, > section {
|
> div,
|
||||||
|
> section {
|
||||||
@include mobile {
|
@include mobile {
|
||||||
grid-column: span 5;
|
grid-column: span 5;
|
||||||
}
|
}
|
||||||
@@ -343,6 +348,8 @@ header {
|
|||||||
|
|
||||||
> div {
|
> div {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
max-height: 638px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
@@ -369,11 +376,14 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.todays-wines {
|
||||||
|
width: 80vw;
|
||||||
|
padding: 0 10vw;
|
||||||
|
|
||||||
.wines-container {
|
@include mobile {
|
||||||
display: flex;
|
width: 90vw;
|
||||||
flex-wrap: wrap;
|
padding: 0 5vw;
|
||||||
margin-bottom: 4rem;
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,439 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="page-container">
|
|
||||||
<h1 class="title">Virtuelt lotteri registrering</h1>
|
|
||||||
<br />
|
|
||||||
<div class="draw-winner-container" v-if="attendees.length > 0">
|
|
||||||
<div v-if="drawingWinner">
|
|
||||||
<span>
|
|
||||||
Trekker {{ currentWinners }} av {{ numberOfWinners }} vinnere.
|
|
||||||
{{ secondsLeft }} sekunder av {{ drawTime }} igjen
|
|
||||||
</span>
|
|
||||||
<button class="vin-button no-margin" @click="stopDraw">Stopp trekning</button>
|
|
||||||
</div>
|
|
||||||
<div class="draw-container" v-if="!drawingWinner">
|
|
||||||
<button class="vin-button no-margin" @click="drawWinner">Trekk vinnere</button>
|
|
||||||
<input type="number" v-model="numberOfWinners" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
|
||||||
<div class="winners" v-if="winners.length > 0">
|
|
||||||
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
|
||||||
<div :class="winner.color + '-raffle'" class="raffle-element">
|
|
||||||
<span>{{ winner.name }}</span>
|
|
||||||
<span>{{ winner.phoneNumber }}</span>
|
|
||||||
<span>Rød: {{ winner.red }}</span>
|
|
||||||
<span>Blå: {{ winner.blue }}</span>
|
|
||||||
<span>Grønn: {{ winner.green }}</span>
|
|
||||||
<span>Gul: {{ winner.yellow }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="delete-buttons" v-if="attendees.length > 0 || winners.length > 0">
|
|
||||||
<button
|
|
||||||
class="vin-button"
|
|
||||||
v-if="winners.length > 0"
|
|
||||||
@click="deleteAllWinners"
|
|
||||||
>Slett virtuelle vinnere</button>
|
|
||||||
<button
|
|
||||||
class="vin-button"
|
|
||||||
v-if="attendees.length > 0"
|
|
||||||
@click="deleteAllAttendees"
|
|
||||||
>Slett virtuelle deltakere</button>
|
|
||||||
</div>
|
|
||||||
<div class="attendees" v-if="attendees.length > 0">
|
|
||||||
<h2>Deltakere ({{ attendees.length }})</h2>
|
|
||||||
<div class="attendee" v-for="(attendee, index) in attendees" :key="index">
|
|
||||||
<div class="name-and-phone">
|
|
||||||
<span class="name">{{ attendee.name }}</span>
|
|
||||||
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="raffles-container">
|
|
||||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
|
||||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
|
||||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
|
||||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="attendee-registration-container">
|
|
||||||
<h2>Legg til deltaker</h2>
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="name">Navn</label>
|
|
||||||
<input id="name" type="text" placeholder="Navn" v-model="name" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="phoneNumber">Telefonnummer</label>
|
|
||||||
<input id="phoneNumber" type="text" placeholder="Telefonnummer" v-model="phoneNumber" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="randomColors">Tilfeldig farger?</label>
|
|
||||||
<input
|
|
||||||
id="randomColors"
|
|
||||||
type="checkbox"
|
|
||||||
placeholder="Tilfeldig farger"
|
|
||||||
v-model="randomColors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!randomColors">
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="red">Rød</label>
|
|
||||||
<input id="red" type="number" placeholder="Rød" v-model="red" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="blue">Blå</label>
|
|
||||||
<input id="blue" type="number" placeholder="Blå" v-model="blue" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="green">Grønn</label>
|
|
||||||
<input id="green" type="number" placeholder="Grønn" v-model="green" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div class="label-div">
|
|
||||||
<label for="yellow">Gul</label>
|
|
||||||
<input id="yellow" type="number" placeholder="Gul" v-model="yellow" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
|
||||||
|
|
||||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import io from "socket.io-client";
|
|
||||||
import {
|
|
||||||
addAttendee,
|
|
||||||
getVirtualWinner,
|
|
||||||
attendeesSecure,
|
|
||||||
attendees,
|
|
||||||
winnersSecure,
|
|
||||||
deleteWinners,
|
|
||||||
deleteAttendees,
|
|
||||||
finishedDraw,
|
|
||||||
prelottery
|
|
||||||
} from "@/api";
|
|
||||||
import TextToast from "@/ui/TextToast";
|
|
||||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
RaffleGenerator,
|
|
||||||
TextToast
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: null,
|
|
||||||
phoneNumber: null,
|
|
||||||
red: 0,
|
|
||||||
blue: 0,
|
|
||||||
green: 0,
|
|
||||||
yellow: 0,
|
|
||||||
raffles: 0,
|
|
||||||
randomColors: false,
|
|
||||||
attendees: [],
|
|
||||||
winners: [],
|
|
||||||
drawingWinner: false,
|
|
||||||
secondsLeft: 20,
|
|
||||||
drawTime: 20,
|
|
||||||
currentWinners: 1,
|
|
||||||
numberOfWinners: 4,
|
|
||||||
socket: null,
|
|
||||||
toastText: undefined,
|
|
||||||
showToast: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getAttendees();
|
|
||||||
this.getWinners();
|
|
||||||
|
|
||||||
this.socket = io(`${window.location.hostname}:${window.location.port}`);
|
|
||||||
|
|
||||||
this.socket.on("winner", async msg => {
|
|
||||||
this.getWinners();
|
|
||||||
this.getAttendees();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.on("refresh_data", async msg => {
|
|
||||||
this.getAttendees();
|
|
||||||
this.getWinners();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.on("new_attendee", async msg => {
|
|
||||||
this.getAttendees();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.finishedDraw = finishedDraw;
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setWithRandomColors(colors) {
|
|
||||||
Object.keys(colors).forEach(color => (this[color] = colors[color]));
|
|
||||||
},
|
|
||||||
sendAttendee: async function() {
|
|
||||||
if (this.red == 0 && this.blue == 0 && this.green == 0 && this.yellow == 0) {
|
|
||||||
alert('Ingen farger valgt!')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.name == 0 && this.phoneNumber) {
|
|
||||||
alert('Ingen navn eller tlf satt!')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = await addAttendee({
|
|
||||||
name: this.name,
|
|
||||||
phoneNumber: this.phoneNumber,
|
|
||||||
red: this.red,
|
|
||||||
blue: this.blue,
|
|
||||||
green: this.green,
|
|
||||||
yellow: this.yellow,
|
|
||||||
raffles: this.raffles
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response == true) {
|
|
||||||
this.toastText = `Sendt inn deltaker: ${this.name}`;
|
|
||||||
this.showToast = true;
|
|
||||||
|
|
||||||
this.name = null;
|
|
||||||
this.phoneNumber = null;
|
|
||||||
this.yellow = 0;
|
|
||||||
this.green = 0;
|
|
||||||
this.red = 0;
|
|
||||||
this.blue = 0;
|
|
||||||
|
|
||||||
this.getAttendees();
|
|
||||||
} else {
|
|
||||||
alert("Klarte ikke sende inn.. Er du logget inn?");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getAttendees: async function() {
|
|
||||||
let response = await attendeesSecure();
|
|
||||||
this.attendees = response;
|
|
||||||
},
|
|
||||||
stopDraw: function() {
|
|
||||||
this.drawingWinner = false;
|
|
||||||
this.secondsLeft = this.drawTime;
|
|
||||||
},
|
|
||||||
drawWinner: async function() {
|
|
||||||
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
|
||||||
this.drawingWinner = true;
|
|
||||||
let response = await getVirtualWinner();
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
console.log("Winner:", response.winner);
|
|
||||||
if (this.currentWinners < this.numberOfWinners) {
|
|
||||||
this.countdown();
|
|
||||||
} else {
|
|
||||||
this.drawingWinner = false;
|
|
||||||
let finished = await finishedDraw();
|
|
||||||
if(finished) {
|
|
||||||
alert("SMS'er er sendt ut!");
|
|
||||||
} else {
|
|
||||||
alert("Noe gikk galt under SMS utsendelser.. Sjekk logg og database for id'er.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.getWinners();
|
|
||||||
this.getAttendees();
|
|
||||||
} else {
|
|
||||||
this.drawingWinner = false;
|
|
||||||
alert("Noe gikk galt under trekningen..! " + response["message"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
countdown: function() {
|
|
||||||
this.secondsLeft -= 1;
|
|
||||||
if (!this.drawingWinner) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.secondsLeft <= 0) {
|
|
||||||
this.secondsLeft = this.drawTime;
|
|
||||||
this.currentWinners += 1;
|
|
||||||
if (this.currentWinners <= this.numberOfWinners) {
|
|
||||||
this.drawWinner();
|
|
||||||
} else {
|
|
||||||
this.drawingWinner = false;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.countdown();
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
deleteAllWinners: async function() {
|
|
||||||
if (window.confirm("Er du sikker på at du vil slette vinnere?")) {
|
|
||||||
let response = await deleteWinners();
|
|
||||||
if (response) {
|
|
||||||
this.getWinners();
|
|
||||||
} else {
|
|
||||||
alert("Klarte ikke hente ut vinnere");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteAllAttendees: async function() {
|
|
||||||
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
|
|
||||||
let response = await deleteAttendees();
|
|
||||||
if (response) {
|
|
||||||
this.getAttendees();
|
|
||||||
} else {
|
|
||||||
alert("Klarte ikke hente ut vinnere");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getWinners: async function() {
|
|
||||||
let response = await winnersSecure();
|
|
||||||
if (response) {
|
|
||||||
this.winners = response;
|
|
||||||
} else {
|
|
||||||
alert("Klarte ikke hente ut vinnere");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../styles/global.scss";
|
|
||||||
@import "../styles/media-queries.scss";
|
|
||||||
|
|
||||||
.draw-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draw-winner-container,
|
|
||||||
.delete-buttons {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-buttons {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-family: knowit, Arial;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-family: knowit, Arial;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
width: 90%;
|
|
||||||
margin: 2rem auto;
|
|
||||||
color: grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-container {
|
|
||||||
padding: 0 1.5rem 3rem;
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
max-width: 60vw;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#randomColors {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
&:checked {
|
|
||||||
background: green;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.raffle-element {
|
|
||||||
width: 140px;
|
|
||||||
height: 150px;
|
|
||||||
margin: 20px 0;
|
|
||||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
mask-image: url(/public/assets/images/lodd.svg);
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: bold;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&.small {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.green-raffle {
|
|
||||||
background-color: $light-green;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.blue-raffle {
|
|
||||||
background-color: $light-blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.yellow-raffle {
|
|
||||||
background-color: $light-yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.red-raffle {
|
|
||||||
background-color: $light-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: flex !important;
|
|
||||||
margin: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.winners {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attendees {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attendee {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 50%;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
& .name-and-phone,
|
|
||||||
& .raffles-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .name-and-phone {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .raffles-container {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div v-if="!posted">
|
<div v-if="!posted" class="container">
|
||||||
<h1 v-if="name">Gratulerer {{name}}!</h1>
|
<h1 v-if="name">Gratulerer {{ name }}!</h1>
|
||||||
|
|
||||||
<p v-if="name">
|
<p v-if="name">
|
||||||
Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.
|
Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.
|
||||||
</p>
|
</p>
|
||||||
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
|
|
||||||
|
<h1 v-else-if="!turn && wines.length" class="sent-container">Finner ikke noen vinner her..</h1>
|
||||||
|
|
||||||
<h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1>
|
<h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1>
|
||||||
|
|
||||||
<div class="wines-container" v-if="name">
|
<div class="wines-container" v-if="name">
|
||||||
<Wine :wine="wine" v-for="wine in wines" :key="wine">
|
<Wine :wine="wine" v-for="wine in wines" :key="wine">
|
||||||
<button
|
<button @click="chooseWine(wine)" class="vin-button select-wine">Velg denne vinnen</button>
|
||||||
@click="chooseWine(wine.name)"
|
|
||||||
class="vin-button select-wine"
|
|
||||||
>Velg denne vinnen</button>
|
|
||||||
</Wine>
|
</Wine>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="posted" class="sent-container">
|
<div v-else-if="posted" class="sent-container">
|
||||||
<h1>Valget ditt er sendt inn!</h1>
|
<h1>Valget ditt er sendt inn!</h1>
|
||||||
<p>Du får mer info om henting snarest!</p>
|
<p>Du får mer info om henting snarest!</p>
|
||||||
@@ -24,15 +26,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getAmIWinner, postWineChosen, prelottery } from "@/api";
|
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Wine },
|
components: { Wine },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
existing: false,
|
|
||||||
fetched: false,
|
|
||||||
turn: false,
|
turn: false,
|
||||||
name: null,
|
name: null,
|
||||||
wines: [],
|
wines: [],
|
||||||
@@ -40,30 +40,43 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.id = this.$router.currentRoute.params.id;
|
const { id } = this.$router.currentRoute.params;
|
||||||
|
|
||||||
let winnerObject = await getAmIWinner(this.id);
|
this.id = id;
|
||||||
this.fetched = true;
|
this.getPrizes(id);
|
||||||
if (!winnerObject || !winnerObject.existing) {
|
|
||||||
console.error("non existing", winnerObject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.existing = true;
|
|
||||||
if (winnerObject.existing && !winnerObject.turn) {
|
|
||||||
console.error("not your turn yet", winnerObject);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.turn = true;
|
|
||||||
this.name = winnerObject.name;
|
|
||||||
this.wines = await prelottery();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
chooseWine: async function(name) {
|
getPrizes(id) {
|
||||||
let posted = await postWineChosen(this.id, name);
|
fetch(`/api/lottery/prize-distribution/prizes/${id}`)
|
||||||
console.log("response", posted);
|
.then(resp => resp.json())
|
||||||
if (posted.success) {
|
.then(response => {
|
||||||
this.posted = true;
|
if (response.success) {
|
||||||
}
|
this.wines = response.wines;
|
||||||
|
this.name = response.winner.name;
|
||||||
|
this.turn = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
chooseWine(wine) {
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ wine })
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/api/lottery/prize-distribution/prize/${this.id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.$toast.info({ title: `Valgt vin: ${wine.name}` });
|
||||||
|
this.posted = true;
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Klarte ikke velge vin :(",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -74,9 +87,19 @@ export default {
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 2000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wines-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.sent-container {
|
.sent-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
@@ -90,11 +113,4 @@ export default {
|
|||||||
.select-wine {
|
.select-wine {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.wines-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<h1>Trekk vinnere</h1>
|
||||||
|
|
||||||
|
<div class="draw-winner-container">
|
||||||
|
<div v-if="drawingWinner == false" class="draw-container">
|
||||||
|
<input type="number" v-model="winnersToDraw" />
|
||||||
|
<button class="vin-button no-margin" @click="startDrawingWinners">Trekk vinnere</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="wines.length" class="wines-left">
|
||||||
|
<span>Antall vin igjen: {{ winnersToDraw }} av {{ wines.length }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="drawingWinner == true">
|
||||||
|
<p>Trekker vinner {{ winners.length }} av {{ wines.length }}.</p>
|
||||||
|
<p>Neste trekning om {{ secondsLeft }} sekunder av {{ drawTime }}</p>
|
||||||
|
|
||||||
|
<div class="button-container draw-winner-actions">
|
||||||
|
<button class="vin-button danger" @click="stopDraw">
|
||||||
|
Stopp trekning
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="vin-button"
|
||||||
|
:class="{ 'pulse-button': secondsLeft == 0 }"
|
||||||
|
:disabled="secondsLeft > 0"
|
||||||
|
@click="drawWinner"
|
||||||
|
>
|
||||||
|
Trekk neste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prize-distribution">
|
||||||
|
<h2>Prisutdeling</h2>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button class="vin-button" @click="startPrizeDistribution">Start automatisk prisutdeling med SMS</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||||
|
<div class="winners" v-if="winners.length > 0">
|
||||||
|
<div :class="winner.color + '-raffle'" class="raffle-element" v-for="(winner, index) in winners" :key="index">
|
||||||
|
<span>{{ winner.name }}</span>
|
||||||
|
<span>Phone: {{ winner.phoneNumber }}</span>
|
||||||
|
<span>Rød: {{ winner.red }}</span>
|
||||||
|
<span>Blå: {{ winner.blue }}</span>
|
||||||
|
<span>Grønn: {{ winner.green }}</span>
|
||||||
|
<span>Gul: {{ winner.yellow }}</span>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button class="vin-button small" @click="editingWinner = editingWinner == winner ? false : winner">
|
||||||
|
{{ editingWinner == winner ? "Lukk" : "Rediger" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="editingWinner == winner" class="edit">
|
||||||
|
<div class="label-div" v-for="key in Object.keys(winner)" :key="key">
|
||||||
|
<label>{{ key }}</label>
|
||||||
|
<input type="text" v-model="winner[key]" :placeholder="key" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="editingWinner == winner" class="button-container column">
|
||||||
|
<button class="vin-button small" @click="notifyWinner(winner)">
|
||||||
|
Send SMS
|
||||||
|
</button>
|
||||||
|
<button class="vin-button small warning" @click="updateWinner(winner)">
|
||||||
|
Oppdater
|
||||||
|
</button>
|
||||||
|
<button class="vin-button small danger" @click="deleteWinner(winner)">
|
||||||
|
Slett
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container margin-md" v-if="winners.length > 0">
|
||||||
|
<button class="vin-button danger" v-if="winners.length > 0" @click="deleteAllWinners">
|
||||||
|
Slett virtuelle vinnere
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
wines: [],
|
||||||
|
drawingWinner: false,
|
||||||
|
secondsLeft: 20,
|
||||||
|
drawTime: 20,
|
||||||
|
winners: [],
|
||||||
|
editingWinner: undefined
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchLotterWines();
|
||||||
|
this.fetchLotterWinners();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
winnersToDraw() {
|
||||||
|
if (this.wines.length == undefined || this.winners.length == undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.wines.length - this.winners.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
winners(val) {
|
||||||
|
this.$emit("counter", val.length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchLotterWines() {
|
||||||
|
return fetch("/api/lottery/wines")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.wines = response.wines));
|
||||||
|
},
|
||||||
|
fetchLotterWinners() {
|
||||||
|
return fetch("/api/lottery/winners")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.winners = response.winners));
|
||||||
|
},
|
||||||
|
countdown() {
|
||||||
|
if (this.drawingWinner == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.secondsLeft > 0) {
|
||||||
|
this.secondsLeft -= 1;
|
||||||
|
|
||||||
|
setTimeout(_ => {
|
||||||
|
this.countdown();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
if (this.winners.length == this.wines.length) {
|
||||||
|
this.drawingWinner = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startDrawingWinners() {
|
||||||
|
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||||
|
this.drawWinner();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawWinner() {
|
||||||
|
if (this.winnersToDraw <= 0) {
|
||||||
|
this.$toast.error({ title: "No more wines to draw" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.secondsLeft = this.drawTime;
|
||||||
|
this.drawingWinner = true;
|
||||||
|
|
||||||
|
fetch("/api/lottery/draw")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
const { winner, color, success, message } = response;
|
||||||
|
|
||||||
|
if (success == false) {
|
||||||
|
this.$toast.error({ title: message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
winner.color = color;
|
||||||
|
this.winners.push(winner);
|
||||||
|
this.countdown();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error) {
|
||||||
|
this.$toast.error({ title: error.message });
|
||||||
|
}
|
||||||
|
this.drawingWinner = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
stopDraw() {
|
||||||
|
this.drawingWinner = false;
|
||||||
|
this.secondsLeft = this.drawTime;
|
||||||
|
},
|
||||||
|
startPrizeDistribution() {
|
||||||
|
if (!window.confirm("Er du sikker på at du vil starte prisutdeling?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawingWinner = false;
|
||||||
|
|
||||||
|
const options = { method: "POST" };
|
||||||
|
fetch(`/api/lottery/prize-distribution/start`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: `Startet prisutdeling. SMS'er sendt ut!`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: `Klarte ikke starte prisutdeling`,
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
notifyWinner(winner) {
|
||||||
|
const options = { method: "POST" };
|
||||||
|
|
||||||
|
fetch(`/api/lottery/messages/winner/${winner.id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: `Sendte sms til vinner ${winner.name}.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: `Klarte ikke sende sms til vinner ${winner.name}`,
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateWinner(winner) {
|
||||||
|
const options = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ winner })
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: `Oppdaterte vinner ${winner.name}.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: `Klarte ikke oppdatere vinner ${winner.name}`,
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteWinner(winner) {
|
||||||
|
if (winner._id != null && window.confirm(`Er du sikker på at du vil slette vinner ${winner.name}?`)) {
|
||||||
|
const options = { method: "DELETE" };
|
||||||
|
|
||||||
|
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.winners = this.winners.filter(w => w.id != winner.id);
|
||||||
|
|
||||||
|
this.$toast.info({
|
||||||
|
title: `Slettet vinner ${winner.name}.`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: `Klarte ikke slette vinner ${winner.name}`,
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteAllWinners() {
|
||||||
|
if (window.confirm("Er du sikker på at du vil slette alle vinnere?")) {
|
||||||
|
const options = { method: "DELETE" };
|
||||||
|
|
||||||
|
fetch("/api/lottery/winners", options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.winners = [];
|
||||||
|
this.$toast.info({
|
||||||
|
title: "Slettet alle vinnere."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Klarte ikke slette vinnere",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wines-left {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
padding: 7px;
|
||||||
|
margin: 0;
|
||||||
|
width: 10rem;
|
||||||
|
height: 3rem;
|
||||||
|
border: 1px solid rgba(#333333, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-winner-actions {
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winners {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.raffle-element {
|
||||||
|
width: 220px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 250px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
// text-align: center;
|
||||||
|
|
||||||
|
-webkit-mask-size: cover;
|
||||||
|
-moz-mask-size: cover;
|
||||||
|
mask-size: cover;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
span:first-of-type {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.active {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
frontend/components/admin/PushPage.vue
Normal file
59
frontend/components/admin/PushPage.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<h1>Send push melding</h1>
|
||||||
|
|
||||||
|
<div class="notification-element">
|
||||||
|
<div class="label-div">
|
||||||
|
<label for="notification">Melding</label>
|
||||||
|
<textarea id="notification" type="text" rows="3" v-model="pushMessage" placeholder="Push meldingtekst" />
|
||||||
|
</div>
|
||||||
|
<div class="label-div">
|
||||||
|
<label for="notification-link">Push åpner lenke</label>
|
||||||
|
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-container margin-top-sm">
|
||||||
|
<button class="vin-button" @click="sendPush">Send push</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pushMessage: "",
|
||||||
|
pushLink: "/"
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendPush: async function() {
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: this.pushMessage,
|
||||||
|
link: this.pushLink
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch("/subscription/send-notification", options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: "Sendt!"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Noe gikk galt!",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
308
frontend/components/admin/RegisterWinePage.vue
Normal file
308
frontend/components/admin/RegisterWinePage.vue
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Register vin</h1>
|
||||||
|
|
||||||
|
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button class="vin-button" @click="showCamera = !showCamera">
|
||||||
|
{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="vin-button" @click="manualyFillInnWine">
|
||||||
|
Legg til en vin manuelt
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="vin-button" @click="showImportLink = !showImportLink">
|
||||||
|
{{ showImportLink ? "Skjul importer fra link" : "Importer fra link" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showImportLink" class="import-from-link">
|
||||||
|
<label>Importer vin fra vinmonopolet link:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Vinmonopol lenke"
|
||||||
|
ref="vinmonopoletLinkInput"
|
||||||
|
autocapitalize="none"
|
||||||
|
@input="addWineByUrl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="linkError" class="error">
|
||||||
|
{{ linkError }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="wines.length > 0" class="wine-edit-container">
|
||||||
|
<h2>Dagens registrerte viner</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="vin-button" @click="sendWines">Send inn dagens viner</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wines">
|
||||||
|
<wine v-for="wine in wines" :key="wine.id" :wine="wine">
|
||||||
|
<template v-slot:default>
|
||||||
|
<div v-if="editingWine == wine" class="wine-edit">
|
||||||
|
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
|
||||||
|
<label>{{ key }}</label>
|
||||||
|
<input type="text" v-model="wine[key]" :placeholder="key" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:bottom>
|
||||||
|
<div class="button-container row small">
|
||||||
|
<button v-if="editingWine == wine && wine._id" class="vin-button warning" @click="updateWine(wine)">
|
||||||
|
Oppdater vin
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="vin-button" @click="editingWine = editingWine == wine ? false : wine">
|
||||||
|
{{ editingWine == wine ? "Lukk" : "Rediger" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="danger vin-button" @click="deleteWine(wine)">
|
||||||
|
Slett
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</wine>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container" v-if="wines.length > 0"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
||||||
|
import Wine from "@/ui/Wine";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ScanToVinmonopolet, Wine },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
wines: [],
|
||||||
|
editingWine: undefined,
|
||||||
|
showCamera: false,
|
||||||
|
showImportLink: false,
|
||||||
|
linkError: undefined
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
wines() {
|
||||||
|
this.$emit("counter", this.wines.length);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchLotterWines();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchLotterWines() {
|
||||||
|
fetch("/api/lottery/wines")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.wines = response.wines));
|
||||||
|
},
|
||||||
|
wineFromVinmonopoletScan(wineResponse) {
|
||||||
|
if (this.wines.map(wine => wine.name).includes(wineResponse.name)) {
|
||||||
|
this.toastText = "Vinen er allerede lagt til.";
|
||||||
|
this.showToast = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastText = "Fant og la til vin:<br>" + wineResponse.name;
|
||||||
|
this.showToast = true;
|
||||||
|
|
||||||
|
this.wines.unshift(wineResponse);
|
||||||
|
},
|
||||||
|
manualyFillInnWine() {
|
||||||
|
fetch("/api/lottery/wine/schema")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.schema)
|
||||||
|
.then(wineSchema => {
|
||||||
|
this.editingWine = wineSchema;
|
||||||
|
this.wines.unshift(wineSchema);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addWineByUrl(event) {
|
||||||
|
const url = event.target.value;
|
||||||
|
this.linkError = null;
|
||||||
|
|
||||||
|
if (!url.includes("vinmonopolet.no")) {
|
||||||
|
this.linkError = "Dette er ikke en gydlig vinmonopolet lenke.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = url.split("/").pop();
|
||||||
|
|
||||||
|
fetch(`/api/vinmonopolet/wine/by-id/${id}`)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
const { wine } = response;
|
||||||
|
this.wines.unshift(wine);
|
||||||
|
this.$refs.vinmonopoletLinkInput.value = "";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sendWines() {
|
||||||
|
const filterOutExistingWines = wine => wine["_id"] == null;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
wines: this.wines.filter(filterOutExistingWines)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("/api/lottery/wines", options).then(resp => {
|
||||||
|
try {
|
||||||
|
if (resp.ok == false) {
|
||||||
|
throw resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.json().then(response => {
|
||||||
|
if (response.success == false) {
|
||||||
|
throw response;
|
||||||
|
} else {
|
||||||
|
this.$toast.info({
|
||||||
|
title: "Viner sendt inn!",
|
||||||
|
timeout: 4000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Feil oppsto ved innsending!",
|
||||||
|
description: error.message,
|
||||||
|
timeout: 4000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateWine(updatedWine) {
|
||||||
|
const options = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ wine: updatedWine })
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/api/lottery/wine/${updatedWine._id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
this.editingWine = null;
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: response.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteWine(deletedWine) {
|
||||||
|
this.wines = this.wines.filter(wine => wine.name != deletedWine.name);
|
||||||
|
|
||||||
|
if (deletedWine._id == null) return;
|
||||||
|
|
||||||
|
const options = { method: "DELETE" };
|
||||||
|
fetch(`/api/lottery/wine/${deletedWine._id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
this.editingWine = null;
|
||||||
|
|
||||||
|
this.$toast.info({
|
||||||
|
title: response.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/styles/media-queries.scss";
|
||||||
|
@import "@/styles/variables.scss";
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin: 1.5rem 0 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-from-link {
|
||||||
|
width: 70%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 1.5rem auto 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background-color: $light-red;
|
||||||
|
color: $red;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wine-edit-container {
|
||||||
|
max-width: 1500px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.wines {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
button:not(:last-child) {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
441
frontend/components/admin/archiveLotteryPage.vue
Normal file
441
frontend/components/admin/archiveLotteryPage.vue
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<h1>Arkiver lotteri</h1>
|
||||||
|
|
||||||
|
<h2>Registrer lodd kjøpt</h2>
|
||||||
|
|
||||||
|
<div class="colors">
|
||||||
|
<div v-for="color in lotteryColors" :class="color.key + ' colors-box'" :key="color">
|
||||||
|
<div class="colors-overlay">
|
||||||
|
<p>{{ color.name }} kjøpt</p>
|
||||||
|
<input v-model.number="color.value" min="0" :placeholder="0" type="number" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="label-div">
|
||||||
|
<label>Penger mottatt på vipps:</label>
|
||||||
|
<input v-model.number="payed" placeholder="NOK" type="number" :step="price || 1" min="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="wines.length > 0">
|
||||||
|
<h2>Vinneres vin-valg</h2>
|
||||||
|
|
||||||
|
<div class="winner-container">
|
||||||
|
<wine v-for="wine in wines" :key="wine.id" :wine="wine">
|
||||||
|
<div class="label-div">
|
||||||
|
<label for="potential-winner-name">Virtuelle vinnere</label>
|
||||||
|
<select id="potential-winner-name" type="text" placeholder="Navn" v-model="wine.winner">
|
||||||
|
<option v-for="winner in winners" :value="winner">{{ winner.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="winner-element">
|
||||||
|
<div class="color-selector">
|
||||||
|
<div class="label-div">
|
||||||
|
<label>Farge vunnet</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="blue"
|
||||||
|
:class="{ active: wine.winner.color == 'blue' }"
|
||||||
|
@click="wine.winner.color = 'blue'"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="red"
|
||||||
|
:class="{ active: wine.winner.color == 'red' }"
|
||||||
|
@click="wine.winner.color = 'red'"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="green"
|
||||||
|
:class="{ active: wine.winner.color == 'green' }"
|
||||||
|
@click="wine.winner.color = 'green'"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="yellow"
|
||||||
|
:class="{ active: wine.winner.color == 'yellow' }"
|
||||||
|
@click="wine.winner.color = 'yellow'"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="label-div">
|
||||||
|
<label for="winner-name">Navn vinner</label>
|
||||||
|
<input id="winner-name" type="text" placeholder="Navn" v-model="wine.winner.name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</wine>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="wines.length > 0" class="button-container column">
|
||||||
|
<button class="vin-button" @click="archiveLottery">Send inn og arkiver</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { dateString } from "@/utils";
|
||||||
|
import Wine from "@/ui/Wine";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Wine },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
payed: undefined,
|
||||||
|
wines: [],
|
||||||
|
winners: [],
|
||||||
|
attendees: [],
|
||||||
|
lotteryColors: [
|
||||||
|
{ value: 0, name: "Blå", key: "blue" },
|
||||||
|
{ value: 0, name: "Rød", key: "red" },
|
||||||
|
{ value: 0, name: "Grønn", key: "green" },
|
||||||
|
{ value: 0, name: "Gul", key: "yellow" }
|
||||||
|
],
|
||||||
|
price: __PRICE__ || 10
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchLotteryWines();
|
||||||
|
this.fetchLotteryWinners();
|
||||||
|
this.fetchLotteryAttendees();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
lotteryColors: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.payed = this.getRaffleValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
payed(val) {
|
||||||
|
this.$emit("counter", val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
wineWithWinnerMapper(wine) {
|
||||||
|
if (wine.winner == undefined) {
|
||||||
|
wine.winner = {
|
||||||
|
name: undefined,
|
||||||
|
color: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return wine;
|
||||||
|
},
|
||||||
|
fetchLotteryWines() {
|
||||||
|
return fetch("/api/lottery/wines")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.wines = response.wines.map(this.wineWithWinnerMapper);
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Klarte ikke hente viner.",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchLotteryWinners() {
|
||||||
|
return fetch("/api/lottery/winners")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.winners = response.winners;
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Klarte ikke hente vinnere.",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchLotteryAttendees() {
|
||||||
|
return fetch("/api/lottery/attendees")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success && response.attendees) {
|
||||||
|
this.attendees = response.attendees;
|
||||||
|
this.updateLotteryColorsWithAttendees(response.attendees)
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Klarte ikke hente deltakere.",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateLotteryColorsWithAttendees(attendees) {
|
||||||
|
this.attendees.map(attendee => {
|
||||||
|
this.lotteryColors.map(color => (color.value += attendee[color.key]));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getRaffleValue() {
|
||||||
|
let rafflesBought = 0;
|
||||||
|
this.lotteryColors.map(color => rafflesBought += Number(color.value));
|
||||||
|
|
||||||
|
return rafflesBought * this.price;
|
||||||
|
},
|
||||||
|
archiveLottery: async function(event) {
|
||||||
|
const validation = this.wines.every(wine => {
|
||||||
|
if (wine.winner.name == undefined || wine.winner.name == "") {
|
||||||
|
this.$toast.error({
|
||||||
|
title: `Navn på vinner må defineres for vin: ${wine.name}`
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (wine.winner.color == undefined || wine.winner.color == "") {
|
||||||
|
this.$toast.error({
|
||||||
|
title: `Farge vunnet må defineres for vin: ${wine.name}`
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validation == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rafflesPayload = {};
|
||||||
|
this.lotteryColors.map(el => rafflesPayload.[el.key] = el.value);
|
||||||
|
|
||||||
|
let stolen = 0;
|
||||||
|
const payedDiff = this.payed - this.getRaffleValue()
|
||||||
|
if (payedDiff) {
|
||||||
|
stolen = payedDiff / this.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
wines: this.wines,
|
||||||
|
raffles: rafflesPayload,
|
||||||
|
stolen: stolen
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
lottery: payload
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch("/api/lottery/archive", options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: "Lotteriet er sendt inn og arkivert! Du kan nå slette viner, deltakere & vinnere slettes.",
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Noe gikk galt under innsending!",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/styles/global.scss";
|
||||||
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
|
select {
|
||||||
|
margin: 0 0 auto;
|
||||||
|
height: 2rem;
|
||||||
|
min-width: 0;
|
||||||
|
width: 98%;
|
||||||
|
padding: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
padding: 0 1.5rem 3rem;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
max-width: 60vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.winner-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin: 1rem;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-element {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-selector {
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
min-width: 175px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
max-width: 25vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border: 2px solid unset;
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
border-color: $green;
|
||||||
|
}
|
||||||
|
&.blue {
|
||||||
|
border-color: $dark-blue;
|
||||||
|
}
|
||||||
|
&.red {
|
||||||
|
border-color: $red;
|
||||||
|
}
|
||||||
|
&.yellow {
|
||||||
|
border-color: $dark-yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
|
||||||
|
// disable-dbl-tap-zoom
|
||||||
|
touch-action: manipulation;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
background: #c8f9df;
|
||||||
|
}
|
||||||
|
&.blue {
|
||||||
|
background: #d4f2fe;
|
||||||
|
}
|
||||||
|
&.red {
|
||||||
|
background: #fbd7de;
|
||||||
|
}
|
||||||
|
&.yellow {
|
||||||
|
background: #fff6d6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.colors {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
margin: 1.8rem auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-div {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.colors-box {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
margin: 20px;
|
||||||
|
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
mask-image: url(/public/assets/images/lodd.svg);
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.colors-overlay {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.4rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 70%;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 3rem;
|
||||||
|
height: unset;
|
||||||
|
max-height: unset;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.green,
|
||||||
|
.green .colors-overlay > input {
|
||||||
|
background-color: $light-green;
|
||||||
|
color: $green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue,
|
||||||
|
.blue .colors-overlay > input {
|
||||||
|
background-color: $light-blue;
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow,
|
||||||
|
.yellow .colors-overlay > input {
|
||||||
|
background-color: $light-yellow;
|
||||||
|
color: $yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red,
|
||||||
|
.red .colors-overlay > input {
|
||||||
|
background-color: $light-red;
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
329
frontend/components/admin/registerAttendeePage.vue
Normal file
329
frontend/components/admin/registerAttendeePage.vue
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<h1>Legg til deltaker</h1>
|
||||||
|
|
||||||
|
<div class="attendee-registration-container">
|
||||||
|
<div class="row flex">
|
||||||
|
<div class="label-div">
|
||||||
|
<label for="name" ref="name">Navn</label>
|
||||||
|
<input id="name" type="text" placeholder="Navn" v-model="name" />
|
||||||
|
|
||||||
|
<ul class="autocomplete" v-if="autocompleteAttendees.length">
|
||||||
|
<a
|
||||||
|
v-for="attendee in autocompleteAttendees"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.enter="setName(attendee)"
|
||||||
|
@keydown.space="setName(attendee)"
|
||||||
|
>
|
||||||
|
<li @click="setName(attendee)">
|
||||||
|
{{ attendee }}
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="label-div">
|
||||||
|
<label for="phoneNumber">Telefonnummer</label>
|
||||||
|
<input
|
||||||
|
id="phoneNumber"
|
||||||
|
ref="phone"
|
||||||
|
type="phone"
|
||||||
|
pattern="[0-9]"
|
||||||
|
placeholder="Telefonnummer"
|
||||||
|
v-model="phoneNumber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="label-div">
|
||||||
|
<label for="randomColors">Tilfeldig farger?</label>
|
||||||
|
<input id="randomColors" type="checkbox" placeholder="Tilfeldig farger" v-model="randomColors" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!randomColors">
|
||||||
|
<div class="row flex">
|
||||||
|
<div class="label-div" v-for="color in colors">
|
||||||
|
<label :for="color.key">{{ color.name }}</label>
|
||||||
|
<input :id="color.key" type="number" :placeholder="color.name" v-model="color.value" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
||||||
|
|
||||||
|
<div v-if="randomColors">
|
||||||
|
<RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" :compact="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Attendees :attendees="attendees" :admin="isAdmin" />
|
||||||
|
|
||||||
|
<div v-if="attendees.length" class="button-container" style="margin-top: 2rem;">
|
||||||
|
<button class="vin-button danger" @click="deleteAllAttendees">
|
||||||
|
Slett alle deltakere
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import io from "socket.io-client";
|
||||||
|
import Attendees from "@/ui/Attendees";
|
||||||
|
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Attendees,
|
||||||
|
RaffleGenerator
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
red: {
|
||||||
|
name: "Rød",
|
||||||
|
key: "red",
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
name: "Blå",
|
||||||
|
key: "blue",
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
name: "Grønn",
|
||||||
|
key: "green",
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
name: "Gul",
|
||||||
|
key: "yellow",
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
isAdmin: false,
|
||||||
|
name: null,
|
||||||
|
phoneNumber: null,
|
||||||
|
raffles: 0,
|
||||||
|
randomColors: false,
|
||||||
|
attendees: [],
|
||||||
|
autocompleteAttendees: [],
|
||||||
|
socket: null,
|
||||||
|
previousAttendees: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
attendees() {
|
||||||
|
this.$emit("counter", this.attendees.length || 0);
|
||||||
|
},
|
||||||
|
randomColors(val) {
|
||||||
|
if (val == false) {
|
||||||
|
this.colors.map(color => (color.value = 0));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name(newVal, oldVal) {
|
||||||
|
if (newVal == "" || newVal == null) {
|
||||||
|
this.autocompleteAttendees = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.autocompleteAttendees.includes(newVal)) {
|
||||||
|
this.autocompleteAttendees = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.previousAttendees.length == 0) {
|
||||||
|
fetch(`/api/history`)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.previousAttendees = response.winners));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.autocompleteAttendees = this.previousAttendees
|
||||||
|
.filter(attendee => attendee.name.toLowerCase().includes(newVal))
|
||||||
|
.map(attendee => attendee.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getAttendees();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
colors() {
|
||||||
|
return [this.red, this.blue, this.green, this.yellow];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setName(name) {
|
||||||
|
this.name = name;
|
||||||
|
this.$refs.phone.focus();
|
||||||
|
},
|
||||||
|
setWithRandomColors(colors) {
|
||||||
|
Object.keys(colors).forEach(color => (this[color].value = colors[color]));
|
||||||
|
},
|
||||||
|
checkIfAdmin(resp) {
|
||||||
|
this.isAdmin = resp.headers.get("vinlottis-admin") == "true" || false;
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
getAttendees: async function() {
|
||||||
|
return fetch("/api/lottery/attendees")
|
||||||
|
.then(resp => this.checkIfAdmin(resp))
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => (this.attendees = response.attendees));
|
||||||
|
},
|
||||||
|
sendAttendee: async function() {
|
||||||
|
const { red, blue, green, yellow } = this;
|
||||||
|
|
||||||
|
if (red.value == 0 && blue.value == 0 && green.value == 0 && yellow.value == 0) {
|
||||||
|
this.$toast.error({ title: "Ingen farger valgt!" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.name == 0 && this.phoneNumber) {
|
||||||
|
this.$toast.error({ title: "Ingen navn eller tlf satt!" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attendee = {
|
||||||
|
name: this.name,
|
||||||
|
phoneNumber: Number(this.phoneNumber),
|
||||||
|
red: Number(red.value),
|
||||||
|
blue: Number(blue.value),
|
||||||
|
green: Number(green.value),
|
||||||
|
yellow: Number(yellow.value),
|
||||||
|
raffles: Number(this.raffles)
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ attendee })
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch("/api/lottery/attendee", options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success == true) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: `Sendt inn deltaker: ${this.name}`,
|
||||||
|
timeout: 4000
|
||||||
|
});
|
||||||
|
|
||||||
|
this.name = "";
|
||||||
|
this.phoneNumber = null;
|
||||||
|
this.yellow.value = 0;
|
||||||
|
this.green.value = 0;
|
||||||
|
this.red.value = 0;
|
||||||
|
this.blue.value = 0;
|
||||||
|
this.randomColors = false;
|
||||||
|
|
||||||
|
this.$refs.name.focus();
|
||||||
|
this.getAttendees();
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: `Klarte ikke sende deltaker`,
|
||||||
|
description: response.message,
|
||||||
|
timeout: 4000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteAllAttendees() {
|
||||||
|
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
|
||||||
|
const options = { method: "DELETE" };
|
||||||
|
|
||||||
|
fetch("/api/lottery/attendees", options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
this.attendees = [];
|
||||||
|
this.$toast.info({
|
||||||
|
title: "Slettet alle deltakere."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: "Klarte ikke slette deltakere",
|
||||||
|
description: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
// global styling for disabling height of attendee class
|
||||||
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
|
.attendee {
|
||||||
|
max-height: unset;
|
||||||
|
|
||||||
|
.raffle-element {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/styles/global.scss";
|
||||||
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
|
.attendee-registration-container {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.flex .label-div {
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e1e4e8;
|
||||||
|
|
||||||
|
& li {
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e1e4e8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
width: 90%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
padding: 0 1.5rem 3rem;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
max-width: 60vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#randomColors {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:checked::after {
|
||||||
|
content: "✅";
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
font-size: 2.1rem;
|
||||||
|
content: "❌";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
frontend/plugins/Toast/Toast.vue
Normal file
166
frontend/plugins/Toast/Toast.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="slide">
|
||||||
|
<div class="toast" :class="type" v-if="show" ref="toast">
|
||||||
|
<div class="message">
|
||||||
|
<span v-html="title"></span>
|
||||||
|
<span class="description" v-if="description">
|
||||||
|
{{ description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button @click="dismiss">Lukk</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: this.$root.type || "info",
|
||||||
|
title: this.$root.title || undefined,
|
||||||
|
description: this.$root.description || undefined,
|
||||||
|
image: this.$root.image || undefined,
|
||||||
|
link: this.$root.link || undefined,
|
||||||
|
timeout: this.$root.timeout || 4500,
|
||||||
|
show: false,
|
||||||
|
mouseover: false,
|
||||||
|
timedOut: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Here we set show when mounted in-order to get the transition animation to be displayed correctly
|
||||||
|
this.show = true;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.log("Your toast time is up 👋");
|
||||||
|
|
||||||
|
if (this.mouseover === false) {
|
||||||
|
this.show = false;
|
||||||
|
} else {
|
||||||
|
this.timedOut = true;
|
||||||
|
}
|
||||||
|
}, this.timeout);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const { toast } = this.$refs;
|
||||||
|
|
||||||
|
if (toast) {
|
||||||
|
toast.addEventListener("mouseenter", _ => {
|
||||||
|
this.mouseover = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.addEventListener("mouseleave", _ => {
|
||||||
|
this.mouseover = false;
|
||||||
|
|
||||||
|
if (this.timedOut === true) {
|
||||||
|
this.show = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
dismiss() {
|
||||||
|
this.show = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
|
.slide-enter-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.slide-enter,
|
||||||
|
.slide-leave-to {
|
||||||
|
transform: translateY(100vh);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.slide-leave-active {
|
||||||
|
transition: all 2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.3rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 80vw;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
width: 85vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&.description {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .button-container {
|
||||||
|
& button {
|
||||||
|
color: #2d2d2d;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 3px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
height: max-content;
|
||||||
|
border: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background-color: #5bc2a1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
border-left: 6px solid #f6993f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background-color: var(--red);
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: var(--dark-red);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--dark-red);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
frontend/plugins/Toast/index.js
Normal file
51
frontend/plugins/Toast/index.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import Vue from "vue";
|
||||||
|
import ToastComponent from "./Toast";
|
||||||
|
|
||||||
|
const optionsDefaults = {
|
||||||
|
data: {
|
||||||
|
type: "info",
|
||||||
|
show: true,
|
||||||
|
timeout: 4500,
|
||||||
|
|
||||||
|
onCreate(created = null) {},
|
||||||
|
onEdit(editted = null) {},
|
||||||
|
onRemove(removed = null) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function toast(options) {
|
||||||
|
// merge the default options with the passed options.
|
||||||
|
const root = new Vue({
|
||||||
|
data: {
|
||||||
|
...optionsDefaults.data,
|
||||||
|
...options
|
||||||
|
},
|
||||||
|
render: createElement => createElement(ToastComponent)
|
||||||
|
});
|
||||||
|
|
||||||
|
root.$mount(document.body.appendChild(document.createElement("div")));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(vue) {
|
||||||
|
console.log("Installing toast plugin!");
|
||||||
|
|
||||||
|
Vue.prototype.$toast = {
|
||||||
|
info(options) {
|
||||||
|
toast({ type: "info", ...options });
|
||||||
|
},
|
||||||
|
success(options) {
|
||||||
|
toast({ type: "success", ...options });
|
||||||
|
},
|
||||||
|
warning(options) {
|
||||||
|
toast({ type: "warning", ...options });
|
||||||
|
},
|
||||||
|
error(options) {
|
||||||
|
toast({ type: "error", ...options });
|
||||||
|
},
|
||||||
|
simple(options) {
|
||||||
|
toast({ type: "simple", ...options });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -20,6 +20,8 @@ body {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -51,8 +53,10 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
margin-top: 0.7rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -76,6 +80,7 @@ a {
|
|||||||
|
|
||||||
> *:not(:last-child) {
|
> *:not(:last-child) {
|
||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.column {
|
&.column {
|
||||||
@@ -95,7 +100,7 @@ a {
|
|||||||
|
|
||||||
> *:not(:last-child) {
|
> *:not(:last-child) {
|
||||||
margin-right: unset;
|
margin-right: unset;
|
||||||
margin-bottom: .75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +110,8 @@ input,
|
|||||||
textarea {
|
textarea {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
border: 1px solid rgba(#333333, 0.3);
|
border: 1px solid rgba(#333333, 0.3);
|
||||||
@@ -136,6 +143,11 @@ textarea {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background-color: #f9826c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
background-color: $red;
|
background-color: $red;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -151,9 +163,12 @@ textarea {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), 0 4px 8px rgba(0, 0, 0, 0.07),
|
||||||
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
|
0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||||
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
@@ -163,7 +178,7 @@ textarea {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:disabled{
|
&:disabled {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
@@ -173,6 +188,21 @@ textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pulse-button:not(:hover) {
|
||||||
|
animation: pulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
from {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cursor {
|
.cursor {
|
||||||
&-pointer {
|
&-pointer {
|
||||||
@@ -193,11 +223,23 @@ textarea {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: $matte-text-color;
|
color: $matte-text-color;
|
||||||
|
|
||||||
&:focus, &:hover {
|
&:focus,
|
||||||
|
&:hover {
|
||||||
border-color: $link-color;
|
border-color: $link-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin {
|
||||||
|
&-md {
|
||||||
|
margin: 3rem;
|
||||||
|
}
|
||||||
|
&-sm {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
&-0 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.margin-top {
|
.margin-top {
|
||||||
&-md {
|
&-md {
|
||||||
@@ -269,14 +311,29 @@ textarea {
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wines-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
grid-gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.raffle-element {
|
.raffle-element {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
|
color: #333333;
|
||||||
|
|
||||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
mask-image: url(/public/assets/images/lodd.svg);
|
mask-image: url(/public/assets/images/lodd.svg);
|
||||||
-webkit-mask-repeat: no-repeat;
|
-webkit-mask-repeat: no-repeat;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
color: #333333;
|
|
||||||
|
|
||||||
&.green-raffle {
|
&.green-raffle {
|
||||||
background-color: $light-green;
|
background-color: $light-green;
|
||||||
@@ -293,11 +350,16 @@ textarea {
|
|||||||
&.red-raffle {
|
&.red-raffle {
|
||||||
background-color: $light-red;
|
background-color: $light-red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin raffle {
|
@mixin raffle {
|
||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
&::before, &::after {
|
&::before,
|
||||||
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -309,11 +371,11 @@ textarea {
|
|||||||
background-position: 0 25px;
|
background-position: 0 25px;
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
}
|
}
|
||||||
&::after{
|
&::after {
|
||||||
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
|
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
|
||||||
background-size: 50px 50px;
|
background-size: 50px 50px;
|
||||||
background-position: 25px -25px;
|
background-position: 25px -25px;
|
||||||
bottom: -25px
|
bottom: -25px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,4 +389,4 @@ textarea {
|
|||||||
@include desktop {
|
@include desktop {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -7,6 +9,10 @@
|
|||||||
|
|
||||||
&.row {
|
&.row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.wrap {
|
&.wrap {
|
||||||
@@ -43,4 +49,4 @@
|
|||||||
&-right {
|
&-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,49 @@
|
|||||||
$primary: #b7debd;
|
body {
|
||||||
|
--primary: #b7debd;
|
||||||
|
|
||||||
$light-green: #c8f9df;
|
--light-green: #c8f9df;
|
||||||
$green: #0be881;
|
--green: #0be881;
|
||||||
$dark-green: #0ed277;
|
--dark-green: #0ed277;
|
||||||
|
|
||||||
$light-blue: #d4f2fe;
|
--light-blue: #d4f2fe;
|
||||||
$blue: #4bcffa;
|
--blue: #4bcffa;
|
||||||
$dark-blue: #24acda;
|
--dark-blue: #24acda;
|
||||||
|
|
||||||
$light-yellow: #fff6d6;
|
--light-yellow: #fff6d6;
|
||||||
$yellow: #ffde5d;
|
--yellow: #ffde5d;
|
||||||
$dark-yellow: #ecc31d;
|
--dark-yellow: #ecc31d;
|
||||||
|
|
||||||
$light-red: #fbd7de;
|
--light-red: #fbd7de;
|
||||||
$red: #ef5878;
|
--red: #ef5878;
|
||||||
$dark-red: #ec3b61;
|
--dark-red: #ec3b61;
|
||||||
|
|
||||||
$link-color: #ff5fff;
|
--link-color: #ff5fff;
|
||||||
|
--underlinenav-text: #e1e4e8;
|
||||||
|
--underlinenav-text-active: #f9826c;
|
||||||
|
--underlinenav-text-hover: #d1d5da;
|
||||||
|
|
||||||
$matte-text-color: #333333;
|
--matte-text-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
$primary: var(--primary);
|
||||||
|
|
||||||
|
$light-green: var(--light-green);
|
||||||
|
$green: var(--green);
|
||||||
|
$dark-green: var(--dark-green);
|
||||||
|
|
||||||
|
$light-blue: var(--light-blue);
|
||||||
|
$blue: var(--blue);
|
||||||
|
$dark-blue: var(--dark-blue);
|
||||||
|
|
||||||
|
$light-yellow: var(--light-yellow);
|
||||||
|
$yellow: var(--yellow);
|
||||||
|
$dark-yellow: var(--dark-yellow);
|
||||||
|
|
||||||
|
$light-red: var(--light-red);
|
||||||
|
$red: var(--red);
|
||||||
|
$dark-red: var(--dark-red);
|
||||||
|
|
||||||
|
$link-color: var(--link-color);
|
||||||
|
$underlinenav-text-active: var(--underlinenav-text-active);
|
||||||
|
|
||||||
|
$matte-text-color: var(--matte-text-color);
|
||||||
|
|||||||
@@ -1,12 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="attendees" v-if="attendees.length > 0">
|
<div v-if="attendees.length > 0" class="attendee-container">
|
||||||
<div class="attendees-container" ref="attendees">
|
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
<div class="attendee-info">
|
||||||
<span class="attendee-name">{{ attendee.name }}</span>
|
<router-link class="attendee-name" :to="`/highscore/${attendee.name}`">
|
||||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
{{ attendee.name }}
|
||||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
</router-link>
|
||||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
|
||||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
<div v-if="admin" class="flex column justify-center margin-top-sm">
|
||||||
|
<span>Phone: {{ attendee.phoneNumber }}</span>
|
||||||
|
<span>Has won: {{ attendee.winner }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raffle-container">
|
||||||
|
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||||
|
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||||
|
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||||
|
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="admin" class="attendee-admin">
|
||||||
|
<button class="vin-button small" @click="editingAttendee = editingAttendee == attendee ? false : attendee">
|
||||||
|
{{ editingAttendee == attendee ? "Lukk" : "Rediger" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="editingAttendee == attendee" class="attendee-edit">
|
||||||
|
<div class="label-div" v-for="key in Object.keys(attendee)" :key="key">
|
||||||
|
<label>{{ key }}</label>
|
||||||
|
<input type="text" v-model="attendee[key]" :placeholder="key" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="editingAttendee == attendee">
|
||||||
|
<button class="vin-button small warning" @click="updateAttendee(attendee)">
|
||||||
|
Oppdater deltaker
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="vin-button small danger" @click="deleteAttendee(attendee)">
|
||||||
|
Slett deltaker
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,33 +50,79 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
attendees: {
|
attendees: {
|
||||||
type: Array
|
type: Array
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
data() {
|
||||||
flipList: (list) => list.slice().reverse()
|
return {
|
||||||
|
editingAttendee: undefined
|
||||||
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
methods: {
|
||||||
attendees: {
|
flipList: list => list.slice().reverse(),
|
||||||
deep: true,
|
updateAttendee(updatedAttendee) {
|
||||||
handler() {
|
const options = {
|
||||||
if (this.$refs && this.$refs.history) {
|
method: "PUT",
|
||||||
setTimeout(() => {
|
headers: { "Content-Type": "application/json" },
|
||||||
this.$refs.attendees.scrollTop = this.$refs.attendees.scrollHeight;
|
body: JSON.stringify({ attendee: updatedAttendee })
|
||||||
}, 50);
|
};
|
||||||
}
|
|
||||||
}
|
fetch(`/api/lottery/attendee/${updatedAttendee._id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
this.editingAttendee = null;
|
||||||
|
|
||||||
|
const { message, success } = response;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: response.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteAttendee(deletedAttendee) {
|
||||||
|
const options = {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ attendee: deletedAttendee })
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/api/lottery/attendee/${deletedAttendee._id}`, options)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
this.editingAttendee = null;
|
||||||
|
|
||||||
|
const { message, success } = response;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.$toast.info({
|
||||||
|
title: response.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$toast.error({
|
||||||
|
title: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/global.scss";
|
@import "@/styles/variables.scss";
|
||||||
@import "../styles/variables.scss";
|
@import "@/styles/media-queries.scss";
|
||||||
@import "../styles/media-queries.scss";
|
|
||||||
|
|
||||||
.attendee-name {
|
.attendee-name {
|
||||||
width: 60%;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@@ -51,45 +130,60 @@ hr {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.raffle-element {
|
.attendee-container {
|
||||||
font-size: 0.75rem;
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.attendees {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attendees-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
|
||||||
max-height: 550px;
|
padding: 1rem;
|
||||||
|
|
||||||
|
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
|
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.attendee {
|
.attendee {
|
||||||
|
padding: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
@include mobile {
|
||||||
margin: 0 auto;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
border-bottom: 2px solid #d7d8d7;
|
border-bottom: 2px solid #d7d8d7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-info {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-edit {
|
||||||
|
button {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.raffle-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'" >
|
<a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'">
|
||||||
<span class="menu-toggle"></span>
|
<span class="menu-toggle"></span>
|
||||||
<span class="menu-toggle"></span>
|
<span class="menu-toggle"></span>
|
||||||
<span class="menu-toggle"></span>
|
<span class="menu-toggle"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'" >
|
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'">
|
||||||
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link" >
|
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link">
|
||||||
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a>
|
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a>
|
||||||
<i class="icon icon--arrow-right"></i>
|
<i class="icon icon--arrow-right"></i>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -21,8 +21,9 @@
|
|||||||
<div class="clock">
|
<div class="clock">
|
||||||
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
|
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
|
||||||
<span v-if="days > 0">{{ pad(days) }}:</span>
|
<span v-if="days > 0">{{ pad(days) }}:</span>
|
||||||
<span>{{ pad(hours) }}</span>:
|
<span>{{ pad(hours) }}</span
|
||||||
<span>{{ pad(minutes) }}</span>:
|
>: <span>{{ pad(minutes) }}</span
|
||||||
|
>:
|
||||||
<span>{{ pad(seconds) }}</span>
|
<span>{{ pad(seconds) }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
||||||
@@ -41,7 +42,7 @@ export default {
|
|||||||
minutes: 0,
|
minutes: 0,
|
||||||
seconds: 0,
|
seconds: 0,
|
||||||
distance: 0,
|
distance: 0,
|
||||||
interval: null,
|
interval: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -68,7 +69,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleMenu(){
|
toggleMenu() {
|
||||||
this.isOpen = this.isOpen ? false : true;
|
this.isOpen = this.isOpen ? false : true;
|
||||||
},
|
},
|
||||||
pad: function(num) {
|
pad: function(num) {
|
||||||
@@ -91,10 +92,7 @@ export default {
|
|||||||
let nowDate = new Date();
|
let nowDate = new Date();
|
||||||
let now = nowDate.getTime();
|
let now = nowDate.getTime();
|
||||||
if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) {
|
if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) {
|
||||||
let _diff =
|
let _diff = (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * 60 * -1;
|
||||||
(nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) *
|
|
||||||
60 *
|
|
||||||
-1;
|
|
||||||
nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff);
|
nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff);
|
||||||
}
|
}
|
||||||
this.nextLottery = nextDayOfLottery;
|
this.nextLottery = nextDayOfLottery;
|
||||||
@@ -110,12 +108,8 @@ export default {
|
|||||||
|
|
||||||
// Time calculations for days, hours, minutes and seconds
|
// Time calculations for days, hours, minutes and seconds
|
||||||
this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24));
|
this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24));
|
||||||
this.hours = Math.floor(
|
this.hours = Math.floor((this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
(this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
this.minutes = Math.floor((this.distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
);
|
|
||||||
this.minutes = Math.floor(
|
|
||||||
(this.distance % (1000 * 60 * 60)) / (1000 * 60)
|
|
||||||
);
|
|
||||||
this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000);
|
this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000);
|
||||||
if (this.days == 7) {
|
if (this.days == 7) {
|
||||||
this.days = 0;
|
this.days = 0;
|
||||||
@@ -124,7 +118,7 @@ export default {
|
|||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
this.interval = setTimeout(this.countdown, 500);
|
this.interval = setTimeout(this.countdown, 500);
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<span class="logged-in-username" v-if="username">Logget inn som: <span class="username">{{ username }}</span> <button @click="removeUsername">Logg ut</button></span>
|
<span class="logged-in-username" v-if="username"
|
||||||
|
>Logget inn som: <span class="username">{{ username }}</span>
|
||||||
|
<button @click="removeUsername">Logg ut</button></span
|
||||||
|
>
|
||||||
|
|
||||||
<div class="history" ref="history" v-if="chatHistory.length > 0">
|
<div class="history" ref="history" v-if="chatHistory.length > 0">
|
||||||
<div class="opaque-skirt"></div>
|
<div class="opaque-skirt"></div>
|
||||||
@@ -8,7 +11,8 @@
|
|||||||
<button @click="loadMoreHistory">Hent eldre meldinger</button>
|
<button @click="loadMoreHistory">Hent eldre meldinger</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-message"
|
<div
|
||||||
|
class="history-message"
|
||||||
v-for="(history, index) in chatHistory"
|
v-for="(history, index) in chatHistory"
|
||||||
:key="`${history.username}-${history.timestamp}-${index}`"
|
:key="`${history.username}-${history.timestamp}-${index}`"
|
||||||
>
|
>
|
||||||
@@ -61,12 +65,11 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
getChatHistory(1, this.pageSize)
|
getChatHistory(1, this.pageSize).then(resp => {
|
||||||
.then(resp => {
|
this.chatHistory = resp.messages;
|
||||||
this.chatHistory = resp.messages;
|
this.hasMorePages = resp.total != resp.messages.length;
|
||||||
this.hasMorePages = resp.total != resp.messages.length;
|
});
|
||||||
});
|
const username = window.localStorage.getItem("username");
|
||||||
const username = window.localStorage.getItem('username');
|
|
||||||
if (username) {
|
if (username) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.emitUsernameOnConnect = true;
|
this.emitUsernameOnConnect = true;
|
||||||
@@ -77,8 +80,7 @@ export default {
|
|||||||
handler: function(newVal, oldVal) {
|
handler: function(newVal, oldVal) {
|
||||||
if (oldVal.length == 0) {
|
if (oldVal.length == 0) {
|
||||||
this.scrollToBottomOfHistory();
|
this.scrollToBottomOfHistory();
|
||||||
}
|
} else if (newVal && newVal.length == oldVal.length) {
|
||||||
else if (newVal && newVal.length == oldVal.length) {
|
|
||||||
if (this.isScrollPositionAtBottom()) {
|
if (this.isScrollPositionAtBottom()) {
|
||||||
this.scrollToBottomOfHistory();
|
this.scrollToBottomOfHistory();
|
||||||
}
|
}
|
||||||
@@ -105,10 +107,7 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("connect", msg => {
|
this.socket.on("connect", msg => {
|
||||||
if (
|
if (this.emitUsernameOnConnect || (this.wasDisconnected && this.username != null)) {
|
||||||
this.emitUsernameOnConnect ||
|
|
||||||
(this.wasDisconnected && this.username != null)
|
|
||||||
) {
|
|
||||||
this.setUsername(this.username);
|
this.setUsername(this.username);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -133,12 +132,11 @@ export default {
|
|||||||
let { page, pageSize } = this;
|
let { page, pageSize } = this;
|
||||||
page = page + 1;
|
page = page + 1;
|
||||||
|
|
||||||
getChatHistory(page, pageSize)
|
getChatHistory(page, pageSize).then(resp => {
|
||||||
.then(resp => {
|
this.chatHistory = resp.messages.concat(this.chatHistory);
|
||||||
this.chatHistory = resp.messages.concat(this.chatHistory);
|
this.page = page;
|
||||||
this.page = page;
|
this.hasMorePages = resp.total != this.chatHistory.length;
|
||||||
this.hasMorePages = resp.total != this.chatHistory.length;
|
});
|
||||||
});
|
|
||||||
},
|
},
|
||||||
pad(num) {
|
pad(num) {
|
||||||
if (num > 9) return num;
|
if (num > 9) return num;
|
||||||
@@ -146,9 +144,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getTime(timestamp) {
|
getTime(timestamp) {
|
||||||
let date = new Date(timestamp);
|
let date = new Date(timestamp);
|
||||||
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
const timeString = `${this.pad(date.getHours())}:${this.pad(date.getMinutes())}:${this.pad(date.getSeconds())}`;
|
||||||
date.getMinutes()
|
|
||||||
)}:${this.pad(date.getSeconds())}`;
|
|
||||||
|
|
||||||
if (date.getDate() == new Date().getDate()) {
|
if (date.getDate() == new Date().getDate()) {
|
||||||
return timeString;
|
return timeString;
|
||||||
@@ -158,10 +154,10 @@ export default {
|
|||||||
sendMessage() {
|
sendMessage() {
|
||||||
const message = { message: this.message };
|
const message = { message: this.message };
|
||||||
this.socket.emit("chat", message);
|
this.socket.emit("chat", message);
|
||||||
this.message = '';
|
this.message = "";
|
||||||
this.scrollToBottomOfHistory();
|
this.scrollToBottomOfHistory();
|
||||||
},
|
},
|
||||||
setUsername(username=undefined) {
|
setUsername(username = undefined) {
|
||||||
if (this.temporaryUsername) {
|
if (this.temporaryUsername) {
|
||||||
username = this.temporaryUsername;
|
username = this.temporaryUsername;
|
||||||
}
|
}
|
||||||
@@ -178,7 +174,7 @@ export default {
|
|||||||
if (history) {
|
if (history) {
|
||||||
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
|
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
},
|
},
|
||||||
scrollToBottomOfHistory() {
|
scrollToBottomOfHistory() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -189,15 +185,15 @@ export default {
|
|||||||
scrollToMessageElement(message) {
|
scrollToMessageElement(message) {
|
||||||
const elemTimestamp = this.getTime(message.timestamp);
|
const elemTimestamp = this.getTime(message.timestamp);
|
||||||
const self = this;
|
const self = this;
|
||||||
const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText;
|
const getTimeStamp = elem => elem.getElementsByClassName("timestamp")[0].innerText;
|
||||||
const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp;
|
const prevOldestMessageInNewList = elem => getTimeStamp(elem) == elemTimestamp;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const { history } = self.$refs;
|
const { history } = self.$refs;
|
||||||
const childrenElements = Array.from(history.getElementsByClassName('history-message'));
|
const childrenElements = Array.from(history.getElementsByClassName("history-message"));
|
||||||
|
|
||||||
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
|
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
|
||||||
history.scrollTop = elemInNewList.offsetTop - 70
|
history.scrollTop = elemInNewList.offsetTop - 70;
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +206,7 @@ export default {
|
|||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: translate3d(0,0,0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@@ -241,7 +237,6 @@ input {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.history {
|
.history {
|
||||||
height: 75%;
|
height: 75%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@@ -276,11 +271,7 @@ input {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: linear-gradient(
|
background: linear-gradient(to bottom, white, rgba(255, 255, 255, 0));
|
||||||
to bottom,
|
|
||||||
white,
|
|
||||||
rgba(255, 255, 255, 0)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .fetch-older-history {
|
& .fetch-older-history {
|
||||||
@@ -310,7 +301,7 @@ input {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2.1rem;
|
top: 2.1rem;
|
||||||
left: 2rem;
|
left: 2rem;
|
||||||
|
|||||||
@@ -1,60 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="highscores" v-if="highscore.length > 0">
|
<div class="highscores" v-if="highscore.length > 0">
|
||||||
|
|
||||||
<section class="heading">
|
<section class="heading">
|
||||||
<h3>
|
<h3>
|
||||||
Topp 5 vinnere
|
Topp vinnere
|
||||||
</h3>
|
</h3>
|
||||||
<router-link to="highscore" class="">
|
<router-link to="highscore" class="">
|
||||||
<span class="vin-link">Se alle vinnere</span>
|
<span class="vin-link">Se alle vinnere</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ol class="winner-list-container">
|
<ol class="winner-list-container">
|
||||||
<li v-for="(person, index) in highscore" :key="person._id" class="single-winner">
|
<li v-for="(person, index) in highscore" :key="person._id">
|
||||||
<span class="placement">{{index + 1}}.</span>
|
<router-link :to="`/highscore/${person.name}`" class="single-winner">
|
||||||
<i class="icon icon--medal"></i>
|
<span class="placement">{{ index + 1 }}.</span>
|
||||||
<p class="winner-name">{{ person.name }}</p>
|
<i class="icon icon--medal"></i>
|
||||||
|
<p class="winner-name">{{ person.name }}</p>
|
||||||
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { highscoreStatistics } from "@/api";
|
import { highscoreStatistics } from "@/api";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return { highscore: [] };
|
return {
|
||||||
|
highscore: [],
|
||||||
|
limit: 22
|
||||||
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let response = await highscoreStatistics();
|
return fetch(`/api/history/by-wins?limit=${this.limit}`)
|
||||||
response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1)
|
.then(resp => resp.json())
|
||||||
this.highscore = this.generateScoreBoard(response.slice(0, 5));
|
.then(response => {
|
||||||
},
|
this.highscore = response.winners;
|
||||||
methods: {
|
});
|
||||||
generateScoreBoard(highscore=this.highscore) {
|
|
||||||
let place = 0;
|
|
||||||
let highestWinCount = -1;
|
|
||||||
|
|
||||||
return highscore.map(win => {
|
|
||||||
const wins = win.wins.length
|
|
||||||
if (wins != highestWinCount) {
|
|
||||||
place += 1
|
|
||||||
highestWinCount = wins
|
|
||||||
}
|
|
||||||
|
|
||||||
const placeString = place.toString().padStart(2, "0");
|
|
||||||
win.rank = placeString;
|
|
||||||
return win
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/variables.scss";
|
@import "@/styles/variables.scss";
|
||||||
.heading {
|
.heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -81,8 +69,8 @@ ol {
|
|||||||
|
|
||||||
.winner-list-container {
|
.winner-list-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(12em, 1fr));
|
||||||
gap: 5%;
|
gap: 2rem;
|
||||||
|
|
||||||
.single-winner {
|
.single-winner {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -91,7 +79,7 @@ ol {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
@@ -110,11 +98,71 @@ ol {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.winner-count {
|
||||||
|
grid-row: 3;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.winner-icon {
|
.winner-icon {
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I'm sorry mama
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
*:nth-child(n + 7) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1295px) {
|
||||||
|
*:nth-child(n + 7) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1630px) {
|
||||||
|
*:nth-child(n + 9) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1968px) {
|
||||||
|
*:nth-child(n + 11) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 2300px) {
|
||||||
|
*:nth-child(n + 13) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 2645px) {
|
||||||
|
*:nth-child(n + 15) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 2975px) {
|
||||||
|
*:nth-child(n + 17) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 3311px) {
|
||||||
|
*:nth-child(n + 19) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 3647px) {
|
||||||
|
*:nth-child(n + 21) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,89 +2,47 @@
|
|||||||
<div class="chart">
|
<div class="chart">
|
||||||
<canvas ref="purchase-chart" width="100" height="50"></canvas>
|
<canvas ref="purchase-chart" width="100" height="50"></canvas>
|
||||||
<div ref="chartjsLegend" class="chartjsLegend"></div>
|
<div ref="chartjsLegend" class="chartjsLegend"></div>
|
||||||
|
<div class="year-select" v-if="years.length">
|
||||||
|
<button
|
||||||
|
class="vin-button small"
|
||||||
|
v-for="year in years"
|
||||||
|
:class="{ active: yearSelected == year }"
|
||||||
|
@click="yearFilterClicked(year)"
|
||||||
|
>
|
||||||
|
{{ year }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Chartjs from "chart.js";
|
import Chartjs from "chart.js";
|
||||||
import { chartPurchaseByColor } from "@/api";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lotteries: [],
|
||||||
|
years: [],
|
||||||
|
yearSelected: undefined,
|
||||||
|
chart: undefined
|
||||||
|
};
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let canvas = this.$refs["purchase-chart"].getContext("2d");
|
let canvas = this.$refs["purchase-chart"].getContext("2d");
|
||||||
|
|
||||||
let response = await chartPurchaseByColor();
|
this.lotteries = await this.chartPurchaseByColor();
|
||||||
let labels = [];
|
if (this.lotteries?.length) this.years = [...new Set(this.lotteries.map(lot => lot.date.slice(0, 4)))];
|
||||||
let blue = {
|
|
||||||
label: "Blå",
|
const dataset = this.calculateChartDatapoints();
|
||||||
borderColor: "#57d2fb",
|
|
||||||
backgroundColor: "#d4f2fe",
|
let chartData = {
|
||||||
borderWidth: 2,
|
labels: dataset.labels,
|
||||||
data: []
|
datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
|
||||||
};
|
|
||||||
let yellow = {
|
|
||||||
label: "Gul",
|
|
||||||
borderColor: "#ffde5d",
|
|
||||||
backgroundColor: "#fff6d6",
|
|
||||||
borderWidth: 2,
|
|
||||||
data: []
|
|
||||||
};
|
|
||||||
let red = {
|
|
||||||
label: "Rød",
|
|
||||||
borderColor: "#ef5878",
|
|
||||||
backgroundColor: "#fbd7de",
|
|
||||||
borderWidth: 2,
|
|
||||||
data: []
|
|
||||||
};
|
|
||||||
let green = {
|
|
||||||
label: "Grønn",
|
|
||||||
borderColor: "#10e783",
|
|
||||||
backgroundColor: "#c8f9df",
|
|
||||||
borderWidth: 2,
|
|
||||||
data: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response.length == 1) {
|
this.chart = new Chart(canvas, {
|
||||||
labels.push("");
|
|
||||||
blue.data.push(0);
|
|
||||||
yellow.data.push(0);
|
|
||||||
red.data.push(0);
|
|
||||||
green.data.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let highestNumber = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < response.length; i++) {
|
|
||||||
let thisDate = response[i];
|
|
||||||
let dateObject = new Date(thisDate.date);
|
|
||||||
labels.push(this.getPrettierDateString(dateObject));
|
|
||||||
|
|
||||||
blue.data.push(thisDate.blue);
|
|
||||||
yellow.data.push(thisDate.yellow);
|
|
||||||
red.data.push(thisDate.red);
|
|
||||||
green.data.push(thisDate.green);
|
|
||||||
|
|
||||||
if (thisDate.blue > highestNumber) {
|
|
||||||
highestNumber = thisDate.blue;
|
|
||||||
}
|
|
||||||
if (thisDate.yellow > highestNumber) {
|
|
||||||
highestNumber = thisDate.yellow;
|
|
||||||
}
|
|
||||||
if (thisDate.green > highestNumber) {
|
|
||||||
highestNumber = thisDate.green;
|
|
||||||
}
|
|
||||||
if (thisDate.red > highestNumber) {
|
|
||||||
highestNumber = thisDate.red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let datasets = [blue, yellow, green, red];
|
|
||||||
let chartdata = {
|
|
||||||
labels: labels,
|
|
||||||
datasets: datasets
|
|
||||||
};
|
|
||||||
let chart = new Chart(canvas, {
|
|
||||||
type: "line",
|
type: "line",
|
||||||
data: chartdata,
|
data: chartData,
|
||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: {
|
animation: {
|
||||||
@@ -110,8 +68,7 @@ export default {
|
|||||||
yAxes: [
|
yAxes: [
|
||||||
{
|
{
|
||||||
ticks: {
|
ticks: {
|
||||||
beginAtZero: true,
|
beginAtZero: true
|
||||||
suggestedMax: highestNumber + 5
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -120,10 +77,82 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async yearFilterClicked(year) {
|
||||||
|
this.yearSelected = this.yearSelected === year ? null : year;
|
||||||
|
|
||||||
|
this.lotteries = await this.chartPurchaseByColor();
|
||||||
|
const dataset = this.calculateChartDatapoints();
|
||||||
|
let chartData = {
|
||||||
|
labels: dataset.labels,
|
||||||
|
datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.chart.data = chartData;
|
||||||
|
this.chart.update();
|
||||||
|
},
|
||||||
|
setupDataset() {
|
||||||
|
let blue = {
|
||||||
|
label: "Blå",
|
||||||
|
borderColor: "#57d2fb",
|
||||||
|
backgroundColor: "#d4f2fe",
|
||||||
|
borderWidth: 2,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
let yellow = {
|
||||||
|
label: "Gul",
|
||||||
|
borderColor: "#ffde5d",
|
||||||
|
backgroundColor: "#fff6d6",
|
||||||
|
borderWidth: 2,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
let red = {
|
||||||
|
label: "Rød",
|
||||||
|
borderColor: "#ef5878",
|
||||||
|
backgroundColor: "#fbd7de",
|
||||||
|
borderWidth: 2,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
let green = {
|
||||||
|
label: "Grønn",
|
||||||
|
borderColor: "#10e783",
|
||||||
|
backgroundColor: "#c8f9df",
|
||||||
|
borderWidth: 2,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: [""],
|
||||||
|
blue,
|
||||||
|
green,
|
||||||
|
red,
|
||||||
|
yellow
|
||||||
|
};
|
||||||
|
},
|
||||||
|
calculateChartDatapoints() {
|
||||||
|
let dataset = this.setupDataset();
|
||||||
|
|
||||||
|
this.lotteries.map(lottery => {
|
||||||
|
const date = new Date(lottery.date);
|
||||||
|
dataset.labels.push(this.getPrettierDateString(date));
|
||||||
|
|
||||||
|
dataset.blue.data.push(lottery.blue);
|
||||||
|
dataset.green.data.push(lottery.green);
|
||||||
|
dataset.red.data.push(lottery.red);
|
||||||
|
dataset.yellow.data.push(lottery.yellow);
|
||||||
|
});
|
||||||
|
|
||||||
|
return dataset;
|
||||||
|
},
|
||||||
|
chartPurchaseByColor() {
|
||||||
|
const url = new URL("/api/lotteries", window.location);
|
||||||
|
if (this.yearSelected != null) url.searchParams.set("year", this.yearSelected);
|
||||||
|
|
||||||
|
return fetch(url.href)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.lotteries);
|
||||||
|
},
|
||||||
getPrettierDateString(date) {
|
getPrettierDateString(date) {
|
||||||
return `${this.pad(date.getDate())}.${this.pad(
|
return `${this.pad(date.getDate())}.${this.pad(date.getMonth() + 1)}.${this.pad(date.getYear() - 100)}`;
|
||||||
date.getMonth() + 1
|
|
||||||
)}.${this.pad(date.getYear() - 100)}`;
|
|
||||||
},
|
},
|
||||||
pad(num) {
|
pad(num) {
|
||||||
if (num < 10) {
|
if (num < 10) {
|
||||||
@@ -136,11 +165,19 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/media-queries.scss";
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.year-select {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
button:not(:first-of-type) {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,28 +2,28 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="input-line">
|
<div class="input-line">
|
||||||
<label for="redCheckbox">
|
<label for="redCheckbox">
|
||||||
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/>
|
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors" />
|
||||||
<span class="border">
|
<span class="border">
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text">Rød</span>
|
<span class="text">Rød</span>
|
||||||
</label>
|
</label>
|
||||||
<label for="blueCheckbox">
|
<label for="blueCheckbox">
|
||||||
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors"/>
|
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors" />
|
||||||
<span class="border">
|
<span class="border">
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text">Blå</span>
|
<span class="text">Blå</span>
|
||||||
</label>
|
</label>
|
||||||
<label for="yellowCheckbox">
|
<label for="yellowCheckbox">
|
||||||
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors"/>
|
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors" />
|
||||||
<span class="border">
|
<span class="border">
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text">Gul</span>
|
<span class="text">Gul</span>
|
||||||
</label>
|
</label>
|
||||||
<label for="greenCheckbox">
|
<label for="greenCheckbox">
|
||||||
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors"/>
|
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors" />
|
||||||
<span class="border">
|
<span class="border">
|
||||||
<span class="checkmark"></span>
|
<span class="checkmark"></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -31,15 +31,10 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-line">
|
<div class="input-line">
|
||||||
<input
|
<input type="number" placeholder="Antall lodd" @keyup.enter="generateColors" v-model="numberOfRaffles" />
|
||||||
type="number"
|
|
||||||
placeholder="Antall lodd"
|
|
||||||
@keyup.enter="generateColors"
|
|
||||||
v-model="numberOfRaffles"
|
|
||||||
/>
|
|
||||||
<button class="vin-button" @click="generateColors">Generer</button>
|
<button class="vin-button" @click="generateColors">Generer</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="colors">
|
<div class="colors" :class="{ compact }">
|
||||||
<div
|
<div
|
||||||
v-for="color in colors"
|
v-for="color in colors"
|
||||||
:class="getColorClass(color)"
|
:class="getColorClass(color)"
|
||||||
@@ -47,13 +42,6 @@
|
|||||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="color-count-container" v-if="generated">
|
|
||||||
<span>Rød: {{ red }}</span>
|
|
||||||
<span>Blå: {{ blue }}</span>
|
|
||||||
<span>Gul: {{ yellow }}</span>
|
|
||||||
<span>Grønn: {{ green }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -64,11 +52,15 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
compact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
numberOfRaffles: 4,
|
numberOfRaffles: 6,
|
||||||
colors: [],
|
colors: [],
|
||||||
blue: 0,
|
blue: 0,
|
||||||
red: 0,
|
red: 0,
|
||||||
@@ -101,18 +93,21 @@ export default {
|
|||||||
if (time == 5) {
|
if (time == 5) {
|
||||||
this.generating = false;
|
this.generating = false;
|
||||||
this.generated = true;
|
this.generated = true;
|
||||||
if (this.numberOfRaffles > 1 &&
|
if (
|
||||||
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) {
|
this.numberOfRaffles > 1 &&
|
||||||
return
|
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true)
|
||||||
|
.length == 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new Set(this.colors).size == 1) {
|
if (new Set(this.colors).size == 1) {
|
||||||
alert("BINGO");
|
alert("BINGO");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emitColors()
|
this.emitColors();
|
||||||
|
|
||||||
window.ga('send', {
|
window.ga("send", {
|
||||||
hitType: "event",
|
hitType: "event",
|
||||||
eventCategory: "Raffles",
|
eventCategory: "Raffles",
|
||||||
eventAction: "Generate",
|
eventAction: "Generate",
|
||||||
@@ -147,8 +142,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.numberOfRaffles > 0) {
|
if (this.numberOfRaffles > 0) {
|
||||||
for (let i = 0; i < this.numberOfRaffles; i++) {
|
for (let i = 0; i < this.numberOfRaffles; i++) {
|
||||||
let color =
|
let color = randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||||
randomArray[Math.floor(Math.random() * randomArray.length)];
|
|
||||||
this.colors.push(color);
|
this.colors.push(color);
|
||||||
if (color == 1) {
|
if (color == 1) {
|
||||||
this.red += 1;
|
this.red += 1;
|
||||||
@@ -201,12 +195,12 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/variables.scss";
|
@import "@/styles/variables.scss";
|
||||||
@import "../styles/global.scss";
|
@import "@/styles/global.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin: auto;
|
// margin: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -282,6 +276,15 @@ label .text {
|
|||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 3rem auto 0;
|
margin: 3rem auto 0;
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
> .color-box {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
margin: 1.8rem auto 0;
|
margin: 1.8rem auto 0;
|
||||||
}
|
}
|
||||||
@@ -309,20 +312,6 @@ label .text {
|
|||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-count-container {
|
|
||||||
margin: auto;
|
|
||||||
width: 300px;
|
|
||||||
justify-content: space-around;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-family: Arial;
|
|
||||||
margin-top: 35px;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: 80vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.green {
|
.green {
|
||||||
background-color: $light-green;
|
background-color: $light-green;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<div class="requested-count cursor-pointer" @click="request">
|
<div class="requested-count cursor-pointer" @click="request">
|
||||||
<span>{{ requestedElement.count }}</span>
|
<span>{{ requestedElement.count }}</span>
|
||||||
<i class="icon icon--heart" :class="{ 'active': locallyRequested }" />
|
<i class="icon icon--heart" :class="{ active: locallyRequested }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,10 +17,9 @@
|
|||||||
|
|
||||||
<template v-slot:bottom>
|
<template v-slot:bottom>
|
||||||
<div class="float-left request">
|
<div class="float-left request">
|
||||||
<i class="icon icon--heart request-icon" :class="{ 'active': locallyRequested }"></i>
|
<i class="icon icon--heart request-icon" :class="{ active: locallyRequested }"></i>
|
||||||
<a aria-role="button" tabindex="0" class="link" @click="request"
|
<a aria-role="button" tabindex="0" class="link" @click="request" :class="{ active: locallyRequested }">
|
||||||
:class="{ 'active': locallyRequested }">
|
{{ locallyRequested ? "Anbefalt" : "Anbefal" }}
|
||||||
{{ locallyRequested ? 'Anbefalt' : 'Anbefal' }}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -35,14 +34,14 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Wine
|
Wine
|
||||||
},
|
},
|
||||||
data(){
|
data() {
|
||||||
return {
|
return {
|
||||||
wine: this.requestedElement.wine,
|
wine: this.requestedElement.wine,
|
||||||
locallyRequested: false
|
locallyRequested: false
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
requestedElement: {
|
requestedElement: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object
|
type: Object
|
||||||
},
|
},
|
||||||
@@ -53,27 +52,26 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
request(){
|
request() {
|
||||||
if (this.locallyRequested)
|
if (this.locallyRequested) return;
|
||||||
return
|
|
||||||
console.log("requesting", this.wine)
|
this.locallyRequested = true;
|
||||||
this.locallyRequested = true
|
this.requestedElement.count = this.requestedElement.count + 1;
|
||||||
this.requestedElement.count = this.requestedElement.count +1
|
requestNewWine(this.wine);
|
||||||
requestNewWine(this.wine)
|
|
||||||
},
|
},
|
||||||
async deleteWine() {
|
async deleteWine() {
|
||||||
const wine = this.wine
|
const wine = this.wine;
|
||||||
if (window.confirm("Er du sikker på at du vil slette vinen?")) {
|
if (window.confirm("Er du sikker på at du vil slette vinen?")) {
|
||||||
let response = await deleteRequestedWine(wine);
|
let response = await deleteRequestedWine(wine);
|
||||||
if (response['success'] == true) {
|
if (response["success"] == true) {
|
||||||
this.$emit('wineDeleted', wine);
|
this.$emit("wineDeleted", wine);
|
||||||
} else {
|
} else {
|
||||||
alert("Klarte ikke slette vinen");
|
alert("Klarte ikke slette vinen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -83,7 +81,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: -0.5rem;
|
margin-top: -0.5rem;
|
||||||
background-color: rgb(244,244,244);
|
background-color: rgb(244, 244, 244);
|
||||||
border-radius: 1.1rem;
|
border-radius: 1.1rem;
|
||||||
padding: 0.25rem 1rem;
|
padding: 0.25rem 1rem;
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
@@ -93,14 +91,14 @@ export default {
|
|||||||
line-height: 1.25em;
|
line-height: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon--heart{
|
.icon--heart {
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
&.link {
|
&.link {
|
||||||
border-color: $link-color
|
border-color: $link-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.icon--heart {
|
&.icon--heart {
|
||||||
@@ -121,4 +119,4 @@ export default {
|
|||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div id="camera-stream">
|
||||||
<h2 v-if="errorMessage">{{ errorMessage }}</h2>
|
<h2 v-if="errorMessage">{{ errorMessage }}</h2>
|
||||||
<video playsinline autoplay class="hidden"></video>
|
<video playsinline autoplay class="hidden"></video>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,13 +47,8 @@ export default {
|
|||||||
this.searchVideoForBarcode(this.video);
|
this.searchVideoForBarcode(this.video);
|
||||||
},
|
},
|
||||||
handleError(error) {
|
handleError(error) {
|
||||||
console.log(
|
console.log("navigator.MediaDevices.getUserMedia error: ", error.message, error.name);
|
||||||
"navigator.MediaDevices.getUserMedia error: ",
|
this.errorMessage = "Feil ved oppstart av kamera! Feilmelding: " + error.message;
|
||||||
error.message,
|
|
||||||
error.name
|
|
||||||
);
|
|
||||||
this.errorMessage =
|
|
||||||
"Feil ved oppstart av kamera! Feilmelding: " + error.message;
|
|
||||||
},
|
},
|
||||||
searchVideoForBarcode(video) {
|
searchVideoForBarcode(video) {
|
||||||
const codeReader = new BrowserBarcodeReader();
|
const codeReader = new BrowserBarcodeReader();
|
||||||
@@ -84,10 +79,7 @@ export default {
|
|||||||
this.errorMessage = "Feil! " + error.message || error;
|
this.errorMessage = "Feil! " + error.message || error;
|
||||||
},
|
},
|
||||||
scrollIntoView() {
|
scrollIntoView() {
|
||||||
window.scrollTo(
|
window.scrollTo(0, document.getElementById("camera-stream").offsetTop - 10);
|
||||||
0,
|
|
||||||
document.getElementById("addwine-title").offsetTop - 10
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -112,4 +104,4 @@ h2 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,51 +1,80 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="tab-container">
|
<nav class="tab-container">
|
||||||
<div
|
<a
|
||||||
class="tab"
|
class="tab"
|
||||||
v-for="(tab, index) in tabs"
|
v-for="(tab, index) in tabs"
|
||||||
:key="index"
|
:key="index"
|
||||||
@click="changeTab(index)"
|
@click="changeTab(index)"
|
||||||
|
@keydown.enter="changeTab(index)"
|
||||||
|
tabindex="0"
|
||||||
:class="chosenTab == index ? 'active' : null"
|
:class="chosenTab == index ? 'active' : null"
|
||||||
>{{ tab.name }}</div>
|
>
|
||||||
</div>
|
{{ tab.name }}
|
||||||
|
|
||||||
|
<span v-if="tab.counter" class="counter">{{ tab.counter }}</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="tab-elements">
|
<div class="tab-elements">
|
||||||
<component :is="tabs[chosenTab].component" />
|
<component :is="tabs[chosenTab].component" @counter="updateCounter" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import eventBus from "@/mixins/EventBus";
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
tabs: {
|
tabs: {
|
||||||
type: Array
|
type: Array
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.chosenTab = this.active;
|
const url = location.href;
|
||||||
|
|
||||||
|
if (url.includes("tab=")) {
|
||||||
|
const tabParameter = url.split("tab=")[1];
|
||||||
|
const matchingSlug = this.tabs.findIndex(tab => tab.slug == tabParameter);
|
||||||
|
console.log("matchingSlug:", matchingSlug);
|
||||||
|
this.chosenTab = matchingSlug;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
chosenTab: 0
|
chosenTab: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
activeTab() {
|
||||||
|
return this.tabs[this.chosenTab];
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeTab: function(num) {
|
changeTab(num) {
|
||||||
this.chosenTab = num;
|
this.chosenTab = num;
|
||||||
this.$emit("tabChange", num);
|
|
||||||
eventBus.$emit("tab-change");
|
let url = location.href;
|
||||||
|
const tabParameterIndex = url.indexOf("tab=");
|
||||||
|
|
||||||
|
if (tabParameterIndex > 0) {
|
||||||
|
url = url.split("tab=")[0] + `tab=${this.activeTab.slug}`;
|
||||||
|
} else {
|
||||||
|
url = url + `?tab=${this.activeTab.slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.history.pushState({}, "", url);
|
||||||
|
},
|
||||||
|
updateCounter(val) {
|
||||||
|
this.activeTab.counter = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import "@/styles/variables.scss";
|
||||||
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -54,28 +83,50 @@ h1 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 25px;
|
// margin-top: 25px;
|
||||||
border-bottom: 1px solid #333333;
|
border-bottom: 1px solid var(--underlinenav-text);
|
||||||
|
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
display: flex;
|
padding: 8px 16px;
|
||||||
justify-content: center;
|
border-bottom: 2px solid transparent;
|
||||||
align-items: center;
|
color: rgba($matte-text-color, 0.9);
|
||||||
padding: 20px;
|
|
||||||
margin: 0 15px;
|
|
||||||
border: 1px solid #333333;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
background: #00000008;
|
|
||||||
border-bottom: 1px solid #333333;
|
|
||||||
margin-bottom: -1px;
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-bottom: 1px solid white;
|
color: $matte-text-color;
|
||||||
|
border-color: var(--underlinenav-text-active) !important;
|
||||||
background: white;
|
background: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--underlinenav-text-hover);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .counter {
|
||||||
|
margin-left: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(209, 213, 218, 0.5);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="update-toast" :class="showClass">
|
|
||||||
<span v-html="text"></span>
|
|
||||||
<div class="button-container">
|
|
||||||
<button @click="closeToast">Lukk</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
text: { type: String, required: true },
|
|
||||||
refreshButton: { type: Boolean, required: false }
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return { showClass: null };
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.showClass = "show";
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.refreshButton) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$emit("closeToast");
|
|
||||||
}, 5000);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refresh: function() {
|
|
||||||
location.reload();
|
|
||||||
},
|
|
||||||
closeToast: function() {
|
|
||||||
this.$emit("closeToast");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../styles/media-queries.scss";
|
|
||||||
|
|
||||||
.update-toast {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 1.3rem;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: auto;
|
|
||||||
background: #2d2d2d;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 80vw;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&.show {
|
|
||||||
pointer-events: all;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
-webkit-transition: opacity 0.5s ease-in-out;
|
|
||||||
-moz-transition: opacity 0.5s ease-in-out;
|
|
||||||
-ms-transition: opacity 0.5s ease-in-out;
|
|
||||||
-o-transition: opacity 0.5s ease-in-out;
|
|
||||||
transition: opacity 0.5s ease-in-out;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: 85vw;
|
|
||||||
border-bottom-left-radius: 0px;
|
|
||||||
border-bottom-right-radius: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .button-container {
|
|
||||||
& button {
|
|
||||||
color: #2d2d2d;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0 3px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
height: max-content;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: #2d2d2d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,37 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="outer-bought">
|
<div>
|
||||||
<h3>Loddstatistikk</h3>
|
<h3>Loddstatistikk</h3>
|
||||||
|
|
||||||
<div class="total-raffles">
|
<div class="total-raffles">
|
||||||
Totalt
|
Totalt
|
||||||
<span class="total">{{ total }}</span>
|
<span class="total">{{ total }}</span>
|
||||||
kjøpte,
|
kjøpte,
|
||||||
<span>{{ totalWin }} vinn og </span>
|
<span>{{ totalWin }} vinn og </span>
|
||||||
<span> {{ stolen }} stjålet </span>
|
<span> {{ stolen }} stjålet </span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="bought-container">
|
<div class="bought-container">
|
||||||
<div
|
<div
|
||||||
v-for="color in colors"
|
v-for="color in colors"
|
||||||
:class="
|
:class="color.name + '-container ' + color.name + '-raffle raffle-element-local'"
|
||||||
color.name +
|
|
||||||
'-container ' +
|
|
||||||
color.name +
|
|
||||||
'-raffle raffle-element-local'
|
|
||||||
"
|
|
||||||
:key="color.name"
|
:key="color.name"
|
||||||
>
|
>
|
||||||
<p class="winner-chance">
|
<p class="winner-chance">{{ translate(color.name) }} vinnersjanse</p>
|
||||||
{{translate(color.name)}} vinnersjanse
|
|
||||||
</p>
|
|
||||||
<span class="win-percentage">{{ color.totalPercentage }}% </span>
|
<span class="win-percentage">{{ color.totalPercentage }}% </span>
|
||||||
<p class="total-bought-color">{{ color.total }} kjøpte</p>
|
<p class="total-bought-color">{{ color.total }} kjøpte</p>
|
||||||
<p class="amount-of-wins"> {{ color.win }} vinn </p>
|
<p class="amount-of-wins">{{ color.win }} vinn</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { colorStatistics } from "@/api";
|
import { colorStatistics } from "@/api";
|
||||||
|
|
||||||
@@ -45,109 +38,128 @@ export default {
|
|||||||
green: 0,
|
green: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
totalWin: 0,
|
totalWin: 0,
|
||||||
stolen: 0,
|
stolen: 0
|
||||||
wins: 0,
|
|
||||||
redPercentage: 0,
|
|
||||||
yellowPercentage: 0,
|
|
||||||
greenPercentage: 0,
|
|
||||||
bluePercentage: 0
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let response = await colorStatistics();
|
this.allLotteries().then(this.computeColors);
|
||||||
|
|
||||||
this.red = response.red;
|
|
||||||
this.blue = response.blue;
|
|
||||||
this.green = response.green;
|
|
||||||
this.yellow = response.yellow;
|
|
||||||
this.total = response.total;
|
|
||||||
|
|
||||||
this.totalWin =
|
|
||||||
this.red.win + this.yellow.win + this.blue.win + this.green.win;
|
|
||||||
this.stolen = response.stolen;
|
|
||||||
|
|
||||||
this.redPercentage = this.round(
|
|
||||||
this.red.win == 0 ? 0 : (this.red.win / this.totalWin) * 100
|
|
||||||
);
|
|
||||||
this.greenPercentage = this.round(
|
|
||||||
this.green.win == 0 ? 0 : (this.green.win / this.totalWin) * 100
|
|
||||||
);
|
|
||||||
this.bluePercentage = this.round(
|
|
||||||
this.blue.win == 0 ? 0 : (this.blue.win / this.totalWin) * 100
|
|
||||||
);
|
|
||||||
this.yellowPercentage = this.round(
|
|
||||||
this.yellow.win == 0 ? 0 : (this.yellow.win / this.totalWin) * 100
|
|
||||||
);
|
|
||||||
|
|
||||||
this.colors.push({
|
|
||||||
name: "red",
|
|
||||||
total: this.red.total,
|
|
||||||
win: this.red.win,
|
|
||||||
totalPercentage: this.getPercentage(this.red.win, this.totalWin),
|
|
||||||
percentage: this.getPercentage(this.red.win, this.red.total)
|
|
||||||
});
|
|
||||||
this.colors.push({
|
|
||||||
name: "blue",
|
|
||||||
total: this.blue.total,
|
|
||||||
win: this.blue.win,
|
|
||||||
totalPercentage: this.getPercentage(this.blue.win, this.totalWin),
|
|
||||||
percentage: this.getPercentage(this.blue.win, this.blue.total)
|
|
||||||
});
|
|
||||||
this.colors.push({
|
|
||||||
name: "yellow",
|
|
||||||
total: this.yellow.total,
|
|
||||||
win: this.yellow.win,
|
|
||||||
totalPercentage: this.getPercentage(this.yellow.win, this.totalWin),
|
|
||||||
percentage: this.getPercentage(this.yellow.win, this.yellow.total)
|
|
||||||
});
|
|
||||||
this.colors.push({
|
|
||||||
name: "green",
|
|
||||||
total: this.green.total,
|
|
||||||
win: this.green.win,
|
|
||||||
totalPercentage: this.getPercentage(this.green.win, this.totalWin),
|
|
||||||
percentage: this.getPercentage(this.green.win, this.green.total)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
translate(color){
|
allLotteries() {
|
||||||
switch(color) {
|
return fetch("/api/lotteries?includeWinners=true")
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => response.lotteries);
|
||||||
|
},
|
||||||
|
translate(color) {
|
||||||
|
switch (color) {
|
||||||
case "blue":
|
case "blue":
|
||||||
return "Blå"
|
return "Blå";
|
||||||
break;
|
break;
|
||||||
case "red":
|
case "red":
|
||||||
return "Rød"
|
return "Rød";
|
||||||
break;
|
break;
|
||||||
case "green":
|
case "green":
|
||||||
return "Grønn"
|
return "Grønn";
|
||||||
break;
|
break;
|
||||||
case "yellow":
|
case "yellow":
|
||||||
return "Gul"
|
return "Gul";
|
||||||
|
break;
|
||||||
break;
|
break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPercentage: function(win, total) {
|
getPercentage: function(win, total) {
|
||||||
return this.round(win == 0 ? 0 : (win / total) * 100);
|
return this.round(win == 0 ? 0 : (win / total) * 100);
|
||||||
},
|
},
|
||||||
round: function(number) {
|
round: function(number) {
|
||||||
|
|
||||||
//this can make the odds added together more than 100%, maybe rework?
|
//this can make the odds added together more than 100%, maybe rework?
|
||||||
let actualPercentage = Math.round(number * 100) / 100;
|
let actualPercentage = Math.round(number * 100) / 100;
|
||||||
let rounded = actualPercentage.toFixed(0);
|
let rounded = actualPercentage.toFixed(0);
|
||||||
return rounded;
|
return rounded;
|
||||||
|
},
|
||||||
|
computeColors(lotteries) {
|
||||||
|
let totalRed = 0;
|
||||||
|
let totalGreen = 0;
|
||||||
|
let totalYellow = 0;
|
||||||
|
let totalBlue = 0;
|
||||||
|
let total = 0;
|
||||||
|
let stolen = 0;
|
||||||
|
|
||||||
|
const colorAccumulatedWins = {
|
||||||
|
blue: 0,
|
||||||
|
green: 0,
|
||||||
|
red: 0,
|
||||||
|
yellow: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const accumelatedColors = (winners, colorAccumulatedWins) => {
|
||||||
|
winners.forEach(winner => {
|
||||||
|
const winnerColor = winner.color;
|
||||||
|
colorAccumulatedWins[winnerColor] += 1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
lotteries.forEach(lottery => {
|
||||||
|
totalRed += lottery.red;
|
||||||
|
totalGreen += lottery.green;
|
||||||
|
totalYellow += lottery.yellow;
|
||||||
|
totalBlue += lottery.blue;
|
||||||
|
total += lottery.bought;
|
||||||
|
stolen += lottery.stolen;
|
||||||
|
|
||||||
|
accumelatedColors(lottery.winners, colorAccumulatedWins);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.red = totalRed;
|
||||||
|
this.yellow = totalYellow;
|
||||||
|
this.green = totalGreen;
|
||||||
|
this.blue = totalBlue;
|
||||||
|
this.total = total;
|
||||||
|
|
||||||
|
this.totalWin =
|
||||||
|
colorAccumulatedWins.red + colorAccumulatedWins.yellow + colorAccumulatedWins.blue + colorAccumulatedWins.green;
|
||||||
|
this.stolen = stolen;
|
||||||
|
|
||||||
|
this.colors.push({
|
||||||
|
name: "red",
|
||||||
|
total: totalRed,
|
||||||
|
win: colorAccumulatedWins.red,
|
||||||
|
totalPercentage: this.getPercentage(colorAccumulatedWins.red, this.totalWin),
|
||||||
|
percentage: this.getPercentage(colorAccumulatedWins.red, this.red.total)
|
||||||
|
});
|
||||||
|
this.colors.push({
|
||||||
|
name: "blue",
|
||||||
|
total: totalBlue,
|
||||||
|
win: colorAccumulatedWins.blue,
|
||||||
|
totalPercentage: this.getPercentage(colorAccumulatedWins.blue, this.totalWin),
|
||||||
|
percentage: this.getPercentage(colorAccumulatedWins.blue, this.blue.total)
|
||||||
|
});
|
||||||
|
this.colors.push({
|
||||||
|
name: "yellow",
|
||||||
|
total: totalYellow,
|
||||||
|
win: colorAccumulatedWins.yellow,
|
||||||
|
totalPercentage: this.getPercentage(colorAccumulatedWins.yellow, this.totalWin),
|
||||||
|
percentage: this.getPercentage(colorAccumulatedWins.yellow, this.yellow.total)
|
||||||
|
});
|
||||||
|
this.colors.push({
|
||||||
|
name: "green",
|
||||||
|
total: totalGreen,
|
||||||
|
win: colorAccumulatedWins.green,
|
||||||
|
totalPercentage: this.getPercentage(colorAccumulatedWins.green, this.totalWin),
|
||||||
|
percentage: this.getPercentage(colorAccumulatedWins.green, this.green.total)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/variables.scss";
|
@import "@/styles/variables.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "@/styles/media-queries.scss";
|
||||||
@import "../styles/global.scss";
|
@import "@/styles/global.scss";
|
||||||
|
|
||||||
@include mobile{
|
@include mobile {
|
||||||
section {
|
section {
|
||||||
margin-top: 5em;
|
margin-top: 5em;
|
||||||
}
|
}
|
||||||
@@ -182,7 +194,7 @@ export default {
|
|||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.total-bought-color{
|
&.total-bought-color {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { chartWinsByColor } from "@/api";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
methods: {
|
||||||
|
fetchWinsByColor() {
|
||||||
|
return fetch("/api/history/by-color").then(resp => resp.json());
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let canvas = this.$refs["win-chart"].getContext("2d");
|
let canvas = this.$refs["win-chart"].getContext("2d");
|
||||||
|
|
||||||
let response = await chartWinsByColor();
|
let response = await this.fetchWinsByColor();
|
||||||
|
const { colors } = response;
|
||||||
let labels = ["Vunnet"];
|
let labels = ["Vunnet"];
|
||||||
let blue = {
|
let blue = {
|
||||||
label: "Blå",
|
label: "Blå",
|
||||||
@@ -42,23 +46,26 @@ export default {
|
|||||||
data: []
|
data: []
|
||||||
};
|
};
|
||||||
|
|
||||||
blue.data.push(response.blue.win);
|
const findColorWinners = (colorSelect, colors) => {
|
||||||
yellow.data.push(response.yellow.win);
|
return colors.filter(color => color.color == colorSelect)[0].count;
|
||||||
red.data.push(response.red.win);
|
};
|
||||||
green.data.push(response.green.win);
|
|
||||||
|
const blueWinCount = findColorWinners("blue", colors);
|
||||||
|
const redWinCount = findColorWinners("red", colors);
|
||||||
|
const greenWinCount = findColorWinners("green", colors);
|
||||||
|
const yellowWinCount = findColorWinners("yellow", colors);
|
||||||
|
|
||||||
|
blue.data.push(blueWinCount);
|
||||||
|
red.data.push(redWinCount);
|
||||||
|
green.data.push(greenWinCount);
|
||||||
|
yellow.data.push(yellowWinCount);
|
||||||
|
|
||||||
let highestNumber = 0;
|
let highestNumber = 0;
|
||||||
if (response.blue.win > highestNumber) {
|
[blueWinCount, redWinCount, greenWinCount, greenWinCount].forEach(winCount => {
|
||||||
highestNumber = response.blue.win;
|
if (winCount > highestNumber) {
|
||||||
}
|
highestNumber = winCount;
|
||||||
if (response.red.win > highestNumber) {
|
}
|
||||||
highestNumber = response.red.win;
|
});
|
||||||
}
|
|
||||||
if (response.green.win > highestNumber) {
|
|
||||||
highestNumber = response.green.win;
|
|
||||||
}
|
|
||||||
if (response.yellow.win > highestNumber) {
|
|
||||||
highestNumber = response.yellow.win;
|
|
||||||
}
|
|
||||||
|
|
||||||
let datasets = [blue, yellow, green, red];
|
let datasets = [blue, yellow, green, red];
|
||||||
let chartdata = {
|
let chartdata = {
|
||||||
@@ -102,8 +109,6 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/media-queries.scss";
|
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
|
|||||||
@@ -2,10 +2,7 @@
|
|||||||
<div class="wine">
|
<div class="wine">
|
||||||
<slot name="top"></slot>
|
<slot name="top"></slot>
|
||||||
<div class="wine-image">
|
<div class="wine-image">
|
||||||
<img
|
<img v-if="wine.image && loadImage" :src="wine.image" />
|
||||||
v-if="wine.image && loadImage"
|
|
||||||
:src="wine.image"
|
|
||||||
/>
|
|
||||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loadImage: false
|
loadImage: false
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setImage(entries) {
|
setImage(entries) {
|
||||||
@@ -53,7 +50,7 @@ export default {
|
|||||||
this.observer = new IntersectionObserver(this.setImage, {
|
this.observer = new IntersectionObserver(this.setImage, {
|
||||||
root: this.$el,
|
root: this.$el,
|
||||||
threshold: 0
|
threshold: 0
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.observer.observe(this.$el);
|
this.observer.observe(this.$el);
|
||||||
@@ -66,16 +63,17 @@ export default {
|
|||||||
@import "@/styles/variables";
|
@import "@/styles/variables";
|
||||||
|
|
||||||
.wine {
|
.wine {
|
||||||
|
align-self: flex-start;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
@include tablet {
|
@include tablet {
|
||||||
width: 250px;
|
max-width: 280px;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,19 +83,18 @@ export default {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 250px;
|
height: 280px;
|
||||||
@include mobile {
|
@include mobile {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
max-width: 90px;
|
max-width: 90px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.wine-placeholder {
|
.wine-placeholder {
|
||||||
height: 250px;
|
height: 280px;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.wine-details {
|
.wine-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -107,7 +104,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wine-name{
|
.wine-name {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
@@ -120,6 +117,7 @@ export default {
|
|||||||
.bottom-section {
|
.bottom-section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: $matte-text-color;
|
color: $matte-text-color;
|
||||||
@@ -135,4 +133,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
<div v-if="wines.length > 0" class="wines-main-container">
|
<div v-if="wines.length > 0" class="wines-main-container">
|
||||||
<div class="info-and-link">
|
<div class="info-and-link">
|
||||||
<h3>
|
<h3>
|
||||||
Topp 5 viner
|
Topp viner
|
||||||
</h3>
|
</h3>
|
||||||
<router-link to="viner">
|
<router-link to="viner">
|
||||||
<span class="vin-link">Se alle viner </span>
|
<span class="vin-link">Se alle viner </span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="wine-container">
|
<div class="wines-container">
|
||||||
<Wine v-for="wine in wines" :key="wine" :wine="wine">
|
<Wine v-for="wine in wines" :key="wine" :wine="wine">
|
||||||
<template v-slot:top>
|
<template v-slot:top>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<div class="requested-count cursor-pointer">
|
<div class="requested-count cursor-pointer">
|
||||||
<span> {{ wine.occurences }} </span>
|
<span> {{ wine.occurences }} </span>
|
||||||
<i class="icon icon--heart" />
|
<i class="icon icon--heart" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,32 +32,36 @@ export default {
|
|||||||
Wine
|
Wine
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
wines: [],
|
wines: [],
|
||||||
clickedWine: null,
|
clickedWine: null,
|
||||||
|
limit: 18
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let response = await overallWineStatistics();
|
this.getAllWines();
|
||||||
|
|
||||||
response.sort();
|
|
||||||
response = response
|
|
||||||
.filter(wine => wine.name != null && wine.name != "")
|
|
||||||
.sort(
|
|
||||||
this.predicate(
|
|
||||||
{
|
|
||||||
name: "occurences",
|
|
||||||
reverse: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rating",
|
|
||||||
reverse: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.wines = response.slice(0, 5);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getAllWines() {
|
||||||
|
return fetch(`/api/wines?limit=${this.limit}`)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(response => {
|
||||||
|
let { wines, success } = response;
|
||||||
|
|
||||||
|
this.wines = wines.sort(
|
||||||
|
this.predicate(
|
||||||
|
{
|
||||||
|
name: "occurences",
|
||||||
|
reverse: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rating",
|
||||||
|
reverse: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
predicate: function() {
|
predicate: function() {
|
||||||
var fields = [],
|
var fields = [],
|
||||||
n_fields = arguments.length,
|
n_fields = arguments.length,
|
||||||
@@ -125,42 +129,72 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "@/styles/variables.scss";
|
@import "@/styles/variables.scss";
|
||||||
@import "@/styles/global.scss";
|
@import "@/styles/global.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "@/styles/media-queries.scss";
|
||||||
|
|
||||||
.wines-main-container {
|
.wines-main-container {
|
||||||
margin-bottom: 10em;
|
margin-bottom: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-and-link{
|
.info-and-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wine-container {
|
.requested-count {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
align-items: center;
|
||||||
grid-gap: 2rem;
|
margin-top: -0.5rem;
|
||||||
|
background-color: rgb(244, 244, 244);
|
||||||
|
border-radius: 1.1rem;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
.requested-count {
|
span {
|
||||||
display: flex;
|
padding-right: 0.5rem;
|
||||||
align-items: center;
|
line-height: 1.25em;
|
||||||
margin-top: -0.5rem;
|
}
|
||||||
background-color: rgb(244,244,244);
|
.icon--heart {
|
||||||
border-radius: 1.1rem;
|
font-size: 1.5rem;
|
||||||
padding: 0.25rem 1rem;
|
color: var(--link-color);
|
||||||
font-size: 1.25em;
|
|
||||||
|
|
||||||
span {
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
line-height: 1.25em;
|
|
||||||
}
|
|
||||||
.icon--heart{
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: $link-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call for help
|
||||||
|
.wines-container {
|
||||||
|
@media (max-width: 1643px) {
|
||||||
|
*:nth-child(n + 7) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 2066px) {
|
||||||
|
*:nth-child(n + 9) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 2490px) {
|
||||||
|
*:nth-child(n + 11) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 2915px) {
|
||||||
|
*:nth-child(n + 13) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 3335px) {
|
||||||
|
*:nth-child(n + 15) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 3758px) {
|
||||||
|
*:nth-child(n + 17) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -85,9 +85,7 @@ export default {
|
|||||||
this.startConfetti(this.currentName);
|
this.startConfetti(this.currentName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentName = this.attendees[
|
this.currentName = this.attendees[this.nameRounds % this.attendees.length].name;
|
||||||
this.nameRounds % this.attendees.length
|
|
||||||
].name;
|
|
||||||
this.nameRounds += 1;
|
this.nameRounds += 1;
|
||||||
clearTimeout(this.nameTimeout);
|
clearTimeout(this.nameTimeout);
|
||||||
this.nameTimeout = setTimeout(() => {
|
this.nameTimeout = setTimeout(() => {
|
||||||
@@ -136,8 +134,8 @@ export default {
|
|||||||
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
|
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
|
||||||
var duration = 7 * 1000;
|
var duration = 7 * 1000;
|
||||||
var animationEnd = Date.now() + duration;
|
var animationEnd = Date.now() + duration;
|
||||||
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20};
|
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20 };
|
||||||
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35}
|
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35 };
|
||||||
|
|
||||||
function randomInRange(min, max) {
|
function randomInRange(min, max) {
|
||||||
return Math.random() * (max - min) + min;
|
return Math.random() * (max - min) + min;
|
||||||
@@ -148,27 +146,27 @@ export default {
|
|||||||
var timeLeft = animationEnd - Date.now();
|
var timeLeft = animationEnd - Date.now();
|
||||||
if (timeLeft <= 0) {
|
if (timeLeft <= 0) {
|
||||||
self.drawing = false;
|
self.drawing = false;
|
||||||
console.time("drawing finished")
|
console.time("drawing finished");
|
||||||
return clearInterval(interval);
|
return clearInterval(interval);
|
||||||
}
|
}
|
||||||
if (currentName == "Amund Brandsrud") {
|
if (currentName == "Amund Brandsrud") {
|
||||||
runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135});
|
runCannon(uberDefaults, { x: 1, y: 1 }, { angle: 135 });
|
||||||
runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45});
|
runCannon(uberDefaults, { x: 0, y: 1 }, { angle: 45 });
|
||||||
runCannon(uberDefaults, {y: 1 }, {angle: 90});
|
runCannon(uberDefaults, { y: 1 }, { angle: 90 });
|
||||||
runCannon(uberDefaults, {x: 0 }, {angle: 45});
|
runCannon(uberDefaults, { x: 0 }, { angle: 45 });
|
||||||
runCannon(uberDefaults, {x: 1 }, {angle: 135});
|
runCannon(uberDefaults, { x: 1 }, { angle: 135 });
|
||||||
} else {
|
} else {
|
||||||
runCannon(defaults, {x: 0 }, {angle: 45});
|
runCannon(defaults, { x: 0 }, { angle: 45 });
|
||||||
runCannon(defaults, {x: 1 }, {angle: 135});
|
runCannon(defaults, { x: 1 }, { angle: 135 });
|
||||||
runCannon(defaults, {y: 1 }, {angle: 90});
|
runCannon(defaults, { y: 1 }, { angle: 90 });
|
||||||
}
|
}
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
function runCannon(confettiDefaultValues, originPoint, launchAngle){
|
function runCannon(confettiDefaultValues, originPoint, launchAngle) {
|
||||||
confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle))
|
confetti(Object.assign({}, confettiDefaultValues, { origin: originPoint }, launchAngle));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ordinalNumber(number=this.currentWinnerLocal.winnerCount) {
|
ordinalNumber(number = this.currentWinnerLocal.winnerCount) {
|
||||||
const dictonary = {
|
const dictonary = {
|
||||||
1: "første",
|
1: "første",
|
||||||
2: "andre",
|
2: "andre",
|
||||||
@@ -187,7 +185,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<h2>{{ title ? title : 'Vinnere' }}</h2>
|
<h2>{{ title ? title : "Vinnere" }}</h2>
|
||||||
<div class="winning-raffles" v-if="winners.length > 0">
|
<div class="winning-raffles" v-if="winners.length > 0">
|
||||||
<div v-for="(winner, index) in winners" :key="index">
|
<div v-for="(winner, index) in winners" :key="index">
|
||||||
<router-link :to="`/highscore/${ encodeURIComponent(winner.name) }`">
|
<router-link :to="`/highscore/${winner.name}`">
|
||||||
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
|
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +26,7 @@ export default {
|
|||||||
type: Array
|
type: Array
|
||||||
},
|
},
|
||||||
drawing: {
|
drawing: {
|
||||||
type: Boolean,
|
type: Boolean
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
|
const dateString = date => {
|
||||||
const dateString = (date) => {
|
if (typeof date == "string") {
|
||||||
if (typeof(date) == "string") {
|
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
}
|
}
|
||||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||||
|
|
||||||
return `${ye}-${mo}-${da}`
|
return `${ye}-${mo}-${da}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
function humanReadableDate(date) {
|
function humanReadableDate(date) {
|
||||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
const options = { year: "numeric", month: "long", day: "numeric" };
|
||||||
return new Date(date).toLocaleDateString(undefined, options);
|
return new Date(date).toLocaleDateString(undefined, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,8 +19,4 @@ function daysAgo(date) {
|
|||||||
return Math.round(Math.abs((new Date() - new Date(date)) / day));
|
return Math.round(Math.abs((new Date() - new Date(date)) / day));
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { dateString, humanReadableDate, daysAgo };
|
||||||
dateString,
|
|
||||||
humanReadableDate,
|
|
||||||
daysAgo
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,43 +3,47 @@ import VueRouter from "vue-router";
|
|||||||
import { routes } from "@/router.js";
|
import { routes } from "@/router.js";
|
||||||
import Vinlottis from "@/Vinlottis";
|
import Vinlottis from "@/Vinlottis";
|
||||||
|
|
||||||
|
import Toast from "@/plugins/Toast";
|
||||||
|
|
||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
import { Vue as VueIntegration } from "@sentry/integrations";
|
import { Vue as VueIntegration } from "@sentry/integrations";
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
Vue.use(Toast);
|
||||||
|
|
||||||
const ENV = window.location.href.includes("localhost") ? "development" : "production";
|
const ENV = window.location.href.includes("localhost") ? "development" : "production";
|
||||||
if (ENV !== "development") {
|
if (ENV !== "development") {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
|
dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
|
||||||
integrations: [
|
integrations: [new VueIntegration({ Vue })],
|
||||||
new VueIntegration({ Vue })
|
|
||||||
],
|
|
||||||
beforeSend: event => {
|
beforeSend: event => {
|
||||||
console.error(event);
|
console.error(event);
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add global GA variables
|
// Add global GA variables
|
||||||
window.ga = window.ga || function(){
|
window.ga =
|
||||||
window.ga.q = window.ga.q || [];
|
window.ga ||
|
||||||
window.ga.q.push(arguments);
|
function() {
|
||||||
};
|
window.ga.q = window.ga.q || [];
|
||||||
|
window.ga.q.push(arguments);
|
||||||
|
};
|
||||||
ga.l = 1 * new Date();
|
ga.l = 1 * new Date();
|
||||||
|
|
||||||
// Initiate
|
// Initiate
|
||||||
ga('create', __GA_TRACKINGID__, {
|
ga("create", __GA_TRACKINGID__, {
|
||||||
'allowAnchor': false,
|
allowAnchor: false,
|
||||||
'cookieExpires': __GA_COOKIELIFETIME__, // Time in seconds
|
cookieExpires: __GA_COOKIELIFETIME__, // Time in seconds
|
||||||
'cookieFlags': 'SameSite=Strict; Secure'
|
cookieFlags: "SameSite=Strict; Secure"
|
||||||
});
|
});
|
||||||
ga('set', 'anonymizeIp', true); // Enable IP Anonymization/IP masking
|
ga("set", "anonymizeIp", true); // Enable IP Anonymization/IP masking
|
||||||
ga('send', 'pageview');
|
ga("send", "pageview");
|
||||||
|
|
||||||
if (ENV == 'development')
|
if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true;
|
||||||
window[`ga-disable-${__GA_TRACKINGID__}`] = true;
|
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
routes: routes
|
routes: routes
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env NODE_ENV=production webpack --progress",
|
"build": "cross-env NODE_ENV=production webpack --progress",
|
||||||
"build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress",
|
"build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress",
|
||||||
"dev": "yarn webpack serve --mode development --env development",
|
"watch": "yarn webpack serve --mode development --env development",
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"start-noauth": "cross-env NODE_ENV=development node server.js",
|
"dev": "cross-env NODE_ENV=development node server.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
40
server.js
40
server.js
@@ -18,24 +18,36 @@ const MongoStore = require("connect-mongo")(session);
|
|||||||
// mongoose / database
|
// mongoose / database
|
||||||
console.log("Trying to connect with mongodb..");
|
console.log("Trying to connect with mongodb..");
|
||||||
mongoose.promise = global.Promise;
|
mongoose.promise = global.Promise;
|
||||||
mongoose.connect("mongodb://localhost/vinlottis", {
|
mongoose
|
||||||
useCreateIndex: true,
|
.connect("mongodb://localhost/vinlottis", {
|
||||||
useNewUrlParser: true,
|
useCreateIndex: true,
|
||||||
useUnifiedTopology: true,
|
useNewUrlParser: true,
|
||||||
serverSelectionTimeoutMS: 10000 // initial connection timeout
|
useUnifiedTopology: true,
|
||||||
}).then(_ => console.log("Mongodb connection established!"))
|
serverSelectionTimeoutMS: 10000 // initial connection timeout
|
||||||
.catch(err => {
|
})
|
||||||
console.log(err);
|
.then(_ => console.log("Mongodb connection established!"))
|
||||||
console.error("ERROR! Mongodb required to run.");
|
.catch(err => {
|
||||||
process.exit(1);
|
console.log(err);
|
||||||
})
|
console.error("ERROR! Mongodb required to run.");
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
mongoose.set("debug", false);
|
mongoose.set("debug", false);
|
||||||
|
|
||||||
// middleware
|
// middleware
|
||||||
const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS"));
|
const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS"));
|
||||||
const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders"));
|
const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders"));
|
||||||
app.use(setupCORS)
|
app.use(setupCORS);
|
||||||
app.use(setupHeaders)
|
app.use(setupHeaders);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == "development") {
|
||||||
|
console.info(`NODE_ENV=development set, your are now always an authenticated user.`);
|
||||||
|
const alwaysAuthenticatedWhenLocalhost = require(path.join(
|
||||||
|
__dirname,
|
||||||
|
"/api/middleware/alwaysAuthenticatedWhenLocalhost"
|
||||||
|
));
|
||||||
|
|
||||||
|
app.use(alwaysAuthenticatedWhenLocalhost);
|
||||||
|
}
|
||||||
|
|
||||||
// parse application/json
|
// parse application/json
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -52,7 +64,7 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
app.set('socketio', io); // set io instance to key "socketio"
|
app.set("socketio", io); // set io instance to key "socketio"
|
||||||
|
|
||||||
const passport = require("passport");
|
const passport = require("passport");
|
||||||
const LocalStrategy = require("passport-local");
|
const LocalStrategy = require("passport-local");
|
||||||
|
|||||||
Reference in New Issue
Block a user