Compare commits
188 Commits
feat/contr
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a77ebdc4 | ||
| 168aad21e9 | |||
| 836a06537a | |||
| a44a2f5e2a | |||
| 560fb81a31 | |||
| ddd497f074 | |||
| 45d1df92a1 | |||
| b15c9cecb6 | |||
| 6c708532ac | |||
| d4f059945d | |||
| b94ea75941 | |||
| 019e763341 | |||
| 19c1f18ef6 | |||
| 5a69001efd | |||
| 9f3b3777d9 | |||
| 783373da22 | |||
| 105820cdbb | |||
| d21a33ab42 | |||
| 3188e83aeb | |||
| f91466f1bf | |||
| 7092fb1959 | |||
| 210f9ddbc9 | |||
| c96b52c935 | |||
| 8c964a4815 | |||
| 127fc6741f | |||
| 66253f7bfe | |||
| 1d2a443915 | |||
| ab15e24574 | |||
| 908b61f5bb | |||
| fb4e7d4506 | |||
| 753e4eb422 | |||
| d71a3a4c5a | |||
| a700de6e2e | |||
| c0d98af6e1 | |||
| 947d8958a0 | |||
| 9503967e7e | |||
| 57ddd77493 | |||
| d44cc3cd39 | |||
| 7b406ef432 | |||
|
|
e498dad860 | ||
| e637455059 | |||
|
|
56072ff282 | ||
|
|
0e3c4d98e6 | ||
|
|
64377b7cc0 | ||
|
|
59b4366ed5 | ||
| ea10f95a22 | |||
| bd4d833533 | |||
| 4ed912df46 | |||
| 5af082784c | |||
| b0424a519c | |||
| 33fa7c14c6 | |||
| 4a270f05b8 | |||
| b0000293a6 | |||
| dbcd56a98b | |||
| 8df082dd48 | |||
| 80e6c91045 | |||
| 7e2b5a5bb0 | |||
| ac6a5195a5 | |||
| 113f286f03 | |||
| b260991116 | |||
| 7402fb7a7c | |||
| e89952d965 | |||
| 60044859eb | |||
| d108f331ca | |||
| 3388bed400 | |||
| 2fce0e66ab | |||
| 704ed51db5 | |||
| 48c1842b8b | |||
| 7bd2e9d931 | |||
| d337329765 | |||
| 83d9b30048 | |||
| a37c08880c | |||
| 6b33e03aae | |||
| 9e25b374e0 | |||
| 9913b5984a | |||
| 1d4b74b56b | |||
| f1a0f2a0f2 | |||
| 9b2d0f2d31 | |||
| e20e952573 | |||
| 8d3a21825d | |||
| b02472ef75 | |||
| 3f77722f4f | |||
| 324ca5d9bc | |||
| b493fa2bea | |||
| 1e6ec3d4c8 | |||
| 1b12453df0 | |||
| 2eb933f03e | |||
| de664b3a29 | |||
| 07dd0d43f5 | |||
| cff64999b3 | |||
| 710f276a9b | |||
| 6e0b2b76fe | |||
| 7234c2fbba | |||
| d9de155174 | |||
| 7267c5f5bd | |||
| be70fa6ddf | |||
| 30a9d30b1e | |||
| 2734e9a840 | |||
| 3886313351 | |||
| fc261b9274 | |||
| 442e0ffbfd | |||
| 20dc2b8e38 | |||
| 2477f36f96 | |||
| 4ab67877b9 | |||
| 6968ccf389 | |||
| 8bd41cc691 | |||
| eaf57115e8 | |||
| cded690fba | |||
| eb9e7d4b43 | |||
| b2755add12 | |||
| b5cca00ed4 | |||
| 2cf4095b97 | |||
| 72c1896747 | |||
| 011aec3dea | |||
| b57fb5f1f8 | |||
| 9823197a48 | |||
| d0fa89b92b | |||
| fc029f80df | |||
| 824bd60c02 | |||
| 6003151e3b | |||
| ab58a45da5 | |||
| dcaaeae51f | |||
| 9fd67a6bc3 | |||
| 70c80849df | |||
| a28a8ccacb | |||
| 4bd3b688e9 | |||
| 930c458d9c | |||
| 787882e753 | |||
| 68b4e96ad0 | |||
| 56d2513a9c | |||
| 1c40fae69d | |||
| bca4558d59 | |||
| 38eb98e68b | |||
| c98ccbc3f0 | |||
| 3d99a3e5f2 | |||
| 7292cf7983 | |||
| 57fe7d444b | |||
| cb4a30b5e9 | |||
| 56095cb3e2 | |||
| b321f2cfdd | |||
| ce480e790a | |||
| ba86bf3ada | |||
| f4a16bc417 | |||
| 4c33708ff4 | |||
| 87257fd5b2 | |||
| ac829052b6 | |||
| 1b1a99ccc3 | |||
| 5e018f071d | |||
| 939e7e34df | |||
| 33070ae31a | |||
| 6e02c5e393 | |||
| b596dc28e8 | |||
| 03c0513da3 | |||
| afab4387cc | |||
| 1c1f52308f | |||
| f5d3b16f27 | |||
| b5b61784cc | |||
| 2f3a6aeba7 | |||
| 84fa1ff925 | |||
| 53135acc05 | |||
| fac50805bd | |||
| 6d5f0e824f | |||
| 4d822ccb64 | |||
| 7aa5f7e9ce | |||
| 18d8c2c7ca | |||
| edc4d26647 | |||
| e07e6ae09a | |||
| 93854bc131 | |||
| 872f1f5fa3 | |||
| ccba3e5f10 | |||
| 8d320e73c0 | |||
| bfa13892d5 | |||
|
|
c430d07703 | ||
|
|
3ed0ce7dac | ||
| e36c6b42eb | |||
| 11b988ba19 | |||
| e9ece6963e | |||
| 53780878af | |||
| 5e06a3fc28 | |||
| 54c6c0eb97 | |||
| e754f0a909 | |||
| a010641a8e | |||
| 89389ddc59 | |||
| c03f5aa0cf | |||
| ca6f6cb2ba | |||
| 4043954f95 | |||
| fc69accea3 | |||
| 3906816b80 |
@@ -33,7 +33,7 @@ steps:
|
||||
- drone-test
|
||||
status: success
|
||||
settings:
|
||||
host: 10.0.0.52
|
||||
host: vinlottis.schleppe
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
|
||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"quoteProps": "consistent",
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": true
|
||||
}
|
||||
81
api/attendee.js
Normal file
81
api/attendee.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const path = require("path");
|
||||
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const { UserNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const redactAttendeeInfoMapper = attendee => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||
red: attendee.red,
|
||||
blue: attendee.blue,
|
||||
green: attendee.green,
|
||||
yellow: attendee.yellow
|
||||
};
|
||||
};
|
||||
|
||||
const allAttendees = (isAdmin = false) => {
|
||||
if (!isAdmin) {
|
||||
return Attendee.find().then(attendees => attendees.map(redactAttendeeInfoMapper));
|
||||
} else {
|
||||
return Attendee.find();
|
||||
}
|
||||
};
|
||||
|
||||
const addAttendee = attendee => {
|
||||
const { name, red, blue, green, yellow, phoneNumber } = attendee;
|
||||
|
||||
let newAttendee = new Attendee({
|
||||
name,
|
||||
red,
|
||||
blue,
|
||||
green,
|
||||
yellow,
|
||||
phoneNumber,
|
||||
winner: false
|
||||
});
|
||||
|
||||
return newAttendee.save().then(_ => newAttendee);
|
||||
};
|
||||
|
||||
const updateAttendeeById = (id, updateModel) => {
|
||||
return Attendee.findOne({ _id: id }).then(attendee => {
|
||||
if (attendee == null) {
|
||||
throw new UserNotFound();
|
||||
}
|
||||
|
||||
const updatedAttendee = {
|
||||
name: updateModel.name != null ? updateModel.name : attendee.name,
|
||||
green: updateModel.green != null ? updateModel.green : attendee.green,
|
||||
red: updateModel.red != null ? updateModel.red : attendee.red,
|
||||
blue: updateModel.blue != null ? updateModel.blue : attendee.blue,
|
||||
yellow: updateModel.yellow != null ? updateModel.yellow : attendee.yellow,
|
||||
phoneNumber: updateModel.phoneNumber != null ? updateModel.phoneNumber : attendee.phoneNumber,
|
||||
winner: updateModel.winner != null ? updateModel.winner : attendee.winner
|
||||
};
|
||||
|
||||
return Attendee.updateOne({ _id: id }, updatedAttendee).then(_ => updatedAttendee);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendeeById = id => {
|
||||
return Attendee.findOne({ _id: id }).then(attendee => {
|
||||
if (attendee == null) {
|
||||
throw new UserNotFound();
|
||||
}
|
||||
|
||||
return Attendee.deleteOne({ _id: id }).then(_ => attendee);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendees = () => {
|
||||
return Attendee.deleteMany();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allAttendees,
|
||||
addAttendee,
|
||||
updateAttendeeById,
|
||||
deleteAttendeeById,
|
||||
deleteAttendees
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
const path = require("path");
|
||||
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
|
||||
const { history, clearHistory } = require(path.join(__dirname + "/../redis"));
|
||||
console.log("loading chat");
|
||||
|
||||
const getAllHistory = (req, res) => {
|
||||
let { page, limit } = req.query;
|
||||
@@ -8,19 +9,23 @@ const getAllHistory = (req, res) => {
|
||||
|
||||
return history(page, limit)
|
||||
.then(messages => res.json(messages))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
.catch(error =>
|
||||
res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const deleteHistory = (req, res) => {
|
||||
return clearHistory()
|
||||
.then(message => res.json(message))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
.catch(error =>
|
||||
res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
261
api/controllers/historyController.js
Normal file
261
api/controllers/historyController.js
Normal file
@@ -0,0 +1,261 @@
|
||||
const path = require("path");
|
||||
const historyRepository = require(path.join(__dirname, "../history"));
|
||||
|
||||
const sortOptions = ["desc", "asc"];
|
||||
const includeWinesOptions = ["true", "false"];
|
||||
|
||||
const all = (req, res) => {
|
||||
const { sort, includeWines } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.all(includeWines == "true")
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: sort !== "asc" ? winners : winners.reverse(),
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winners."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const byDate = (req, res) => {
|
||||
let { date } = req.params;
|
||||
|
||||
const regexDate = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
|
||||
if (!isNaN(date)) {
|
||||
date = new Date(new Date(parseInt(date * 1000)).setHours(0, 0, 0, 0));
|
||||
} else if (regexDate.test(date)) {
|
||||
date = new Date(date);
|
||||
} else if (date !== undefined) {
|
||||
return res.status(400).send({
|
||||
message: "Invalid date parameter, allowed epoch seconds or YYYY-MM-DD.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.byDate(date)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
date: date,
|
||||
winners: winners,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by date."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const groupByDate = (req, res) => {
|
||||
const { sort, includeWines } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.groupByDate(includeWines == "true", sort)
|
||||
.then(lotteries =>
|
||||
res.send({
|
||||
lotteries: lotteries,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by date."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const latest = (req, res) => {
|
||||
return historyRepository
|
||||
.latest()
|
||||
.then(winners =>
|
||||
res.send({
|
||||
...winners,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by date."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const byName = (req, res) => {
|
||||
const { name } = req.params;
|
||||
const { sort } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.byName(name, sort)
|
||||
.then(winner =>
|
||||
res.send({
|
||||
winner: winner,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by name."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const search = (req, res) => {
|
||||
const { name, sort } = req.query;
|
||||
|
||||
if (sort !== undefined && !sortOptions.includes(sort)) {
|
||||
return res.status(400).send({
|
||||
message: `Sort option must be: '${sortOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.search(name, sort)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: winners || [],
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winner by name."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const groupByColor = (req, res) => {
|
||||
const { includeWines } = req.query;
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.groupByColor(includeWines == "true")
|
||||
.then(colors =>
|
||||
res.send({
|
||||
colors: colors,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winners by color."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const orderByWins = (req, res) => {
|
||||
let { includeWines, limit } = req.query;
|
||||
|
||||
if (includeWines !== undefined && !includeWinesOptions.includes(includeWines)) {
|
||||
return res.status(400).send({
|
||||
message: `includeWines option must be: '${includeWinesOptions.join(", ")}'`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (limit && isNaN(limit)) {
|
||||
return res.status(400).send({
|
||||
message: "If limit query parameter is provided it must be a number",
|
||||
success: false
|
||||
});
|
||||
} else if (!!!isNaN(limit)) {
|
||||
limit = Number(limit);
|
||||
}
|
||||
|
||||
return historyRepository
|
||||
.orderByWins(includeWines == "true", limit)
|
||||
.then(winners =>
|
||||
res.send({
|
||||
winners: winners,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch winners by color."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
all,
|
||||
byDate,
|
||||
groupByDate,
|
||||
latest,
|
||||
byName,
|
||||
search,
|
||||
groupByColor,
|
||||
orderByWins
|
||||
};
|
||||
135
api/controllers/lotteryAttendeeController.js
Normal file
135
api/controllers/lotteryAttendeeController.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const path = require("path");
|
||||
const attendeeRepository = require(path.join(__dirname, "../attendee"));
|
||||
|
||||
const allAttendees = (req, res) => {
|
||||
const isAdmin = req.isAuthenticated();
|
||||
|
||||
return attendeeRepository
|
||||
.allAttendees(isAdmin)
|
||||
.then(attendees =>
|
||||
res.send({
|
||||
attendees: attendees,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
success: false,
|
||||
message: message || "Unable to fetch lottery attendees."
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addAttendee = (req, res) => {
|
||||
const { attendee } = req.body;
|
||||
|
||||
const requiredColors = [attendee["red"], attendee["blue"], attendee["green"], attendee["yellow"]];
|
||||
const correctColorsTypes = requiredColors.filter(color => typeof color === "number");
|
||||
if (requiredColors.length !== correctColorsTypes.length) {
|
||||
return res.status(400).send({
|
||||
message: "Incorrect or missing color, required type Number for keys: 'blue', 'red', 'green' & 'yellow'.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof attendee["name"] !== "string" || typeof attendee["phoneNumber"] !== "number") {
|
||||
return res.status(400).send({
|
||||
message: "Incorrect or missing attendee keys 'name' or 'phoneNumber'.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
return attendeeRepository
|
||||
.addAttendee(attendee)
|
||||
.then(savedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("new_attendee", {});
|
||||
return savedAttendee;
|
||||
})
|
||||
.then(savedAttendee =>
|
||||
res.send({
|
||||
attendee: savedAttendee,
|
||||
message: `Successfully added attendee ${attendee.name} to lottery.`,
|
||||
success: true
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateAttendeeById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { attendee } = req.body;
|
||||
|
||||
return attendeeRepository
|
||||
.updateAttendeeById(id, attendee)
|
||||
.then(updatedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
return updatedAttendee;
|
||||
})
|
||||
.then(attendee =>
|
||||
res.send({
|
||||
attendee,
|
||||
message: `Updated attendee: ${attendee.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleteing attendee by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendeeById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
return attendeeRepository
|
||||
.deleteAttendeeById(id)
|
||||
.then(removedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
return removedAttendee;
|
||||
})
|
||||
.then(attendee =>
|
||||
res.send({
|
||||
message: `Removed attendee: ${attendee.name}`,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || "Unexpected error occured while deleteing attendee by id.",
|
||||
success: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAttendees = (req, res) => {
|
||||
return attendeeRepository
|
||||
.deleteAttendees()
|
||||
.then(removedAttendee => {
|
||||
var io = req.app.get("socketio");
|
||||
io.emit("refresh_data", {});
|
||||
})
|
||||
.then(_ =>
|
||||
res.send({
|
||||
message: "Removed all attendees",
|
||||
success: true
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allAttendees,
|
||||
addAttendee,
|
||||
updateAttendeeById,
|
||||
deleteAttendeeById,
|
||||
deleteAttendees
|
||||
};
|
||||
213
api/controllers/lotteryController.js
Normal file
213
api/controllers/lotteryController.js
Normal file
@@ -0,0 +1,213 @@
|
||||
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
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const latestLottery = (req, res) => {
|
||||
return lotteryRepository
|
||||
.latestLottery()
|
||||
.then(lottery =>
|
||||
res.send({
|
||||
lottery,
|
||||
message: "Latest lottery.",
|
||||
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,
|
||||
latestLottery
|
||||
};
|
||||
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
|
||||
};
|
||||
101
api/controllers/vinmonopoletController.js
Normal file
101
api/controllers/vinmonopoletController.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || `Unexpected error occured trying to search for wine: ${name} at page: ${page}`,
|
||||
success: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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(wine =>
|
||||
res.json({
|
||||
wine: wine,
|
||||
success: true
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
const { statusCode, message } = error;
|
||||
|
||||
return res.status(statusCode || 500).send({
|
||||
message: message || `Unexpected error occured trying to fetch wine with id: ${id}`,
|
||||
success: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
348
api/history.js
Normal file
348
api/history.js
Normal file
@@ -0,0 +1,348 @@
|
||||
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
|
||||
};
|
||||
400
api/lottery.js
400
api/lottery.js
@@ -1,132 +1,296 @@
|
||||
const path = require('path');
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const Highscore = require(path.join(__dirname, '/schemas/Highscore'));
|
||||
const Wine = require(path.join(__dirname, '/schemas/Wine'));
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
// Utils
|
||||
const epochToDateString = date => new Date(parseInt(date)).toDateString();
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const historyRepository = require(path.join(__dirname, "/history"));
|
||||
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||
const winnerRepository = require(path.join(__dirname, "/winner"));
|
||||
const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine"));
|
||||
|
||||
const sortNewestFirst = (lotteries) => {
|
||||
return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1)
|
||||
const {
|
||||
WinnerNotFound,
|
||||
NoMoreAttendeesToWin,
|
||||
CouldNotFindNewWinnerAfterNTries,
|
||||
LotteryByDateNotFound
|
||||
} = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const moveUnfoundPrelotteryWineToWines = async (error, tempWine) => {
|
||||
if(!(error instanceof WineNotFound)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if(!tempWine.winner) {
|
||||
throw new WinnerNotFound()
|
||||
}
|
||||
|
||||
const prelotteryWine = await prelotteryWineRepository.wineById(tempWine._id);
|
||||
const winner = await winnerRepository.winnerById(tempWine.winner.id, true);
|
||||
|
||||
return wineRepository
|
||||
.addWine(prelotteryWine)
|
||||
.then(_ => prelotteryWineRepository.addWinnerToWine(prelotteryWine, winner)) // prelotteryWine.deleteById
|
||||
.then(_ => historyRepository.addWinnerWithWine(winner, prelotteryWine))
|
||||
.then(_ => winnerRepository.setWinnerChosenById(winner.id))
|
||||
}
|
||||
|
||||
const groupHighscoreByDate = async (highscore=undefined) => {
|
||||
if (highscore == undefined)
|
||||
highscore = await Highscore.find();
|
||||
const archive = (date, raffles, stolen, wines) => {
|
||||
const { blue, red, yellow, green } = raffles;
|
||||
const bought = blue + red + yellow + green;
|
||||
|
||||
const highscoreByDate = [];
|
||||
|
||||
highscore.forEach(person => {
|
||||
person.wins.map(win => {
|
||||
const epochDate = new Date(win.date).setHours(0,0,0,0);
|
||||
const winnerObject = {
|
||||
name: person.name,
|
||||
color: win.color,
|
||||
wine: win.wine,
|
||||
date: epochDate
|
||||
}
|
||||
|
||||
const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate)
|
||||
if (existingDateIndex > -1)
|
||||
highscoreByDate[existingDateIndex].winners.push(winnerObject);
|
||||
else
|
||||
highscoreByDate.push({
|
||||
date: epochDate,
|
||||
winners: [winnerObject]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return sortNewestFirst(highscoreByDate);
|
||||
}
|
||||
|
||||
const resolveWineReferences = (highscoreObject, key) => {
|
||||
const listWithWines = highscoreObject[key]
|
||||
|
||||
return Promise.all(listWithWines.map(element =>
|
||||
Wine.findById(element.wine)
|
||||
.then(wine => {
|
||||
element.wine = wine
|
||||
return element
|
||||
}))
|
||||
)
|
||||
.then(resolvedListWithWines => {
|
||||
highscoreObject[key] = resolvedListWithWines;
|
||||
return highscoreObject
|
||||
})
|
||||
}
|
||||
// end utils
|
||||
|
||||
// Routes
|
||||
const all = (req, res) => {
|
||||
return Highscore.find()
|
||||
.then(highscore => groupHighscoreByDate(highscore))
|
||||
.then(lotteries => res.send({
|
||||
message: "Lotteries by date!",
|
||||
lotteries
|
||||
}))
|
||||
}
|
||||
|
||||
const latest = (req, res) => {
|
||||
return groupHighscoreByDate()
|
||||
.then(lotteries => lotteries.shift()) // first element in list
|
||||
.then(latestLottery => resolveWineReferences(latestLottery, "winners"))
|
||||
.then(lottery => res.send({
|
||||
message: "Latest lottery!",
|
||||
winners: lottery.winners
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const byEpochDate = (req, res) => {
|
||||
let { date } = req.params;
|
||||
date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime()
|
||||
const dateString = epochToDateString(date);
|
||||
|
||||
return groupHighscoreByDate()
|
||||
.then(lotteries => {
|
||||
const lottery = lotteries.filter(lottery => lottery.date == date)
|
||||
if (lottery.length > 0) {
|
||||
return lottery[0]
|
||||
} else {
|
||||
return res.status(404).send({
|
||||
message: `No lottery found for date: ${ dateString }`
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(lottery => resolveWineReferences(lottery, "winners"))
|
||||
.then(lottery => res.send({
|
||||
message: `Lottery for date: ${ dateString}`,
|
||||
return Promise.all(
|
||||
wines.map(wine => wineRepository
|
||||
.findWine(wine)
|
||||
.catch(error => moveUnfoundPrelotteryWineToWines(error, wine)
|
||||
.then(_ => wineRepository.findWine(wine))
|
||||
))
|
||||
).then(resolvedWines => {
|
||||
const lottery = new Lottery({
|
||||
date,
|
||||
winners: lottery.winners
|
||||
}))
|
||||
}
|
||||
blue,
|
||||
red,
|
||||
yellow,
|
||||
green,
|
||||
bought,
|
||||
stolen,
|
||||
wines: resolvedWines
|
||||
});
|
||||
|
||||
const byName = (req, res) => {
|
||||
const { name } = req.params;
|
||||
const regexName = new RegExp(name, "i"); // lowercase regex of the name
|
||||
return lottery.save();
|
||||
});
|
||||
};
|
||||
|
||||
return Highscore.find({ name })
|
||||
.then(highscore => {
|
||||
if (highscore.length > 0) {
|
||||
return highscore[0]
|
||||
} else {
|
||||
return res.status(404).send({
|
||||
message: `Name: ${ name } not found in leaderboards.`
|
||||
})
|
||||
const lotteryByDate = date => {
|
||||
const startOfDay = new Date(date.setHours(0, 0, 0, 0));
|
||||
const endOfDay = new Date(date.setHours(24, 59, 59, 99));
|
||||
|
||||
const query = [
|
||||
{
|
||||
$match: {
|
||||
date: {
|
||||
$gte: startOfDay,
|
||||
$lte: endOfDay
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(highscore => resolveWineReferences(highscore, "wins"))
|
||||
.then(highscore => res.send({
|
||||
message: `Lottery winnings for name: ${ name }.`,
|
||||
name: highscore.name,
|
||||
highscore: sortNewestFirst(highscore.wins)
|
||||
}))
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wines",
|
||||
foreignField: "_id",
|
||||
as: "wines"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const aggregateLottery = Lottery.aggregate(query);
|
||||
return aggregateLottery.project("-_id -__v").then(lotteries => {
|
||||
if (lotteries.length == 0) {
|
||||
throw new LotteryByDateNotFound(date);
|
||||
}
|
||||
return lotteries[0];
|
||||
});
|
||||
};
|
||||
|
||||
const allLotteries = (sort = "asc", yearFilter = undefined) => {
|
||||
const sortDirection = sort == "asc" ? 1 : -1;
|
||||
|
||||
let startQueryDate = new Date("1970-01-01");
|
||||
let endQueryDate = new Date("2999-01-01");
|
||||
if (yearFilter) {
|
||||
startQueryDate = new Date(`${yearFilter}-01-01`);
|
||||
endQueryDate = new Date(`${Number(yearFilter) + 1}-01-01`);
|
||||
}
|
||||
|
||||
const query = [
|
||||
{
|
||||
$match: {
|
||||
date: {
|
||||
$gte: startQueryDate,
|
||||
$lte: endQueryDate
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
date: sortDirection
|
||||
}
|
||||
},
|
||||
{
|
||||
$unset: ["_id", "__v"]
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "wines",
|
||||
localField: "wines",
|
||||
foreignField: "_id",
|
||||
as: "wines"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return Lottery.aggregate(query);
|
||||
};
|
||||
|
||||
const allLotteriesIncludingWinners = async (sort = "asc", yearFilter = undefined) => {
|
||||
const lotteries = await allLotteries(sort, yearFilter);
|
||||
const allWinners = await historyRepository.groupByDate(false, sort);
|
||||
|
||||
return lotteries.map(lottery => {
|
||||
const { winners } = allWinners.pop();
|
||||
|
||||
return {
|
||||
wines: lottery.wines,
|
||||
date: lottery.date,
|
||||
blue: lottery.blue,
|
||||
green: lottery.green,
|
||||
yellow: lottery.yellow,
|
||||
red: lottery.red,
|
||||
bought: lottery.bought,
|
||||
stolen: lottery.stolen,
|
||||
winners: winners
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const latestLottery = async () => {
|
||||
return Lottery.findOne().sort({ date: -1 });
|
||||
};
|
||||
|
||||
const drawWinner = async () => {
|
||||
let allContestants = await Attendee.find({ winner: false });
|
||||
|
||||
if (allContestants.length == 0) {
|
||||
throw new NoMoreAttendeesToWin();
|
||||
}
|
||||
|
||||
let raffleColors = [];
|
||||
for (let i = 0; i < allContestants.length; i++) {
|
||||
let currentContestant = allContestants[i];
|
||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||
raffleColors.push("blue");
|
||||
}
|
||||
for (let red = 0; red < currentContestant.red; red++) {
|
||||
raffleColors.push("red");
|
||||
}
|
||||
for (let green = 0; green < currentContestant.green; green++) {
|
||||
raffleColors.push("green");
|
||||
}
|
||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||
raffleColors.push("yellow");
|
||||
}
|
||||
}
|
||||
|
||||
raffleColors = shuffle(raffleColors);
|
||||
|
||||
let colorToChooseFrom = raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||
let findObject = { winner: false };
|
||||
|
||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||
|
||||
let tries = 0;
|
||||
const maxTries = 3;
|
||||
let contestantsToChooseFrom = undefined;
|
||||
while (contestantsToChooseFrom == undefined && tries < maxTries) {
|
||||
const hit = await Attendee.find(findObject);
|
||||
if (hit && hit.length) {
|
||||
contestantsToChooseFrom = hit;
|
||||
break;
|
||||
}
|
||||
tries++;
|
||||
}
|
||||
if (contestantsToChooseFrom == undefined) {
|
||||
throw new CouldNotFindNewWinnerAfterNTries(maxTries);
|
||||
}
|
||||
|
||||
let attendeeListDemocratic = [];
|
||||
|
||||
let currentContestant;
|
||||
for (let i = 0; i < contestantsToChooseFrom.length; i++) {
|
||||
currentContestant = contestantsToChooseFrom[i];
|
||||
for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) {
|
||||
attendeeListDemocratic.push({
|
||||
name: currentContestant.name,
|
||||
phoneNumber: currentContestant.phoneNumber,
|
||||
red: currentContestant.red,
|
||||
blue: currentContestant.blue,
|
||||
green: currentContestant.green,
|
||||
yellow: currentContestant.yellow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attendeeListDemocratic = shuffle(attendeeListDemocratic);
|
||||
|
||||
let winner = attendeeListDemocratic[Math.floor(Math.random() * attendeeListDemocratic.length)];
|
||||
|
||||
let newWinnerElement = new VirtualWinner({
|
||||
name: winner.name,
|
||||
phoneNumber: winner.phoneNumber,
|
||||
color: colorToChooseFrom,
|
||||
red: winner.red,
|
||||
blue: winner.blue,
|
||||
green: winner.green,
|
||||
yellow: winner.yellow,
|
||||
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||
timestamp_drawn: new Date().getTime()
|
||||
});
|
||||
|
||||
await newWinnerElement.save();
|
||||
await Attendee.updateOne({ name: winner.name, phoneNumber: winner.phoneNumber }, { $set: { winner: true } });
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
|
||||
return { winner, color: colorToChooseFrom, winners };
|
||||
};
|
||||
|
||||
/** - - UTILS - - **/
|
||||
const genRandomString = function(length) {
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString("hex") /** convert to hexadecimal format */
|
||||
.slice(0, length); /** return required number of characters */
|
||||
};
|
||||
|
||||
const sha512 = function(password, salt) {
|
||||
var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */
|
||||
hash.update(password);
|
||||
var value = hash.digest("hex");
|
||||
return value;
|
||||
};
|
||||
|
||||
function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
temporaryValue,
|
||||
randomIndex;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (0 !== currentIndex) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
// And swap it with the current element.
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
all,
|
||||
latest,
|
||||
byEpochDate,
|
||||
byName
|
||||
drawWinner,
|
||||
archive,
|
||||
lotteryByDate,
|
||||
allLotteries,
|
||||
allLotteriesIncludingWinners,
|
||||
latestLottery
|
||||
};
|
||||
|
||||
126
api/message.js
126
api/message.js
@@ -2,34 +2,51 @@ const https = require("https");
|
||||
const path = require("path");
|
||||
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
|
||||
|
||||
const dateString = (date) => {
|
||||
if (typeof(date) == "string") {
|
||||
const dateString = date => {
|
||||
if (typeof date == "string") {
|
||||
date = new Date(date);
|
||||
}
|
||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
||||
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||
|
||||
return `${da}-${mo}-${ye}`
|
||||
return `${da}-${mo}-${ye}`;
|
||||
};
|
||||
|
||||
async function sendInitialMessageToWinners(winners) {
|
||||
const numbers = winners.map(winner => ({ msisdn: `47${winner.phoneNumber}` }));
|
||||
|
||||
const body = {
|
||||
sender: "Vinlottis",
|
||||
message:
|
||||
"Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
|
||||
recipients: numbers,
|
||||
};
|
||||
|
||||
return gatewayRequest(body);
|
||||
}
|
||||
|
||||
async function sendWineSelectMessage(winnerObject) {
|
||||
winnerObject.timestamp_sent = new Date().getTime();
|
||||
winnerObject.timestamp_limit = new Date().getTime() * 600000;
|
||||
await winnerObject.save();
|
||||
async function sendPrizeSelectionLink(winner) {
|
||||
winner.timestamp_sent = new Date().getTime();
|
||||
winner.timestamp_limit = new Date().getTime() + 1000 * 600;
|
||||
await winner.save();
|
||||
|
||||
let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin");
|
||||
const { id, name, phoneNumber } = winner;
|
||||
const url = new URL(`/winner/${id}`, `https://${config.domain}`);
|
||||
const message = `Gratulerer som heldig vinner av vinlotteriet ${name}! Her er linken for \
|
||||
å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst \
|
||||
i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.`;
|
||||
|
||||
return sendMessageToUser(
|
||||
winnerObject.phoneNumber,
|
||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Her er linken for å velge hva slags vin du vil ha, du har 10 minutter på å velge ut noe før du blir lagt bakerst i køen. ${url.href}. (Hvis den siden kommer opp som tom må du prøve å refreshe siden noen ganger.)`
|
||||
)
|
||||
return sendMessageToNumber(phoneNumber, message);
|
||||
}
|
||||
|
||||
async function sendWineConfirmation(winnerObject, wineObject, date) {
|
||||
date = dateString(date);
|
||||
return sendMessageToUser(winnerObject.phoneNumber,
|
||||
`Bekreftelse på din vin ${ winnerObject.name }.\nDato vunnet: ${ date }.\nVin valgt: ${ wineObject.name }.\nDu vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`)
|
||||
return sendMessageToNumber(
|
||||
winnerObject.phoneNumber,
|
||||
`Bekreftelse på din vin ${winnerObject.name}.\nDato vunnet: ${date}.\nVin valgt: ${wineObject.name}.\
|
||||
\nDu vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendLastWinnerMessage(winnerObject, wineObject) {
|
||||
@@ -38,84 +55,69 @@ async function sendLastWinnerMessage(winnerObject, wineObject) {
|
||||
winnerObject.timestamp_limit = new Date().getTime();
|
||||
await winnerObject.save();
|
||||
|
||||
return sendMessageToUser(
|
||||
return sendMessageToNumber(
|
||||
winnerObject.phoneNumber,
|
||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`
|
||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, \
|
||||
du vil bli kontaktet av ${config.name} ang henting. Ha en ellers fin helg!`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendWineSelectMessageTooLate(winnerObject) {
|
||||
return sendMessageToUser(
|
||||
return sendMessageToNumber(
|
||||
winnerObject.phoneNumber,
|
||||
`Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.`
|
||||
`Hei ${winnerObject.name}, du har dessverre brukt mer enn 10 minutter på å velge premie og blir derfor \
|
||||
puttet bakerst i køen. Du vil få en ny SMS når det er din tur igjen.`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendMessageToUser(phoneNumber, message) {
|
||||
console.log(`Attempting to send message to ${ phoneNumber }.`)
|
||||
async function sendMessageToNumber(phoneNumber, message) {
|
||||
console.log(`Attempting to send message to ${phoneNumber}.`);
|
||||
|
||||
const body = {
|
||||
sender: "Vinlottis",
|
||||
message: message,
|
||||
recipients: [{ msisdn: `47${ phoneNumber }`}]
|
||||
recipients: [{ msisdn: `47${phoneNumber}` }],
|
||||
};
|
||||
|
||||
return gatewayRequest(body);
|
||||
}
|
||||
|
||||
|
||||
async function sendInitialMessageToWinners(winners) {
|
||||
let numbers = [];
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
numbers.push({ msisdn: `47${winners[i].phoneNumber}` });
|
||||
}
|
||||
|
||||
const body = {
|
||||
sender: "Vinlottis",
|
||||
message:
|
||||
"Gratulerer som vinner av vinlottisen! Du vil snart få en SMS med oppdatering om hvordan gangen går!",
|
||||
recipients: numbers
|
||||
}
|
||||
|
||||
return gatewayRequest(body);
|
||||
}
|
||||
|
||||
async function gatewayRequest(body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: "gatewayapi.com",
|
||||
post: 443,
|
||||
path: `/rest/mtsms?token=${ config.gatewayToken }`,
|
||||
path: `/rest/mtsms?token=${config.gatewayToken}`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
console.log(`statusCode: ${ res.statusCode }`);
|
||||
console.log(`statusMessage: ${ res.statusMessage }`);
|
||||
const req = https.request(options, res => {
|
||||
console.log(`statusCode: ${res.statusCode}`);
|
||||
console.log(`statusMessage: ${res.statusMessage}`);
|
||||
|
||||
res.setEncoding('utf8');
|
||||
res.setEncoding("utf8");
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
res.on("data", (data) => {
|
||||
console.log("Response from message gateway:", data)
|
||||
res.on("data", data => {
|
||||
console.log("Response from message gateway:", data);
|
||||
|
||||
resolve(JSON.parse(data))
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
} else {
|
||||
res.on("data", (data) => {
|
||||
res.on("data", data => {
|
||||
data = JSON.parse(data);
|
||||
return reject('Gateway error: ' + data['message'] || data)
|
||||
return reject("Gateway error: " + data["message"] || data);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
req.on("error", (error) => {
|
||||
console.error(`Error from sms service: ${ error }`);
|
||||
reject(`Error from sms service: ${ error }`);
|
||||
})
|
||||
req.on("error", error => {
|
||||
console.error(`Error from sms service: ${error}`);
|
||||
reject(`Error from sms service: ${error}`);
|
||||
});
|
||||
|
||||
req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
@@ -123,9 +125,9 @@ async function gatewayRequest(body) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendWineSelectMessage,
|
||||
sendInitialMessageToWinners,
|
||||
sendPrizeSelectionLink,
|
||||
sendWineConfirmation,
|
||||
sendLastWinnerMessage,
|
||||
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) => {
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
|
||||
req.isAuthenticated = () => true;
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
const path = require("path");
|
||||
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
|
||||
|
||||
async function findSavePerson(foundWinner, wonWine, date) {
|
||||
let person = await Highscore.findOne({
|
||||
name: foundWinner.name
|
||||
});
|
||||
|
||||
if (person == undefined) {
|
||||
let newPerson = new Highscore({
|
||||
name: foundWinner.name,
|
||||
wins: [
|
||||
{
|
||||
color: foundWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await newPerson.save();
|
||||
} else {
|
||||
person.wins.push({
|
||||
color: foundWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
});
|
||||
person.markModified("wins");
|
||||
await person.save();
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
module.exports.findSavePerson = findSavePerson;
|
||||
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
|
||||
});
|
||||
console.log(newPrelotteryWine)
|
||||
return newPrelotteryWine.save();
|
||||
});
|
||||
|
||||
return Promise.all(prelotteryWines);
|
||||
};
|
||||
|
||||
const wineById = id => {
|
||||
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
const updateWineById = (id, updateModel) => {
|
||||
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
const updatedWine = {
|
||||
name: updateModel.name != null ? updateModel.name : wine.name,
|
||||
vivinoLink: updateModel.vivinoLink != null ? updateModel.vivinoLink : wine.vivinoLink,
|
||||
rating: updateModel.rating != null ? updateModel.rating : wine.rating,
|
||||
year: updateModel.year != null ? updateModel.year : wine.year,
|
||||
image: updateModel.image != null ? updateModel.image : wine.image,
|
||||
price: updateModel.price != null ? updateModel.price : wine.price,
|
||||
country: updateModel.country != null ? updateModel.country : wine.country,
|
||||
id: updateModel.id != null ? updateModel.id : wine.id
|
||||
};
|
||||
|
||||
return PreLotteryWine.updateOne({ _id: id }, updatedWine).then(_ => updatedWine);
|
||||
});
|
||||
};
|
||||
|
||||
const addWinnerToWine = (wine, winner) => {
|
||||
wine.winner = winner;
|
||||
winner.prize_selected = true;
|
||||
return Promise.all([wine.save(), winner.save()]);
|
||||
};
|
||||
|
||||
const deleteWineById = id => {
|
||||
return PreLotteryWine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
return PreLotteryWine.deleteOne({ _id: id }).then(_ => wine);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteWines = () => {
|
||||
return PreLotteryWine.deleteMany();
|
||||
};
|
||||
|
||||
const wineSchema = () => {
|
||||
let schema = { ...PreLotteryWine.schema.obj };
|
||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||
accumulator[current] = "";
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
return Promise.resolve(nulledSchema);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allWines,
|
||||
allWinesWithoutWinner,
|
||||
addWines,
|
||||
wineById,
|
||||
addWinnerToWine,
|
||||
updateWineById,
|
||||
deleteWineById,
|
||||
deleteWines,
|
||||
wineSchema
|
||||
};
|
||||
110
api/prizeDistribution.js
Normal file
110
api/prizeDistribution.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const path = require("path");
|
||||
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
|
||||
const message = require(path.join(__dirname, "/message"));
|
||||
const historyRepository = require(path.join(__dirname, "/history"));
|
||||
const winnerRepository = require(path.join(__dirname, "/winner"));
|
||||
const wineRepository = require(path.join(__dirname, "/wine"));
|
||||
const prelotteryWineRepository = require(path.join(__dirname, "/prelotteryWine"));
|
||||
|
||||
const { WinnerNotFound, WineSelectionWinnerNotNextInLine, WinnersTimelimitExpired } = require(path.join(
|
||||
__dirname,
|
||||
"/vinlottisErrors"
|
||||
));
|
||||
|
||||
const verifyWinnerNextInLine = async id => {
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
throw new WinnerNotFound();
|
||||
} else if (foundWinner.timestamp_limit < new Date().getTime()) {
|
||||
throw new WinnersTimelimitExpired();
|
||||
}
|
||||
|
||||
let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
|
||||
if (
|
||||
foundWinner.timestamp_limit == undefined ||
|
||||
foundWinner.timestamp_sent == undefined ||
|
||||
foundWinner.prize_selected == true
|
||||
) {
|
||||
throw new WineSelectionWinnerNotNextInLine();
|
||||
}
|
||||
|
||||
return Promise.resolve(foundWinner);
|
||||
};
|
||||
|
||||
const claimPrize = (wine, winner) => {
|
||||
return wineRepository
|
||||
.addWine(wine)
|
||||
.then(_ => prelotteryWineRepository.addWinnerToWine(wine, winner)) // prelotteryWine.deleteById
|
||||
.then(_ => historyRepository.addWinnerWithWine(winner, wine)) // wines.js : addWine
|
||||
.then(_ => message.sendWineConfirmation(winner, wine));
|
||||
};
|
||||
|
||||
const notifyNextWinner = async () => {
|
||||
let nextWinner = undefined;
|
||||
|
||||
const winnersLeft = await VirtualWinner.find({ prize_selected: false }).sort({ timestamp_drawn: 1 });
|
||||
const winesLeft = await PreLotteryWine.find({ winner: { $exists: false } });
|
||||
|
||||
if (winnersLeft.length > 1) {
|
||||
console.log("multiple winners left, choose next in line");
|
||||
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
||||
console.log("one winner left, but multiple wines");
|
||||
nextWinner = winnersLeft[0]; // one winner left, but multiple wines
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
||||
console.log("one winner and one wine left, choose for user");
|
||||
nextWinner = winnersLeft[0]; // one winner and one wine left, choose for user
|
||||
wine = winesLeft[0];
|
||||
return claimPrize(wine, nextWinner);
|
||||
}
|
||||
|
||||
if (nextWinner) {
|
||||
return message.sendPrizeSelectionLink(nextWinner).then(_ => startTimeout(nextWinner.id));
|
||||
} else {
|
||||
console.info("All winners notified. Could start cleanup here.");
|
||||
return Promise.resolve({
|
||||
message: "All winners notified."
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// these need to be register somewhere to cancel if something
|
||||
// goes wrong and we want to start prize distribution again
|
||||
function startTimeout(id) {
|
||||
const minute = 60000;
|
||||
const minutesForTimeout = 10;
|
||||
|
||||
console.log(`Starting timeout for user ${id}.`);
|
||||
console.log(`Timeout duration: ${minutesForTimeout * minute}`);
|
||||
setTimeout(async () => {
|
||||
let virtualWinner = await VirtualWinner.findOne({ id: id, prize_selected: false });
|
||||
if (!virtualWinner) {
|
||||
console.log(`Timeout done for user ${id}, but user has already sent data.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Timeout done for user ${id}, sending update to user.`);
|
||||
|
||||
message.sendWineSelectMessageTooLate(virtualWinner);
|
||||
|
||||
virtualWinner.timestamp_drawn = new Date().getTime();
|
||||
virtualWinner.timestamp_limit = null;
|
||||
virtualWinner.timestamp_sent = null;
|
||||
await virtualWinner.save();
|
||||
|
||||
notifyNextWinner();
|
||||
}, minutesForTimeout * minute);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyWinnerNextInLine,
|
||||
claimPrize,
|
||||
notifyNextWinner
|
||||
};
|
||||
@@ -1,41 +1,20 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const RequestedWine = require(path.join(
|
||||
__dirname, "/schemas/RequestedWine"
|
||||
));
|
||||
const Wine = require(path.join(
|
||||
__dirname, "/schemas/Wine"
|
||||
));
|
||||
const RequestedWine = require(path.join(__dirname, "/schemas/RequestedWine"));
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
|
||||
const deleteRequestedWineById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
if(id == null){
|
||||
return res.json({
|
||||
message: "Id er ikke definert",
|
||||
success: false
|
||||
})
|
||||
class RequestedWineNotFound extends Error {
|
||||
constructor(message = "Wine with this id was not found.") {
|
||||
super(message);
|
||||
this.name = "RequestedWineNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
await RequestedWine.deleteOne({wineId: id})
|
||||
return res.json({
|
||||
message: `Slettet vin med id: ${id}`,
|
||||
success: true
|
||||
});
|
||||
}
|
||||
|
||||
const getAllRequestedWines = async (req, res) => {
|
||||
const allWines = await RequestedWine.find({}).populate("wine");
|
||||
const addNew = async wine => {
|
||||
let foundWine = await Wine.findOne({ id: wine.id });
|
||||
|
||||
return res.json(allWines);
|
||||
}
|
||||
|
||||
const requestNewWine = async (req, res) => {
|
||||
const {wine} = req.body
|
||||
|
||||
let thisWineIsLOKO = await Wine.findOne({id: wine.id})
|
||||
|
||||
if(thisWineIsLOKO == undefined){
|
||||
thisWineIsLOKO = new Wine({
|
||||
if (foundWine == undefined) {
|
||||
foundWine = new Wine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: null,
|
||||
@@ -43,27 +22,47 @@ const requestNewWine = async (req, res) => {
|
||||
image: wine.image,
|
||||
id: wine.id
|
||||
});
|
||||
await thisWineIsLOKO.save()
|
||||
await foundWine.save();
|
||||
}
|
||||
|
||||
let requestedWine = await RequestedWine.findOne({ "wineId": wine.id})
|
||||
let requestedWine = await RequestedWine.findOne({ wineId: wine.id });
|
||||
|
||||
if(requestedWine == undefined){
|
||||
if (requestedWine == undefined) {
|
||||
requestedWine = new RequestedWine({
|
||||
count: 1,
|
||||
wineId: wine.id,
|
||||
wine: thisWineIsLOKO
|
||||
})
|
||||
wine: foundWine
|
||||
});
|
||||
} else {
|
||||
requestedWine.count += 1;
|
||||
}
|
||||
await requestedWine.save()
|
||||
await requestedWine.save();
|
||||
|
||||
return res.send(requestedWine);
|
||||
}
|
||||
return requestedWine;
|
||||
};
|
||||
|
||||
const getById = id => {
|
||||
return RequestedWine.findOne({ wineId: id })
|
||||
.populate("wine")
|
||||
.then(wine => {
|
||||
if (wine == null) {
|
||||
throw new RequestedWineNotFound();
|
||||
}
|
||||
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteById = id => {
|
||||
return getById(id).then(requestedWine => RequestedWine.deleteOne({ _id: requestedWine._id }));
|
||||
};
|
||||
|
||||
const getAll = () => {
|
||||
return RequestedWine.find({}).populate("wine");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
requestNewWine,
|
||||
getAllRequestedWines,
|
||||
deleteRequestedWineById
|
||||
addNew,
|
||||
getAll,
|
||||
deleteById
|
||||
};
|
||||
|
||||
154
api/retrieve.js
154
api/retrieve.js
@@ -1,154 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const Purchase = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
const prelotteryWines = async (req, res) => {
|
||||
let wines = await PreLotteryWine.find();
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
const allPurchase = async (req, res) => {
|
||||
let purchases = await Purchase.find()
|
||||
.populate("wines")
|
||||
.sort({ date: 1 });
|
||||
return res.json(purchases);
|
||||
};
|
||||
|
||||
const purchaseByColor = async (req, res) => {
|
||||
const countColor = await Purchase.find();
|
||||
let red = 0;
|
||||
let blue = 0;
|
||||
let yellow = 0;
|
||||
let green = 0;
|
||||
let stolen = 0;
|
||||
for (let i = 0; i < countColor.length; i++) {
|
||||
let element = countColor[i];
|
||||
red += element.red;
|
||||
blue += element.blue;
|
||||
yellow += element.yellow;
|
||||
green += element.green;
|
||||
if (element.stolen != undefined) {
|
||||
stolen += element.stolen;
|
||||
}
|
||||
}
|
||||
|
||||
const highscore = await Highscore.find();
|
||||
let redWin = 0;
|
||||
let blueWin = 0;
|
||||
let yellowWin = 0;
|
||||
let greenWin = 0;
|
||||
for (let i = 0; i < highscore.length; i++) {
|
||||
let element = highscore[i];
|
||||
for (let y = 0; y < element.wins.length; y++) {
|
||||
let currentWin = element.wins[y];
|
||||
switch (currentWin.color) {
|
||||
case "blue":
|
||||
blueWin += 1;
|
||||
break;
|
||||
case "red":
|
||||
redWin += 1;
|
||||
break;
|
||||
case "yellow":
|
||||
yellowWin += 1;
|
||||
break;
|
||||
case "green":
|
||||
greenWin += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = red + yellow + blue + green;
|
||||
|
||||
return res.json({
|
||||
red: {
|
||||
total: red,
|
||||
win: redWin
|
||||
},
|
||||
blue: {
|
||||
total: blue,
|
||||
win: blueWin
|
||||
},
|
||||
green: {
|
||||
total: green,
|
||||
win: greenWin
|
||||
},
|
||||
yellow: {
|
||||
total: yellow,
|
||||
win: yellowWin
|
||||
},
|
||||
stolen: stolen,
|
||||
total: total
|
||||
});
|
||||
};
|
||||
|
||||
const highscore = async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
|
||||
return res.json(highscore);
|
||||
};
|
||||
|
||||
const allWines = async (req, res) => {
|
||||
const wines = await Wine.find();
|
||||
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
const allWinesSummary = async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
let wines = {};
|
||||
|
||||
for (let i = 0; i < highscore.length; i++) {
|
||||
let person = highscore[i];
|
||||
for (let y = 0; y < person.wins.length; y++) {
|
||||
let wine = person.wins[y].wine;
|
||||
let date = person.wins[y].date;
|
||||
let color = person.wins[y].color;
|
||||
|
||||
if (wines[wine._id] == undefined) {
|
||||
wines[wine._id] = {
|
||||
name: wine.name,
|
||||
occurences: wine.occurences,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
id: wine.id,
|
||||
_id: wine._id,
|
||||
dates: [date],
|
||||
winners: [person.name],
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0
|
||||
};
|
||||
wines[wine._id][color] += 1;
|
||||
} else {
|
||||
wines[wine._id].dates.push(date);
|
||||
wines[wine._id].winners.push(person.name);
|
||||
if (wines[wine._id][color] == undefined) {
|
||||
wines[wine._id][color] = 1;
|
||||
} else {
|
||||
wines[wine._id][color] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wines = Object.values(wines).reverse()
|
||||
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
prelotteryWines,
|
||||
allPurchase,
|
||||
purchaseByColor,
|
||||
highscore,
|
||||
allWines,
|
||||
allWinesSummary
|
||||
};
|
||||
137
api/router.js
137
api/router.js
@@ -4,67 +4,104 @@ const path = require("path");
|
||||
const mustBeAuthenticated = require(path.join(__dirname, "/middleware/mustBeAuthenticated"));
|
||||
const setAdminHeaderIfAuthenticated = require(path.join(__dirname, "/middleware/setAdminHeaderIfAuthenticated"));
|
||||
|
||||
const update = require(path.join(__dirname, "/update"));
|
||||
const retrieve = require(path.join(__dirname, "/retrieve"));
|
||||
const request = require(path.join(__dirname, "/request"));
|
||||
const subscriptionApi = require(path.join(__dirname, "/subscriptions"));
|
||||
const userApi = require(path.join(__dirname, "/user"));
|
||||
const wineinfo = require(path.join(__dirname, "/wineinfo"));
|
||||
const virtualApi = require(path.join(__dirname, "/virtualLottery"));
|
||||
const virtualRegistrationApi = require(path.join(
|
||||
__dirname, "/virtualRegistration"
|
||||
));
|
||||
const lottery = require(path.join(__dirname, "/lottery"));
|
||||
const chatHistoryApi = require(path.join(__dirname, "/chatHistory"));
|
||||
const requestController = require(path.join(__dirname, "/controllers/requestController"));
|
||||
const vinmonopoletController = require(path.join(__dirname, "/controllers/vinmonopoletController"));
|
||||
const chatController = require(path.join(__dirname, "/controllers/chatController"));
|
||||
const userController = require(path.join(__dirname, "/controllers/userController"));
|
||||
const historyController = require(path.join(__dirname, "/controllers/historyController"));
|
||||
const attendeeController = require(path.join(__dirname, "/controllers/lotteryAttendeeController"));
|
||||
const prelotteryWineController = require(path.join(__dirname, "/controllers/lotteryWineController"));
|
||||
const winnerController = require(path.join(__dirname, "/controllers/lotteryWinnerController"));
|
||||
const lotteryController = require(path.join(__dirname, "/controllers/lotteryController"));
|
||||
const prizeDistributionController = require(path.join(__dirname, "/controllers/prizeDistributionController"));
|
||||
const wineController = require(path.join(__dirname, "/controllers/wineController"));
|
||||
const messageController = require(path.join(__dirname, "/controllers/messageController"));
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/wineinfo/search", wineinfo.wineSearch);
|
||||
router.get("/vinmonopolet/wine/search", vinmonopoletController.searchWines);
|
||||
router.get("/vinmonopolet/wine/by-ean/:ean", vinmonopoletController.wineByEAN);
|
||||
router.get("/vinmonopolet/wine/by-id/:id", vinmonopoletController.wineById);
|
||||
router.get("/vinmonopolet/stores/", vinmonopoletController.allStores);
|
||||
router.get("/vinmonopolet/stores/search", vinmonopoletController.searchStores);
|
||||
|
||||
router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines);
|
||||
router.post("/request/new-wine", request.requestNewWine);
|
||||
router.delete("/request/:id", request.deleteRequestedWineById);
|
||||
router.get("/requests", setAdminHeaderIfAuthenticated, requestController.allRequests);
|
||||
router.post("/request", requestController.addRequest);
|
||||
router.delete("/request/:id", mustBeAuthenticated, requestController.deleteRequest);
|
||||
|
||||
router.get("/wineinfo/schema", mustBeAuthenticated, update.schema);
|
||||
router.get("/wineinfo/:ean", wineinfo.byEAN);
|
||||
router.get("/wines", wineController.allWines); // sort = by-date, by-name, by-occurences
|
||||
router.get("/wine/:id", wineController.wineById); // sort = by-date, by-name, by-occurences
|
||||
// router.update("/wine/:id", mustBeAuthenticated, wineController.update);
|
||||
|
||||
router.post("/log/wines", mustBeAuthenticated, update.submitWines);
|
||||
router.post("/lottery", update.submitLottery);
|
||||
router.post("/lottery/wines", update.submitWinesToLottery);
|
||||
// router.delete("/lottery/wine/:id", update.deleteWineFromLottery);
|
||||
router.post("/lottery/winners", update.submitWinnersToLottery);
|
||||
router.get("/history", historyController.all);
|
||||
router.get("/history/latest", historyController.latest);
|
||||
router.get("/history/by-wins/", historyController.orderByWins);
|
||||
router.get("/history/by-color/", historyController.groupByColor);
|
||||
router.get("/history/by-date/:date", historyController.byDate);
|
||||
router.get("/history/by-name/:name", historyController.byName);
|
||||
router.get("/history/search/", historyController.search);
|
||||
router.get("/history/by-date/", historyController.groupByDate);
|
||||
|
||||
router.get("/wines/prelottery", retrieve.prelotteryWines);
|
||||
router.get("/purchase/statistics", retrieve.allPurchase);
|
||||
router.get("/purchase/statistics/color", retrieve.purchaseByColor);
|
||||
router.get("/highscore/statistics", retrieve.highscore)
|
||||
router.get("/wines/statistics", retrieve.allWines);
|
||||
router.get("/wines/statistics/overall", retrieve.allWinesSummary);
|
||||
// router.get("/purchases", purchaseController.lotteryPurchases);
|
||||
// // returns list per date and count of each colors that where bought
|
||||
// router.get("/purchases/summary", purchaseController.lotteryPurchases);
|
||||
// // returns total, wins?, stolen
|
||||
// router.get("/purchase/:date", purchaseController.lotteryPurchaseByDate);
|
||||
|
||||
router.get("/lottery/all", lottery.all);
|
||||
router.get("/lottery/latest", lottery.latest);
|
||||
router.get("/lottery/by-date/:date", lottery.byEpochDate);
|
||||
router.get("/lottery/by-name/:name", lottery.byName);
|
||||
router.get("/lottery/wines", prelotteryWineController.allWines);
|
||||
router.get("/lottery/wine/schema", mustBeAuthenticated, prelotteryWineController.wineSchema);
|
||||
router.get("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.wineById);
|
||||
router.post("/lottery/wines", mustBeAuthenticated, prelotteryWineController.addWines);
|
||||
router.delete("/lottery/wines", mustBeAuthenticated, prelotteryWineController.deleteWines);
|
||||
router.put("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.updateWineById);
|
||||
router.delete("/lottery/wine/:id", mustBeAuthenticated, prelotteryWineController.deleteWineById);
|
||||
|
||||
router.delete('/virtual/winner/all', mustBeAuthenticated, virtualApi.deleteWinners);
|
||||
router.delete('/virtual/attendee/all', mustBeAuthenticated, virtualApi.deleteAttendees);
|
||||
router.get('/virtual/winner/draw', virtualApi.drawWinner);
|
||||
router.get('/virtual/winner/all', virtualApi.winners);
|
||||
router.get('/virtual/winner/all/secure', mustBeAuthenticated, virtualApi.winnersSecure);
|
||||
router.post('/virtual/finish', mustBeAuthenticated, virtualApi.finish);
|
||||
router.get('/virtual/attendee/all', virtualApi.attendees);
|
||||
router.get('/virtual/attendee/all/secure', mustBeAuthenticated, virtualApi.attendeesSecure);
|
||||
router.post('/virtual/attendee/add', mustBeAuthenticated, virtualApi.addAttendee);
|
||||
router.get("/lottery/attendees", setAdminHeaderIfAuthenticated, attendeeController.allAttendees);
|
||||
router.delete("/lottery/attendees", mustBeAuthenticated, attendeeController.deleteAttendees);
|
||||
router.post("/lottery/attendee", mustBeAuthenticated, attendeeController.addAttendee);
|
||||
router.put("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.updateAttendeeById);
|
||||
router.delete("/lottery/attendee/:id", mustBeAuthenticated, attendeeController.deleteAttendeeById);
|
||||
|
||||
router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById);
|
||||
router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById);
|
||||
router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection);
|
||||
router.get("/lottery/winners", winnerController.allWinners);
|
||||
router.get("/lottery/winner/:id", winnerController.winnerById);
|
||||
router.post("/lottery/winners", mustBeAuthenticated, winnerController.addWinners);
|
||||
router.delete("/lottery/winners", mustBeAuthenticated, winnerController.deleteWinners);
|
||||
router.put("/lottery/winner/:id", mustBeAuthenticated, winnerController.updateWinnerById);
|
||||
router.delete("/lottery/winner/:id", mustBeAuthenticated, winnerController.deleteWinnerById);
|
||||
|
||||
router.get('/chat/history', chatHistoryApi.getAllHistory)
|
||||
router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory)
|
||||
router.get("/lottery/draw", mustBeAuthenticated, lotteryController.drawWinner);
|
||||
router.post("/lottery/archive", mustBeAuthenticated, lotteryController.archiveLottery);
|
||||
router.get("/lottery/latest", lotteryController.latestLottery);
|
||||
router.get("/lottery/:epoch", lotteryController.lotteryByDate);
|
||||
router.get("/lotteries/", lotteryController.allLotteries);
|
||||
|
||||
router.post('/login', userApi.login);
|
||||
router.post('/register', mustBeAuthenticated, userApi.register);
|
||||
router.get('/logout', userApi.logout);
|
||||
// router.get("/lottery/prize-distribution/status", mustBeAuthenticated, prizeDistributionController.status);
|
||||
router.post("/lottery/prize-distribution/start", mustBeAuthenticated, prizeDistributionController.start);
|
||||
// router.post("/lottery/prize-distribution/stop", mustBeAuthenticated, prizeDistributionController.stop);
|
||||
router.get("/lottery/prize-distribution/prizes/:id", prizeDistributionController.getPrizesForWinnerById);
|
||||
router.post("/lottery/prize-distribution/prize/:id", prizeDistributionController.submitPrizeForWinnerById);
|
||||
|
||||
router.post("/lottery/messages/winner/:id", mustBeAuthenticated, messageController.notifyWinnerById);
|
||||
|
||||
router.get("/chat/history", chatController.getAllHistory);
|
||||
router.delete("/chat/history", mustBeAuthenticated, chatController.deleteHistory);
|
||||
|
||||
router.post("/login", userController.login);
|
||||
router.get("/logout", userController.logout);
|
||||
if(process.env !== "production") {
|
||||
// We don't want to hide registering behind a
|
||||
// authentication-wall if we are in dev
|
||||
router.post("/register", userController.register);
|
||||
} else {
|
||||
router.post("/register", mustBeAuthenticated, userController.register);
|
||||
}
|
||||
|
||||
// router.get("/", documentation.apiInfo);
|
||||
|
||||
// router.get("/wine/schema", mustBeAuthenticated, update.schema);
|
||||
// router.get("/purchase/statistics", retrieve.allPurchase);
|
||||
// router.get("/highscore/statistics", retrieve.highscore);
|
||||
// router.get("/wines/statistics", retrieve.allWines);
|
||||
// router.get("/wines/statistics/overall", retrieve.allWinesSummary);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -6,9 +6,14 @@ const PreLotteryWine = new Schema({
|
||||
vivinoLink: String,
|
||||
rating: Number,
|
||||
id: String,
|
||||
year: Number,
|
||||
image: String,
|
||||
price: String,
|
||||
country: String
|
||||
country: String,
|
||||
winner: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "VirtualWinner"
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);
|
||||
|
||||
@@ -10,6 +10,10 @@ const VirtualWinner = new Schema({
|
||||
red: Number,
|
||||
yellow: Number,
|
||||
id: String,
|
||||
prize_selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
timestamp_drawn: Number,
|
||||
timestamp_sent: Number,
|
||||
timestamp_limit: Number
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
const mongoose = require("mongoose");
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const Wine = new Schema({
|
||||
const WineSchema = new Schema({
|
||||
name: String,
|
||||
vivinoLink: String,
|
||||
rating: Number,
|
||||
occurences: Number,
|
||||
id: String,
|
||||
year: Number,
|
||||
image: String,
|
||||
price: String,
|
||||
country: String
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Wine", Wine);
|
||||
module.exports = mongoose.model("Wine", WineSchema);
|
||||
|
||||
142
api/update.js
142
api/update.js
@@ -1,142 +0,0 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
|
||||
const sub = require(path.join(__dirname, "/subscriptions"));
|
||||
|
||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
||||
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
|
||||
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
const submitWines = async (req, res) => {
|
||||
const wines = req.body;
|
||||
for (let i = 0; i < wines.length; i++) {
|
||||
let wine = wines[i];
|
||||
let newWonWine = new PreLotteryWine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
price: wine.price,
|
||||
country: wine.country,
|
||||
id: wine.id
|
||||
});
|
||||
await newWonWine.save();
|
||||
}
|
||||
|
||||
let subs = await Subscription.find();
|
||||
console.log("Sending new wines w/ push notification to all subscribers.")
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
let subscription = subs[i]; //get subscription from your databse here.
|
||||
|
||||
const message = JSON.stringify({
|
||||
message: "Dagens vin er lagt til, se den på lottis.vin/dagens!",
|
||||
title: "Ny vin!",
|
||||
link: "/#/dagens"
|
||||
});
|
||||
|
||||
try {
|
||||
sub.sendNotification(subscription, message);
|
||||
} catch (error) {
|
||||
console.error("Error when trying to send push notification to subscriber.");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return res.send({
|
||||
message: "Submitted and notified push subscribers of new wines!",
|
||||
success: true
|
||||
});
|
||||
};
|
||||
|
||||
const schema = async (req, res) => {
|
||||
let schema = { ...PreLotteryWine.schema.obj };
|
||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||
accumulator[current] = "";
|
||||
return accumulator
|
||||
}, {});
|
||||
|
||||
return res.send(nulledSchema);
|
||||
}
|
||||
|
||||
// TODO IMPLEMENT WITH FRONTEND (unused)
|
||||
const submitWinesToLottery = async (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
const { date, wines } = lottery;
|
||||
const wineObjects = await Promise.all(wines.map(async (wine) => await _wineFunctions.findSaveWine(wine)))
|
||||
|
||||
return Lottery.findOneAndUpdate({ date: date }, {
|
||||
date: date,
|
||||
wines: wineObjects
|
||||
}, {
|
||||
upsert: true
|
||||
}).then(_ => res.send(true))
|
||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving wine to lottery.',
|
||||
success: false,
|
||||
exception: err.message }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiParam (Request body) {Array} winners List of winners
|
||||
*/
|
||||
const submitWinnersToLottery = async (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
const { winners, date } = lottery;
|
||||
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
let currentWinner = winners[i];
|
||||
let wonWine = await _wineFunctions.findSaveWine(currentWinner.wine); // TODO rename to findAndSaveWineToLottery
|
||||
await _personFunctions.findSavePerson(currentWinner, wonWine, date); // TODO rename to findAndSaveWineToPerson
|
||||
}
|
||||
|
||||
return res.json(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiParam (Request body) {Date} date Date of lottery
|
||||
* @apiParam (Request body) {Number} blue Number of blue tickets
|
||||
* @apiParam (Request body) {Number} red Number of red tickets
|
||||
* @apiParam (Request body) {Number} green Number of green tickets
|
||||
* @apiParam (Request body) {Number} yellow Number of yellow tickets
|
||||
* @apiParam (Request body) {Number} bought Number of tickets bought
|
||||
* @apiParam (Request body) {Number} stolen Number of tickets stolen
|
||||
*/
|
||||
const submitLottery = async (req, res) => {
|
||||
const { lottery } = req.body
|
||||
|
||||
const { date,
|
||||
blue,
|
||||
red,
|
||||
yellow,
|
||||
green,
|
||||
bought,
|
||||
stolen } = lottery;
|
||||
|
||||
return Lottery.findOneAndUpdate({ date: date }, {
|
||||
date: date,
|
||||
blue: blue,
|
||||
yellow: yellow,
|
||||
red: red,
|
||||
green: green,
|
||||
bought: bought,
|
||||
stolen: stolen
|
||||
}, {
|
||||
upsert: true
|
||||
}).then(_ => res.send(true))
|
||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving lottery.',
|
||||
success: false,
|
||||
exception: err.message }));
|
||||
|
||||
return res.send(true);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
submitWines,
|
||||
schema,
|
||||
submitLottery,
|
||||
submitWinnersToLottery,
|
||||
submitWinesToLottery
|
||||
};
|
||||
111
api/user.js
111
api/user.js
@@ -1,51 +1,90 @@
|
||||
const passport = require("passport");
|
||||
const path = require("path");
|
||||
const User = require(path.join(__dirname, "/schemas/User"));
|
||||
const router = require("express").Router();
|
||||
|
||||
const register = (req, res, next) => {
|
||||
User.register(
|
||||
new User({ username: req.body.username }),
|
||||
req.body.password,
|
||||
function(err) {
|
||||
class UserExistsError extends Error {
|
||||
constructor(message = "Username already exists.") {
|
||||
super(message);
|
||||
this.name = "UserExists";
|
||||
this.statusCode = 409;
|
||||
}
|
||||
}
|
||||
|
||||
class MissingUsernameError extends Error {
|
||||
constructor(message = "No username given.") {
|
||||
super(message);
|
||||
this.name = "MissingUsernameError";
|
||||
this.statusCode = 400;
|
||||
}
|
||||
}
|
||||
|
||||
class MissingPasswordError extends Error {
|
||||
constructor(message = "No password given.") {
|
||||
super(message);
|
||||
this.name = "MissingPasswordError";
|
||||
this.statusCode = 400;
|
||||
}
|
||||
}
|
||||
|
||||
class IncorrectUserCredentialsError extends Error {
|
||||
constructor(message = "Incorrect username or password") {
|
||||
super(message);
|
||||
this.name = "IncorrectUserCredentialsError";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
function userAuthenticationErrorHandler(err) {
|
||||
if (err.name == "UserExistsError") {
|
||||
throw new UserExistsError(err.message);
|
||||
} else if (err.name == "MissingUsernameError") {
|
||||
throw new MissingUsernameError(err.message);
|
||||
} else if (err.name == "MissingPasswordError") {
|
||||
throw new MissingPasswordError(err.message);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const register = (username, password) => {
|
||||
return User.register(new User({ username: username }), password).catch(userAuthenticationErrorHandler);
|
||||
};
|
||||
|
||||
const authenticate = req => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (username == undefined) throw new MissingUsernameError();
|
||||
if (password == undefined) throw new MissingPasswordError();
|
||||
|
||||
passport.authenticate("local", function(err, user, info) {
|
||||
if (err) {
|
||||
if (err.name == "UserExistsError")
|
||||
res.status(409).send({ success: false, message: err.message })
|
||||
else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
|
||||
res.status(400).send({ success: false, message: err.message })
|
||||
return next(err);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
|
||||
}
|
||||
);
|
||||
if (!user) {
|
||||
reject(new IncorrectUserCredentialsError());
|
||||
}
|
||||
|
||||
resolve(user);
|
||||
})(req);
|
||||
});
|
||||
};
|
||||
|
||||
const login = (req, res, next) => {
|
||||
passport.authenticate("local", function(err, user, info) {
|
||||
if (err) {
|
||||
if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
|
||||
return res.status(400).send({ message: err.message, success: false })
|
||||
return next(err);
|
||||
}
|
||||
const login = (req, user) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.logIn(user, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false })
|
||||
|
||||
req.logIn(user, (err) => {
|
||||
if (err) { return next(err) }
|
||||
|
||||
return res.status(200).send({ message: "Velkommen " + user.username, success: true })
|
||||
})
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
const logout = (req, res) => {
|
||||
req.logout();
|
||||
res.redirect("/");
|
||||
resolve(user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
logout
|
||||
authenticate,
|
||||
login
|
||||
};
|
||||
|
||||
90
api/vinlottisErrors.js
Normal file
90
api/vinlottisErrors.js
Normal file
@@ -0,0 +1,90 @@
|
||||
class UserNotFound extends Error {
|
||||
constructor(message = "User not found.") {
|
||||
super(message);
|
||||
this.name = "UserNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class WineNotFound extends Error {
|
||||
constructor(message = "Wine not found.") {
|
||||
super(message);
|
||||
this.name = "WineNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class WinnerNotFound extends Error {
|
||||
constructor(message = "Winner not found.") {
|
||||
super(message);
|
||||
this.name = "WinnerNotFound";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class WinnersTimelimitExpired extends Error {
|
||||
constructor(message = "Timelimit expired, you will need to wait until it's your turn again.") {
|
||||
super(message);
|
||||
this.name = "WinnersTimelimitExpired";
|
||||
this.statusCode = 403;
|
||||
}
|
||||
}
|
||||
|
||||
class WineSelectionWinnerNotNextInLine extends Error {
|
||||
constructor(message = "Not the winner next in line!") {
|
||||
super(message);
|
||||
this.name = "WineSelectionWinnerNotNextInLine";
|
||||
this.statusCode = 403;
|
||||
}
|
||||
|
||||
// TODO log missing user
|
||||
}
|
||||
|
||||
class NoMoreAttendeesToWin extends Error {
|
||||
constructor(message = "No more attendees left to drawn from.") {
|
||||
super(message);
|
||||
this.name = "NoMoreAttendeesToWin";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class CouldNotFindNewWinnerAfterNTries extends Error {
|
||||
constructor(tries) {
|
||||
let message = `Could not a new winner after ${tries} tries.`;
|
||||
super(message);
|
||||
this.name = "CouldNotFindNewWinnerAfterNTries";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class LotteryByDateNotFound extends Error {
|
||||
constructor(date) {
|
||||
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||
|
||||
const dateString = `${ye}-${mo}-${da}`;
|
||||
const dateUnix = date.getTime();
|
||||
const message = `Could not find lottery for date: ${dateString}.`;
|
||||
super(message);
|
||||
this.name = "LotteryByDateNotFoundError";
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UserNotFound,
|
||||
WineNotFound,
|
||||
WinnerNotFound,
|
||||
WinnersTimelimitExpired,
|
||||
WineSelectionWinnerNotNextInLine,
|
||||
NoMoreAttendeesToWin,
|
||||
CouldNotFindNewWinnerAfterNTries,
|
||||
LotteryByDateNotFound
|
||||
};
|
||||
138
api/vinmonopolet.js
Normal file
138
api/vinmonopolet.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const fetch = require("node-fetch");
|
||||
const path = require("path");
|
||||
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
||||
const vinmonopoletCache = require(path.join(__dirname, "vinmonopoletCache"));
|
||||
|
||||
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 convertVinmonopoletProductResponseToWineObject = wine => {
|
||||
return {
|
||||
name: wine.name,
|
||||
vivinoLink: "https://www.vinmonopolet.no" + wine.url,
|
||||
rating: null,
|
||||
occurences: 0,
|
||||
id: wine.code,
|
||||
year: wine.year,
|
||||
image: wine.images[1].url,
|
||||
price: wine.price.value,
|
||||
country: wine.main_country.name
|
||||
}
|
||||
};
|
||||
|
||||
const convertToOurStoreObject = store => {
|
||||
return {
|
||||
id: store.storeId,
|
||||
name: store.storeName,
|
||||
...store.address
|
||||
};
|
||||
};
|
||||
|
||||
const searchWinesByName = (name, page = 1) => {
|
||||
const pageSize = 25;
|
||||
|
||||
return vinmonopoletCache.wineByQueryName(name, page, pageSize)
|
||||
.catch(_ => {
|
||||
console.log(`No wines matching query: ${name} at page ${page} found in elastic index, searching vinmonopolet..`)
|
||||
|
||||
const url = `https://www.vinmonopolet.no/api/search?q=${name}:relevance:visibleInSearch:true&searchType=product&pageSize=${pageSize}¤tPage=${page-1}`
|
||||
const options = {
|
||||
headers: { "Content-Type": 'application/json' }
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(resp => {
|
||||
if (resp.ok == false) {
|
||||
return Promise.reject({
|
||||
statusCode: 404,
|
||||
message: `No wines matching query ${name} at page ${page} found in local cache or at vinmonopolet.`,
|
||||
})
|
||||
}
|
||||
|
||||
return resp.json()
|
||||
.then(response => response?.productSearchResult?.products)
|
||||
})
|
||||
})
|
||||
.then(wines => wines.map(convertVinmonopoletProductResponseToWineObject))
|
||||
};
|
||||
|
||||
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 => {
|
||||
return vinmonopoletCache.wineById(id)
|
||||
.catch(_ => {
|
||||
console.log(`Wine id: ${id} not found in elastic index, searching vinmonopolet..`)
|
||||
|
||||
const url = `https://www.vinmonopolet.no/api/products/${id}?fields=FULL`
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(resp => {
|
||||
if (resp.ok == false) {
|
||||
return Promise.reject({
|
||||
statusCode: 404,
|
||||
message: `Wine with id ${id} not found in local cache or at vinmonopolet.`,
|
||||
})
|
||||
}
|
||||
|
||||
return resp.json()
|
||||
})
|
||||
})
|
||||
.then(wine => convertVinmonopoletProductResponseToWineObject(wine))
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
98
api/vinmonopoletCache.js
Normal file
98
api/vinmonopoletCache.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const ELASTIC_URL = 'http://localhost:9200';
|
||||
const INDEX_URL = `${ELASTIC_URL}/wines*`;
|
||||
|
||||
const verifyAndUnpackElasticSearchResult = response => {
|
||||
const searchHits = response?.hits?.hits;
|
||||
|
||||
if (searchHits == null || searchHits.length == 0) {
|
||||
return Promise.reject({
|
||||
statusCode: 404,
|
||||
message: `Nothing found in vinmonopolet cache matching this.`,
|
||||
})
|
||||
}
|
||||
|
||||
return searchHits;
|
||||
}
|
||||
|
||||
const getWineObjectFromSearchHit = hit => {
|
||||
const { wine } = hit?._source;
|
||||
|
||||
if (wine == null) {
|
||||
return Promise.reject({
|
||||
statusCode: 500,
|
||||
message: `Found response, but it's missing a wine object. Unable to convert!`,
|
||||
})
|
||||
}
|
||||
|
||||
return wine;
|
||||
}
|
||||
|
||||
const wineById = id => {
|
||||
const url = `${INDEX_URL}/_search`
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
"size": 1,
|
||||
"query": {
|
||||
"match": {
|
||||
"wine.code": id
|
||||
}
|
||||
},
|
||||
"_source": {
|
||||
"includes": "wine"
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"@timestamp": "desc"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return fetch(url, options)
|
||||
.then(resp => resp.json())
|
||||
.then(verifyAndUnpackElasticSearchResult)
|
||||
.then(searchHits => getWineObjectFromSearchHit(searchHits[0]))
|
||||
}
|
||||
|
||||
const wineByQueryName = (name, page=1, size=25) => {
|
||||
const url = `${INDEX_URL}/_search`
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', },
|
||||
body: JSON.stringify({
|
||||
"from": page - 1,
|
||||
"size": size,
|
||||
"query": {
|
||||
"multi_match" : {
|
||||
"query" : name,
|
||||
"fields": ["wine.name"],
|
||||
"fuzziness": 2
|
||||
}
|
||||
},
|
||||
"sort": [
|
||||
{
|
||||
"_score": {
|
||||
"order": "desc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"_source": {
|
||||
"includes": "wine"
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(resp => resp.json())
|
||||
.then(verifyAndUnpackElasticSearchResult)
|
||||
.then(searchHits => Promise.all(searchHits.map(getWineObjectFromSearchHit)))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
wineById,
|
||||
wineByQueryName
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const config = require(path.join(__dirname, "/../config/defaults/lottery"));
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration"));
|
||||
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
|
||||
|
||||
const winners = async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
let winnersRedacted = [];
|
||||
let winner;
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
winner = winners[i];
|
||||
winnersRedacted.push({
|
||||
name: winner.name,
|
||||
color: winner.color
|
||||
});
|
||||
}
|
||||
res.json(winnersRedacted);
|
||||
};
|
||||
|
||||
const winnersSecure = async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
|
||||
return res.json(winners);
|
||||
};
|
||||
|
||||
const deleteWinners = async (req, res) => {
|
||||
await VirtualWinner.deleteMany();
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("refresh_data", {});
|
||||
return res.json(true);
|
||||
};
|
||||
|
||||
const attendees = async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
let attendeesRedacted = [];
|
||||
let attendee;
|
||||
for (let i = 0; i < attendees.length; i++) {
|
||||
attendee = attendees[i];
|
||||
attendeesRedacted.push({
|
||||
name: attendee.name,
|
||||
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||
red: attendee.red,
|
||||
blue: attendee.blue,
|
||||
green: attendee.green,
|
||||
yellow: attendee.yellow
|
||||
});
|
||||
}
|
||||
return res.json(attendeesRedacted);
|
||||
};
|
||||
|
||||
const attendeesSecure = async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
|
||||
return res.json(attendees);
|
||||
};
|
||||
|
||||
const addAttendee = async (req, res) => {
|
||||
const attendee = req.body;
|
||||
const { red, blue, yellow, green } = attendee;
|
||||
|
||||
let newAttendee = new Attendee({
|
||||
name: attendee.name,
|
||||
red,
|
||||
blue,
|
||||
green,
|
||||
yellow,
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
winner: false
|
||||
});
|
||||
await newAttendee.save();
|
||||
|
||||
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("new_attendee", {});
|
||||
|
||||
return res.send(true);
|
||||
};
|
||||
|
||||
const deleteAttendees = async (req, res) => {
|
||||
await Attendee.deleteMany();
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("refresh_data", {});
|
||||
return res.json(true);
|
||||
};
|
||||
|
||||
const drawWinner = async (req, res) => {
|
||||
let allContestants = await Attendee.find({ winner: false });
|
||||
|
||||
if (allContestants.length == 0) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No attendees left that have not won."
|
||||
});
|
||||
}
|
||||
let raffleColors = [];
|
||||
for (let i = 0; i < allContestants.length; i++) {
|
||||
let currentContestant = allContestants[i];
|
||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||
raffleColors.push("blue");
|
||||
}
|
||||
for (let red = 0; red < currentContestant.red; red++) {
|
||||
raffleColors.push("red");
|
||||
}
|
||||
for (let green = 0; green < currentContestant.green; green++) {
|
||||
raffleColors.push("green");
|
||||
}
|
||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||
raffleColors.push("yellow");
|
||||
}
|
||||
}
|
||||
|
||||
raffleColors = shuffle(raffleColors);
|
||||
|
||||
let colorToChooseFrom =
|
||||
raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||
let findObject = { winner: false };
|
||||
|
||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||
|
||||
let tries = 0;
|
||||
const maxTries = 3;
|
||||
let contestantsToChooseFrom = undefined;
|
||||
while (contestantsToChooseFrom == undefined && tries < maxTries) {
|
||||
const hit = await Attendee.find(findObject);
|
||||
if (hit && hit.length) {
|
||||
contestantsToChooseFrom = hit;
|
||||
break;
|
||||
}
|
||||
tries++;
|
||||
}
|
||||
if (contestantsToChooseFrom == undefined) {
|
||||
return res.status(404).send({
|
||||
success: false,
|
||||
message: `Klarte ikke trekke en vinner etter ${maxTries} forsøk.`
|
||||
});
|
||||
}
|
||||
|
||||
let attendeeListDemocratic = [];
|
||||
|
||||
let currentContestant;
|
||||
for (let i = 0; i < contestantsToChooseFrom.length; i++) {
|
||||
currentContestant = contestantsToChooseFrom[i];
|
||||
for (let y = 0; y < currentContestant[colorToChooseFrom]; y++) {
|
||||
attendeeListDemocratic.push({
|
||||
name: currentContestant.name,
|
||||
phoneNumber: currentContestant.phoneNumber,
|
||||
red: currentContestant.red,
|
||||
blue: currentContestant.blue,
|
||||
green: currentContestant.green,
|
||||
yellow: currentContestant.yellow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attendeeListDemocratic = shuffle(attendeeListDemocratic);
|
||||
|
||||
let winner =
|
||||
attendeeListDemocratic[
|
||||
Math.floor(Math.random() * attendeeListDemocratic.length)
|
||||
];
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("winner", {
|
||||
color: colorToChooseFrom,
|
||||
name: winner.name,
|
||||
winner_count: winners.length + 1
|
||||
});
|
||||
|
||||
let newWinnerElement = new VirtualWinner({
|
||||
name: winner.name,
|
||||
phoneNumber: winner.phoneNumber,
|
||||
color: colorToChooseFrom,
|
||||
red: winner.red,
|
||||
blue: winner.blue,
|
||||
green: winner.green,
|
||||
yellow: winner.yellow,
|
||||
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||
timestamp_drawn: new Date().getTime()
|
||||
});
|
||||
|
||||
await Attendee.update(
|
||||
{ name: winner.name, phoneNumber: winner.phoneNumber },
|
||||
{ $set: { winner: true } }
|
||||
);
|
||||
|
||||
await newWinnerElement.save();
|
||||
return res.json({
|
||||
success: true,
|
||||
winner
|
||||
});
|
||||
};
|
||||
|
||||
const finish = async (req, res) => {
|
||||
if (!config.gatewayToken) {
|
||||
return res.json({
|
||||
message: "Missing api token for sms gateway.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
|
||||
if (winners.length == 0) {
|
||||
return res.json({
|
||||
message: "No winners to draw from.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
Message.sendInitialMessageToWinners(winners.slice(1));
|
||||
|
||||
return findAndNotifyNextWinner()
|
||||
.then(() => res.json({
|
||||
success: true,
|
||||
message: "Sent wine select message to first winner and update message to rest of winners."
|
||||
}))
|
||||
.catch(error => res.json({
|
||||
message: error["message"] || "Unable to send message to first winner.",
|
||||
success: false
|
||||
}))
|
||||
};
|
||||
|
||||
const genRandomString = function(length) {
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString("hex") /** convert to hexadecimal format */
|
||||
.slice(0, length); /** return required number of characters */
|
||||
};
|
||||
|
||||
const sha512 = function(password, salt) {
|
||||
var hash = crypto.createHmac("md5", salt); /** Hashing algorithm sha512 */
|
||||
hash.update(password);
|
||||
var value = hash.digest("hex");
|
||||
return value;
|
||||
};
|
||||
|
||||
function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
temporaryValue,
|
||||
randomIndex;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (0 !== currentIndex) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
// And swap it with the current element.
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
winners,
|
||||
winnersSecure,
|
||||
drawWinner,
|
||||
finish,
|
||||
attendees,
|
||||
attendeesSecure,
|
||||
addAttendee
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const VirtualWinner = require(path.join(
|
||||
__dirname, "/schemas/VirtualWinner"
|
||||
));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
|
||||
const getWinesToWinnerById = async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No winner with this id.",
|
||||
existing: false,
|
||||
turn: false
|
||||
});
|
||||
}
|
||||
|
||||
let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
if (
|
||||
allWinners[0].id != foundWinner.id ||
|
||||
foundWinner.timestamp_limit == undefined ||
|
||||
foundWinner.timestamp_sent == undefined
|
||||
) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Not the winner next in line!",
|
||||
existing: true,
|
||||
turn: false
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
existing: true,
|
||||
turn: true,
|
||||
name: foundWinner.name,
|
||||
color: foundWinner.color
|
||||
});
|
||||
};
|
||||
|
||||
const registerWinnerSelection = async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let wineName = req.body.wineName;
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No winner with this id."
|
||||
})
|
||||
} else if (foundWinner.timestamp_limit < new Date().getTime()) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Timelimit expired, you will receive a wine after other users have chosen.",
|
||||
limit: true
|
||||
})
|
||||
}
|
||||
|
||||
let date = new Date();
|
||||
date.setHours(5, 0, 0, 0);
|
||||
let prelotteryWine = await PreLotteryWine.findOne({ name: wineName });
|
||||
|
||||
if (!prelotteryWine) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No wine with this name.",
|
||||
wine: false
|
||||
});
|
||||
}
|
||||
|
||||
let wonWine = await _wineFunctions.findSaveWine(prelotteryWine);
|
||||
await prelotteryWine.delete();
|
||||
await _personFunctions.findSavePerson(foundWinner, wonWine, date);
|
||||
await Message.sendWineConfirmation(foundWinner, wonWine, date);
|
||||
|
||||
await foundWinner.delete();
|
||||
console.info("Saved winners choice.");
|
||||
|
||||
return findAndNotifyNextWinner()
|
||||
.then(() => res.json({
|
||||
message: "Choice saved and next in line notified.",
|
||||
success: true
|
||||
}))
|
||||
.catch(error => res.json({
|
||||
message: error["message"] || "Error when notifing next winner.",
|
||||
success: false
|
||||
}))
|
||||
};
|
||||
|
||||
const chooseLastWineForUser = (winner, preLotteryWine) => {
|
||||
let date = new Date();
|
||||
date.setHours(5, 0, 0, 0);
|
||||
|
||||
return _wineFunctions.findSaveWine(preLotteryWine)
|
||||
.then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date))
|
||||
.then(() => preLotteryWine.delete())
|
||||
.then(() => Message.sendLastWinnerMessage(winner, preLotteryWine))
|
||||
.then(() => winner.delete())
|
||||
.catch(err => {
|
||||
console.log("Error thrown from chooseLastWineForUser: " + err);
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
|
||||
const findAndNotifyNextWinner = async () => {
|
||||
let nextWinner = undefined;
|
||||
|
||||
let winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
let winesLeft = await PreLotteryWine.find();
|
||||
|
||||
if (winnersLeft.length > 1) {
|
||||
console.log("multiple winners left, choose next in line")
|
||||
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
||||
console.log("one winner left, but multiple wines")
|
||||
nextWinner = winnersLeft[0] // one winner left, but multiple wines
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
||||
console.log("one winner and one wine left, choose for user")
|
||||
nextWinner = winnersLeft[0] // one winner and one wine left, choose for user
|
||||
wine = winesLeft[0]
|
||||
return chooseLastWineForUser(nextWinner, wine);
|
||||
}
|
||||
|
||||
if (nextWinner) {
|
||||
return Message.sendWineSelectMessage(nextWinner)
|
||||
.then(messageResponse => startTimeout(nextWinner.id))
|
||||
} else {
|
||||
console.info("All winners notified. Could start cleanup here.");
|
||||
return Promise.resolve({
|
||||
message: "All winners notified."
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotificationToWinnerById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
let winner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!winner) {
|
||||
return res.json({
|
||||
message: "No winner with this id.",
|
||||
success: false
|
||||
})
|
||||
}
|
||||
|
||||
return Message.sendWineSelectMessage(winner)
|
||||
.then(success => res.json({
|
||||
success: success,
|
||||
message: `Message sent to winner ${id} successfully!`
|
||||
}))
|
||||
.catch(err => res.json({
|
||||
success: false,
|
||||
message: "Error while trying to send sms.",
|
||||
error: err
|
||||
}))
|
||||
}
|
||||
|
||||
function startTimeout(id) {
|
||||
const minute = 60000;
|
||||
const minutesForTimeout = 10;
|
||||
|
||||
console.log(`Starting timeout for user ${id}.`);
|
||||
console.log(`Timeout duration: ${ minutesForTimeout * minute }`)
|
||||
setTimeout(async () => {
|
||||
let virtualWinner = await VirtualWinner.findOne({ id: id });
|
||||
if (!virtualWinner) {
|
||||
console.log(`Timeout done for user ${id}, but user has already sent data.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Timeout done for user ${id}, sending update to user.`);
|
||||
|
||||
Message.sendWineSelectMessageTooLate(virtualWinner);
|
||||
|
||||
virtualWinner.timestamp_drawn = new Date().getTime();
|
||||
virtualWinner.timestamp_limit = null;
|
||||
virtualWinner.timestamp_sent = null;
|
||||
await virtualWinner.save();
|
||||
|
||||
findAndNotifyNextWinner();
|
||||
}, minutesForTimeout * minute);
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWinesToWinnerById,
|
||||
registerWinnerSelection,
|
||||
findAndNotifyNextWinner,
|
||||
|
||||
sendNotificationToWinnerById
|
||||
};
|
||||
72
api/wine.js
72
api/wine.js
@@ -1,27 +1,63 @@
|
||||
const path = require("path");
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
|
||||
async function findSaveWine(prelotteryWine) {
|
||||
let wonWine = await Wine.findOne({ name: prelotteryWine.name });
|
||||
if (wonWine == undefined) {
|
||||
let newWonWine = new Wine({
|
||||
name: prelotteryWine.name,
|
||||
vivinoLink: prelotteryWine.vivinoLink,
|
||||
rating: prelotteryWine.rating,
|
||||
const { WineNotFound } = require(path.join(__dirname, "/vinlottisErrors"));
|
||||
|
||||
const addWine = async wine => {
|
||||
let existingWine = await Wine.findOne({ name: wine.name, id: wine.id, year: wine.year });
|
||||
|
||||
if (existingWine == undefined) {
|
||||
let newWine = new Wine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
occurences: 1,
|
||||
image: prelotteryWine.image,
|
||||
id: prelotteryWine.id
|
||||
id: wine.id,
|
||||
year: wine.year,
|
||||
image: wine.image,
|
||||
price: wine.price,
|
||||
country: wine.country
|
||||
});
|
||||
await newWonWine.save();
|
||||
wonWine = newWonWine;
|
||||
await newWine.save();
|
||||
return newWine;
|
||||
} else {
|
||||
wonWine.occurences += 1;
|
||||
wonWine.image = prelotteryWine.image;
|
||||
wonWine.id = prelotteryWine.id;
|
||||
await wonWine.save();
|
||||
existingWine.occurences += 1;
|
||||
await existingWine.save();
|
||||
return existingWine;
|
||||
}
|
||||
};
|
||||
|
||||
return wonWine;
|
||||
}
|
||||
const allWines = (limit = undefined) => {
|
||||
if (limit) {
|
||||
return Wine.find().limit(limit);
|
||||
} else {
|
||||
return Wine.find();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.findSaveWine = findSaveWine;
|
||||
const wineById = id => {
|
||||
return Wine.findOne({ _id: id }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
const findWine = wine => {
|
||||
return Wine.findOne({ name: wine.name, id: wine.id, year: wine.year }).then(wine => {
|
||||
if (wine == null) {
|
||||
throw new WineNotFound();
|
||||
}
|
||||
|
||||
return wine;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addWine,
|
||||
allWines,
|
||||
wineById,
|
||||
findWine
|
||||
};
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
const fetch = require('node-fetch')
|
||||
const path = require('path')
|
||||
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
||||
|
||||
const convertToOurWineObject = wine => {
|
||||
if(wine.basic.ageLimit === "18"){
|
||||
return {
|
||||
name: wine.basic.productShortName,
|
||||
vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId,
|
||||
rating: wine.basic.alcoholContent,
|
||||
occurences: 0,
|
||||
id: wine.basic.productId,
|
||||
image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`,
|
||||
price: wine.prices[0].salesPrice.toString(),
|
||||
country: wine.origins.origin.country
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wineSearch = async (req, res) => {
|
||||
const {query} = req.query
|
||||
let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`)
|
||||
url.searchParams.set('productShortNameContains', query)
|
||||
|
||||
const vinmonopoletResponse = await fetch(url, {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||
}
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(err => console.error(err))
|
||||
|
||||
|
||||
if (vinmonopoletResponse.errors != null) {
|
||||
return vinmonopoletResponse.errors.map(error => {
|
||||
if (error.type == "UnknownProductError") {
|
||||
return res.status(404).json({
|
||||
message: error.message
|
||||
})
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
})
|
||||
}
|
||||
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean)
|
||||
|
||||
return res.send(winesConverted);
|
||||
}
|
||||
|
||||
const byEAN = async (req, res) => {
|
||||
const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean)
|
||||
.then(resp => resp.json())
|
||||
|
||||
if (vinmonopoletResponse.errors != null) {
|
||||
return vinmonopoletResponse.errors.map(error => {
|
||||
if (error.type == "UnknownProductError") {
|
||||
return res.status(404).json({
|
||||
message: error.message
|
||||
})
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return res.send(vinmonopoletResponse);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
byEAN,
|
||||
wineSearch
|
||||
};
|
||||
107
api/winner.js
Normal file
107
api/winner.js
Normal file
@@ -0,0 +1,107 @@
|
||||
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 addWinner = winner => {
|
||||
let newWinner = new VirtualWinner({
|
||||
name: winner.name,
|
||||
color: winner.color,
|
||||
timestamp_drawn: new Date().getTime()
|
||||
});
|
||||
|
||||
return newWinner.save()
|
||||
}
|
||||
|
||||
const addWinners = winners => {
|
||||
return Promise.all(
|
||||
winners.map(winner => addWinner(winner))
|
||||
);
|
||||
};
|
||||
|
||||
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 setWinnerChosenById = (id) => {
|
||||
return VirtualWinner.findOne({id: id}).then(winner => {
|
||||
winner.prize_selected = true
|
||||
winner.markModified("wins")
|
||||
return winner.save()
|
||||
})
|
||||
}
|
||||
|
||||
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 = {
|
||||
addWinner,
|
||||
addWinners,
|
||||
allWinners,
|
||||
winnerById,
|
||||
updateWinnerById,
|
||||
deleteWinnerById,
|
||||
deleteWinners,
|
||||
setWinnerChosenById
|
||||
};
|
||||
5
config/env/lottery.config.example.js
vendored
5
config/env/lottery.config.example.js
vendored
@@ -8,5 +8,6 @@ module.exports = {
|
||||
gatewayToken: undefined,
|
||||
vinmonopoletToken: undefined,
|
||||
googleanalytics_trackingId: undefined,
|
||||
googleanalytics_cookieLifetime: 60 * 60 * 24 * 14
|
||||
};
|
||||
googleanalytics_cookieLifetime: 60 * 60 * 24 * 14,
|
||||
sites: [],
|
||||
};
|
||||
|
||||
@@ -11,15 +11,15 @@ const webpackConfig = function(isDev) {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue"],
|
||||
alias: {
|
||||
vue$: "vue/dist/vue.min.js",
|
||||
"@": helpers.root("frontend")
|
||||
}
|
||||
"vue$": "vue/dist/vue.min.js",
|
||||
"@": helpers.root("frontend"),
|
||||
},
|
||||
},
|
||||
entry: {
|
||||
vinlottis: helpers.root("frontend", "vinlottis-init")
|
||||
vinlottis: helpers.root("frontend", "vinlottis-init"),
|
||||
},
|
||||
externals: {
|
||||
moment: 'moment' // comes with chart.js
|
||||
moment: "moment", // comes with chart.js
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -31,45 +31,45 @@ const webpackConfig = function(isDev) {
|
||||
options: {
|
||||
loaders: {
|
||||
scss: "vue-style-loader!css-loader!sass-loader",
|
||||
sass: "vue-style-loader!css-loader!sass-loader?indentedSyntax"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
sass: "vue-style-loader!css-loader!sass-loader?indentedSyntax",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: [ "babel-loader" ],
|
||||
include: [helpers.root("frontend")]
|
||||
use: ["babel-loader"],
|
||||
include: [helpers.root("frontend")],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
||||
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
{ loader: "sass-loader", options: { sourceMap: isDev } },
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.woff(2)?(\?[a-z0-9]+)?$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: "application/font-woff"
|
||||
}
|
||||
mimetype: "application/font-woff",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/,
|
||||
loader: "file-loader"
|
||||
}
|
||||
]
|
||||
loader: "file-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
@@ -83,9 +83,10 @@ const webpackConfig = function(isDev) {
|
||||
__HOURS__: env.hours,
|
||||
__PUSHENABLED__: JSON.stringify(require("./defaults/push") != false),
|
||||
__GA_TRACKINGID__: JSON.stringify(env.googleanalytics_trackingId),
|
||||
__GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime
|
||||
})
|
||||
]
|
||||
__GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime,
|
||||
__sites__: JSON.stringify(env.sites),
|
||||
}),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -15,51 +15,52 @@ let webpackConfig = merge(commonConfig(true), {
|
||||
output: {
|
||||
path: helpers.root("dist"),
|
||||
publicPath: "/",
|
||||
filename: "js/[name].bundle.js"
|
||||
filename: "js/[name].bundle.js",
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: true,
|
||||
splitChunks: {
|
||||
chunks: "initial"
|
||||
}
|
||||
chunks: "initial",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin(environment),
|
||||
new FriendlyErrorsPlugin(),
|
||||
new MiniCSSExtractPlugin({
|
||||
filename: "css/[name].css"
|
||||
})
|
||||
filename: "css/[name].css",
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
compress: true,
|
||||
historyApiFallback: true,
|
||||
host: "0.0.0.0",
|
||||
disableHostCheck: true,
|
||||
hot: true,
|
||||
overlay: true,
|
||||
stats: {
|
||||
normal: true
|
||||
normal: true,
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:30030",
|
||||
changeOrigin: true
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/socket.io": {
|
||||
target: "ws://localhost:30030",
|
||||
changeOrigin: false,
|
||||
ws: true
|
||||
}
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
writeToDisk: false
|
||||
}
|
||||
writeToDisk: false,
|
||||
},
|
||||
});
|
||||
|
||||
webpackConfig = merge(webpackConfig, {
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: "frontend/templates/Index.html"
|
||||
})
|
||||
]
|
||||
template: "frontend/templates/Index.html",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = webpackConfig;
|
||||
|
||||
78
db/seedSingleDay.js
Normal file
78
db/seedSingleDay.js
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
|
||||
const session = require("express-session");
|
||||
const mongoose = require("mongoose");
|
||||
const MongoStore = require("connect-mongo")(session);
|
||||
mongoose.promise = global.Promise;
|
||||
mongoose
|
||||
.connect("mongodb://localhost/vinlottis", {
|
||||
useCreateIndex: true,
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
serverSelectionTimeoutMS: 10000 // initial connection timeout
|
||||
})
|
||||
.then(_ => console.log("Mongodb connection established!"))
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
console.error("ERROR! Mongodb required to run.");
|
||||
process.exit(1);
|
||||
});
|
||||
mongoose.set("debug", false);
|
||||
|
||||
const path = require("path")
|
||||
const prelotteryWineRepository = require(path.join(__dirname, "../api/prelotteryWine"));
|
||||
const attendeeRepository = require(path.join(__dirname, "../api/attendee"));
|
||||
|
||||
async function add() {
|
||||
const wines = [
|
||||
{
|
||||
vivinoLink: 'https://www.vinmonopolet.no/Land/Frankrike/Devevey-Bourgogne-Hautes-C%C3%B4tes-de-Beaune-Rouge-2018/p/12351301',
|
||||
name: 'Devevey Bourgogne Hautes-Côtes de Beaune Rouge 2018',
|
||||
rating: 3,
|
||||
id: '12351301',
|
||||
year: 2018,
|
||||
image: "https://bilder.vinmonopolet.no/cache/300x300-0/12351301-1.jpg",
|
||||
price: '370',
|
||||
country: "Frankrike"
|
||||
},
|
||||
{
|
||||
vivinoLink: 'https://www.vinmonopolet.no/Land/Frankrike/Devevey-Rully-La-Chaume-Rouge-2018/p/12351101',
|
||||
name: 'Devevey Rully La Chaume Rouge 2018',
|
||||
rating: 4,
|
||||
id: '12351101',
|
||||
year: 2018,
|
||||
image: 'https://bilder.vinmonopolet.no/cache/300x300-0/12351101-1.jpg',
|
||||
price: '372',
|
||||
country: 'Frankrike'
|
||||
}
|
||||
]
|
||||
|
||||
const attendees = [
|
||||
{
|
||||
name: "Kasper Rynning-Tønnesen",
|
||||
red: 0,
|
||||
blue: 10,
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
phoneNumber: 97777777,
|
||||
winner: false
|
||||
},
|
||||
{
|
||||
name: "Kevin Midbøe",
|
||||
red: 3,
|
||||
blue: 3,
|
||||
green: 3,
|
||||
yellow: 3,
|
||||
phoneNumber: 95012321,
|
||||
winner: false
|
||||
}
|
||||
]
|
||||
|
||||
await prelotteryWineRepository.addWines(wines)
|
||||
await Promise.all(attendees.map(attendee => attendeeRepository.addAttendee(attendee)))
|
||||
|
||||
console.log("Added some wines, and 2 attendees to database.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
add()
|
||||
@@ -1,14 +1,9 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<banner :routes="routes"/>
|
||||
<banner :routes="routes" />
|
||||
<router-view />
|
||||
<Footer />
|
||||
<UpdateToast
|
||||
v-if="showToast"
|
||||
:text="toastText"
|
||||
:refreshButton="refreshToast"
|
||||
v-on:closeToast="closeToast"
|
||||
/>
|
||||
<UpdateToast v-if="showToast" :text="toastText" :refreshButton="refreshToast" v-on:closeToast="closeToast" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,33 +25,33 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
name: "Virtuelt lotteri",
|
||||
route: "/lottery"
|
||||
route: "/lottery",
|
||||
},
|
||||
{
|
||||
name: "Dagens viner",
|
||||
route: "/dagens/"
|
||||
route: "/dagens/",
|
||||
},
|
||||
{
|
||||
name: "Highscore",
|
||||
route: "/highscore"
|
||||
route: "/highscore",
|
||||
},
|
||||
{
|
||||
name: "Historie",
|
||||
route: "/history/"
|
||||
route: "/history/",
|
||||
},
|
||||
{
|
||||
name: "Foreslå vin",
|
||||
route: "/request"
|
||||
route: "/request",
|
||||
},
|
||||
{
|
||||
name: "Foreslåtte viner",
|
||||
route: "/requested-wines"
|
||||
route: "/requested-wines",
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
route: "/login"
|
||||
}
|
||||
]
|
||||
route: "/login",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -78,7 +73,7 @@ export default {
|
||||
closeToast: function() {
|
||||
this.showToast = false;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -99,7 +94,7 @@ body {
|
||||
display: grid;
|
||||
grid-template-rows: 80px auto 100px;
|
||||
|
||||
.main-container{
|
||||
.main-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ const allRequestedWines = () => {;
|
||||
return fetch("/api/request/all")
|
||||
.then(resp => {
|
||||
const isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||
return Promise.all([resp.json(), isAdmin]);
|
||||
const getWinesFromBody = (resp) => resp.json().then(body => body.wines);
|
||||
return Promise.all([getWinesFromBody(resp), isAdmin]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -109,8 +110,7 @@ const deleteRequestedWine = wineToBeDeleted => {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(wineToBeDeleted)
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch("api/request/" + wineToBeDeleted.id, options)
|
||||
@@ -148,14 +148,12 @@ const attendees = () => {
|
||||
|
||||
const requestNewWine = (wine) => {
|
||||
const options = {
|
||||
body: JSON.stringify({
|
||||
wine: wine
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "post"
|
||||
body: JSON.stringify({ wine })
|
||||
}
|
||||
|
||||
return fetch("/api/request/new-wine", options)
|
||||
|
||||
208
frontend/components/AccessCodePage.vue
Normal file
208
frontend/components/AccessCodePage.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="floating-video">
|
||||
<video autoplay loop muted playsinline id="office-party" ref="video">
|
||||
<source src="/public/assets/videos/office-party.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="container--code label-div row">
|
||||
<label>Din vinlottis kode:</label>
|
||||
</div>
|
||||
|
||||
<div class="codeinput-container">
|
||||
<input v-model="code" placeholder="KODE" @keyup.enter="submit" />
|
||||
<button class="vin-button" @click="submit">ENTER</button>
|
||||
</div>
|
||||
|
||||
<button class="mute-button" @click="toggleMute">
|
||||
{{ muted ? "🔇" : "🔈" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Footer></Footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Footer from "@/ui/FooterUnbranded";
|
||||
import { createCookie } from "@/utils";
|
||||
|
||||
export default {
|
||||
components: { Footer },
|
||||
data() {
|
||||
return {
|
||||
muted: true,
|
||||
code: undefined,
|
||||
// volume: 50,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
const site = __sites__.find(site => site.code == this.code);
|
||||
},
|
||||
// watch: {
|
||||
// volume(newValue) {
|
||||
// this.$refs.video.volume = newValue / 100;
|
||||
// },
|
||||
// },
|
||||
methods: {
|
||||
toggleMute() {
|
||||
const { video } = this.$refs;
|
||||
this.muted = !this.muted;
|
||||
video.muted = this.muted;
|
||||
},
|
||||
togglePlayback() {
|
||||
const { video } = this.$refs;
|
||||
video.paused ? video.play() : video.pause();
|
||||
},
|
||||
submit() {
|
||||
const site = __sites__.find(site => site.code == this.code);
|
||||
|
||||
if (site) {
|
||||
createCookie("accesscode", site.code, 14);
|
||||
window.location.href = `${window.location.protocol}//${site.domain}`;
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries";
|
||||
|
||||
.floating-video {
|
||||
position: absolute;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: var(--primary);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.mute-button {
|
||||
z-index: 10;
|
||||
-webkit-appearance: unset;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 1.5rem;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
bottom: calc(75px + 1rem);
|
||||
cursor: pointer;
|
||||
|
||||
input[type="range"] {
|
||||
transform: rotate(90deg);
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
display: block;
|
||||
// left: 0;
|
||||
height: 100%;
|
||||
|
||||
// -o-filter: blur(1px);
|
||||
filter: blur(5px);
|
||||
object-fit: cover;
|
||||
transform: scale(1.02);
|
||||
|
||||
@include mobile {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.codeinput-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@include mobile {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
input {
|
||||
max-width: 24rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
background-color: white;
|
||||
|
||||
@include mobile {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
height: 100%;
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px);
|
||||
margin: auto;
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
justify-content: center;
|
||||
|
||||
@include desktop {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
position: relative;
|
||||
// text-align: center;
|
||||
font-weight: 600;
|
||||
// color: white;
|
||||
|
||||
@include desktop {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-line {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin-top: 2.4rem;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Admin-side</h1>
|
||||
<Tabs :tabs="tabs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tabs from "@/ui/Tabs";
|
||||
import RegisterPage from "@/components/RegisterPage";
|
||||
import VirtualLotteryRegistrationPage from "@/components/VirtualLotteryRegistrationPage";
|
||||
import RegisterWinePage from "@/components/admin/RegisterWinePage";
|
||||
import archiveLotteryPage from "@/components/admin/archiveLotteryPage";
|
||||
import registerAttendeePage from "@/components/admin/registerAttendeePage";
|
||||
import DrawWinnerPage from "@/components/admin/DrawWinnerPage";
|
||||
import PushPage from "@/components/admin/PushPage";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tabs,
|
||||
RegisterPage,
|
||||
VirtualLotteryRegistrationPage
|
||||
Tabs
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
{ name: "Registrering", component: RegisterPage },
|
||||
{ name: "Virtuelt lotteri", component: VirtualLotteryRegistrationPage }
|
||||
{
|
||||
name: "Vin",
|
||||
component: RegisterWinePage,
|
||||
slug: "vin",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Legg til deltakere",
|
||||
component: registerAttendeePage,
|
||||
slug: "attendees",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Trekk vinner",
|
||||
component: DrawWinnerPage,
|
||||
slug: "draw",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Arkiver lotteri",
|
||||
component: archiveLotteryPage,
|
||||
slug: "reg",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Push meldinger",
|
||||
component: PushPage,
|
||||
slug: "push"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
<style lang="scss">
|
||||
@import "@/styles/media-queries";
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,41 +1,59 @@
|
||||
<template>
|
||||
<main class="container">
|
||||
<h1>Alle foreslåtte viner</h1>
|
||||
<div class="header">
|
||||
<h1>Alle foreslåtte viner</h1>
|
||||
<router-link class="vin-button" to="/anbefal">
|
||||
Anbefal ny vin
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<section class="requested-wines-container">
|
||||
<p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p>
|
||||
<section class="wines-container">
|
||||
<p v-if="wines == undefined || wines.length == 0">
|
||||
Ingen har foreslått noe enda!
|
||||
</p>
|
||||
|
||||
<RequestedWineCard v-for="requestedEl in wines" :key="requestedEl.wine._id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/>
|
||||
<RequestedWineCard
|
||||
v-for="requestedWine in wines"
|
||||
:key="requestedWine.wine._id"
|
||||
:requestedElement="requestedWine"
|
||||
@wineDeleted="filterOutDeletedWine"
|
||||
:showDeleteButton="isAdmin"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { allRequestedWines } from "@/api";
|
||||
import RequestedWineCard from "@/ui/RequestedWineCard";
|
||||
export default {
|
||||
components: {
|
||||
RequestedWineCard
|
||||
RequestedWineCard,
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
data() {
|
||||
return {
|
||||
wines: undefined,
|
||||
canRequest: true,
|
||||
isAdmin: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterOutDeletedWine(wine){
|
||||
this.wines = this.wines.filter(item => item.wine._id !== wine._id)
|
||||
},
|
||||
async refreshData(){
|
||||
[this.wines, this.isAdmin] = await allRequestedWines() || [[], false]
|
||||
}
|
||||
isAdmin: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.refreshData()
|
||||
}
|
||||
}
|
||||
this.fetchRequestedWines();
|
||||
},
|
||||
methods: {
|
||||
filterOutDeletedWine(wine) {
|
||||
this.wines = this.wines.filter((item) => item.wine._id !== wine._id);
|
||||
},
|
||||
fetchRequestedWines() {
|
||||
return fetch("/api/requests")
|
||||
.then((resp) => {
|
||||
this.isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||
return resp;
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then((response) => (this.wines = response.wines));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -56,9 +74,29 @@ h1 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.requested-wines-container{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
|
||||
a {
|
||||
align-self: flex-end;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
a.vin-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: calc(4rem - 20px);
|
||||
}
|
||||
|
||||
a .icon {
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
<div class="container">
|
||||
<h1 class="">Alle viner</h1>
|
||||
|
||||
<div id="wines-container">
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id">
|
||||
<div class="winners-container">
|
||||
|
||||
<span class="label">Vinnende lodd:</span>
|
||||
<div class="flex row">
|
||||
<span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span>
|
||||
@@ -19,43 +18,44 @@
|
||||
<ul class="names">
|
||||
<li v-for="(winner, index) in wine.winners">
|
||||
<router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link>
|
||||
-
|
||||
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ dateString(wine.dates[index]) }}</router-link>
|
||||
-
|
||||
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{
|
||||
dateString(wine.dates[index])
|
||||
}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Wine>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Banner from "@/ui/Banner";
|
||||
import Wine from "@/ui/Wine";
|
||||
import { overallWineStatistics } from "@/api";
|
||||
import { dateString } from "@/utils";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Banner,
|
||||
Wine
|
||||
},
|
||||
components: { Wine },
|
||||
data() {
|
||||
return {
|
||||
wines: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.overallWineStatistics();
|
||||
},
|
||||
methods: {
|
||||
winDateUrl(date) {
|
||||
const timestamp = new Date(date).getTime();
|
||||
return `/history/${timestamp}`
|
||||
return `/history/${timestamp}`;
|
||||
},
|
||||
overallWineStatistics() {
|
||||
return fetch("/api/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
},
|
||||
dateString: dateString
|
||||
},
|
||||
async mounted() {
|
||||
this.wines = await overallWineStatistics();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -84,18 +84,6 @@ h1 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#wines-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
|
||||
|
||||
> div {
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.name-wins {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
||||
</p>
|
||||
|
||||
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" />
|
||||
<RaffleGenerator @numberOfRaffles="val => (this.numberOfRaffles = val)" />
|
||||
|
||||
<Vipps class="vipps" :amount="numberOfRaffles" />
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
@@ -43,16 +43,16 @@ export default {
|
||||
this.hardStart = true;
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/lottery/generate');
|
||||
window.ga("send", "pageview", "/lottery/generate");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/global.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
h1 {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -67,7 +67,9 @@ p {
|
||||
}
|
||||
|
||||
.vipps {
|
||||
margin: 5rem auto 2.5rem auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 4rem;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 2rem;
|
||||
@@ -75,7 +77,6 @@ p {
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
<p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p>
|
||||
|
||||
<ol v-if="highscore.length > 0" class="highscore-list">
|
||||
<li v-for="person in filteredResults" @click="selectWinner(person)" @keydown.enter="selectWinner(person)" tabindex="0">
|
||||
<li
|
||||
v-for="person in filteredResults"
|
||||
@click="goToWinner(person)"
|
||||
@keydown.enter="goToWinner(person)"
|
||||
tabindex="0"
|
||||
>
|
||||
<b>{{ person.rank }}.</b> {{ person.name }} - {{ person.wins.length }}
|
||||
</li>
|
||||
</ol>
|
||||
@@ -24,8 +29,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highscoreStatistics } from "@/api";
|
||||
import { humanReadableDate, daysAgo } from "@/utils";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
@@ -34,18 +37,12 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
highscore: [],
|
||||
filterInput: ''
|
||||
}
|
||||
filterInput: ""
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await highscoreStatistics();
|
||||
response.sort((a, b) => {
|
||||
return a.wins.length > b.wins.length ? -1 : 1;
|
||||
});
|
||||
response = response.filter(
|
||||
person => person.name != null && person.name != ""
|
||||
);
|
||||
this.highscore = this.generateScoreBoard(response);
|
||||
const winners = await this.highscoreStatistics();
|
||||
this.highscore = this.generateScoreBoard(winners);
|
||||
},
|
||||
computed: {
|
||||
filteredResults() {
|
||||
@@ -53,37 +50,42 @@ export default {
|
||||
let val = this.filterInput;
|
||||
|
||||
if (val.length) {
|
||||
val = val.toLowerCase()
|
||||
const nameIncludesString = (person) => person.name.toLowerCase().includes(val);
|
||||
highscore = highscore.filter(nameIncludesString)
|
||||
val = val.toLowerCase();
|
||||
const nameIncludesString = person => person.name.toLowerCase().includes(val);
|
||||
highscore = highscore.filter(nameIncludesString);
|
||||
}
|
||||
|
||||
return highscore
|
||||
return highscore;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generateScoreBoard(highscore=this.highscore) {
|
||||
highscoreStatistics() {
|
||||
return fetch("/api/history/by-wins")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.winners);
|
||||
},
|
||||
generateScoreBoard(highscore = this.highscore) {
|
||||
let place = 0;
|
||||
let highestWinCount = -1;
|
||||
|
||||
return highscore.map(win => {
|
||||
const wins = win.wins.length
|
||||
const wins = win.wins.length;
|
||||
if (wins != highestWinCount) {
|
||||
place += 1
|
||||
highestWinCount = wins
|
||||
place += 1;
|
||||
highestWinCount = wins;
|
||||
}
|
||||
|
||||
const placeString = place.toString().padStart(2, "0");
|
||||
win.rank = placeString;
|
||||
return win
|
||||
})
|
||||
return win;
|
||||
});
|
||||
},
|
||||
resetFilter() {
|
||||
this.filterInput = '';
|
||||
document.getElementsByTagName('input')[0].focus();
|
||||
this.filterInput = "";
|
||||
document.getElementsByTagName("input")[0].focus();
|
||||
},
|
||||
selectWinner(winner) {
|
||||
const path = "/highscore/" + encodeURIComponent(winner.name)
|
||||
goToWinner(winner) {
|
||||
const path = "/highscore/" + encodeURIComponent(winner.name);
|
||||
this.$router.push(path);
|
||||
},
|
||||
humanReadableDate: humanReadableDate,
|
||||
@@ -152,7 +154,8 @@ h1 {
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 2px solid transparent;
|
||||
&:hover, &:focus {
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,42 +3,51 @@
|
||||
<h1>Historie fra tidligere lotteri</h1>
|
||||
|
||||
<div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries">
|
||||
<Winners :winners="lottery.winners" :title="`Vinnere fra ${ humanReadableDate(lottery.date) }`" />
|
||||
<Winners :winners="lottery.winners" :title="`Vinnere fra ${humanReadableDate(lottery.date)}`" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { historyByDate, historyAll } from '@/api'
|
||||
import { historyByDate, historyAll } from "@/api";
|
||||
import { humanReadableDate } from "@/utils";
|
||||
import Winners from '@/ui/Winners'
|
||||
import Winners from "@/ui/Winners";
|
||||
|
||||
export default {
|
||||
name: 'History page of prev lotteries',
|
||||
name: "History page of prev lotteries",
|
||||
components: { Winners },
|
||||
data() {
|
||||
return {
|
||||
lotteries: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanReadableDate: humanReadableDate
|
||||
lotteries: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
const dateFromUrl = this.$route.params.date;
|
||||
|
||||
if (dateFromUrl !== undefined)
|
||||
historyByDate(dateFromUrl)
|
||||
.then(history => this.lotteries = { "lottery": history })
|
||||
else
|
||||
historyAll()
|
||||
.then(history => this.lotteries = history.lotteries)
|
||||
if (dateFromUrl !== undefined) {
|
||||
this.fetchHistoryByDate(dateFromUrl).then(history => (this.lotteries = [history]));
|
||||
} else {
|
||||
this.fetchHistory().then(history => (this.lotteries = history));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanReadableDate: humanReadableDate,
|
||||
fetchHistory() {
|
||||
return fetch("/api/history/by-date")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.lotteries);
|
||||
},
|
||||
fetchHistoryByDate(date) {
|
||||
return fetch(`/api/history/by-date/${date}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<input
|
||||
type="text"
|
||||
v-model="username"
|
||||
ref="username"
|
||||
placeholder="Brukernavn"
|
||||
autocapitalize="none"
|
||||
@keyup.enter="submit"
|
||||
@@ -34,6 +35,9 @@ export default {
|
||||
error: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.username.focus();
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
login(this.username, this.password)
|
||||
|
||||
@@ -14,20 +14,25 @@
|
||||
|
||||
<h4 class="margin-bottom-0">Vinnende farger:</h4>
|
||||
<div class="raffle-container el-spacing">
|
||||
<div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)" :key="color">
|
||||
<div
|
||||
class="raffle-element"
|
||||
:class="color + `-raffle`"
|
||||
v-for="[color, occurences] in Object.entries(winningColors)"
|
||||
:key="color"
|
||||
>
|
||||
{{ occurences }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="el-spacing">Flasker vunnet:</h4>
|
||||
|
||||
<div v-for="win in winner.highscore" :key="win._id">
|
||||
<div v-for="win in winner.wins" :key="win._id">
|
||||
<router-link :to="winDateUrl(win.date)" class="days-ago">
|
||||
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
|
||||
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }}
|
||||
</router-link>
|
||||
|
||||
<div class="won-wine">
|
||||
<img :src="smallerWineImage(win.wine.image)">
|
||||
|
||||
<div class="won-wine" v-if="win.wine">
|
||||
<img :src="smallerWineImage(win.wine.image)" />
|
||||
|
||||
<div class="won-wine-details">
|
||||
<h3>{{ win.wine.name }}</h3>
|
||||
@@ -38,6 +43,11 @@
|
||||
|
||||
<div class="raffle-element small" :class="win.color + `-raffle`"></div>
|
||||
</div>
|
||||
<div class="won-wine" v-else>
|
||||
<div class="won-wine-details">
|
||||
<h3>Oisann! Klarte ikke finne vin.</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -49,67 +59,71 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getWinnerByName } from "@/api";
|
||||
import { humanReadableDate, daysAgo } from "@/utils";
|
||||
import { dateString, humanReadableDate, daysAgo } from "@/utils";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
winner: undefined,
|
||||
name: undefined,
|
||||
error: undefined,
|
||||
previousRoute: {
|
||||
default: true,
|
||||
name: "topplisten",
|
||||
path: "/highscore"
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
next(vm => {
|
||||
if (from.name != null)
|
||||
vm.previousRoute = from
|
||||
})
|
||||
if (from.name != null) vm.previousRoute = from;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
numberOfWins() {
|
||||
return this.winner.highscore.length
|
||||
return this.winner.wins.length;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const nameFromURL = this.$route.params.name;
|
||||
getWinnerByName(nameFromURL)
|
||||
this.name = this.$route.params.name;
|
||||
this.getWinnerByName(this.name)
|
||||
.then(winner => this.setWinner(winner))
|
||||
.catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`)
|
||||
.catch(err => (this.error = `Ingen med navn: "${nameFromURL}" funnet.`));
|
||||
},
|
||||
methods: {
|
||||
getWinnerByName(name) {
|
||||
return fetch(`/api/history/by-name/${name}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.winner);
|
||||
},
|
||||
setWinner(winner) {
|
||||
this.winner = {
|
||||
name: winner.name,
|
||||
highscore: [],
|
||||
...winner
|
||||
}
|
||||
this.winningColors = this.findWinningColors()
|
||||
};
|
||||
this.winningColors = this.findWinningColors();
|
||||
},
|
||||
smallerWineImage(image) {
|
||||
if (image && image.includes(`515x515`))
|
||||
return image.replace(`515x515`, `175x175`)
|
||||
return image
|
||||
if (image && image.includes(`515x515`)) return image.replace(`515x515`, `175x175`);
|
||||
if (image && image.includes(`500x500`)) return image.replace(`500x500`, `175x175`);
|
||||
return image;
|
||||
},
|
||||
findWinningColors() {
|
||||
const colors = this.winner.highscore.map(win => win.color)
|
||||
const colorOccurences = {}
|
||||
const colors = this.winner.wins.map(win => win.color);
|
||||
const colorOccurences = {};
|
||||
colors.forEach(color => {
|
||||
if (colorOccurences[color] == undefined) {
|
||||
colorOccurences[color] = 1
|
||||
colorOccurences[color] = 1;
|
||||
} else {
|
||||
colorOccurences[color] += 1
|
||||
colorOccurences[color] += 1;
|
||||
}
|
||||
})
|
||||
return colorOccurences
|
||||
});
|
||||
return colorOccurences;
|
||||
},
|
||||
winDateUrl(date) {
|
||||
const timestamp = new Date(date).getTime();
|
||||
return `/history/${timestamp}`
|
||||
const dateParameter = dateString(new Date(date));
|
||||
return `/history/${dateParameter}`;
|
||||
},
|
||||
navigateBack() {
|
||||
if (this.previousRoute.default) {
|
||||
@@ -119,9 +133,16 @@ export default {
|
||||
}
|
||||
},
|
||||
humanReadableDate: humanReadableDate,
|
||||
daysAgo: daysAgo
|
||||
daysAgo(date) {
|
||||
const days = daysAgo(date);
|
||||
if (days == 0) {
|
||||
return "i dag";
|
||||
} else {
|
||||
return `${days} dager siden`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -142,7 +163,7 @@ $elementSpacing: 3rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90vw;
|
||||
width: 90vw;
|
||||
margin: 3rem auto;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 3rem;
|
||||
@@ -233,7 +254,7 @@ h1 {
|
||||
@include tablet {
|
||||
width: calc(100% - 160px - 80px);
|
||||
}
|
||||
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -259,10 +280,9 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.backdrop {
|
||||
$background: rgb(244,244,244);
|
||||
|
||||
$background: rgb(244, 244, 244);
|
||||
|
||||
--padding: 2rem;
|
||||
@include desktop {
|
||||
--padding: 5rem;
|
||||
@@ -270,4 +290,4 @@ h1 {
|
||||
background-color: $background;
|
||||
padding: var(--padding);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,728 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Registrering</h1>
|
||||
<br />
|
||||
<br />
|
||||
<div class="notification-element">
|
||||
<div class="label-div">
|
||||
<label for="notification">Push-melding</label>
|
||||
<textarea
|
||||
id="notification"
|
||||
type="text"
|
||||
rows="3"
|
||||
v-model="pushMessage"
|
||||
placeholder="Push meldingtekst"
|
||||
/>
|
||||
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="sendPush">Send push</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2 id="addwine-title">Prelottery</h2>
|
||||
|
||||
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
|
||||
|
||||
<div class="button-container">
|
||||
<button
|
||||
class="vin-button"
|
||||
@click="showCamera = !showCamera"
|
||||
>{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}</button>
|
||||
|
||||
<button class="vin-button" @click="addWine">Legg til en vin manuelt</button>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length > 0" class="edit-container">
|
||||
<wine v-for="wine in wines" :key="key" :wine="wine">
|
||||
<div class="edit">
|
||||
<div class="button-container row">
|
||||
<button
|
||||
class="vin-button"
|
||||
@click="editWine = amIBeingEdited(wine) ? false : wine"
|
||||
>{{ amIBeingEdited(wine) ? "Lukk" : "Rediger" }}</button>
|
||||
<button class="red vin-button" @click="deleteWine(wine)">Slett</button>
|
||||
</div>
|
||||
|
||||
<div v-if="amIBeingEdited(wine)" class="wine-edit">
|
||||
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="wine[key]" :placeholder="key" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</wine>
|
||||
</div>
|
||||
<div class="button-container" v-if="wines.length > 0">
|
||||
<button class="vin-button" @click="sendWines">Send inn viner</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Lottery</h2>
|
||||
|
||||
<h3>Legg til lodd kjøpt</h3>
|
||||
<div class="colors">
|
||||
<div v-for="color in lotteryColors" :class="color.css + ' colors-box'" :key="color">
|
||||
<div class="colors-overlay">
|
||||
<p>{{ color.name }} kjøpt</p>
|
||||
<input v-model="color.value" min="0" :placeholder="0" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="label-div">
|
||||
<label>Totalt kjøpt for:</label>
|
||||
<input v-model="payed" placeholder="NOK" type="number" :step="price || 1" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="submitLottery">Send inn lotteri</button>
|
||||
</div>
|
||||
|
||||
<h3>Vinnere</h3>
|
||||
<a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a>
|
||||
<div class="winner-container" v-if="winners.length > 0">
|
||||
<wine v-for="winner in winners" :key="winner" :wine="winner.wine">
|
||||
<div class="winner-element">
|
||||
<div class="color-selector">
|
||||
<div class="label-div">
|
||||
<label>Farge vunnet</label>
|
||||
</div>
|
||||
<button
|
||||
class="blue"
|
||||
:class="{ active: winner.color == 'blue' }"
|
||||
@click="winner.color = 'blue'"
|
||||
></button>
|
||||
<button
|
||||
class="red"
|
||||
:class="{ active: winner.color == 'red' }"
|
||||
@click="winner.color = 'red'"
|
||||
></button>
|
||||
<button
|
||||
class="green"
|
||||
:class="{ active: winner.color == 'green' }"
|
||||
@click="winner.color = 'green'"
|
||||
></button>
|
||||
<button
|
||||
class="yellow"
|
||||
:class="{ active: winner.color == 'yellow' }"
|
||||
@click="winner.color = 'yellow'"
|
||||
></button>
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="winner-name">Navn vinner</label>
|
||||
<input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="potential-winner-name">Virtuelle vinnere</label>
|
||||
<select
|
||||
id="potential-winner-name"
|
||||
type="text"
|
||||
placeholder="Navn"
|
||||
v-model="winner.potentialWinner"
|
||||
@change="potentialChange($event, winner)"
|
||||
>
|
||||
<option
|
||||
v-for="fetchedWinner in fetchedWinners"
|
||||
:value="stringify(fetchedWinner)"
|
||||
>{{fetchedWinner.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</wine>
|
||||
|
||||
<div class="button-container column">
|
||||
<button class="vin-button" @click="submitLotteryWinners">Send inn vinnere</button>
|
||||
<button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventBus from "@/mixins/EventBus";
|
||||
import { dateString } from '@/utils'
|
||||
import {
|
||||
prelottery,
|
||||
sendLotteryWinners,
|
||||
sendLottery,
|
||||
logWines,
|
||||
wineSchema,
|
||||
winnersSecure,
|
||||
attendees
|
||||
} from "@/api";
|
||||
import TextToast from "@/ui/TextToast";
|
||||
import Wine from "@/ui/Wine";
|
||||
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
||||
|
||||
export default {
|
||||
components: { TextToast, Wine, ScanToVinmonopolet },
|
||||
data() {
|
||||
return {
|
||||
payed: undefined,
|
||||
winners: [],
|
||||
fetchedWinners: [],
|
||||
wines: [],
|
||||
pushMessage: "",
|
||||
pushLink: "/",
|
||||
toastText: undefined,
|
||||
showToast: false,
|
||||
showCamera: false,
|
||||
editWine: false,
|
||||
lotteryColors: [
|
||||
{ value: null, name: "Blå", css: "blue" },
|
||||
{ value: null, name: "Rød", css: "red" },
|
||||
{ value: null, name: "Grønn", css: "green" },
|
||||
{ value: null, name: "Gul", css: "yellow" }
|
||||
],
|
||||
price: __PRICE__
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchAndAddPrelotteryWines().then(this.getWinnerdataFromStorage);
|
||||
|
||||
window.addEventListener("unload", this.setWinnerdataToStorage);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.setWinnerdataToStorage();
|
||||
eventBus.$off("tab-change", () => {
|
||||
this.fetchColorsAndWinners();
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.fetchColorsAndWinners();
|
||||
|
||||
eventBus.$on("tab-change", () => {
|
||||
this.fetchColorsAndWinners();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
stringify(json) {
|
||||
return JSON.stringify(json);
|
||||
},
|
||||
potentialChange(event, winner) {
|
||||
let data = JSON.parse(event.target.value);
|
||||
winner.name = data.name;
|
||||
winner.color = data.color;
|
||||
},
|
||||
async fetchColorsAndWinners() {
|
||||
let winners = await winnersSecure();
|
||||
let _attendees = await attendees();
|
||||
let colors = {
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0
|
||||
};
|
||||
this.payed = 0;
|
||||
for (let i = 0; i < _attendees.length; i++) {
|
||||
let attendee = _attendees[i];
|
||||
colors.red += attendee.red;
|
||||
colors.blue += attendee.blue;
|
||||
colors.green += attendee.green;
|
||||
colors.yellow += attendee.yellow;
|
||||
this.payed +=
|
||||
(attendee.red + attendee.blue + attendee.green + attendee.yellow) *
|
||||
10;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.lotteryColors.length; i++) {
|
||||
let currentColor = this.lotteryColors[i];
|
||||
switch (currentColor.css) {
|
||||
case "red":
|
||||
currentColor.value = colors.red;
|
||||
break;
|
||||
case "blue":
|
||||
currentColor.value = colors.blue;
|
||||
break;
|
||||
a;
|
||||
case "green":
|
||||
currentColor.value = colors.green;
|
||||
break;
|
||||
case "yellow":
|
||||
currentColor.value = colors.yellow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.fetchedWinners = winners;
|
||||
},
|
||||
amIBeingEdited(wine) {
|
||||
return this.editWine.id == wine.id && this.editWine.name == wine.name;
|
||||
},
|
||||
async fetchAndAddPrelotteryWines() {
|
||||
const wines = await prelottery();
|
||||
|
||||
for (let i = 0; i < wines.length; i++) {
|
||||
let wine = wines[i];
|
||||
this.winners.push({
|
||||
name: "",
|
||||
color: "",
|
||||
potentialWinner: "",
|
||||
wine: {
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
id: wine.id
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
wineFromVinmonopoletScan(wineResponse) {
|
||||
if (this.wines.map(wine => wine.name).includes(wineResponse.name)) {
|
||||
this.toastText = "Vinen er allerede lagt til.";
|
||||
this.showToast = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastText = "Fant og la til vin:<br>" + wineResponse.name;
|
||||
this.showToast = true;
|
||||
|
||||
this.wines.unshift(wineResponse);
|
||||
},
|
||||
sendPush: async function() {
|
||||
let _response = await fetch("/subscription/send-notification", {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message: this.pushMessage, link: this.pushLink })
|
||||
});
|
||||
let response = await _response.json();
|
||||
if (response) {
|
||||
alert("Sendt!");
|
||||
} else {
|
||||
alert("Noe gikk galt!");
|
||||
}
|
||||
},
|
||||
addWine: async function(event) {
|
||||
const wine = await wineSchema();
|
||||
|
||||
this.editWine = wine;
|
||||
this.wines.unshift(wine);
|
||||
},
|
||||
deleteWine(deletedWine) {
|
||||
this.wines = this.wines.filter(wine => wine.name != deletedWine.name);
|
||||
},
|
||||
sendWines: async function() {
|
||||
let response = await logWines(this.wines);
|
||||
if (response.success == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("Noe gikk galt under innsending");
|
||||
}
|
||||
},
|
||||
addWinner: function(event) {
|
||||
this.winners.push({
|
||||
name: "",
|
||||
color: "",
|
||||
wine: {
|
||||
name: "",
|
||||
vivinoLink: "",
|
||||
rating: ""
|
||||
}
|
||||
});
|
||||
},
|
||||
submitLottery: async function(event) {
|
||||
const colors = {
|
||||
red: this.lotteryColors.filter(c => c.css == "red")[0].value,
|
||||
green: this.lotteryColors.filter(c => c.css == "green")[0].value,
|
||||
blue: this.lotteryColors.filter(c => c.css == "blue")[0].value,
|
||||
yellow: this.lotteryColors.filter(c => c.css == "yellow")[0].value
|
||||
};
|
||||
|
||||
let sendObject = {
|
||||
lottery: {
|
||||
date: dateString(new Date()),
|
||||
...colors
|
||||
}
|
||||
};
|
||||
|
||||
if (sendObject.lottery.red == undefined) {
|
||||
alert("Rød må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.green == undefined) {
|
||||
alert("Grønn må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.yellow == undefined) {
|
||||
alert("Gul må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.blue == undefined) {
|
||||
alert("Blå må defineres");
|
||||
return;
|
||||
}
|
||||
|
||||
sendObject.lottery.bought =
|
||||
parseInt(colors.blue) +
|
||||
parseInt(colors.red) +
|
||||
parseInt(colors.green) +
|
||||
parseInt(colors.yellow);
|
||||
const stolen = sendObject.lottery.bought - parseInt(this.payed) / 10;
|
||||
if (isNaN(stolen) || stolen == undefined) {
|
||||
alert("Betalt må registreres");
|
||||
return;
|
||||
}
|
||||
sendObject.lottery.stolen = stolen;
|
||||
|
||||
let response = await sendLottery(sendObject);
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.message || "Noe gikk galt under innsending");
|
||||
}
|
||||
},
|
||||
submitLotteryWinners: async function(event) {
|
||||
let sendObject = {
|
||||
lottery: {
|
||||
date: dateString(new Date()),
|
||||
winners: this.winners
|
||||
}
|
||||
}
|
||||
|
||||
if (sendObject.lottery.winners.length == 0) {
|
||||
alert("Det må være med vinnere");
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < sendObject.lottery.winners.length; i++) {
|
||||
let currentWinner = sendObject.lottery.winners[i];
|
||||
|
||||
if (currentWinner.name == undefined || currentWinner.name == "") {
|
||||
alert("Navn må defineres");
|
||||
return;
|
||||
}
|
||||
if (currentWinner.color == undefined || currentWinner.color == "") {
|
||||
alert("Farge må defineres");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let response = await sendLotteryWinners(sendObject);
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.message || "Noe gikk galt under innsending");
|
||||
}
|
||||
},
|
||||
getWinnerdataFromStorage() {
|
||||
let localWinners = localStorage.getItem("winners");
|
||||
if (localWinners && this.winners.length) {
|
||||
localWinners = JSON.parse(localWinners);
|
||||
|
||||
this.winners = this.winners.map(winner => {
|
||||
const localWinnerMatch = localWinners.filter(
|
||||
localWinner =>
|
||||
localWinner.wine.name == winner.wine.name ||
|
||||
localWinner.wine.id == winner.wine.id
|
||||
);
|
||||
|
||||
if (localWinnerMatch.length > 0) {
|
||||
winner.name = localWinnerMatch[0].name || winner.name;
|
||||
winner.color = localWinnerMatch[0].color || winner.name;
|
||||
}
|
||||
|
||||
return winner;
|
||||
});
|
||||
}
|
||||
|
||||
let localColors = localStorage.getItem("colorValues");
|
||||
if (localColors) {
|
||||
localColors = localColors.split(",");
|
||||
this.lotteryColors.forEach((color, i) => {
|
||||
const localColorValue = Number(localColors[i]);
|
||||
color.value = localColorValue == 0 ? null : localColorValue;
|
||||
});
|
||||
}
|
||||
},
|
||||
setWinnerdataToStorage() {
|
||||
localStorage.setItem("winners", JSON.stringify(this.winners));
|
||||
localStorage.setItem(
|
||||
"colorValues",
|
||||
this.lotteryColors.map(color => Number(color.value))
|
||||
);
|
||||
window.removeEventListener("unload", this.setWinnerdataToStorage);
|
||||
},
|
||||
resetWinnerDataInStorage() {
|
||||
this.winners = [];
|
||||
this.fetchAndAddPrelotteryWines().then(resp => (this.winners = resp));
|
||||
this.lotteryColors.map(color => (color.value = null));
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
select {
|
||||
margin: 0 0 auto;
|
||||
height: 2rem;
|
||||
min-width: 0;
|
||||
width: 98%;
|
||||
padding: 1%;
|
||||
}
|
||||
h1 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
.wine-link {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid $link-color;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 90%;
|
||||
margin: 2rem auto;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.winner-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.button-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.edit-container {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .wine {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
.edit {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-element {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.winner-element {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.wine-element {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.generate-link {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.wine-edit {
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
label {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.color-selector {
|
||||
margin-bottom: 0.65rem;
|
||||
margin-right: 1rem;
|
||||
|
||||
@include desktop {
|
||||
min-width: 175px;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
max-width: 25vw;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 2px solid unset;
|
||||
|
||||
&.green {
|
||||
border-color: $green;
|
||||
}
|
||||
&.blue {
|
||||
border-color: $dark-blue;
|
||||
}
|
||||
&.red {
|
||||
border-color: $red;
|
||||
}
|
||||
&.yellow {
|
||||
border-color: $dark-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: 2px solid transparent;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
|
||||
// disable-dbl-tap-zoom
|
||||
touch-action: manipulation;
|
||||
|
||||
@include mobile {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #c8f9df;
|
||||
}
|
||||
&.blue {
|
||||
background: #d4f2fe;
|
||||
}
|
||||
&.red {
|
||||
background: #fbd7de;
|
||||
}
|
||||
&.yellow {
|
||||
background: #fff6d6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
margin: 3rem auto 1rem;
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
}
|
||||
|
||||
.label-div {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-box {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
@include mobile {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
position: absolute;
|
||||
top: 0.4rem;
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 70%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font-size: 3rem;
|
||||
height: unset;
|
||||
max-height: unset;
|
||||
position: absolute;
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.green,
|
||||
.green .colors-overlay > input {
|
||||
background-color: $light-green;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.blue,
|
||||
.blue .colors-overlay > input {
|
||||
background-color: $light-blue;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.yellow,
|
||||
.yellow .colors-overlay > input {
|
||||
background-color: $light-yellow;
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
.red,
|
||||
.red .colors-overlay > input {
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,29 @@
|
||||
<template>
|
||||
<section class="main-container">
|
||||
<Modal
|
||||
v-if="showModal"
|
||||
modalText="Ønsket ditt har blitt lagt til"
|
||||
<Modal
|
||||
v-if="showModal"
|
||||
modalText="Ønsket ditt har blitt lagt til"
|
||||
:buttons="modalButtons"
|
||||
@click="emitFromModalButton"
|
||||
></Modal>
|
||||
<h1>
|
||||
Foreslå en vin!
|
||||
</h1>
|
||||
|
||||
<section class="search-container">
|
||||
<section class="search-section">
|
||||
<input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷" class="search-input-field">
|
||||
<button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button>
|
||||
</section>
|
||||
<section v-for="(wine, index) in this.wines" :key="index" class="single-result">
|
||||
<img
|
||||
v-if="wine.image"
|
||||
:src="wine.image"
|
||||
class="wine-image"
|
||||
:class="{ 'fullscreen': fullscreen }"
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchString"
|
||||
@keyup.enter="searchWines()"
|
||||
placeholder="Søk etter en vin du liker her!🍷"
|
||||
class="search-input-field"
|
||||
/>
|
||||
<button :disabled="!searchString" @click="searchWines()" class="vin-button">Søk</button>
|
||||
</section>
|
||||
|
||||
<section v-for="(wine, index) in wines" :key="index" class="single-result">
|
||||
<img v-if="wine.image" :src="wine.image" class="wine-image" :class="{ fullscreen: fullscreen }" />
|
||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||
<section class="wine-info">
|
||||
<h2 v-if="wine.name">{{ wine.name }}</h2>
|
||||
@@ -29,37 +32,38 @@
|
||||
<span v-if="wine.rating">{{ wine.rating }}%</span>
|
||||
<span v-if="wine.price">{{ wine.price }} NOK</span>
|
||||
<span v-if="wine.country">{{ wine.country }}</span>
|
||||
<span v-if="wine.year && wine.year !== '0000'">{{ wine.year }}</span>
|
||||
</div>
|
||||
</section>
|
||||
<button class="vin-button" @click="request(wine)">Foreslå denne</button>
|
||||
<a
|
||||
v-if="wine.vivinoLink"
|
||||
:href="wine.vivinoLink"
|
||||
class="wine-link"
|
||||
>Les mer</a>
|
||||
<button class="vin-button" @click="requestWine(wine)">Foreslå denne</button>
|
||||
<a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="wine-link">Les mer</a>
|
||||
</section>
|
||||
<p v-if="this.wines && this.wines.length == 0">
|
||||
<p v-if="loading == false && wines && wines.length == 0">
|
||||
Fant ingen viner med det navnet!
|
||||
</p>
|
||||
<p v-else-if="loading">Loading...</p>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { searchForWine, requestNewWine } from "@/api";
|
||||
import { searchForWine } from "@/api";
|
||||
import Wine from "@/ui/Wine";
|
||||
import Modal from "@/ui/Modal";
|
||||
import RequestedWineCard from "@/ui/RequestedWineCard";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Wine,
|
||||
Modal
|
||||
Modal,
|
||||
RequestedWineCard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchString: undefined,
|
||||
wines: undefined,
|
||||
showModal: false,
|
||||
loading: false,
|
||||
modalButtons: [
|
||||
{
|
||||
text: "Legg til flere viner",
|
||||
@@ -70,30 +74,59 @@ export default {
|
||||
action: "move"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetchWineFromVin(){
|
||||
if(this.searchString){
|
||||
this.wines = []
|
||||
let localSearchString = this.searchString.replace(/ /g,"_");
|
||||
searchForWine(localSearchString)
|
||||
.then(res => this.wines = res)
|
||||
fetchWinesByQuery(query) {
|
||||
let url = new URL("/api/vinmonopolet/wine/search", window.location);
|
||||
url.searchParams.set("name", query);
|
||||
|
||||
this.wines = [];
|
||||
this.loading = true;
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines))
|
||||
.finally(wines => (this.loading = false));
|
||||
},
|
||||
searchWines() {
|
||||
if (this.searchString) {
|
||||
let localSearchString = this.searchString.replace(/ /g, "_");
|
||||
this.fetchWinesByQuery(localSearchString);
|
||||
}
|
||||
},
|
||||
request(wine){
|
||||
requestNewWine(wine)
|
||||
.then(() => this.showModal = true)
|
||||
requestWine(wine) {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wine: wine })
|
||||
};
|
||||
|
||||
return fetch("/api/request", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.showModal = true;
|
||||
this.$toast.info({
|
||||
title: `Vinen ${wine.name} har blitt foreslått!`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Obs, her oppsto det en feil! Feilen er logget.",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
emitFromModalButton(action){
|
||||
if(action == "stay"){
|
||||
this.showModal = false
|
||||
emitFromModalButton(action) {
|
||||
if (action == "stay") {
|
||||
this.showModal = false;
|
||||
} else {
|
||||
this.$router.push("/requested-wines");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -101,12 +134,11 @@ export default {
|
||||
@import "@/styles/global";
|
||||
@import "@/styles/variables";
|
||||
|
||||
|
||||
h1{
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-container{
|
||||
.main-container {
|
||||
margin: auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
@@ -120,66 +152,63 @@ input[type="text"] {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
|
||||
.search-container{
|
||||
.search-container {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.search-section{
|
||||
.search-section {
|
||||
display: grid;
|
||||
grid: 1fr / 1fr .2fr;
|
||||
grid: 1fr / 1fr 0.2fr;
|
||||
|
||||
@include mobile{
|
||||
.vin-button{
|
||||
@include mobile {
|
||||
.vin-button {
|
||||
display: none;
|
||||
}
|
||||
.search-input-field{
|
||||
.search-input-field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.single-result{
|
||||
.single-result {
|
||||
margin-top: 1rem;
|
||||
display: grid;
|
||||
grid: 1fr / .5fr 2fr .5fr .5fr;
|
||||
grid: 1fr / 0.5fr 2fr 0.5fr 0.5fr;
|
||||
grid-template-areas: "picture details button-left button-right";
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
grid-gap: 1em;
|
||||
padding-bottom: 1em;
|
||||
margin-bottom: 1em;
|
||||
box-shadow: 0 1px 0 0 rgba(0,0,0,0.2);
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2);
|
||||
|
||||
@include mobile{
|
||||
@include mobile {
|
||||
grid: 1fr 0.5fr / 0.5fr 1fr;
|
||||
grid-template-areas:
|
||||
"picture details"
|
||||
"button-left button-right";
|
||||
grid-gap: 0.5em;
|
||||
|
||||
grid: 1fr .5fr / .5fr 1fr;
|
||||
grid-template-areas: "picture details"
|
||||
"button-left button-right";
|
||||
grid-gap: .5em;
|
||||
|
||||
.vin-button{
|
||||
.vin-button {
|
||||
grid-area: button-right;
|
||||
padding: .5em;
|
||||
padding: 0.5em;
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.wine-link{
|
||||
.wine-link {
|
||||
grid-area: button-left;
|
||||
}
|
||||
|
||||
h2{
|
||||
h2 {
|
||||
font-size: 1em;
|
||||
max-width: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.wine-image {
|
||||
height: 100px;
|
||||
@@ -192,14 +221,14 @@ input[type="text"] {
|
||||
grid-area: picture;
|
||||
}
|
||||
|
||||
.wine-info{
|
||||
.wine-info {
|
||||
grid-area: details;
|
||||
width: 100%;
|
||||
|
||||
h2{
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
.details{
|
||||
.details {
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -216,22 +245,20 @@ input[type="text"] {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.vin-button{
|
||||
.vin-button {
|
||||
grid-area: button-right;
|
||||
}
|
||||
|
||||
@include tablet{
|
||||
h2{
|
||||
|
||||
@include tablet {
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
@include desktop{
|
||||
h2{
|
||||
|
||||
@include desktop {
|
||||
h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
207
frontend/components/SalgsbetingelserPage.vue
Normal file
207
frontend/components/SalgsbetingelserPage.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Slagsbetingelser</h1>
|
||||
<section class="chapter cf" id="chapter-1">
|
||||
<h2 class="h2-title">Innledning</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dette kjøpet er regulert av de nedenstående standard salgsbetingelser for forbrukerkjøp av varer over Internett. Forbrukerkjøp over internett reguleres hovedsakelig av avtaleloven, forbrukerkjøpsloven, markedsføringsloven, angrerettloven og ehandelsloven, og disse lovene gir forbrukeren ufravikelige rettigheter. Lovene er tilgjengelig på
|
||||
<a target="_blank" class="vin-link" href="http://www.lovdata.no/" rel="noopener">www.lovdata.no.</a>
|
||||
Vilkårene i denne avtalen skal ikke forstås som noen begrensning i de lovbestemte rettighetene, men oppstiller partenes viktigste rettigheter og plikter for handelen.
|
||||
</p>
|
||||
<p>
|
||||
Salgsbetingelsene er utarbeidet og anbefalt av Forbrukertilsynet.
|
||||
<a class="vin-link" href="https://forbrukertilsynet.no/lov-og-rett/veiledninger-og-retningslinjer/veiledning-standard-salgsbetingelser-forbrukerkjop-varer-internett">For en bedre forståelse av disse salgsbetingelsene, se Forbrukertilsynets veileder her. </a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-2">
|
||||
<h2 class="h2-title">1. Avtalen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Avtalen består av disse salgsbetingelsene, opplysninger gitt i bestillingsløsningen og eventuelt særskilt avtalte vilkår. Ved eventuell motstrid mellom opplysningene, går det som særskilt er avtalt mellom partene foran, så fremt det ikke strider mot ufravikelig lovgivning.</p>
|
||||
<p>Avtalen vil i tillegg bli utfylt av relevante lovbestemmelser som regulerer kjøp av varer mellom næringsdrivende og forbrukere.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-3">
|
||||
<h2 class="h2-title">2. Partene</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Selger er Kevin Midbøe, Schleppegrells gate 18, questions@vinlottis.no/kevin.midboe@gmail.com, 926432478, og betegnes i det følgende som selger/selgeren.</p>
|
||||
<p>Kjøper er den forbrukeren som foretar bestillingen, og betegnes i det følgende som kjøper/kjøperen.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-4">
|
||||
<h2 class="h2-title">3. Pris</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Den oppgitte prisen for varen og tjenester er den totale prisen kjøper skal betale. Denne prisen inkluderer alle avgifter og tilleggskostnader. Ytterligere kostnader som selger før kjøpet ikke har informert om, skal kjøper ikke bære.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-5">
|
||||
<h2 class="h2-title">4. Avtaleinngåelse</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Avtalen er bindende for begge parter når kjøperen har sendt sin bestilling til selgeren.</p>
|
||||
<p>Avtalen er likevel ikke bindende hvis det har forekommet skrive- eller tastefeil i tilbudet fra selgeren i bestillingsløsningen i nettbutikken eller i kjøperens bestilling, og den annen part innså eller burde ha innsett at det forelå en slik feil.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-6">
|
||||
<h2 class="h2-title">5. Betalingen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Selgeren kan kreve betaling for varen fra det tidspunkt den blir sendt fra selgeren til kjøperen.</p>
|
||||
<p>Dersom kjøperen bruker kredittkort eller debetkort ved betaling, kan selgeren reservere kjøpesummen på kortet ved bestilling. Kortet blir belastet samme dag som varen sendes.</p>
|
||||
<p>Ved betaling med faktura, blir fakturaen til kjøperen utstedt ved forsendelse av varen. Betalingsfristen fremgår av fakturaen og er på minimum 14 dager fra mottak.</p>
|
||||
<p>Kjøpere under 18 år kan ikke betale med etterfølgende faktura.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-7">
|
||||
<h2 class="h2-title">6. Levering</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Levering er skjedd når kjøperen, eller hans representant, har overtatt tingen.</p>
|
||||
<p>Hvis ikke leveringstidspunkt fremgår av bestillingsløsningen, skal selgeren levere varen til kjøper uten unødig opphold og senest 30 dager etter bestillingen fra kunden. Varen skal leveres hos kjøperen med mindre annet er særskilt avtalt mellom partene.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-8">
|
||||
<h2 class="h2-title">7. Risikoen for varen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Risikoen for varen går over på kjøper når han, eller hans representant, har fått varene levert i tråd med punkt 6.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-9">
|
||||
<h2 class="h2-title">8. Angrerett</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Med mindre avtalen er unntatt fra angrerett, kan kjøperen angre kjøpet av varen i henhold til angrerettloven.</p>
|
||||
<p>Kjøperen må gi selger melding om bruk av angreretten innen 14 dager fra fristen begynner å løpe. I fristen inkluderes alle kalenderdager. Dersom fristen ender på en lørdag, helligdag eller høytidsdag forlenges fristen til nærmeste virkedag.</p>
|
||||
<p>Angrefristen anses overholdt dersom melding er sendt før utløpet av fristen. Kjøper har bevisbyrden for at angreretten er blitt gjort gjeldende, og meldingen bør derfor skje skriftlig (angrerettskjema, e-post eller brev).</p>
|
||||
<p>Angrefristen begynner å løpe:</p>
|
||||
<ul>
|
||||
<li>Ved kjøp av enkeltstående varer vil angrefristen løpe fra dagen etter varen(e) er mottatt.</li>
|
||||
<li>Selges et abonnement, eller innebærer avtalen regelmessig levering av identiske varer, løper fristen fra dagen etter første forsendelse er mottatt.</li>
|
||||
<li>Består kjøpet av flere leveranser, vil angrefristen løpe fra dagen etter siste leveranse er mottatt.</li>
|
||||
</ul>
|
||||
<p>Angrefristen utvides til 12 måneder etter utløpet av den opprinnelige fristen dersom selger ikke før avtaleinngåelsen opplyser om at det foreligger angrerett og standardisert angreskjema. Tilsvarende gjelder ved manglende opplysning om vilkår, tidsfrister og fremgangsmåte for å benytte angreretten. Sørger den næringsdrivende for å gi opplysningene i løpet av disse 12 månedene, utløper angrefristen likevel 14 dager etter den dagen kjøperen mottok opplysningene.</p>
|
||||
<p>Ved bruk av angreretten må varen leveres tilbake til selgeren uten unødig opphold og senest 14 dager fra melding om bruk av angreretten er gitt. Kjøper dekker de direkte kostnadene ved å returnere varen, med mindre annet er avtalt eller selger har unnlatt å opplyse om at kjøper skal dekke returkostnadene. Selgeren kan ikke fastsette gebyr for kjøperens bruk av angreretten.</p>
|
||||
<p>Kjøper kan prøve eller teste varen på en forsvarlig måte for å fastslå varens art, egenskaper og funksjon, uten at angreretten faller bort. Dersom prøving eller test av varen går utover hva som er forsvarlig og nødvendig, kan kjøperen bli ansvarlig for eventuell redusert verdi på varen.</p>
|
||||
<p>Selgeren er forpliktet til å tilbakebetale kjøpesummen til kjøperen uten unødig opphold, og senest 14 dager fra selgeren fikk melding om kjøperens beslutning om å benytte angreretten. Selger har rett til å holde tilbake betalingen til han har mottatt varene fra kjøperen, eller til kjøper har lagt frem dokumentasjon for at varene er sendt tilbake.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-10">
|
||||
<h2 class="h2-title">9. Forsinkelse og manglende levering - kjøpernes rettigheter og frist for å melde krav</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dersom selgeren ikke leverer varen eller leverer den for sent i henhold til avtalen mellom partene, og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold til reglene i forbrukerkjøpslovens kapittel 5 etter omstendighetene
|
||||
<em>holde kjøpesummen tilbake</em>
|
||||
, kreve
|
||||
<em>oppfyl</em>
|
||||
<em>lelse</em>
|
||||
,
|
||||
<em>heve </em>
|
||||
avtalen og/eller kreve
|
||||
<em>erstatning </em>
|
||||
fra selgeren.
|
||||
</p>
|
||||
<p>Ved krav om misligholdsbeføyelser bør meldingen av bevishensyn være skriftlig (for eksempel e-post).</p>
|
||||
<h3>Oppfyllelse</h3>
|
||||
<p>Kjøper kan fastholde kjøpet og kreve oppfyllelse fra selger. Kjøper kan imidlertid ikke kreve oppfyllelse dersom det foreligger en hindring som selgeren ikke kan overvinne, eller dersom oppfyllelse vil medføre en så stor ulempe eller kostnad for selger at det står i vesentlig misforhold til kjøperens interesse i at selgeren oppfyller. Skulle vanskene falle bort innen rimelig tid, kan kjøper likevel kreve oppfyllelse.</p>
|
||||
<p>Kjøperen taper sin rett til å kreve oppfyllelse om han eller hun venter urimelig lenge med å fremme kravet.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Dersom selgeren ikke leverer varen på leveringstidspunktet, skal kjøperen oppfordre selger til å levere innen en rimelig tilleggsfrist for oppfyllelse. Dersom selger ikke leverer varen innen tilleggsfristen, kan kjøperen heve kjøpet.</p>
|
||||
<p>Kjøper kan imidlertid heve kjøpet umiddelbart hvis selger nekter å levere varen. Tilsvarende gjelder dersom levering til avtalt tid var avgjørende for inngåelsen av avtalen, eller dersom kjøperen har underrettet selger om at leveringstidspunktet er avgjørende.</p>
|
||||
<p>Leveres tingen etter tilleggsfristen forbrukeren har satt eller etter leveringstidspunktet som var avgjørende for inngåelsen av avtalen, må krav om heving gjøres gjeldende innen rimelig tid etter at kjøperen fikk vite om leveringen.</p>
|
||||
<h3>Erstatning</h3>
|
||||
<p>Kjøperen kan kreve erstatning for lidt tap som følge av forsinkelsen. Dette gjelder imidlertid ikke dersom selgeren godtgjør at forsinkelsen skyldes hindring utenfor selgers kontroll som ikke med rimelighet kunne blitt tatt i betraktning på avtaletiden, unngått, eller overvunnet følgene av.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-11">
|
||||
<h2 class="h2-title">10. Mangel ved varen - kjøperens rettigheter og reklamasjonsfrist</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Hvis det foreligger en mangel ved varen må kjøper innen rimelig tid etter at den ble oppdaget eller burde ha blitt oppdaget, gi selger melding om at han eller hun vil påberope seg mangelen. Kjøper har alltid reklamert tidsnok dersom det skjer innen 2 mnd. fra mangelen ble oppdaget eller burde blitt oppdaget. Reklamasjon kan skje senest to år etter at kjøper overtok varen. Dersom varen eller deler av den er ment å vare vesentlig lenger enn to år, er reklamasjonsfristen fem år.</p>
|
||||
<p>
|
||||
Dersom varen har en mangel og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold til reglene i forbrukerkjøpsloven kapittel 6 etter omstendighetene
|
||||
<em>holde kjøpesummen tilbake</em>
|
||||
, velge mellom
|
||||
<em>retting </em>
|
||||
og
|
||||
<em>omlevering</em>
|
||||
, kreve
|
||||
<em>prisavslag</em>
|
||||
, kreve avtalen hevet og/eller kreve
|
||||
<em>erstatning </em>
|
||||
fra selgeren.
|
||||
</p>
|
||||
<p>Reklamasjon til selgeren bør skje skriftlig.</p>
|
||||
<h3>Retting eller omlevering</h3>
|
||||
<p>Kjøperen kan velge mellom å kreve mangelen rettet eller levering av tilsvarende ting. Selger kan likevel motsette seg kjøperens krav dersom gjennomføringen av kravet er umulig eller volder selgeren urimelige kostnader. Retting eller omlevering skal foretas innen rimelig tid. Selger har i utgangspunktet ikke rett til å foreta mer enn to avhjelpsforsøk for samme mangel.</p>
|
||||
<h3>Prisavslag</h3>
|
||||
<p>Kjøper kan kreve et passende prisavslag dersom varen ikke blir rettet eller omlevert. Dette innebærer at forholdet mellom nedsatt og avtalt pris svarer til forholdet mellom tingens verdi i mangelfull og kontraktsmessig stand. Dersom særlige grunner taler for det, kan prisavslaget i stedet settes lik mangelens betydning for kjøperen.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Dersom varen ikke er rettet eller omlevert, kan kjøperen også heve kjøpet når mangelen ikke er uvesentlig.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-12">
|
||||
<h2 class="h2-title">11. Selgerens rettigheter ved kjøperens mislighold</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dersom kjøperen ikke betaler eller oppfyller de øvrige pliktene etter avtalen eller loven, og dette ikke skyldes selgeren eller forhold på selgerens side, kan selgeren i henhold til reglene i forbrukerkjøpsloven kapittel 9 etter omstendighetene
|
||||
<em>holde</em>
|
||||
<em>varen tilbake</em>
|
||||
, kreve
|
||||
<em>oppfyllelse </em>
|
||||
av avtalen, kreve avtalen
|
||||
<em>hevet </em>
|
||||
samt kreve
|
||||
<em>erstatning </em>
|
||||
fra kjøperen. Selgeren vil også etter omstendighetene kunne kreve
|
||||
<em>renter ved forsinket betaling, inkassogebyr</em>
|
||||
og et rimelig
|
||||
<em>gebyr ved uavhentede varer</em>
|
||||
.
|
||||
</p>
|
||||
<h3>Oppfyllelse</h3>
|
||||
<p>Selger kan fastholde kjøpet og kreve at kjøperen betaler kjøpesummen. Er varen ikke levert, taper selgeren sin rett dersom han venter urimelig lenge med å fremme kravet.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Selger kan heve avtalen dersom det foreligger vesentlig betalingsmislighold eller annet vesentlig mislighold fra kjøperens side. Selger kan likevel ikke heve dersom hele kjøpesummen er betalt. Fastsetter selger en rimelig tilleggsfrist for oppfyllelse og kjøperen ikke betaler innen denne fristen, kan selger heve kjøpet.</p>
|
||||
<h3>Renter ved forsinket betaling/inkassogebyr</h3>
|
||||
<p>Dersom kjøperen ikke betaler kjøpesummen i henhold til avtalen, kan selger kreve renter av kjøpesummen etter forsinkelsesrenteloven. Ved manglende betaling kan kravet, etter forutgående varsel, bli sendt til Kjøper kan da bli holdt ansvarlig for gebyr etter inkassoloven.</p>
|
||||
<h3>Gebyr ved uavhentede ikke-forskuddsbetalte varer</h3>
|
||||
<p>Dersom kjøperen unnlater å hente ubetalte varer, kan selger belaste kjøper med et gebyr. Gebyret skal maksimalt dekke selgerens faktiske utlegg for å levere varen til kjøperen. Et slikt gebyr kan ikke belastes kjøpere under 18 år.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-13">
|
||||
<h2 class="h2-title">12. Garanti</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Garanti som gis av selgeren eller produsenten, gir kjøperen rettigheter i tillegg til de kjøperen allerede har etter ufravikelig lovgivning. En garanti innebærer dermed ingen begrensninger i kjøperens rett til reklamasjon og krav ved forsinkelse eller mangler etter punkt 9 og 10.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-14">
|
||||
<h2 class="h2-title">13. Personopplysninger</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Behandlingsansvarlig for innsamlede personopplysninger er selger. Med mindre kjøperen samtykker til noe annet, kan selgeren, i tråd med personopplysningsloven, kun innhente og lagre de personopplysninger som er nødvendig for at selgeren skal kunne gjennomføre forpliktelsene etter avtalen. Kjøperens personopplysninger vil kun bli utlevert til andre hvis det er nødvendig for at selger skal få gjennomført avtalen med kjøperen, eller i lovbestemte tilfelle.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-15">
|
||||
<h2 class="h2-title">14. Konfliktløsning</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Klager rettes til selger innen rimelig tid, jf. punkt 9 og 10. Partene skal forsøke å løse eventuelle tvister i minnelighet. Dersom dette ikke lykkes, kan kjøperen ta kontakt med Forbrukerrådet for mekling. Forbrukerrådet er tilgjengelig på telefon 23 400 500 eller
|
||||
<a target="_blank" class="vin-link" href="http://www.forbrukerradet.no/" rel="noopener">www.forbrukerradet.no.</a>
|
||||
</p>
|
||||
<p>
|
||||
Europa-Kommisjonens klageportal kan også brukes hvis du ønsker å inngi en klage. Det er særlig relevant, hvis du er forbruker bosatt i et annet EU-land. Klagen inngis her:
|
||||
<a class="vin-link" href="http://ec.europa.eu/odr">http://ec.europa.eu/odr</a>
|
||||
.
|
||||
</p>
|
||||
<p> </p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.container {
|
||||
margin: 3rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-self: center;
|
||||
width: 80%;
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,9 @@ export default {
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
prelottery().then(wines => this.wines = wines);
|
||||
fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -42,19 +44,18 @@ h1 {
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
margin: 0 2rem;
|
||||
width: 90vw;
|
||||
padding: 5vw;
|
||||
|
||||
@include desktop {
|
||||
width: 80vw;
|
||||
padding: 0 10vw;
|
||||
}
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -65,23 +66,6 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
.inner-wine-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: auto;
|
||||
width: 500px;
|
||||
font-family: Arial;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@include desktop {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<main class="main-container">
|
||||
|
||||
<section class="top-container">
|
||||
|
||||
<div class="want-to-win">
|
||||
<h1>
|
||||
Vil du også vinne?
|
||||
@@ -18,8 +16,8 @@
|
||||
</div>
|
||||
|
||||
<router-link to="/lottery" class="participate-button">
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
<p>Trykk her for å delta</p>
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
<p>Trykk her for å delta</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/generate" class="see-details-link">
|
||||
@@ -38,17 +36,16 @@
|
||||
<i class="icon icon--bottle"></i>
|
||||
<i class="icon icon--bottle"></i>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="content-container">
|
||||
|
||||
<div class="scroll-info">
|
||||
<i class ="icon icon--arrow-long-right"></i>
|
||||
<i class="icon icon--arrow-long-right"></i>
|
||||
<p>Scroll for å se vinnere og annen gøy statistikk</p>
|
||||
</div>
|
||||
|
||||
<Highscore class="highscore"/>
|
||||
<Highscore class="highscore" />
|
||||
|
||||
<TotalBought class="total-bought" />
|
||||
|
||||
<section class="chart-container">
|
||||
@@ -56,12 +53,10 @@
|
||||
<WinGraph class="win" />
|
||||
</section>
|
||||
|
||||
<Wines class="wines-container" />
|
||||
|
||||
<Wines class="wine-container" />
|
||||
</section>
|
||||
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -96,14 +91,14 @@ export default {
|
||||
if (!("PushManager" in window)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
Notification.permission !== "granted" ||
|
||||
!this.pushAllowed ||
|
||||
localStorage.getItem("push") == null
|
||||
);
|
||||
return Notification.permission !== "granted" || !this.pushAllowed || localStorage.getItem("push") == null;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
document.getElementsByClassName("participate-button")[0].classList.add("pulse");
|
||||
}, 1800);
|
||||
|
||||
this.$on("push-allowed", () => {
|
||||
this.pushAllowed = true;
|
||||
});
|
||||
@@ -120,7 +115,7 @@ export default {
|
||||
this.hardStart = way;
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/');
|
||||
window.ga("send", "pageview", "/");
|
||||
},
|
||||
startCountdown() {
|
||||
this.hardStart = true;
|
||||
@@ -130,8 +125,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/animations.scss";
|
||||
|
||||
.top-container {
|
||||
height: 30em;
|
||||
@@ -145,7 +141,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
|
||||
@include mobile{
|
||||
@include mobile {
|
||||
padding-bottom: 2em;
|
||||
height: 15em;
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
@@ -156,13 +152,13 @@ export default {
|
||||
grid-column: 2 / -1;
|
||||
display: flex;
|
||||
|
||||
h1{
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
h1{
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
}
|
||||
grid-row: 2 / 4;
|
||||
@@ -170,7 +166,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.notification-request-button{
|
||||
.notification-request-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -182,18 +178,20 @@ export default {
|
||||
border: 4px solid black;
|
||||
padding: 0 1em 0 1em;
|
||||
display: flex;
|
||||
width: 12.5em;
|
||||
width: 17.5em;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
|
||||
i {
|
||||
color: $link-color;
|
||||
font-size: 1.2rem;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-size: 1.4rem;
|
||||
margin: 1rem;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
@@ -229,7 +227,7 @@ export default {
|
||||
.icons-container {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 7 / -1;
|
||||
@include mobile{
|
||||
@include mobile {
|
||||
margin-top: 2em;
|
||||
display: none;
|
||||
}
|
||||
@@ -239,7 +237,7 @@ export default {
|
||||
grid-column: 7 / -1;
|
||||
}
|
||||
|
||||
@include desktop{
|
||||
@include desktop {
|
||||
grid-row: 4 / -3;
|
||||
grid-column: 7 / 11;
|
||||
}
|
||||
@@ -257,30 +255,27 @@ export default {
|
||||
i {
|
||||
font-size: 5em;
|
||||
|
||||
&.icon--heart-sparks{
|
||||
&.icon--heart-sparks {
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 2 / 4;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
|
||||
}
|
||||
&.icon--face-1{
|
||||
&.icon--face-1 {
|
||||
grid-column: 4 / 7;
|
||||
grid-row: 2 / 4;
|
||||
justify-self: center;
|
||||
|
||||
}
|
||||
&.icon--face-3{
|
||||
&.icon--face-3 {
|
||||
grid-column: 7 / 10;
|
||||
grid-row: 1 / 4;
|
||||
align-self: center;
|
||||
}
|
||||
&.icon--ballon{
|
||||
&.icon--ballon {
|
||||
grid-column: 9 / 11;
|
||||
grid-row: 3 / 5;
|
||||
|
||||
}
|
||||
&.icon--bottle{
|
||||
&.icon--bottle {
|
||||
grid-row: 4 / -1;
|
||||
|
||||
&:nth-of-type(5) {
|
||||
@@ -297,14 +292,13 @@ export default {
|
||||
&:nth-of-type(8) {
|
||||
grid-column: 7 / 8;
|
||||
}
|
||||
&:nth-of-type(9){
|
||||
&:nth-of-type(9) {
|
||||
grid-column: 8 / 9;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -312,12 +306,12 @@ h1 {
|
||||
font-family: "knowit";
|
||||
}
|
||||
|
||||
.to-lottery{
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
.to-lottery {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
@@ -326,10 +320,10 @@ h1 {
|
||||
row-gap: 5em;
|
||||
|
||||
.scroll-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
grid-column: 2 / -2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
@@ -346,8 +340,12 @@ h1 {
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
grid-column: 2 / -2;
|
||||
.wine-container {
|
||||
grid-column: 3 / -3;
|
||||
|
||||
@include mobile {
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon--arrow-long-right {
|
||||
@@ -356,8 +354,7 @@ h1 {
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
|
||||
.scroll-info{
|
||||
.scroll-info {
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<div class="instructions">
|
||||
<h1 class="title">Virtuelt lotteri</h1>
|
||||
<ol>
|
||||
<li>Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>, eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link></li>
|
||||
<li>
|
||||
Vurder om du ønsker å bruke <router-link to="/generate" class="vin-link">loddgeneratoren</router-link>,
|
||||
eller sjekke ut <router-link to="/dagens" class="vin-link">dagens fangst.</router-link>
|
||||
</li>
|
||||
<li>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li>
|
||||
<li>Send gjerne melding om fargeønske også.</li>
|
||||
</ol>
|
||||
@@ -15,18 +18,16 @@
|
||||
|
||||
<VippsPill class="vipps-pill mobile-only" />
|
||||
|
||||
<p class="call-to-action">
|
||||
<span class="vin-link">Følg med på utviklingen</span> og <span class="vin-link">chat om trekningen</span>
|
||||
<i class="icon icon--arrow-left" @click="scrollToContent"></i></p>
|
||||
<p class="call-to-action">
|
||||
<span class="vin-link" @click="scrollToContent">Følg med på utviklingen</span> og
|
||||
<span class="vin-link" @click="scrollToContent">chat om trekningen</span>
|
||||
<i class="icon icon--arrow-left" @click="scrollToContent"></i>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container" ref="content">
|
||||
<WinnerDraw
|
||||
:currentWinnerDrawn="currentWinnerDrawn"
|
||||
:currentWinner="currentWinner"
|
||||
:attendees="attendees"
|
||||
/>
|
||||
<WinnerDraw :currentWinnerDrawn="currentWinnerDrawn" :currentWinner="currentWinner" :attendees="attendees" />
|
||||
|
||||
<div class="todays-raffles">
|
||||
<h2>Liste av lodd kjøpt i dag</h2>
|
||||
@@ -51,15 +52,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container wines-container">
|
||||
<div class="todays-wines">
|
||||
<h2>Dagens fangst ({{ wines.length }})</h2>
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { attendees, winners, prelottery } from "@/api";
|
||||
import Chat from "@/ui/Chat";
|
||||
import Vipps from "@/ui/Vipps";
|
||||
import VippsPill from "@/ui/VippsPill";
|
||||
@@ -74,18 +76,18 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
attendees: [],
|
||||
attendeesFetched: false,
|
||||
winners: [],
|
||||
wines: [],
|
||||
currentWinnerDrawn: false,
|
||||
currentWinner: null,
|
||||
socket: null,
|
||||
attendeesFetched: false,
|
||||
wasDisconnected: false,
|
||||
ticketsBought: {
|
||||
"red": 0,
|
||||
"blue": 0,
|
||||
"green": 0,
|
||||
"yellow": 0
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -129,42 +131,45 @@ export default {
|
||||
this.socket = null;
|
||||
},
|
||||
methods: {
|
||||
getWinners: async function() {
|
||||
let response = await winners();
|
||||
if (response) {
|
||||
this.winners = response;
|
||||
}
|
||||
getWinners() {
|
||||
fetch("/api/lottery/winners")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.winners = response.winners));
|
||||
},
|
||||
getTodaysWines() {
|
||||
prelottery()
|
||||
fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.wines)
|
||||
.then(wines => {
|
||||
this.wines = wines;
|
||||
this.todayExists = wines.length > 0;
|
||||
})
|
||||
.catch(_ => this.todayExists = false)
|
||||
.catch(_ => (this.todayExists = false));
|
||||
},
|
||||
getAttendees: async function() {
|
||||
let response = await attendees();
|
||||
if (response) {
|
||||
this.attendees = response;
|
||||
if (this.attendees == undefined || this.attendees.length == 0) {
|
||||
this.attendeesFetched = true;
|
||||
return;
|
||||
}
|
||||
const addValueOfListObjectByKey = (list, key) =>
|
||||
list.map(object => object[key]).reduce((a, b) => a + b);
|
||||
getAttendees() {
|
||||
fetch("/api/lottery/attendees")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const { attendees } = response;
|
||||
this.attendees = attendees || [];
|
||||
|
||||
this.ticketsBought = {
|
||||
red: addValueOfListObjectByKey(response, "red"),
|
||||
blue: addValueOfListObjectByKey(response, "blue"),
|
||||
green: addValueOfListObjectByKey(response, "green"),
|
||||
yellow: addValueOfListObjectByKey(response, "yellow")
|
||||
};
|
||||
}
|
||||
this.attendeesFetched = true;
|
||||
if (attendees == undefined || attendees.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addValueOfListObjectByKey = (list, key) => list.map(object => object[key]).reduce((a, b) => a + b);
|
||||
|
||||
this.ticketsBought = {
|
||||
red: addValueOfListObjectByKey(attendees, "red"),
|
||||
blue: addValueOfListObjectByKey(attendees, "blue"),
|
||||
green: addValueOfListObjectByKey(attendees, "green"),
|
||||
yellow: addValueOfListObjectByKey(attendees, "yellow")
|
||||
};
|
||||
})
|
||||
.finally(_ => (this.attendeesFetched = true));
|
||||
},
|
||||
scrollToContent() {
|
||||
console.log(window.scrollY)
|
||||
console.log(window.scrollY);
|
||||
const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50;
|
||||
const { scrollY } = window;
|
||||
let scrollHeight = intersectingHeaderHeight;
|
||||
@@ -178,14 +183,13 @@ export default {
|
||||
});
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/lottery/game');
|
||||
window.ga("send", "pageview", "/lottery/game");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
@@ -201,7 +205,8 @@ export default {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
> div, > section {
|
||||
> div,
|
||||
> section {
|
||||
@include mobile {
|
||||
grid-column: span 5;
|
||||
}
|
||||
@@ -343,6 +348,8 @@ header {
|
||||
|
||||
> div {
|
||||
padding: 1rem;
|
||||
max-height: 638px;
|
||||
overflow-y: scroll;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
@@ -369,12 +376,23 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.todays-wines {
|
||||
width: 80vw;
|
||||
padding: 0 10vw;
|
||||
|
||||
.wines-container {
|
||||
margin-bottom: 4rem;
|
||||
@include mobile {
|
||||
width: 90vw;
|
||||
padding: 0 5vw;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
grid-column: 1 / 5;
|
||||
}
|
||||
|
||||
.wine {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1 class="title">Virtuelt lotteri registrering</h1>
|
||||
<br />
|
||||
<div class="draw-winner-container" v-if="attendees.length > 0">
|
||||
<div v-if="drawingWinner">
|
||||
<span>
|
||||
Trekker {{ currentWinners }} av {{ numberOfWinners }} vinnere.
|
||||
{{ secondsLeft }} sekunder av {{ drawTime }} igjen
|
||||
</span>
|
||||
<button class="vin-button no-margin" @click="stopDraw">Stopp trekning</button>
|
||||
</div>
|
||||
<div class="draw-container" v-if="!drawingWinner">
|
||||
<button class="vin-button no-margin" @click="drawWinner">Trekk vinnere</button>
|
||||
<input type="number" v-model="numberOfWinners" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">
|
||||
<span>{{ winner.name }}</span>
|
||||
<span>{{ winner.phoneNumber }}</span>
|
||||
<span>Rød: {{ winner.red }}</span>
|
||||
<span>Blå: {{ winner.blue }}</span>
|
||||
<span>Grønn: {{ winner.green }}</span>
|
||||
<span>Gul: {{ winner.yellow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="delete-buttons" v-if="attendees.length > 0 || winners.length > 0">
|
||||
<button
|
||||
class="vin-button"
|
||||
v-if="winners.length > 0"
|
||||
@click="deleteAllWinners"
|
||||
>Slett virtuelle vinnere</button>
|
||||
<button
|
||||
class="vin-button"
|
||||
v-if="attendees.length > 0"
|
||||
@click="deleteAllAttendees"
|
||||
>Slett virtuelle deltakere</button>
|
||||
</div>
|
||||
<div class="attendees" v-if="attendees.length > 0">
|
||||
<h2>Deltakere ({{ attendees.length }})</h2>
|
||||
<div class="attendee" v-for="(attendee, index) in attendees" :key="index">
|
||||
<div class="name-and-phone">
|
||||
<span class="name">{{ attendee.name }}</span>
|
||||
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
||||
</div>
|
||||
<div class="raffles-container">
|
||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attendee-registration-container">
|
||||
<h2>Legg til deltaker</h2>
|
||||
<div class="label-div">
|
||||
<label for="name">Navn</label>
|
||||
<input id="name" type="text" placeholder="Navn" v-model="name" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="phoneNumber">Telefonnummer</label>
|
||||
<input id="phoneNumber" type="text" placeholder="Telefonnummer" v-model="phoneNumber" />
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="randomColors">Tilfeldig farger?</label>
|
||||
<input
|
||||
id="randomColors"
|
||||
type="checkbox"
|
||||
placeholder="Tilfeldig farger"
|
||||
v-model="randomColors"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!randomColors">
|
||||
<br />
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="red">Rød</label>
|
||||
<input id="red" type="number" placeholder="Rød" v-model="red" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="blue">Blå</label>
|
||||
<input id="blue" type="number" placeholder="Blå" v-model="blue" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="green">Grønn</label>
|
||||
<input id="green" type="number" placeholder="Grønn" v-model="green" />
|
||||
</div>
|
||||
<br />
|
||||
<div class="label-div">
|
||||
<label for="yellow">Gul</label>
|
||||
<input id="yellow" type="number" placeholder="Gul" v-model="yellow" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
||||
|
||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import io from "socket.io-client";
|
||||
import {
|
||||
addAttendee,
|
||||
getVirtualWinner,
|
||||
attendeesSecure,
|
||||
attendees,
|
||||
winnersSecure,
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
finishedDraw,
|
||||
prelottery
|
||||
} from "@/api";
|
||||
import TextToast from "@/ui/TextToast";
|
||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RaffleGenerator,
|
||||
TextToast
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
phoneNumber: null,
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
raffles: 0,
|
||||
randomColors: false,
|
||||
attendees: [],
|
||||
winners: [],
|
||||
drawingWinner: false,
|
||||
secondsLeft: 20,
|
||||
drawTime: 20,
|
||||
currentWinners: 1,
|
||||
numberOfWinners: 4,
|
||||
socket: null,
|
||||
toastText: undefined,
|
||||
showToast: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
|
||||
this.socket = io(`${window.location.hostname}:${window.location.port}`);
|
||||
|
||||
this.socket.on("winner", async msg => {
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
});
|
||||
|
||||
this.socket.on("refresh_data", async msg => {
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
});
|
||||
|
||||
this.socket.on("new_attendee", async msg => {
|
||||
this.getAttendees();
|
||||
});
|
||||
|
||||
window.finishedDraw = finishedDraw;
|
||||
},
|
||||
methods: {
|
||||
setWithRandomColors(colors) {
|
||||
Object.keys(colors).forEach(color => (this[color] = colors[color]));
|
||||
},
|
||||
sendAttendee: async function() {
|
||||
if (this.red == 0 && this.blue == 0 && this.green == 0 && this.yellow == 0) {
|
||||
alert('Ingen farger valgt!')
|
||||
return;
|
||||
}
|
||||
if (this.name == 0 && this.phoneNumber) {
|
||||
alert('Ingen navn eller tlf satt!')
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await addAttendee({
|
||||
name: this.name,
|
||||
phoneNumber: this.phoneNumber,
|
||||
red: this.red,
|
||||
blue: this.blue,
|
||||
green: this.green,
|
||||
yellow: this.yellow,
|
||||
raffles: this.raffles
|
||||
});
|
||||
|
||||
if (response == true) {
|
||||
this.toastText = `Sendt inn deltaker: ${this.name}`;
|
||||
this.showToast = true;
|
||||
|
||||
this.name = null;
|
||||
this.phoneNumber = null;
|
||||
this.yellow = 0;
|
||||
this.green = 0;
|
||||
this.red = 0;
|
||||
this.blue = 0;
|
||||
|
||||
this.getAttendees();
|
||||
} else {
|
||||
alert("Klarte ikke sende inn.. Er du logget inn?");
|
||||
}
|
||||
},
|
||||
getAttendees: async function() {
|
||||
let response = await attendeesSecure();
|
||||
this.attendees = response;
|
||||
},
|
||||
stopDraw: function() {
|
||||
this.drawingWinner = false;
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
drawWinner: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||
this.drawingWinner = true;
|
||||
let response = await getVirtualWinner();
|
||||
|
||||
if (response.success) {
|
||||
console.log("Winner:", response.winner);
|
||||
if (this.currentWinners < this.numberOfWinners) {
|
||||
this.countdown();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
let finished = await finishedDraw();
|
||||
if(finished) {
|
||||
alert("SMS'er er sendt ut!");
|
||||
} else {
|
||||
alert("Noe gikk galt under SMS utsendelser.. Sjekk logg og database for id'er.");
|
||||
}
|
||||
}
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
alert("Noe gikk galt under trekningen..! " + response["message"]);
|
||||
}
|
||||
}
|
||||
},
|
||||
countdown: function() {
|
||||
this.secondsLeft -= 1;
|
||||
if (!this.drawingWinner) {
|
||||
return;
|
||||
}
|
||||
if (this.secondsLeft <= 0) {
|
||||
this.secondsLeft = this.drawTime;
|
||||
this.currentWinners += 1;
|
||||
if (this.currentWinners <= this.numberOfWinners) {
|
||||
this.drawWinner();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.countdown();
|
||||
}, 1000);
|
||||
},
|
||||
deleteAllWinners: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil slette vinnere?")) {
|
||||
let response = await deleteWinners();
|
||||
if (response) {
|
||||
this.getWinners();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteAllAttendees: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
|
||||
let response = await deleteAttendees();
|
||||
if (response) {
|
||||
this.getAttendees();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
}
|
||||
},
|
||||
getWinners: async function() {
|
||||
let response = await winnersSecure();
|
||||
if (response) {
|
||||
this.winners = response;
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.draw-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.draw-winner-container,
|
||||
.delete-buttons {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.delete-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h1 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 90%;
|
||||
margin: 2rem auto;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
@include desktop {
|
||||
max-width: 60vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
#randomColors {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
&:checked {
|
||||
background: green;
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
width: 140px;
|
||||
height: 150px;
|
||||
margin: 20px 0;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
|
||||
&.small {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-raffle {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-raffle {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex !important;
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.winners {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attendees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attendee {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
|
||||
& .name-and-phone,
|
||||
& .raffles-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& .name-and-phone {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .raffles-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +1,23 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div v-if="!posted">
|
||||
<h1 v-if="name">Gratulerer {{name}}!</h1>
|
||||
<div>
|
||||
<div v-if="!posted" class="container">
|
||||
<h1 v-if="name">Gratulerer {{ name }}!</h1>
|
||||
|
||||
<p v-if="name">
|
||||
Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.
|
||||
</p>
|
||||
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
|
||||
|
||||
<h1 v-else-if="!turn && wines.length" class="sent-container">Finner ikke noen vinner her..</h1>
|
||||
|
||||
<h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1>
|
||||
|
||||
<div class="wines-container" v-if="name">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine">
|
||||
<button
|
||||
@click="chooseWine(wine.name)"
|
||||
class="vin-button select-wine"
|
||||
>Velg denne vinnen</button>
|
||||
<button @click="chooseWine(wine)" class="vin-button select-wine">Velg denne vinnen</button>
|
||||
</Wine>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="posted" class="sent-container">
|
||||
<h1>Valget ditt er sendt inn!</h1>
|
||||
<p>Du får mer info om henting snarest!</p>
|
||||
@@ -24,15 +26,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAmIWinner, postWineChosen, prelottery } from "@/api";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: { Wine },
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
existing: false,
|
||||
fetched: false,
|
||||
turn: false,
|
||||
name: null,
|
||||
wines: [],
|
||||
@@ -40,30 +40,43 @@ export default {
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.id = this.$router.currentRoute.params.id;
|
||||
const { id } = this.$router.currentRoute.params;
|
||||
|
||||
let winnerObject = await getAmIWinner(this.id);
|
||||
this.fetched = true;
|
||||
if (!winnerObject || !winnerObject.existing) {
|
||||
console.error("non existing", winnerObject);
|
||||
return;
|
||||
}
|
||||
this.existing = true;
|
||||
if (winnerObject.existing && !winnerObject.turn) {
|
||||
console.error("not your turn yet", winnerObject);
|
||||
return;
|
||||
}
|
||||
this.turn = true;
|
||||
this.name = winnerObject.name;
|
||||
this.wines = await prelottery();
|
||||
this.id = id;
|
||||
this.getPrizes(id);
|
||||
},
|
||||
methods: {
|
||||
chooseWine: async function(name) {
|
||||
let posted = await postWineChosen(this.id, name);
|
||||
console.log("response", posted);
|
||||
if (posted.success) {
|
||||
this.posted = true;
|
||||
}
|
||||
getPrizes(id) {
|
||||
fetch(`/api/lottery/prize-distribution/prizes/${id}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.wines = response.wines;
|
||||
this.name = response.winner.name;
|
||||
this.turn = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
chooseWine(wine) {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wine })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/prize-distribution/prize/${this.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({ title: `Valgt vin: ${wine.name}` });
|
||||
this.posted = true;
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke velge vin :(",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -74,9 +87,19 @@ export default {
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-top: 2rem;
|
||||
padding: 2rem;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
max-width: 2000px;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sent-container {
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
@@ -90,11 +113,4 @@ export default {
|
||||
.select-wine {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
356
frontend/components/admin/DrawWinnerPage.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Trekk vinnere</h1>
|
||||
|
||||
<div class="draw-winner-container">
|
||||
<div v-if="drawingWinner == false" class="draw-container">
|
||||
<input type="number" v-model="winnersToDraw" />
|
||||
<button class="vin-button no-margin" @click="startDrawingWinners">Trekk vinnere</button>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length" class="wines-left">
|
||||
<span>Antall vin igjen: {{ winnersToDraw }} av {{ wines.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="drawingWinner == true">
|
||||
<p>Trekker vinner {{ winners.length }} av {{ wines.length }}.</p>
|
||||
<p>Neste trekning om {{ secondsLeft }} sekunder av {{ drawTime }}</p>
|
||||
|
||||
<div class="button-container draw-winner-actions">
|
||||
<button class="vin-button danger" @click="stopDraw">
|
||||
Stopp trekning
|
||||
</button>
|
||||
<button
|
||||
class="vin-button"
|
||||
:class="{ 'pulse-button': secondsLeft == 0 }"
|
||||
:disabled="secondsLeft > 0"
|
||||
@click="drawWinner"
|
||||
>
|
||||
Trekk neste
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prize-distribution">
|
||||
<h2>Prisutdeling</h2>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="startPrizeDistribution">Start automatisk prisutdeling med SMS</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element" v-for="(winner, index) in winners" :key="index">
|
||||
<span>{{ winner.name }}</span>
|
||||
<span>Phone: {{ winner.phoneNumber }}</span>
|
||||
<span>Rød: {{ winner.red }}</span>
|
||||
<span>Blå: {{ winner.blue }}</span>
|
||||
<span>Grønn: {{ winner.green }}</span>
|
||||
<span>Gul: {{ winner.yellow }}</span>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button small" @click="editingWinner = editingWinner == winner ? false : winner">
|
||||
{{ editingWinner == winner ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="editingWinner == winner" class="edit">
|
||||
<div class="label-div" v-for="key in Object.keys(winner)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="winner[key]" :placeholder="key" />
|
||||
</div>
|
||||
|
||||
<div v-if="editingWinner == winner" class="button-container column">
|
||||
<button class="vin-button small" @click="notifyWinner(winner)">
|
||||
Send SMS
|
||||
</button>
|
||||
<button class="vin-button small warning" @click="updateWinner(winner)">
|
||||
Oppdater
|
||||
</button>
|
||||
<button class="vin-button small danger" @click="deleteWinner(winner)">
|
||||
Slett
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container margin-md" v-if="winners.length > 0">
|
||||
<button class="vin-button danger" v-if="winners.length > 0" @click="deleteAllWinners">
|
||||
Slett virtuelle vinnere
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
drawingWinner: false,
|
||||
secondsLeft: 20,
|
||||
drawTime: 20,
|
||||
winners: [],
|
||||
editingWinner: undefined
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchLotterWines();
|
||||
this.fetchLotterWinners();
|
||||
},
|
||||
computed: {
|
||||
winnersToDraw() {
|
||||
if (this.wines.length == undefined || this.winners.length == undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.wines.length - this.winners.length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
winners(val) {
|
||||
this.$emit("counter", val.length);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchLotterWines() {
|
||||
return fetch("/api/lottery/wines")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.wines = response.wines));
|
||||
},
|
||||
fetchLotterWinners() {
|
||||
return fetch("/api/lottery/winners")
|
||||
.then(resp => resp.json())
|
||||
.then(response => (this.winners = response.winners));
|
||||
},
|
||||
countdown() {
|
||||
if (this.drawingWinner == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.secondsLeft > 0) {
|
||||
this.secondsLeft -= 1;
|
||||
|
||||
setTimeout(_ => {
|
||||
this.countdown();
|
||||
}, 1000);
|
||||
} else {
|
||||
if (this.winners.length == this.wines.length) {
|
||||
this.drawingWinner = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
startDrawingWinners() {
|
||||
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||
this.drawWinner();
|
||||
}
|
||||
},
|
||||
drawWinner() {
|
||||
if (this.winnersToDraw <= 0) {
|
||||
this.$toast.error({ title: "No more wines to draw" });
|
||||
return;
|
||||
}
|
||||
this.secondsLeft = this.drawTime;
|
||||
this.drawingWinner = true;
|
||||
|
||||
fetch("/api/lottery/draw")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const { winner, color, success, message } = response;
|
||||
|
||||
if (success == false) {
|
||||
this.$toast.error({ title: message });
|
||||
return;
|
||||
}
|
||||
|
||||
winner.color = color;
|
||||
this.winners.push(winner);
|
||||
this.countdown();
|
||||
})
|
||||
.catch(error => {
|
||||
if (error) {
|
||||
this.$toast.error({ title: error.message });
|
||||
}
|
||||
this.drawingWinner = false;
|
||||
});
|
||||
},
|
||||
stopDraw() {
|
||||
this.drawingWinner = false;
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
startPrizeDistribution() {
|
||||
if (!window.confirm("Er du sikker på at du vil starte prisutdeling?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawingWinner = false;
|
||||
|
||||
const options = { method: "POST" };
|
||||
fetch(`/api/lottery/prize-distribution/start`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Startet prisutdeling. SMS'er sendt ut!`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke starte prisutdeling`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
notifyWinner(winner) {
|
||||
const options = { method: "POST" };
|
||||
|
||||
fetch(`/api/lottery/messages/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Sendte sms til vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke sende sms til vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
updateWinner(winner) {
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ winner })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Oppdaterte vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke oppdatere vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteWinner(winner) {
|
||||
if (winner._id != null && window.confirm(`Er du sikker på at du vil slette vinner ${winner.name}?`)) {
|
||||
const options = { method: "DELETE" };
|
||||
|
||||
fetch(`/api/lottery/winner/${winner.id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.winners = this.winners.filter(w => w.id != winner.id);
|
||||
|
||||
this.$toast.info({
|
||||
title: `Slettet vinner ${winner.name}.`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke slette vinner ${winner.name}`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteAllWinners() {
|
||||
if (window.confirm("Er du sikker på at du vil slette alle vinnere?")) {
|
||||
const options = { method: "DELETE" };
|
||||
|
||||
fetch("/api/lottery/winners", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.winners = [];
|
||||
this.$toast.info({
|
||||
title: "Slettet alle vinnere."
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Klarte ikke slette vinnere",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wines-left {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.draw-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
font-size: 1.7rem;
|
||||
padding: 7px;
|
||||
margin: 0;
|
||||
width: 10rem;
|
||||
height: 3rem;
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.draw-winner-actions {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.winners {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.raffle-element {
|
||||
width: 220px;
|
||||
height: 100%;
|
||||
min-height: 250px;
|
||||
font-size: 1.1rem;
|
||||
padding: 1rem;
|
||||
font-weight: 500;
|
||||
// text-align: center;
|
||||
|
||||
-webkit-mask-size: cover;
|
||||
-moz-mask-size: cover;
|
||||
mask-size: cover;
|
||||
flex-direction: column;
|
||||
|
||||
span:first-of-type {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
span.active {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.edit {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
frontend/components/admin/PushPage.vue
Normal file
59
frontend/components/admin/PushPage.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Send push melding</h1>
|
||||
|
||||
<div class="notification-element">
|
||||
<div class="label-div">
|
||||
<label for="notification">Melding</label>
|
||||
<textarea id="notification" type="text" rows="3" v-model="pushMessage" placeholder="Push meldingtekst" />
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="notification-link">Push åpner lenke</label>
|
||||
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-container margin-top-sm">
|
||||
<button class="vin-button" @click="sendPush">Send push</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
pushMessage: "",
|
||||
pushLink: "/"
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
sendPush: async function() {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: this.pushMessage,
|
||||
link: this.pushLink
|
||||
})
|
||||
};
|
||||
|
||||
return fetch("/subscription/send-notification", options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: "Sendt!"
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: "Noe gikk galt!",
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
308
frontend/components/admin/RegisterWinePage.vue
Normal file
308
frontend/components/admin/RegisterWinePage.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Register vin</h1>
|
||||
|
||||
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="showCamera = !showCamera">
|
||||
{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="manualyFillInnWine">
|
||||
Legg til en vin manuelt
|
||||
</button>
|
||||
|
||||
<button class="vin-button" @click="showImportLink = !showImportLink">
|
||||
{{ showImportLink ? "Skjul importer fra link" : "Importer fra link" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showImportLink" class="import-from-link">
|
||||
<label>Importer vin fra vinmonopolet link:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Vinmonopol lenke"
|
||||
ref="vinmonopoletLinkInput"
|
||||
autocapitalize="none"
|
||||
@input="addWineByUrl"
|
||||
/>
|
||||
|
||||
<div v-if="linkError" class="error">
|
||||
{{ linkError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wines.length > 0" class="wine-edit-container">
|
||||
<h2>Dagens registrerte viner</h2>
|
||||
|
||||
<div>
|
||||
<button class="vin-button" @click="sendWines">Send inn dagens viner</button>
|
||||
</div>
|
||||
|
||||
<div class="wines">
|
||||
<wine v-for="wine in wines" :key="wine.id" :wine="wine">
|
||||
<template v-slot:default>
|
||||
<div v-if="editingWine == wine" class="wine-edit">
|
||||
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="wine[key]" :placeholder="key" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:bottom>
|
||||
<div class="button-container row small">
|
||||
<button v-if="editingWine == wine && wine._id" class="vin-button small warning" @click="updateWine(wine)">
|
||||
Oppdater vin
|
||||
</button>
|
||||
|
||||
<button class="vin-button small" @click="editingWine = editingWine == wine ? false : wine">
|
||||
{{ editingWine == wine ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
|
||||
<button class="danger vin-button small" @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>
|
||||
475
frontend/components/admin/archiveLotteryPage.vue
Normal file
475
frontend/components/admin/archiveLotteryPage.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<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">
|
||||
<p v-if="todaysAlreadySubmitted" class="info-message">
|
||||
Lotteriet er arkivert!<br />Du kan nå slette dagens viner, deltakere & vinnere for å tilbakestille til neste
|
||||
ukes lotteri.
|
||||
</p>
|
||||
|
||||
<button class="vin-button" @click="archiveLottery" :disabled="todaysAlreadySubmitted">
|
||||
{{ todaysAlreadySubmitted == false ? "Send inn og arkiver" : "Dagens lotteri er allerede arkivert" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { dateString } from "@/utils";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: { Wine },
|
||||
data() {
|
||||
return {
|
||||
payed: undefined,
|
||||
todaysAlreadySubmitted: false,
|
||||
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();
|
||||
this.checkIfAlreadySubmittedForToday();
|
||||
},
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
checkIfAlreadySubmittedForToday() {
|
||||
return fetch("/api/lottery/latest")
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
const getDay = d => new Date(d).getDate();
|
||||
|
||||
if (response.lottery.date && (getDay(response.lottery.date) == getDay(new Date()))) {
|
||||
this.todaysAlreadySubmitted = true;
|
||||
} else {
|
||||
this.todaysAlreadySubmitted = false;
|
||||
}
|
||||
})
|
||||
},
|
||||
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.todaysAlreadySubmitted = true;
|
||||
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%;
|
||||
}
|
||||
}
|
||||
|
||||
.info-message {
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
background-color: var(--light-blue);
|
||||
color: var(--matte-text-color);
|
||||
border-radius: 4px;
|
||||
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.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 });
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,124 +1,172 @@
|
||||
const VinlottisPage = () => import(
|
||||
/* webpackChunkName: "landing-page" */
|
||||
"@/components/VinlottisPage");
|
||||
const VirtualLotteryPage = () => import(
|
||||
/* webpackChunkName: "landing-page" */
|
||||
"@/components/VirtualLotteryPage");
|
||||
const GeneratePage = () => import(
|
||||
/* webpackChunkName: "landing-page" */
|
||||
"@/components/GeneratePage");
|
||||
const VinlottisPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "landing-page" */
|
||||
"@/components/VinlottisPage"
|
||||
);
|
||||
const VirtualLotteryPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "landing-page" */
|
||||
"@/components/VirtualLotteryPage"
|
||||
);
|
||||
const GeneratePage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "landing-page" */
|
||||
"@/components/GeneratePage"
|
||||
);
|
||||
|
||||
const TodaysPage = () => import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/TodaysPage");
|
||||
const AllWinesPage = () => import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/AllWinesPage");
|
||||
const HistoryPage = () => import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/HistoryPage");
|
||||
const WinnerPage = () => import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/WinnerPage");
|
||||
const TodaysPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/TodaysPage"
|
||||
);
|
||||
const AllWinesPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/AllWinesPage"
|
||||
);
|
||||
const HistoryPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/HistoryPage"
|
||||
);
|
||||
const WinnerPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/WinnerPage"
|
||||
);
|
||||
const SalgsbetingelserPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/SalgsbetingelserPage"
|
||||
);
|
||||
|
||||
const LoginPage = () => import(
|
||||
/* webpackChunkName: "user" */
|
||||
"@/components/LoginPage");
|
||||
const CreatePage = () => import(
|
||||
/* webpackChunkName: "user" */
|
||||
"@/components/CreatePage");
|
||||
const AdminPage = () => import(
|
||||
/* webpackChunkName: "admin" */
|
||||
"@/components/AdminPage");
|
||||
const LoginPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "user" */
|
||||
"@/components/LoginPage"
|
||||
);
|
||||
const CreatePage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "user" */
|
||||
"@/components/CreatePage"
|
||||
);
|
||||
const AdminPage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "admin" */
|
||||
"@/components/AdminPage"
|
||||
);
|
||||
|
||||
const PersonalHighscorePage = () => import(
|
||||
/* webpackChunkName: "highscore" */
|
||||
"@/components/PersonalHighscorePage");
|
||||
const HighscorePage = () => import(
|
||||
/* webpackChunkName: "highscore" */
|
||||
"@/components/HighscorePage");
|
||||
const PersonalHighscorePage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "highscore" */
|
||||
"@/components/PersonalHighscorePage"
|
||||
);
|
||||
const HighscorePage = () =>
|
||||
import(
|
||||
/* webpackChunkName: "highscore" */
|
||||
"@/components/HighscorePage"
|
||||
);
|
||||
|
||||
const RequestWine = () => import(
|
||||
/* webpackChunkName: "request" */
|
||||
"@/components/RequestWine");
|
||||
const AllRequestedWines = () => import(
|
||||
/* webpackChunkName: "request" */
|
||||
"@/components/AllRequestedWines");
|
||||
const RequestWine = () =>
|
||||
import(
|
||||
/* webpackChunkName: "request" */
|
||||
"@/components/RequestWine"
|
||||
);
|
||||
const AllRequestedWines = () =>
|
||||
import(
|
||||
/* webpackChunkName: "request" */
|
||||
"@/components/AllRequestedWines"
|
||||
);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "*",
|
||||
name: "Hjem",
|
||||
component: VinlottisPage
|
||||
component: VinlottisPage,
|
||||
},
|
||||
{
|
||||
path: "/lottery",
|
||||
name: "Lotteri",
|
||||
component: VirtualLotteryPage
|
||||
component: VirtualLotteryPage,
|
||||
},
|
||||
{
|
||||
path: "/dagens",
|
||||
name: "Dagens vin",
|
||||
component: TodaysPage
|
||||
component: TodaysPage,
|
||||
},
|
||||
{
|
||||
path: "/viner",
|
||||
name: "All viner",
|
||||
component: AllWinesPage
|
||||
component: AllWinesPage,
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: LoginPage
|
||||
component: LoginPage,
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
name: "Registrer",
|
||||
component: CreatePage
|
||||
component: CreatePage,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "Admin side",
|
||||
component: AdminPage
|
||||
component: AdminPage,
|
||||
},
|
||||
{
|
||||
path: "/generate/",
|
||||
component: GeneratePage
|
||||
component: GeneratePage,
|
||||
},
|
||||
{
|
||||
path: "/winner/:id",
|
||||
component: WinnerPage
|
||||
component: WinnerPage,
|
||||
},
|
||||
{
|
||||
{
|
||||
path: "/history/:date",
|
||||
name: "Historie for dato",
|
||||
component: HistoryPage
|
||||
component: HistoryPage,
|
||||
},
|
||||
{
|
||||
path: "/history",
|
||||
name: "Historie",
|
||||
component: HistoryPage
|
||||
component: HistoryPage,
|
||||
},
|
||||
{
|
||||
path: "/highscore/:name",
|
||||
name: "Personlig topplisten",
|
||||
component: PersonalHighscorePage
|
||||
component: PersonalHighscorePage,
|
||||
},
|
||||
{
|
||||
path: "/highscore",
|
||||
name: "Topplisten",
|
||||
component: HighscorePage
|
||||
component: HighscorePage,
|
||||
},
|
||||
{
|
||||
path: "/anbefal",
|
||||
name: "Anbefal ny vin",
|
||||
component: RequestWine,
|
||||
},
|
||||
{
|
||||
path: "/request",
|
||||
name: "Etterspør vin",
|
||||
component: RequestWine
|
||||
component: RequestWine,
|
||||
},
|
||||
{
|
||||
path: "/anbefalte",
|
||||
name: "Anbefalte viner",
|
||||
component: AllRequestedWines,
|
||||
},
|
||||
{
|
||||
path: "/requested-wines",
|
||||
name: "Etterspurte vin",
|
||||
component: AllRequestedWines
|
||||
}
|
||||
component: AllRequestedWines,
|
||||
},
|
||||
{
|
||||
path: "/salgsbetingelser",
|
||||
name: "Salgsbetingelser",
|
||||
component: SalgsbetingelserPage,
|
||||
},
|
||||
];
|
||||
|
||||
export { routes };
|
||||
|
||||
22
frontend/styles/animations.scss
Normal file
22
frontend/styles/animations.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.pulse {
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
|
||||
transform: scale(1);
|
||||
animation: pulse 2s infinite;
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,6 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
row-gap: 3em;
|
||||
|
||||
&.collapsed {
|
||||
max-height: 0%;
|
||||
@@ -238,4 +237,4 @@
|
||||
h2 {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ body {
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -51,8 +53,10 @@ a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
margin-top: 0.7rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -76,6 +80,7 @@ a {
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
&.column {
|
||||
@@ -95,7 +100,7 @@ a {
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: unset;
|
||||
margin-bottom: .75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +110,8 @@ input,
|
||||
textarea {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
font-size: 1.1rem;
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
@@ -136,6 +143,11 @@ textarea {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: #f9826c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: $red;
|
||||
color: white;
|
||||
@@ -151,9 +163,12 @@ textarea {
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
|
||||
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
|
||||
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07), 0 4px 8px rgba(0, 0, 0, 0.07),
|
||||
0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@@ -163,7 +178,7 @@ textarea {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:disabled{
|
||||
&:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -173,6 +188,21 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-button:not(:hover) {
|
||||
animation: pulse 1.5s infinite cubic-bezier(0.66, 0, 0, 1);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
@@ -193,11 +223,23 @@ textarea {
|
||||
text-decoration: none;
|
||||
color: $matte-text-color;
|
||||
|
||||
&:focus, &:hover {
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.margin {
|
||||
&-md {
|
||||
margin: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.margin-top {
|
||||
&-md {
|
||||
@@ -269,14 +311,29 @@ textarea {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
|
||||
margin: 20px 0;
|
||||
color: #333333;
|
||||
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
@@ -293,11 +350,16 @@ textarea {
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin raffle {
|
||||
padding-bottom: 50px;
|
||||
&::before, &::after {
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -309,11 +371,11 @@ textarea {
|
||||
background-position: 0 25px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
&::after{
|
||||
&::after {
|
||||
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
|
||||
background-size: 50px 50px;
|
||||
background-position: 25px -25px;
|
||||
bottom: -25px
|
||||
bottom: -25px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,4 +389,4 @@ textarea {
|
||||
@include desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
|
||||
@@ -7,6 +9,10 @@
|
||||
|
||||
&.row {
|
||||
flex-direction: row;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrap {
|
||||
@@ -43,4 +49,4 @@
|
||||
&-right {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,49 @@
|
||||
$primary: #b7debd;
|
||||
body {
|
||||
--primary: #b7debd;
|
||||
|
||||
$light-green: #c8f9df;
|
||||
$green: #0be881;
|
||||
$dark-green: #0ed277;
|
||||
--light-green: #c8f9df;
|
||||
--green: #0be881;
|
||||
--dark-green: #0ed277;
|
||||
|
||||
$light-blue: #d4f2fe;
|
||||
$blue: #4bcffa;
|
||||
$dark-blue: #24acda;
|
||||
--light-blue: #d4f2fe;
|
||||
--blue: #4bcffa;
|
||||
--dark-blue: #24acda;
|
||||
|
||||
$light-yellow: #fff6d6;
|
||||
$yellow: #ffde5d;
|
||||
$dark-yellow: #ecc31d;
|
||||
--light-yellow: #fff6d6;
|
||||
--yellow: #ffde5d;
|
||||
--dark-yellow: #ecc31d;
|
||||
|
||||
$light-red: #fbd7de;
|
||||
$red: #ef5878;
|
||||
$dark-red: #ec3b61;
|
||||
--light-red: #fbd7de;
|
||||
--red: #ef5878;
|
||||
--dark-red: #ec3b61;
|
||||
|
||||
$link-color: #ff5fff;
|
||||
--link-color: #ff5fff;
|
||||
--underlinenav-text: #e1e4e8;
|
||||
--underlinenav-text-active: #f9826c;
|
||||
--underlinenav-text-hover: #d1d5da;
|
||||
|
||||
$matte-text-color: #333333;
|
||||
--matte-text-color: #333333;
|
||||
}
|
||||
|
||||
$primary: var(--primary);
|
||||
|
||||
$light-green: var(--light-green);
|
||||
$green: var(--green);
|
||||
$dark-green: var(--dark-green);
|
||||
|
||||
$light-blue: var(--light-blue);
|
||||
$blue: var(--blue);
|
||||
$dark-blue: var(--dark-blue);
|
||||
|
||||
$light-yellow: var(--light-yellow);
|
||||
$yellow: var(--yellow);
|
||||
$dark-yellow: var(--dark-yellow);
|
||||
|
||||
$light-red: var(--light-red);
|
||||
$red: var(--red);
|
||||
$dark-red: var(--dark-red);
|
||||
|
||||
$link-color: var(--link-color);
|
||||
$underlinenav-text-active: var(--underlinenav-text-active);
|
||||
|
||||
$matte-text-color: var(--matte-text-color);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Vinlottis</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@@ -1,12 +1,49 @@
|
||||
<template>
|
||||
<div class="attendees" v-if="attendees.length > 0">
|
||||
<div class="attendees-container" ref="attendees">
|
||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||
<span class="attendee-name">{{ attendee.name }}</span>
|
||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||
<div v-if="attendees.length > 0" class="attendee-container">
|
||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||
<div class="attendee-info">
|
||||
<router-link class="attendee-name" :to="`/highscore/${attendee.name}`">
|
||||
{{ attendee.name }}
|
||||
</router-link>
|
||||
|
||||
<div v-if="admin" class="flex column justify-center margin-top-sm">
|
||||
<span>Phone: {{ attendee.phoneNumber }}</span>
|
||||
<span>Has won: {{ attendee.winner }}</span>
|
||||
</div>
|
||||
|
||||
<div class="raffle-container">
|
||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="admin" class="attendee-admin">
|
||||
<button class="vin-button edit small" @click="editingAttendee = editingAttendee == attendee ? false : attendee">
|
||||
{{ editingAttendee == attendee ? "Lukk" : "Rediger" }}
|
||||
</button>
|
||||
|
||||
<button class="vin-button small danger" @click="deleteAttendee(attendee)">
|
||||
Slett deltaker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="editingAttendee == attendee" class="attendee-edit">
|
||||
<div class="label-div" v-for="key in Object.keys(attendee)" :key="key">
|
||||
<label>{{ key }}</label>
|
||||
<input type="text" v-model="attendee[key]" :placeholder="key" />
|
||||
</div>
|
||||
|
||||
<div v-if="editingAttendee == attendee">
|
||||
<button class="vin-button small warning" @click="updateAttendee(attendee)">
|
||||
Oppdater deltaker
|
||||
</button>
|
||||
|
||||
<button class="vin-button small danger" @click="deleteAttendee(attendee)">
|
||||
Slett deltaker
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,33 +54,79 @@ export default {
|
||||
props: {
|
||||
attendees: {
|
||||
type: Array
|
||||
},
|
||||
admin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
flipList: (list) => list.slice().reverse()
|
||||
data() {
|
||||
return {
|
||||
editingAttendee: undefined
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
attendees: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.$refs && this.$refs.history) {
|
||||
setTimeout(() => {
|
||||
this.$refs.attendees.scrollTop = this.$refs.attendees.scrollHeight;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
methods: {
|
||||
flipList: list => list.slice().reverse(),
|
||||
updateAttendee(updatedAttendee) {
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ attendee: updatedAttendee })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/attendee/${updatedAttendee._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingAttendee = null;
|
||||
|
||||
const { message, success } = response;
|
||||
|
||||
if (success) {
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteAttendee(deletedAttendee) {
|
||||
const options = {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ attendee: deletedAttendee })
|
||||
};
|
||||
|
||||
fetch(`/api/lottery/attendee/${deletedAttendee._id}`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.editingAttendee = null;
|
||||
|
||||
const { message, success } = response;
|
||||
|
||||
if (success) {
|
||||
this.$toast.info({
|
||||
title: response.message
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.attendee-name {
|
||||
width: 60%;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
@@ -51,45 +134,60 @@ hr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
font-size: 0.75rem;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.attendee-container {
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.attendees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.attendees-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
max-height: 550px;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.attendee {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@include mobile {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 2px solid #d7d8d7;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&-edit {
|
||||
button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,15 +5,27 @@
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
||||
</router-link>
|
||||
|
||||
<a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'" >
|
||||
<a
|
||||
class="menu-toggle-container"
|
||||
aria-label="show-menu"
|
||||
@click="toggleMenu"
|
||||
:class="isOpen ? 'open' : 'collapsed'"
|
||||
>
|
||||
<span class="menu-toggle"></span>
|
||||
<span class="menu-toggle"></span>
|
||||
<span class="menu-toggle"></span>
|
||||
</a>
|
||||
|
||||
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'" >
|
||||
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link" >
|
||||
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a>
|
||||
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'">
|
||||
<router-link
|
||||
v-for="(route, index) in routes"
|
||||
:key="index"
|
||||
:to="route.route"
|
||||
class="menu-item-link"
|
||||
>
|
||||
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{
|
||||
route.name
|
||||
}}</a>
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
</router-link>
|
||||
</nav>
|
||||
@@ -21,8 +33,9 @@
|
||||
<div class="clock">
|
||||
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
|
||||
<span v-if="days > 0">{{ pad(days) }}:</span>
|
||||
<span>{{ pad(hours) }}</span>:
|
||||
<span>{{ pad(minutes) }}</span>:
|
||||
<span>{{ pad(hours) }}</span
|
||||
>: <span>{{ pad(minutes) }}</span
|
||||
>:
|
||||
<span>{{ pad(seconds) }}</span>
|
||||
</h2>
|
||||
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
||||
@@ -47,8 +60,8 @@ export default {
|
||||
props: {
|
||||
routes: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initialize(), this.countdown();
|
||||
@@ -65,10 +78,10 @@ export default {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleMenu(){
|
||||
toggleMenu() {
|
||||
this.isOpen = this.isOpen ? false : true;
|
||||
},
|
||||
pad: function(num) {
|
||||
@@ -91,10 +104,7 @@ export default {
|
||||
let nowDate = new Date();
|
||||
let now = nowDate.getTime();
|
||||
if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) {
|
||||
let _diff =
|
||||
(nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) *
|
||||
60 *
|
||||
-1;
|
||||
let _diff = (nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) * 60 * -1;
|
||||
nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff);
|
||||
}
|
||||
this.nextLottery = nextDayOfLottery;
|
||||
@@ -110,12 +120,8 @@ export default {
|
||||
|
||||
// Time calculations for days, hours, minutes and seconds
|
||||
this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24));
|
||||
this.hours = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
||||
);
|
||||
this.minutes = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60)) / (1000 * 60)
|
||||
);
|
||||
this.hours = Math.floor((this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
this.minutes = Math.floor((this.distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000);
|
||||
if (this.days == 7) {
|
||||
this.days = 0;
|
||||
@@ -125,7 +131,7 @@ export default {
|
||||
}
|
||||
this.interval = setTimeout(this.countdown, 500);
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<span class="logged-in-username" v-if="username">Logget inn som: <span class="username">{{ username }}</span> <button @click="removeUsername">Logg ut</button></span>
|
||||
<span class="logged-in-username" v-if="username"
|
||||
>Logget inn som: <span class="username">{{ username }}</span>
|
||||
<button @click="removeUsername">Logg ut</button></span
|
||||
>
|
||||
|
||||
<div class="history" ref="history" v-if="chatHistory.length > 0">
|
||||
<div class="opaque-skirt"></div>
|
||||
@@ -8,7 +11,8 @@
|
||||
<button @click="loadMoreHistory">Hent eldre meldinger</button>
|
||||
</div>
|
||||
|
||||
<div class="history-message"
|
||||
<div
|
||||
class="history-message"
|
||||
v-for="(history, index) in chatHistory"
|
||||
:key="`${history.username}-${history.timestamp}-${index}`"
|
||||
>
|
||||
@@ -61,12 +65,11 @@ export default {
|
||||
};
|
||||
},
|
||||
created() {
|
||||
getChatHistory(1, this.pageSize)
|
||||
.then(resp => {
|
||||
this.chatHistory = resp.messages;
|
||||
this.hasMorePages = resp.total != resp.messages.length;
|
||||
});
|
||||
const username = window.localStorage.getItem('username');
|
||||
getChatHistory(1, this.pageSize).then(resp => {
|
||||
this.chatHistory = resp.messages;
|
||||
this.hasMorePages = resp.total != resp.messages.length;
|
||||
});
|
||||
const username = window.localStorage.getItem("username");
|
||||
if (username) {
|
||||
this.username = username;
|
||||
this.emitUsernameOnConnect = true;
|
||||
@@ -77,8 +80,7 @@ export default {
|
||||
handler: function(newVal, oldVal) {
|
||||
if (oldVal.length == 0) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
else if (newVal && newVal.length == oldVal.length) {
|
||||
} else if (newVal && newVal.length == oldVal.length) {
|
||||
if (this.isScrollPositionAtBottom()) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
@@ -105,10 +107,7 @@ export default {
|
||||
});
|
||||
|
||||
this.socket.on("connect", msg => {
|
||||
if (
|
||||
this.emitUsernameOnConnect ||
|
||||
(this.wasDisconnected && this.username != null)
|
||||
) {
|
||||
if (this.emitUsernameOnConnect || (this.wasDisconnected && this.username != null)) {
|
||||
this.setUsername(this.username);
|
||||
}
|
||||
});
|
||||
@@ -133,12 +132,11 @@ export default {
|
||||
let { page, pageSize } = this;
|
||||
page = page + 1;
|
||||
|
||||
getChatHistory(page, pageSize)
|
||||
.then(resp => {
|
||||
this.chatHistory = resp.messages.concat(this.chatHistory);
|
||||
this.page = page;
|
||||
this.hasMorePages = resp.total != this.chatHistory.length;
|
||||
});
|
||||
getChatHistory(page, pageSize).then(resp => {
|
||||
this.chatHistory = resp.messages.concat(this.chatHistory);
|
||||
this.page = page;
|
||||
this.hasMorePages = resp.total != this.chatHistory.length;
|
||||
});
|
||||
},
|
||||
pad(num) {
|
||||
if (num > 9) return num;
|
||||
@@ -146,9 +144,7 @@ export default {
|
||||
},
|
||||
getTime(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
||||
date.getMinutes()
|
||||
)}:${this.pad(date.getSeconds())}`;
|
||||
const timeString = `${this.pad(date.getHours())}:${this.pad(date.getMinutes())}:${this.pad(date.getSeconds())}`;
|
||||
|
||||
if (date.getDate() == new Date().getDate()) {
|
||||
return timeString;
|
||||
@@ -158,10 +154,10 @@ export default {
|
||||
sendMessage() {
|
||||
const message = { message: this.message };
|
||||
this.socket.emit("chat", message);
|
||||
this.message = '';
|
||||
this.message = "";
|
||||
this.scrollToBottomOfHistory();
|
||||
},
|
||||
setUsername(username=undefined) {
|
||||
setUsername(username = undefined) {
|
||||
if (this.temporaryUsername) {
|
||||
username = this.temporaryUsername;
|
||||
}
|
||||
@@ -178,7 +174,7 @@ export default {
|
||||
if (history) {
|
||||
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
},
|
||||
scrollToBottomOfHistory() {
|
||||
setTimeout(() => {
|
||||
@@ -189,15 +185,15 @@ export default {
|
||||
scrollToMessageElement(message) {
|
||||
const elemTimestamp = this.getTime(message.timestamp);
|
||||
const self = this;
|
||||
const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText;
|
||||
const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp;
|
||||
const getTimeStamp = elem => elem.getElementsByClassName("timestamp")[0].innerText;
|
||||
const prevOldestMessageInNewList = elem => getTimeStamp(elem) == elemTimestamp;
|
||||
|
||||
setTimeout(() => {
|
||||
const { history } = self.$refs;
|
||||
const childrenElements = Array.from(history.getElementsByClassName('history-message'));
|
||||
const childrenElements = Array.from(history.getElementsByClassName("history-message"));
|
||||
|
||||
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
|
||||
history.scrollTop = elemInNewList.offsetTop - 70
|
||||
history.scrollTop = elemInNewList.offsetTop - 70;
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +206,7 @@ export default {
|
||||
|
||||
.chat-container {
|
||||
position: relative;
|
||||
transform: translate3d(0,0,0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -241,7 +237,6 @@ input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.history {
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
@@ -276,11 +271,7 @@ input {
|
||||
position: fixed;
|
||||
height: 2rem;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
white,
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
background: linear-gradient(to bottom, white, rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
& .fetch-older-history {
|
||||
@@ -310,7 +301,7 @@ input {
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2.1rem;
|
||||
left: 2rem;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/KevinMidboe/vinlottis" class="github">
|
||||
<span>Open-sourced at github</span>
|
||||
<img src="/public/assets/images/logo-github.png" alt="github logo">
|
||||
<span>Utforsk koden på github</span>
|
||||
<img src="/public/assets/images/logo-github.png" alt="github logo" />
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
</ul>
|
||||
|
||||
<router-link to="/" class="company-logo">
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo">
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
||||
</router-link>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WineFooter'
|
||||
}
|
||||
name: "WineFooter",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -57,7 +57,6 @@ footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
img {
|
||||
margin-left: 0.5rem;
|
||||
height: 30px;
|
||||
@@ -74,7 +73,7 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.company-logo{
|
||||
.company-logo {
|
||||
margin-right: 5em;
|
||||
|
||||
img {
|
||||
@@ -93,5 +92,4 @@ footer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
103
frontend/ui/FooterUnbranded.vue
Normal file
103
frontend/ui/FooterUnbranded.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<footer>
|
||||
<a href="https://github.com/KevinMidboe/vinlottis" class="github">
|
||||
<span>Utforsk koden på github</span>
|
||||
<img src="/public/assets/images/logo-github.png" alt="github logo" />
|
||||
</a>
|
||||
|
||||
<a href="mailto:questions@vinlottis.no" class="mail">
|
||||
<span class="vin-link">questions@vinlottis.no</span>
|
||||
</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "WineFooter",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
height: 75px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f4f4f4;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
||||
> *:first-of-type {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
> *:last-of-type {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
> *:first-of-type {
|
||||
margin-left: 4rem;
|
||||
}
|
||||
> *:last-of-type {
|
||||
margin-right: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin-left: 5rem;
|
||||
|
||||
li:not(:first-of-type) {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $matte-text-color;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
margin-left: 0.5rem;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.mail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
margin-left: 0.5rem;
|
||||
height: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.company-logo {
|
||||
margin-right: 5em;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
$margin: 1rem;
|
||||
ul {
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
.company-logo {
|
||||
margin-right: $margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +1,48 @@
|
||||
<template>
|
||||
<div class="highscores" v-if="highscore.length > 0">
|
||||
|
||||
<section class="heading">
|
||||
<h3>
|
||||
Topp 5 vinnere
|
||||
Topp vinnere
|
||||
</h3>
|
||||
<router-link to="highscore" class="">
|
||||
<span class="vin-link">Se alle vinnere</span>
|
||||
</router-link>
|
||||
</section>
|
||||
|
||||
<ol class="winner-list-container">
|
||||
<li v-for="(person, index) in highscore" :key="person._id" class="single-winner">
|
||||
<span class="placement">{{index + 1}}.</span>
|
||||
<i class="icon icon--medal"></i>
|
||||
<p class="winner-name">{{ person.name }}</p>
|
||||
<li v-for="(person, index) in highscore" :key="person._id">
|
||||
<router-link :to="`/highscore/${person.name}`" class="single-winner">
|
||||
<span class="placement">{{ index + 1 }}.</span>
|
||||
<i class="icon icon--medal"></i>
|
||||
<p class="winner-name">{{ person.name }}</p>
|
||||
</router-link>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highscoreStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { highscore: [] };
|
||||
return {
|
||||
highscore: [],
|
||||
limit: 22
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await highscoreStatistics();
|
||||
response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1)
|
||||
this.highscore = this.generateScoreBoard(response.slice(0, 5));
|
||||
},
|
||||
methods: {
|
||||
generateScoreBoard(highscore=this.highscore) {
|
||||
let place = 0;
|
||||
let highestWinCount = -1;
|
||||
|
||||
return highscore.map(win => {
|
||||
const wins = win.wins.length
|
||||
if (wins != highestWinCount) {
|
||||
place += 1
|
||||
highestWinCount = wins
|
||||
}
|
||||
|
||||
const placeString = place.toString().padStart(2, "0");
|
||||
win.rank = placeString;
|
||||
return win
|
||||
})
|
||||
}
|
||||
return fetch(`/api/history/by-wins?limit=${this.limit}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
this.highscore = response.winners;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -81,8 +69,8 @@ ol {
|
||||
|
||||
.winner-list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr));
|
||||
gap: 5%;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12em, 1fr));
|
||||
gap: 2rem;
|
||||
|
||||
.single-winner {
|
||||
box-sizing: border-box;
|
||||
@@ -91,7 +79,7 @@ ol {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
padding: 1em;
|
||||
|
||||
i {
|
||||
font-size: 3em;
|
||||
@@ -110,11 +98,71 @@ ol {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.winner-count {
|
||||
grid-row: 3;
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.winner-icon {
|
||||
grid-row: 1;
|
||||
grid-column: 3;
|
||||
}
|
||||
}
|
||||
|
||||
// I'm sorry mama
|
||||
@media (max-width: 550px) {
|
||||
*:nth-child(n + 7) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1295px) {
|
||||
*:nth-child(n + 7) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1630px) {
|
||||
*:nth-child(n + 9) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1968px) {
|
||||
*:nth-child(n + 11) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 2300px) {
|
||||
*:nth-child(n + 13) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 2645px) {
|
||||
*:nth-child(n + 15) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 2975px) {
|
||||
*:nth-child(n + 17) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 3311px) {
|
||||
*:nth-child(n + 19) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 3647px) {
|
||||
*:nth-child(n + 21) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,89 +2,47 @@
|
||||
<div class="chart">
|
||||
<canvas ref="purchase-chart" width="100" height="50"></canvas>
|
||||
<div ref="chartjsLegend" class="chartjsLegend"></div>
|
||||
<div class="year-select" v-if="years.length">
|
||||
<button
|
||||
class="vin-button small"
|
||||
v-for="year in years"
|
||||
:class="{ active: yearSelected == year }"
|
||||
@click="yearFilterClicked(year)"
|
||||
>
|
||||
{{ year }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chartjs from "chart.js";
|
||||
import { chartPurchaseByColor } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
lotteries: [],
|
||||
years: [],
|
||||
yearSelected: undefined,
|
||||
chart: undefined
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let canvas = this.$refs["purchase-chart"].getContext("2d");
|
||||
|
||||
let response = await chartPurchaseByColor();
|
||||
let labels = [];
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
borderColor: "#57d2fb",
|
||||
backgroundColor: "#d4f2fe",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let yellow = {
|
||||
label: "Gul",
|
||||
borderColor: "#ffde5d",
|
||||
backgroundColor: "#fff6d6",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let red = {
|
||||
label: "Rød",
|
||||
borderColor: "#ef5878",
|
||||
backgroundColor: "#fbd7de",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let green = {
|
||||
label: "Grønn",
|
||||
borderColor: "#10e783",
|
||||
backgroundColor: "#c8f9df",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
this.lotteries = await this.chartPurchaseByColor();
|
||||
if (this.lotteries?.length) this.years = [...new Set(this.lotteries.map(lot => lot.date.slice(0, 4)))];
|
||||
|
||||
const dataset = this.calculateChartDatapoints();
|
||||
|
||||
let chartData = {
|
||||
labels: dataset.labels,
|
||||
datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
|
||||
};
|
||||
|
||||
if (response.length == 1) {
|
||||
labels.push("");
|
||||
blue.data.push(0);
|
||||
yellow.data.push(0);
|
||||
red.data.push(0);
|
||||
green.data.push(0);
|
||||
}
|
||||
|
||||
let highestNumber = 0;
|
||||
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
let thisDate = response[i];
|
||||
let dateObject = new Date(thisDate.date);
|
||||
labels.push(this.getPrettierDateString(dateObject));
|
||||
|
||||
blue.data.push(thisDate.blue);
|
||||
yellow.data.push(thisDate.yellow);
|
||||
red.data.push(thisDate.red);
|
||||
green.data.push(thisDate.green);
|
||||
|
||||
if (thisDate.blue > highestNumber) {
|
||||
highestNumber = thisDate.blue;
|
||||
}
|
||||
if (thisDate.yellow > highestNumber) {
|
||||
highestNumber = thisDate.yellow;
|
||||
}
|
||||
if (thisDate.green > highestNumber) {
|
||||
highestNumber = thisDate.green;
|
||||
}
|
||||
if (thisDate.red > highestNumber) {
|
||||
highestNumber = thisDate.red;
|
||||
}
|
||||
}
|
||||
let datasets = [blue, yellow, green, red];
|
||||
let chartdata = {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
let chart = new Chart(canvas, {
|
||||
this.chart = new Chart(canvas, {
|
||||
type: "line",
|
||||
data: chartdata,
|
||||
data: chartData,
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
@@ -110,8 +68,7 @@ export default {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
suggestedMax: highestNumber + 5
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -120,10 +77,82 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async yearFilterClicked(year) {
|
||||
this.yearSelected = this.yearSelected === year ? null : year;
|
||||
|
||||
this.lotteries = await this.chartPurchaseByColor();
|
||||
const dataset = this.calculateChartDatapoints();
|
||||
let chartData = {
|
||||
labels: dataset.labels,
|
||||
datasets: [dataset.blue, dataset.green, dataset.red, dataset.yellow]
|
||||
};
|
||||
|
||||
this.chart.data = chartData;
|
||||
this.chart.update();
|
||||
},
|
||||
setupDataset() {
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
borderColor: "#57d2fb",
|
||||
backgroundColor: "#d4f2fe",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let yellow = {
|
||||
label: "Gul",
|
||||
borderColor: "#ffde5d",
|
||||
backgroundColor: "#fff6d6",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let red = {
|
||||
label: "Rød",
|
||||
borderColor: "#ef5878",
|
||||
backgroundColor: "#fbd7de",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let green = {
|
||||
label: "Grønn",
|
||||
borderColor: "#10e783",
|
||||
backgroundColor: "#c8f9df",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
|
||||
return {
|
||||
labels: [""],
|
||||
blue,
|
||||
green,
|
||||
red,
|
||||
yellow
|
||||
};
|
||||
},
|
||||
calculateChartDatapoints() {
|
||||
let dataset = this.setupDataset();
|
||||
|
||||
this.lotteries.map(lottery => {
|
||||
const date = new Date(lottery.date);
|
||||
dataset.labels.push(this.getPrettierDateString(date));
|
||||
|
||||
dataset.blue.data.push(lottery.blue);
|
||||
dataset.green.data.push(lottery.green);
|
||||
dataset.red.data.push(lottery.red);
|
||||
dataset.yellow.data.push(lottery.yellow);
|
||||
});
|
||||
|
||||
return dataset;
|
||||
},
|
||||
chartPurchaseByColor() {
|
||||
const url = new URL("/api/lotteries", window.location);
|
||||
if (this.yearSelected != null) url.searchParams.set("year", this.yearSelected);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.lotteries);
|
||||
},
|
||||
getPrettierDateString(date) {
|
||||
return `${this.pad(date.getDate())}.${this.pad(
|
||||
date.getMonth() + 1
|
||||
)}.${this.pad(date.getYear() - 100)}`;
|
||||
return `${this.pad(date.getDate())}.${this.pad(date.getMonth() + 1)}.${this.pad(date.getYear() - 100)}`;
|
||||
},
|
||||
pad(num) {
|
||||
if (num < 10) {
|
||||
@@ -136,11 +165,22 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
|
||||
@include desktop {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.year-select {
|
||||
margin-top: 1rem;
|
||||
|
||||
button:not(:first-of-type) {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
<div class="container">
|
||||
<div class="input-line">
|
||||
<label for="redCheckbox">
|
||||
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Rød</span>
|
||||
</label>
|
||||
<label for="blueCheckbox">
|
||||
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Blå</span>
|
||||
</label>
|
||||
<label for="yellowCheckbox">
|
||||
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Gul</span>
|
||||
</label>
|
||||
<label for="greenCheckbox">
|
||||
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors"/>
|
||||
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors" />
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
@@ -31,15 +31,10 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Antall lodd"
|
||||
@keyup.enter="generateColors"
|
||||
v-model="numberOfRaffles"
|
||||
/>
|
||||
<input type="number" placeholder="Antall lodd" @keyup.enter="generateColors" v-model="numberOfRaffles" />
|
||||
<button class="vin-button" @click="generateColors">Generer</button>
|
||||
</div>
|
||||
<div class="colors">
|
||||
<div class="colors" :class="{ compact }">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:class="getColorClass(color)"
|
||||
@@ -47,13 +42,6 @@
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="color-count-container" v-if="generated">
|
||||
<span>Rød: {{ red }}</span>
|
||||
<span>Blå: {{ blue }}</span>
|
||||
<span>Gul: {{ yellow }}</span>
|
||||
<span>Grønn: {{ green }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -64,11 +52,15 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
numberOfRaffles: 4,
|
||||
numberOfRaffles: 6,
|
||||
colors: [],
|
||||
blue: 0,
|
||||
red: 0,
|
||||
@@ -101,18 +93,21 @@ export default {
|
||||
if (time == 5) {
|
||||
this.generating = false;
|
||||
this.generated = true;
|
||||
if (this.numberOfRaffles > 1 &&
|
||||
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) {
|
||||
return
|
||||
if (
|
||||
this.numberOfRaffles > 1 &&
|
||||
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true)
|
||||
.length == 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new Set(this.colors).size == 1) {
|
||||
alert("BINGO");
|
||||
this.$toast.info({ title: "BINGO" });
|
||||
}
|
||||
|
||||
this.emitColors()
|
||||
this.emitColors();
|
||||
|
||||
window.ga('send', {
|
||||
window.ga("send", {
|
||||
hitType: "event",
|
||||
eventCategory: "Raffles",
|
||||
eventAction: "Generate",
|
||||
@@ -147,8 +142,7 @@ export default {
|
||||
}
|
||||
if (this.numberOfRaffles > 0) {
|
||||
for (let i = 0; i < this.numberOfRaffles; i++) {
|
||||
let color =
|
||||
randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||
let color = randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||
this.colors.push(color);
|
||||
if (color == 1) {
|
||||
this.red += 1;
|
||||
@@ -201,12 +195,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/global.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.container {
|
||||
margin: auto;
|
||||
// margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -282,6 +276,15 @@ label .text {
|
||||
max-width: 1400px;
|
||||
margin: 3rem auto 0;
|
||||
|
||||
&.compact {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
> .color-box {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
}
|
||||
@@ -309,20 +312,6 @@ label .text {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.color-count-container {
|
||||
margin: auto;
|
||||
width: 300px;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-family: Arial;
|
||||
margin-top: 35px;
|
||||
|
||||
@include mobile {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex justify-end">
|
||||
<div class="requested-count cursor-pointer" @click="request">
|
||||
<span>{{ requestedElement.count }}</span>
|
||||
<i class="icon icon--heart" :class="{ 'active': locallyRequested }" />
|
||||
<i class="icon icon--heart" :class="{ active: locallyRequested }" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,10 +17,9 @@
|
||||
|
||||
<template v-slot:bottom>
|
||||
<div class="float-left request">
|
||||
<i class="icon icon--heart request-icon" :class="{ 'active': locallyRequested }"></i>
|
||||
<a aria-role="button" tabindex="0" class="link" @click="request"
|
||||
:class="{ 'active': locallyRequested }">
|
||||
{{ locallyRequested ? 'Anbefalt' : 'Anbefal' }}
|
||||
<i class="icon icon--heart request-icon" :class="{ active: locallyRequested }"></i>
|
||||
<a aria-role="button" tabindex="0" class="link" @click="request" :class="{ active: locallyRequested }">
|
||||
{{ locallyRequested ? "Anbefalt" : "Anbefal" }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,14 +34,14 @@ export default {
|
||||
components: {
|
||||
Wine
|
||||
},
|
||||
data(){
|
||||
data() {
|
||||
return {
|
||||
wine: this.requestedElement.wine,
|
||||
locallyRequested: false
|
||||
}
|
||||
};
|
||||
},
|
||||
props: {
|
||||
requestedElement: {
|
||||
requestedElement: {
|
||||
required: true,
|
||||
type: Object
|
||||
},
|
||||
@@ -53,27 +52,26 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
request(){
|
||||
if (this.locallyRequested)
|
||||
return
|
||||
console.log("requesting", this.wine)
|
||||
this.locallyRequested = true
|
||||
this.requestedElement.count = this.requestedElement.count +1
|
||||
requestNewWine(this.wine)
|
||||
request() {
|
||||
if (this.locallyRequested) return;
|
||||
|
||||
this.locallyRequested = true;
|
||||
this.requestedElement.count = this.requestedElement.count + 1;
|
||||
requestNewWine(this.wine);
|
||||
},
|
||||
async deleteWine() {
|
||||
const wine = this.wine
|
||||
const wine = this.wine;
|
||||
if (window.confirm("Er du sikker på at du vil slette vinen?")) {
|
||||
let response = await deleteRequestedWine(wine);
|
||||
if (response['success'] == true) {
|
||||
this.$emit('wineDeleted', wine);
|
||||
if (response["success"] == true) {
|
||||
this.$emit("wineDeleted", wine);
|
||||
} else {
|
||||
alert("Klarte ikke slette vinen");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -83,7 +81,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
background-color: rgb(244,244,244);
|
||||
background-color: rgb(244, 244, 244);
|
||||
border-radius: 1.1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
font-size: 1.25em;
|
||||
@@ -93,14 +91,14 @@ export default {
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.icon--heart{
|
||||
.icon--heart {
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
&.link {
|
||||
border-color: $link-color
|
||||
border-color: $link-color;
|
||||
}
|
||||
|
||||
&.icon--heart {
|
||||
@@ -121,4 +119,4 @@ export default {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="camera-stream">
|
||||
<h2 v-if="errorMessage">{{ errorMessage }}</h2>
|
||||
<video playsinline autoplay class="hidden"></video>
|
||||
</div>
|
||||
@@ -47,13 +47,8 @@ export default {
|
||||
this.searchVideoForBarcode(this.video);
|
||||
},
|
||||
handleError(error) {
|
||||
console.log(
|
||||
"navigator.MediaDevices.getUserMedia error: ",
|
||||
error.message,
|
||||
error.name
|
||||
);
|
||||
this.errorMessage =
|
||||
"Feil ved oppstart av kamera! Feilmelding: " + error.message;
|
||||
console.log("navigator.MediaDevices.getUserMedia error: ", error.message, error.name);
|
||||
this.errorMessage = "Feil ved oppstart av kamera! Feilmelding: " + error.message;
|
||||
},
|
||||
searchVideoForBarcode(video) {
|
||||
const codeReader = new BrowserBarcodeReader();
|
||||
@@ -84,10 +79,7 @@ export default {
|
||||
this.errorMessage = "Feil! " + error.message || error;
|
||||
},
|
||||
scrollIntoView() {
|
||||
window.scrollTo(
|
||||
0,
|
||||
document.getElementById("addwine-title").offsetTop - 10
|
||||
);
|
||||
window.scrollTo(0, document.getElementById("camera-stream").offsetTop - 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -112,4 +104,4 @@ h2 {
|
||||
text-align: center;
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,51 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tab-container">
|
||||
<div
|
||||
<nav class="tab-container">
|
||||
<a
|
||||
class="tab"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="changeTab(index)"
|
||||
@keydown.enter="changeTab(index)"
|
||||
tabindex="0"
|
||||
:class="chosenTab == index ? 'active' : null"
|
||||
>{{ tab.name }}</div>
|
||||
</div>
|
||||
>
|
||||
{{ tab.name }}
|
||||
|
||||
<span v-if="tab.counter" class="counter">{{ tab.counter }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="tab-elements">
|
||||
<component :is="tabs[chosenTab].component" />
|
||||
<component :is="tabs[chosenTab].component" @counter="updateCounter" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventBus from "@/mixins/EventBus";
|
||||
export default {
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array
|
||||
},
|
||||
active: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.chosenTab = this.active;
|
||||
const url = location.href;
|
||||
|
||||
if (url.includes("tab=")) {
|
||||
const tabParameter = url.split("tab=")[1];
|
||||
const matchingSlug = this.tabs.findIndex(tab => tab.slug == tabParameter);
|
||||
console.log("matchingSlug:", matchingSlug);
|
||||
this.chosenTab = matchingSlug;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chosenTab: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeTab() {
|
||||
return this.tabs[this.chosenTab];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeTab: function(num) {
|
||||
changeTab(num) {
|
||||
this.chosenTab = num;
|
||||
this.$emit("tabChange", num);
|
||||
eventBus.$emit("tab-change");
|
||||
|
||||
let url = location.href;
|
||||
const tabParameterIndex = url.indexOf("tab=");
|
||||
|
||||
if (tabParameterIndex > 0) {
|
||||
url = url.split("tab=")[0] + `tab=${this.activeTab.slug}`;
|
||||
} else {
|
||||
url = url + `?tab=${this.activeTab.slug}`;
|
||||
}
|
||||
|
||||
window.history.pushState({}, "", url);
|
||||
},
|
||||
updateCounter(val) {
|
||||
this.activeTab.counter = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -54,28 +83,50 @@ h1 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 25px;
|
||||
border-bottom: 1px solid #333333;
|
||||
// margin-top: 25px;
|
||||
border-bottom: 1px solid var(--underlinenav-text);
|
||||
|
||||
margin-top: 2rem;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin: 0 15px;
|
||||
border: 1px solid #333333;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
background: #00000008;
|
||||
border-bottom: 1px solid #333333;
|
||||
margin-bottom: -1px;
|
||||
font-size: 1.1rem;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: rgba($matte-text-color, 0.9);
|
||||
|
||||
&.active {
|
||||
border-bottom: 1px solid white;
|
||||
color: $matte-text-color;
|
||||
border-color: var(--underlinenav-text-active) !important;
|
||||
background: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--underlinenav-text-hover);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
& .counter {
|
||||
margin-left: 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
padding: 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
background-color: rgba(209, 213, 218, 0.5);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="update-toast" :class="showClass">
|
||||
<span v-html="text"></span>
|
||||
<div class="button-container">
|
||||
<button @click="closeToast">Lukk</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: { type: String, required: true },
|
||||
refreshButton: { type: Boolean, required: false }
|
||||
},
|
||||
data() {
|
||||
return { showClass: null };
|
||||
},
|
||||
created() {
|
||||
this.showClass = "show";
|
||||
},
|
||||
mounted() {
|
||||
if (this.refreshButton) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$emit("closeToast");
|
||||
}, 5000);
|
||||
},
|
||||
methods: {
|
||||
refresh: function() {
|
||||
location.reload();
|
||||
},
|
||||
closeToast: function() {
|
||||
this.$emit("closeToast");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.update-toast {
|
||||
position: fixed;
|
||||
bottom: 1.3rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
background: #2d2d2d;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 80vw;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.show {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
-moz-transition: opacity 0.5s ease-in-out;
|
||||
-ms-transition: opacity 0.5s ease-in-out;
|
||||
-o-transition: opacity 0.5s ease-in-out;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
@include mobile {
|
||||
width: 85vw;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
& span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
& .button-container {
|
||||
& button {
|
||||
color: #2d2d2d;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 0 3px;
|
||||
font-size: 0.8rem;
|
||||
height: max-content;
|
||||
|
||||
&:active {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +1,30 @@
|
||||
<template>
|
||||
<section class="outer-bought">
|
||||
<div>
|
||||
<h3>Loddstatistikk</h3>
|
||||
|
||||
<div class="total-raffles">
|
||||
Totalt
|
||||
<span class="total">{{ total }}</span>
|
||||
kjøpte,
|
||||
<span>{{ totalWin }} vinn og </span>
|
||||
<span> {{ stolen }} stjålet </span>
|
||||
Totalt
|
||||
<span class="total">{{ total }}</span>
|
||||
kjøpte,
|
||||
<span>{{ totalWin }} vinn og </span>
|
||||
<span> {{ stolen }} stjålet </span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bought-container">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:class="
|
||||
color.name +
|
||||
'-container ' +
|
||||
color.name +
|
||||
'-raffle raffle-element-local'
|
||||
"
|
||||
:class="color.name + '-container ' + color.name + '-raffle raffle-element-local'"
|
||||
:key="color.name"
|
||||
>
|
||||
<p class="winner-chance">
|
||||
{{translate(color.name)}} vinnersjanse
|
||||
</p>
|
||||
>
|
||||
<p class="winner-chance">{{ translate(color.name) }} vinnersjanse</p>
|
||||
<span class="win-percentage">{{ color.totalPercentage }}% </span>
|
||||
<p class="total-bought-color">{{ color.total }} kjøpte</p>
|
||||
<p class="amount-of-wins"> {{ color.win }} vinn </p>
|
||||
<p class="amount-of-wins">{{ color.win }} vinn</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { colorStatistics } from "@/api";
|
||||
|
||||
@@ -45,109 +38,128 @@ export default {
|
||||
green: 0,
|
||||
total: 0,
|
||||
totalWin: 0,
|
||||
stolen: 0,
|
||||
wins: 0,
|
||||
redPercentage: 0,
|
||||
yellowPercentage: 0,
|
||||
greenPercentage: 0,
|
||||
bluePercentage: 0
|
||||
stolen: 0
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await colorStatistics();
|
||||
|
||||
this.red = response.red;
|
||||
this.blue = response.blue;
|
||||
this.green = response.green;
|
||||
this.yellow = response.yellow;
|
||||
this.total = response.total;
|
||||
|
||||
this.totalWin =
|
||||
this.red.win + this.yellow.win + this.blue.win + this.green.win;
|
||||
this.stolen = response.stolen;
|
||||
|
||||
this.redPercentage = this.round(
|
||||
this.red.win == 0 ? 0 : (this.red.win / this.totalWin) * 100
|
||||
);
|
||||
this.greenPercentage = this.round(
|
||||
this.green.win == 0 ? 0 : (this.green.win / this.totalWin) * 100
|
||||
);
|
||||
this.bluePercentage = this.round(
|
||||
this.blue.win == 0 ? 0 : (this.blue.win / this.totalWin) * 100
|
||||
);
|
||||
this.yellowPercentage = this.round(
|
||||
this.yellow.win == 0 ? 0 : (this.yellow.win / this.totalWin) * 100
|
||||
);
|
||||
|
||||
this.colors.push({
|
||||
name: "red",
|
||||
total: this.red.total,
|
||||
win: this.red.win,
|
||||
totalPercentage: this.getPercentage(this.red.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.red.win, this.red.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "blue",
|
||||
total: this.blue.total,
|
||||
win: this.blue.win,
|
||||
totalPercentage: this.getPercentage(this.blue.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.blue.win, this.blue.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "yellow",
|
||||
total: this.yellow.total,
|
||||
win: this.yellow.win,
|
||||
totalPercentage: this.getPercentage(this.yellow.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.yellow.win, this.yellow.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "green",
|
||||
total: this.green.total,
|
||||
win: this.green.win,
|
||||
totalPercentage: this.getPercentage(this.green.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.green.win, this.green.total)
|
||||
});
|
||||
|
||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||
this.allLotteries().then(this.computeColors);
|
||||
},
|
||||
methods: {
|
||||
translate(color){
|
||||
switch(color) {
|
||||
allLotteries() {
|
||||
return fetch("/api/lotteries?includeWinners=true")
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.lotteries);
|
||||
},
|
||||
translate(color) {
|
||||
switch (color) {
|
||||
case "blue":
|
||||
return "Blå"
|
||||
return "Blå";
|
||||
break;
|
||||
case "red":
|
||||
return "Rød"
|
||||
return "Rød";
|
||||
break;
|
||||
case "green":
|
||||
return "Grønn"
|
||||
return "Grønn";
|
||||
break;
|
||||
case "yellow":
|
||||
return "Gul"
|
||||
return "Gul";
|
||||
break;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
},
|
||||
getPercentage: function(win, total) {
|
||||
return this.round(win == 0 ? 0 : (win / total) * 100);
|
||||
},
|
||||
round: function(number) {
|
||||
|
||||
//this can make the odds added together more than 100%, maybe rework?
|
||||
let actualPercentage = Math.round(number * 100) / 100;
|
||||
let rounded = actualPercentage.toFixed(0);
|
||||
return rounded;
|
||||
},
|
||||
computeColors(lotteries) {
|
||||
let totalRed = 0;
|
||||
let totalGreen = 0;
|
||||
let totalYellow = 0;
|
||||
let totalBlue = 0;
|
||||
let total = 0;
|
||||
let stolen = 0;
|
||||
|
||||
const colorAccumulatedWins = {
|
||||
blue: 0,
|
||||
green: 0,
|
||||
red: 0,
|
||||
yellow: 0
|
||||
};
|
||||
|
||||
const accumelatedColors = (winners, colorAccumulatedWins) => {
|
||||
winners.forEach(winner => {
|
||||
const winnerColor = winner.color;
|
||||
colorAccumulatedWins[winnerColor] += 1;
|
||||
});
|
||||
};
|
||||
|
||||
lotteries.forEach(lottery => {
|
||||
totalRed += lottery.red;
|
||||
totalGreen += lottery.green;
|
||||
totalYellow += lottery.yellow;
|
||||
totalBlue += lottery.blue;
|
||||
total += lottery.bought;
|
||||
stolen += lottery.stolen;
|
||||
|
||||
accumelatedColors(lottery.winners, colorAccumulatedWins);
|
||||
});
|
||||
|
||||
this.red = totalRed;
|
||||
this.yellow = totalYellow;
|
||||
this.green = totalGreen;
|
||||
this.blue = totalBlue;
|
||||
this.total = total;
|
||||
|
||||
this.totalWin =
|
||||
colorAccumulatedWins.red + colorAccumulatedWins.yellow + colorAccumulatedWins.blue + colorAccumulatedWins.green;
|
||||
this.stolen = stolen;
|
||||
|
||||
this.colors.push({
|
||||
name: "red",
|
||||
total: totalRed,
|
||||
win: colorAccumulatedWins.red,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.red, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.red, this.red.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "blue",
|
||||
total: totalBlue,
|
||||
win: colorAccumulatedWins.blue,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.blue, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.blue, this.blue.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "yellow",
|
||||
total: totalYellow,
|
||||
win: colorAccumulatedWins.yellow,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.yellow, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.yellow, this.yellow.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "green",
|
||||
total: totalGreen,
|
||||
win: colorAccumulatedWins.green,
|
||||
totalPercentage: this.getPercentage(colorAccumulatedWins.green, this.totalWin),
|
||||
percentage: this.getPercentage(colorAccumulatedWins.green, this.green.total)
|
||||
});
|
||||
|
||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "../styles/global.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/global.scss";
|
||||
|
||||
@include mobile{
|
||||
@include mobile {
|
||||
section {
|
||||
margin-top: 5em;
|
||||
}
|
||||
@@ -182,7 +194,7 @@ export default {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&.total-bought-color{
|
||||
&.total-bought-color {
|
||||
font-weight: bold;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="vipps-container"
|
||||
:class="isMobile ? 'clickable' : null"
|
||||
@click="openVipps"
|
||||
>
|
||||
<img
|
||||
src="/public/assets/images/vipps-logo.svg"
|
||||
class="vipps-logo"
|
||||
alt="vipps logo"
|
||||
/>
|
||||
<div class="page-container">
|
||||
<div class="vipps-container" :class="isMobile ? 'clickable' : null" @click="openVipps">
|
||||
<img src="/public/assets/images/vipps-logo.svg" class="vipps-logo" alt="vipps logo" />
|
||||
<span v-if="amount * price > price">
|
||||
kr.
|
||||
<span class="big-money">{{ amount * price }},-</span>
|
||||
@@ -20,11 +12,7 @@
|
||||
<span class="big-money">{{ amount * price }},-</span>
|
||||
pr. lodd
|
||||
</span>
|
||||
<ing
|
||||
src="/public/assets/images/vipps-qr.png"
|
||||
class="qr-logo"
|
||||
v-if="qrFailed"
|
||||
/>
|
||||
<ing src="/public/assets/images/vipps-qr.png" class="qr-logo" v-if="qrFailed" />
|
||||
<canvas v-if="!qrFailed" ref="canvas" class="qr-logo"></canvas>
|
||||
<span class="phone-number">{{ phone }}</span>
|
||||
<span class="name">{{ name }}</span>
|
||||
@@ -72,38 +60,24 @@ export default {
|
||||
return this.amount * (this.price * 100);
|
||||
},
|
||||
vippsUrlBasedOnUserAgent: function() {
|
||||
if (navigator.userAgent.includes("iPhone")) {
|
||||
return (
|
||||
"https://qr.vipps.no/28/2/01/031/47" +
|
||||
this.phone.replace(/ /g, "") +
|
||||
"?v=1&m=" +
|
||||
this.message +
|
||||
"&a=" +
|
||||
this.priceToPay
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
"https://qr.vipps.no/28/2/01/031/47" +
|
||||
this.phone.replace(/ /g, "") +
|
||||
"?v=1&m=" +
|
||||
this.message
|
||||
this.message +
|
||||
"&a=" +
|
||||
this.priceToPay
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateQr: function() {
|
||||
let canvas = this.$refs["canvas"];
|
||||
QRCode.toCanvas(
|
||||
canvas,
|
||||
this.vippsUrlBasedOnUserAgent,
|
||||
{ errorCorrectionLevel: "Q" },
|
||||
(err, url) => {
|
||||
if (err != null) {
|
||||
this.qrFailed = true;
|
||||
}
|
||||
QRCode.toCanvas(canvas, this.vippsUrlBasedOnUserAgent, { errorCorrectionLevel: "Q" }, (err, url) => {
|
||||
if (err != null) {
|
||||
this.qrFailed = true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.drawLogoOverCanvas(canvas);
|
||||
},
|
||||
@@ -148,8 +122,17 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/global.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.page-container {
|
||||
@include mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.vipps-container {
|
||||
font-family: Arial;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { chartWinsByColor } from "@/api";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
fetchWinsByColor() {
|
||||
return fetch("/api/history/by-color").then(resp => resp.json());
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
let canvas = this.$refs["win-chart"].getContext("2d");
|
||||
|
||||
let response = await chartWinsByColor();
|
||||
let response = await this.fetchWinsByColor();
|
||||
const { colors } = response;
|
||||
let labels = ["Vunnet"];
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
@@ -42,23 +46,26 @@ export default {
|
||||
data: []
|
||||
};
|
||||
|
||||
blue.data.push(response.blue.win);
|
||||
yellow.data.push(response.yellow.win);
|
||||
red.data.push(response.red.win);
|
||||
green.data.push(response.green.win);
|
||||
const findColorWinners = (colorSelect, colors) => {
|
||||
return colors.filter(color => color.color == colorSelect)[0].count;
|
||||
};
|
||||
|
||||
const blueWinCount = findColorWinners("blue", colors);
|
||||
const redWinCount = findColorWinners("red", colors);
|
||||
const greenWinCount = findColorWinners("green", colors);
|
||||
const yellowWinCount = findColorWinners("yellow", colors);
|
||||
|
||||
blue.data.push(blueWinCount);
|
||||
red.data.push(redWinCount);
|
||||
green.data.push(greenWinCount);
|
||||
yellow.data.push(yellowWinCount);
|
||||
|
||||
let highestNumber = 0;
|
||||
if (response.blue.win > highestNumber) {
|
||||
highestNumber = response.blue.win;
|
||||
}
|
||||
if (response.red.win > highestNumber) {
|
||||
highestNumber = response.red.win;
|
||||
}
|
||||
if (response.green.win > highestNumber) {
|
||||
highestNumber = response.green.win;
|
||||
}
|
||||
if (response.yellow.win > highestNumber) {
|
||||
highestNumber = response.yellow.win;
|
||||
}
|
||||
[blueWinCount, redWinCount, greenWinCount, greenWinCount].forEach(winCount => {
|
||||
if (winCount > highestNumber) {
|
||||
highestNumber = winCount;
|
||||
}
|
||||
});
|
||||
|
||||
let datasets = [blue, yellow, green, red];
|
||||
let chartdata = {
|
||||
@@ -102,8 +109,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
<div class="wine">
|
||||
<slot name="top"></slot>
|
||||
<div class="wine-image">
|
||||
<img
|
||||
v-if="wine.image && loadImage"
|
||||
:src="wine.image"
|
||||
/>
|
||||
<img v-if="wine.image && loadImage" :src="wine.image" />
|
||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||
</div>
|
||||
|
||||
@@ -38,7 +35,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loadImage: false
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setImage(entries) {
|
||||
@@ -53,7 +50,7 @@ export default {
|
||||
this.observer = new IntersectionObserver(this.setImage, {
|
||||
root: this.$el,
|
||||
threshold: 0
|
||||
})
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.observer.observe(this.$el);
|
||||
@@ -66,16 +63,17 @@ export default {
|
||||
@import "@/styles/variables";
|
||||
|
||||
.wine {
|
||||
align-self: flex-start;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
|
||||
@include tablet {
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,19 +83,18 @@ export default {
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
height: 250px;
|
||||
height: 280px;
|
||||
@include mobile {
|
||||
object-fit: cover;
|
||||
max-width: 90px;
|
||||
}
|
||||
}
|
||||
.wine-placeholder {
|
||||
height: 250px;
|
||||
height: 280px;
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.wine-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -107,7 +104,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.wine-name{
|
||||
.wine-name {
|
||||
font-size: 20px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
@@ -120,6 +117,7 @@ export default {
|
||||
.bottom-section {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
align-self: flex-end;
|
||||
|
||||
.link {
|
||||
color: $matte-text-color;
|
||||
@@ -135,4 +133,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
<div v-if="wines.length > 0" class="wines-main-container">
|
||||
<div class="info-and-link">
|
||||
<h3>
|
||||
Topp 5 viner
|
||||
Topp viner
|
||||
</h3>
|
||||
<router-link to="viner">
|
||||
<span class="vin-link">Se alle viner </span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="wine-container">
|
||||
<div class="wines-container">
|
||||
<Wine v-for="wine in wines" :key="wine" :wine="wine">
|
||||
<template v-slot:top>
|
||||
<div class="flex justify-end">
|
||||
<div class="requested-count cursor-pointer">
|
||||
<span> {{ wine.occurences }} </span>
|
||||
<span> {{ wine.occurences }} </span>
|
||||
<i class="icon icon--heart" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,32 +32,36 @@ export default {
|
||||
Wine
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
clickedWine: null,
|
||||
return {
|
||||
wines: [],
|
||||
clickedWine: null,
|
||||
limit: 18
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await overallWineStatistics();
|
||||
|
||||
response.sort();
|
||||
response = response
|
||||
.filter(wine => wine.name != null && wine.name != "")
|
||||
.sort(
|
||||
this.predicate(
|
||||
{
|
||||
name: "occurences",
|
||||
reverse: true
|
||||
},
|
||||
{
|
||||
name: "rating",
|
||||
reverse: true
|
||||
}
|
||||
)
|
||||
);
|
||||
this.wines = response.slice(0, 5);
|
||||
this.getAllWines();
|
||||
},
|
||||
methods: {
|
||||
getAllWines() {
|
||||
return fetch(`/api/wines?limit=${this.limit}`)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
let { wines, success } = response;
|
||||
|
||||
this.wines = wines.sort(
|
||||
this.predicate(
|
||||
{
|
||||
name: "occurences",
|
||||
reverse: true
|
||||
},
|
||||
{
|
||||
name: "rating",
|
||||
reverse: true
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
},
|
||||
predicate: function() {
|
||||
var fields = [],
|
||||
n_fields = arguments.length,
|
||||
@@ -125,42 +129,72 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "@/styles/media-queries.scss";
|
||||
|
||||
.wines-main-container {
|
||||
margin-bottom: 10em;
|
||||
}
|
||||
|
||||
.info-and-link{
|
||||
.info-and-link {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wine-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
.requested-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
background-color: rgb(244, 244, 244);
|
||||
border-radius: 1.1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
font-size: 1.25em;
|
||||
|
||||
.requested-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
background-color: rgb(244,244,244);
|
||||
border-radius: 1.1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
font-size: 1.25em;
|
||||
|
||||
span {
|
||||
padding-right: 0.5rem;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.icon--heart{
|
||||
font-size: 1.5rem;
|
||||
color: $link-color;
|
||||
}
|
||||
span {
|
||||
padding-right: 0.5rem;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.icon--heart {
|
||||
font-size: 1.5rem;
|
||||
color: var(--link-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Call for help
|
||||
.wines-container {
|
||||
@media (max-width: 1643px) {
|
||||
*:nth-child(n + 7) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 2066px) {
|
||||
*:nth-child(n + 9) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 2490px) {
|
||||
*:nth-child(n + 11) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 2915px) {
|
||||
*:nth-child(n + 13) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 3335px) {
|
||||
*:nth-child(n + 15) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 3758px) {
|
||||
*:nth-child(n + 17) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,9 +85,7 @@ export default {
|
||||
this.startConfetti(this.currentName);
|
||||
return;
|
||||
}
|
||||
this.currentName = this.attendees[
|
||||
this.nameRounds % this.attendees.length
|
||||
].name;
|
||||
this.currentName = this.attendees[this.nameRounds % this.attendees.length].name;
|
||||
this.nameRounds += 1;
|
||||
clearTimeout(this.nameTimeout);
|
||||
this.nameTimeout = setTimeout(() => {
|
||||
@@ -136,8 +134,8 @@ export default {
|
||||
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
|
||||
var duration = 7 * 1000;
|
||||
var animationEnd = Date.now() + duration;
|
||||
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20};
|
||||
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35}
|
||||
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20 };
|
||||
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35 };
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
@@ -148,27 +146,26 @@ export default {
|
||||
var timeLeft = animationEnd - Date.now();
|
||||
if (timeLeft <= 0) {
|
||||
self.drawing = false;
|
||||
console.time("drawing finished")
|
||||
return clearInterval(interval);
|
||||
}
|
||||
if (currentName == "Amund Brandsrud") {
|
||||
runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135});
|
||||
runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45});
|
||||
runCannon(uberDefaults, {y: 1 }, {angle: 90});
|
||||
runCannon(uberDefaults, {x: 0 }, {angle: 45});
|
||||
runCannon(uberDefaults, {x: 1 }, {angle: 135});
|
||||
runCannon(uberDefaults, { x: 1, y: 1 }, { angle: 135 });
|
||||
runCannon(uberDefaults, { x: 0, y: 1 }, { angle: 45 });
|
||||
runCannon(uberDefaults, { y: 1 }, { angle: 90 });
|
||||
runCannon(uberDefaults, { x: 0 }, { angle: 45 });
|
||||
runCannon(uberDefaults, { x: 1 }, { angle: 135 });
|
||||
} else {
|
||||
runCannon(defaults, {x: 0 }, {angle: 45});
|
||||
runCannon(defaults, {x: 1 }, {angle: 135});
|
||||
runCannon(defaults, {y: 1 }, {angle: 90});
|
||||
runCannon(defaults, { x: 0 }, { angle: 45 });
|
||||
runCannon(defaults, { x: 1 }, { angle: 135 });
|
||||
runCannon(defaults, { y: 1 }, { angle: 90 });
|
||||
}
|
||||
}, 250);
|
||||
|
||||
function runCannon(confettiDefaultValues, originPoint, launchAngle){
|
||||
confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle))
|
||||
function runCannon(confettiDefaultValues, originPoint, launchAngle) {
|
||||
confetti(Object.assign({}, confettiDefaultValues, { origin: originPoint }, launchAngle));
|
||||
}
|
||||
},
|
||||
ordinalNumber(number=this.currentWinnerLocal.winnerCount) {
|
||||
ordinalNumber(number = this.currentWinnerLocal.winnerCount) {
|
||||
const dictonary = {
|
||||
1: "første",
|
||||
2: "andre",
|
||||
@@ -187,7 +184,6 @@ export default {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<section>
|
||||
<h2>{{ title ? title : 'Vinnere' }}</h2>
|
||||
<h2>{{ title ? title : "Vinnere" }}</h2>
|
||||
<div class="winning-raffles" v-if="winners.length > 0">
|
||||
<div v-for="(winner, index) in winners" :key="index">
|
||||
<router-link :to="`/highscore/${ encodeURIComponent(winner.name) }`">
|
||||
<router-link :to="`/highscore/${winner.name}`">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
type: Array
|
||||
},
|
||||
drawing: {
|
||||
type: Boolean,
|
||||
type: Boolean
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
|
||||
const dateString = (date) => {
|
||||
if (typeof(date) == "string") {
|
||||
const dateString = date => {
|
||||
if (typeof date == "string") {
|
||||
date = new Date(date);
|
||||
}
|
||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
||||
const ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(date);
|
||||
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(date);
|
||||
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(date);
|
||||
|
||||
return `${ye}-${mo}-${da}`
|
||||
}
|
||||
return `${ye}-${mo}-${da}`;
|
||||
};
|
||||
|
||||
function humanReadableDate(date) {
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const options = { year: "numeric", month: "long", day: "numeric" };
|
||||
return new Date(date).toLocaleDateString(undefined, options);
|
||||
}
|
||||
|
||||
@@ -20,8 +19,31 @@ function daysAgo(date) {
|
||||
return Math.round(Math.abs((new Date() - new Date(date)) / day));
|
||||
}
|
||||
|
||||
export {
|
||||
dateString,
|
||||
humanReadableDate,
|
||||
daysAgo
|
||||
export function createCookie(name, value, days) {
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
var expires = "; expires=" + date.toGMTString();
|
||||
} else var expires = "";
|
||||
|
||||
const domain = `${window.location.hostname}`;
|
||||
console.log("cookie:", `${name}=${value + expires}; path=/; domain=${domain}`);
|
||||
document.cookie = `${name}=${value + expires}; path=/; domain=${domain}`;
|
||||
}
|
||||
|
||||
export function readCookie(name) {
|
||||
var nameEQ = name + "=";
|
||||
var ca = document.cookie.split(";");
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == " ") c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function eraseCookie(name) {
|
||||
createCookie(name, "", -1);
|
||||
}
|
||||
|
||||
export { dateString, humanReadableDate, daysAgo };
|
||||
|
||||
@@ -2,53 +2,71 @@ import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import { routes } from "@/router.js";
|
||||
import Vinlottis from "@/Vinlottis";
|
||||
import AccessCodePage from "@/components/AccessCodePage";
|
||||
import { readCookie } from "@/utils";
|
||||
|
||||
import Toast from "@/plugins/Toast";
|
||||
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { Vue as VueIntegration } from "@sentry/integrations";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
// Plugins
|
||||
Vue.use(Toast);
|
||||
|
||||
const ENV = window.location.href.includes("localhost") ? "development" : "production";
|
||||
if (ENV !== "development") {
|
||||
Sentry.init({
|
||||
dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
|
||||
integrations: [
|
||||
new VueIntegration({ Vue })
|
||||
],
|
||||
integrations: [new VueIntegration({ Vue })],
|
||||
beforeSend: event => {
|
||||
console.error(event);
|
||||
return event;
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add global GA variables
|
||||
window.ga = window.ga || function(){
|
||||
window.ga.q = window.ga.q || [];
|
||||
window.ga.q.push(arguments);
|
||||
};
|
||||
window.ga =
|
||||
window.ga ||
|
||||
function() {
|
||||
window.ga.q = window.ga.q || [];
|
||||
window.ga.q.push(arguments);
|
||||
};
|
||||
ga.l = 1 * new Date();
|
||||
|
||||
// Initiate
|
||||
ga('create', __GA_TRACKINGID__, {
|
||||
'allowAnchor': false,
|
||||
'cookieExpires': __GA_COOKIELIFETIME__, // Time in seconds
|
||||
'cookieFlags': 'SameSite=Strict; Secure'
|
||||
ga("create", __GA_TRACKINGID__, {
|
||||
allowAnchor: false,
|
||||
cookieExpires: __GA_COOKIELIFETIME__, // Time in seconds
|
||||
cookieFlags: "SameSite=Strict; Secure",
|
||||
});
|
||||
ga('set', 'anonymizeIp', true); // Enable IP Anonymization/IP masking
|
||||
ga('send', 'pageview');
|
||||
ga("set", "anonymizeIp", true); // Enable IP Anonymization/IP masking
|
||||
ga("send", "pageview");
|
||||
|
||||
if (ENV == 'development')
|
||||
window[`ga-disable-${__GA_TRACKINGID__}`] = true;
|
||||
if (ENV == "development") window[`ga-disable-${__GA_TRACKINGID__}`] = true;
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: routes
|
||||
routes: routes,
|
||||
mode: "history",
|
||||
});
|
||||
|
||||
function redirectIfHasAccessCodeAndOnIncorrectDomain(accessCode) {
|
||||
const site = __sites__.find(site => site.code == accessCode);
|
||||
if (accessCode && site && !!!site.domain.includes(window.location.hostname)) {
|
||||
window.location.href = `${window.location.protocol}//${site.domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
const accessCode = readCookie("accesscode");
|
||||
redirectIfHasAccessCodeAndOnIncorrectDomain(1);
|
||||
const component = accessCode ? Vinlottis : AccessCodePage;
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
router,
|
||||
components: { Vinlottis },
|
||||
template: "<Vinlottis/>",
|
||||
render: h => h(Vinlottis)
|
||||
components: { component },
|
||||
template: "<Vinlottis />",
|
||||
render: h => h(component),
|
||||
});
|
||||
|
||||
10
nodemon.json
Normal file
10
nodemon.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"restartable": "rs",
|
||||
"ignore": [".git", "node_modules/**/node_modules"],
|
||||
"verbose": true,
|
||||
"execMap": {
|
||||
"js": "node --harmony"
|
||||
},
|
||||
"watch": ["./config", "./api"],
|
||||
"ext": "js"
|
||||
}
|
||||
12188
package-lock.json
generated
12188
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -6,36 +6,37 @@
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --progress",
|
||||
"build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress",
|
||||
"dev": "yarn webpack serve --mode development --env development",
|
||||
"watch": "yarn webpack serve --mode development --env development",
|
||||
"start": "node server.js",
|
||||
"start-noauth": "cross-env NODE_ENV=development node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"dev": "cross-env NODE_ENV=development nodemon --exec node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"seed:single": "node db/seedSingleDay.js"
|
||||
},
|
||||
"author": "",
|
||||
"author": "Kevin Midbøe & Kasper Rynning-Tønnesen",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^5.28.0",
|
||||
"@sentry/integrations": "^5.28.0",
|
||||
"@sentry/browser": "^6.2.0",
|
||||
"@sentry/integrations": "^6.2.0",
|
||||
"@zxing/library": "^0.18.3",
|
||||
"canvas-confetti": "^1.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"chart.js": "^2.9.3",
|
||||
"connect-mongo": "^3.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.0",
|
||||
"moment": "^2.24.0",
|
||||
"mongoose": "^5.11.4",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-sass": "^5.0.0",
|
||||
"node-schedule": "^1.3.2",
|
||||
"node-schedule": "^2.0.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-local-mongoose": "^6.0.1",
|
||||
"qrcode": "^1.4.4",
|
||||
"sass": "^1.32.12",
|
||||
"socket.io": "^3.0.3",
|
||||
"socket.io-client": "^3.0.3",
|
||||
"vue": "~2.6",
|
||||
"vue-router": "~3.4.9",
|
||||
"vue-router": "~3.5.1",
|
||||
"vuex": "^3.6.0",
|
||||
"web-push": "^3.4.3"
|
||||
},
|
||||
@@ -44,24 +45,25 @@
|
||||
"@babel/preset-env": "~7.12",
|
||||
"babel-loader": "~8.2.2",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"core-js": "3.8.1",
|
||||
"core-js": "3.9.0",
|
||||
"css-loader": "^5.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"friendly-errors-webpack-plugin": "~1.7",
|
||||
"google-maps-api-loader": "^1.1.1",
|
||||
"html-webpack-plugin": "5.0.0-alpha.15",
|
||||
"html-webpack-plugin": "5.2.0",
|
||||
"mini-css-extract-plugin": "~1.3.2",
|
||||
"nodemon": "2.0.7",
|
||||
"optimize-css-assets-webpack-plugin": "~5.0.4",
|
||||
"redis": "^3.0.2",
|
||||
"sass-loader": "~10.1.0",
|
||||
"sass-loader": "~11.0.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "~15.9.5",
|
||||
"vue-style-loader": "~4.1",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "~5.10.0",
|
||||
"webpack-bundle-analyzer": "^4.2.0",
|
||||
"webpack-cli": "~4.2.0",
|
||||
"webpack": "~5.23.0",
|
||||
"webpack-bundle-analyzer": "~4.4.0",
|
||||
"webpack-cli": "~4.5.0",
|
||||
"webpack-dev-server": "~3.11",
|
||||
"webpack-merge": "~5.4"
|
||||
"webpack-merge": "~5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user