Compare commits
378 Commits
refactor/w
...
feat/contr
| Author | SHA1 | Date | |
|---|---|---|---|
| f061e1c56e | |||
| 554257434d | |||
| 3e84f0e40f | |||
| 6c4b6588d2 | |||
| 8044759264 | |||
| 242aa28847 | |||
| f34857f5a8 | |||
| 593de53073 | |||
| 938b92cf0d | |||
| 342651a90c | |||
| f115ee79e6 | |||
| 326101f7d2 | |||
| fea81dcd63 | |||
| dfe89160b1 | |||
| 9ab3c8c83d | |||
| 7a2b5600c4 | |||
| 30b63a8e61 | |||
| d59d6fbd6c | |||
| 814ee4fa7d | |||
| 18079ae312 | |||
| 8275292526 | |||
| e61a6c0432 | |||
| d0cf99e8f8 | |||
| 50ea05cad3 | |||
| 59328de496 | |||
| 3c0b8d4c06 | |||
| 0144780bb1 | |||
| baa348bc95 | |||
| 1839810e66 | |||
| 9265572e45 | |||
| ce7e05fd43 | |||
| 913268b01c | |||
| 37f41ff894 | |||
| a1a14b5361 | |||
| 466e21aa0e | |||
| 9e7be82f57 | |||
| 2fa0c1085c | |||
| c4c74ca3ef | |||
| ced7ebfcac | |||
| 539386664c | |||
| 6503b309c5 | |||
| da8251c733 | |||
| e9eada0002 | |||
| 8f844dbf85 | |||
| ccc72997c0 | |||
| 055d13af35 | |||
| d58f6dd210 | |||
| edd09c012c | |||
| ea1237464d | |||
| 7c0d7c14ec | |||
| cf06140f60 | |||
| 81ce466318 | |||
| 7bebe07db6 | |||
| ad27ba8b33 | |||
| 236c07f3d0 | |||
| 036f6ea499 | |||
| 51e11d0df0 | |||
| d36aad3f9e | |||
| cc0bef927f | |||
| 594c4cc482 | |||
| a2a81e488e | |||
| 0e8f73ebd5 | |||
| 30fdbb5e2f | |||
| 1f17429c8f | |||
| 7096fbed86 | |||
| 054ff69b27 | |||
| 7eb69aa3f7 | |||
| c85e6ca56a | |||
| 50c79abaf7 | |||
| 2504dc55d6 | |||
| e12e5cafb0 | |||
| ee5f817248 | |||
| f7ac5a96ee | |||
| 9816642362 | |||
| d20acadf87 | |||
| 3b8d6f22ca | |||
|
|
c1c11f9782 | ||
|
|
90f026cfe7 | ||
|
|
03347e6ac9 | ||
| 607d47aee5 | |||
| dc0b9859fc | |||
|
|
0b04e9294f | ||
|
|
8296474538 | ||
|
|
f8b58eb64c | ||
|
|
82d708cf82 | ||
| f690124871 | |||
|
|
3ab9f2d7ab | ||
|
|
d5a7ce1b1e | ||
|
|
c2c3ae153a | ||
|
|
425975b4b9 | ||
|
|
7de2530b9b | ||
| 627f96b420 | |||
| da29e3a8ce | |||
| 8b498f3606 | |||
| 7b05c0da7c | |||
| 003f0d1c4d | |||
| 3aa989d2c1 | |||
|
|
c93671d14e | ||
|
|
58b91e67f9 | ||
| 2fc5fd29b9 | |||
|
|
8758752b26 | ||
|
|
0d0022f420 | ||
|
|
2ee071e2b2 | ||
|
|
c96356127c | ||
|
|
9f2adb54e0 | ||
| 554948d67c | |||
| f01b58c1b6 | |||
| 12d0137987 | |||
| e13b4125be | |||
| 13a9c00b50 | |||
| 87c309d094 | |||
| dda526eb5c | |||
| f1d500936b | |||
| b6e2bde4d1 | |||
| 5cbb3cbe87 | |||
| d49303a42a | |||
| d2242d4b9b | |||
| efbad63fcd | |||
| bce28c5eb9 | |||
| 794a2b06e4 | |||
| d5f3a7a1f0 | |||
| abf673faeb | |||
| 526be378d3 | |||
|
|
61851e4935 | ||
|
|
daeae25e93 | ||
|
|
315af4d50e | ||
|
|
7cf9e41b6f | ||
|
|
f8f5cd519a | ||
|
|
eee7a85cba | ||
|
|
cee0e1c8a6 | ||
|
|
397c635551 | ||
|
|
1f4f4c224e | ||
|
|
a0a9a7205e | ||
| 95105582e7 | |||
| 82512fa4ba | |||
| 56edf7e4d6 | |||
| 72ba0fb398 | |||
| 73d15dcdff | |||
| 46fb5dc6c1 | |||
|
|
74d15bbfc2 | ||
| a7fc12038c | |||
| 859a018c87 | |||
| 53ef555822 | |||
| 82068f22a9 | |||
| 8cdd9cb2c6 | |||
| 2c574020d0 | |||
| da00c7735e | |||
| 40d5062d2f | |||
| 401b9b8ac9 | |||
| cb368ee6a3 | |||
| 3344af6e90 | |||
| ba522c350a | |||
|
|
0ad3d4cafd | ||
| 6603fc489c | |||
| a90b332e27 | |||
| 7e0d3cd75e | |||
| e76e814877 | |||
| b3b5e87ab5 | |||
| 439191008a | |||
| 795f110e1b | |||
| 432d9211b2 | |||
| 25823540ff | |||
| 2013675802 | |||
| d6839dd10b | |||
| f868e03e7b | |||
| a50c94f77c | |||
| 99a94a6bc2 | |||
| 685db1f8f5 | |||
| f44402cf03 | |||
| 98e04ecd63 | |||
| 61fad22ef4 | |||
| d3bfdb632b | |||
| eaf3dbb586 | |||
| 292cdab3dd | |||
| 139b80ccff | |||
| 335d834cd4 | |||
| d68cd17ba1 | |||
| afff3fcdee | |||
| feed7774ce | |||
| 21904f4bb6 | |||
| f2989d2534 | |||
| 9c1b290219 | |||
| a7d673026e | |||
| 587239b799 | |||
| 6670f43b11 | |||
| dc7ffbae7a | |||
| 5f2b29324d | |||
| 64a1a8a93a | |||
| 5f973b199d | |||
| 52dedd1e7c | |||
| 823bbb7437 | |||
| 03c13d9558 | |||
| c0f26f9061 | |||
| 20be3cc5ca | |||
| 90eb557f0b | |||
| 0e74534cde | |||
| 0923965544 | |||
| 2062f64999 | |||
| 9d9947d7dc | |||
| d5558863e4 | |||
| 27f4c8faef | |||
| 0702507963 | |||
| 8ef1a2dd88 | |||
| a59f1e2b17 | |||
| 1383a310b3 | |||
| 584d497c6a | |||
| 98c2707cb0 | |||
|
|
4ce8ca1a99 | ||
| cb286b6894 | |||
| 73f1614ed4 | |||
| bd96a19faa | |||
| c01692bf1e | |||
| 59792f9aae | |||
| e87a59b1e6 | |||
| 4c02c81cc3 | |||
| 0e8bf9546a | |||
| 9b2de75910 | |||
| 05b07ca465 | |||
|
|
5ffb10468c | ||
|
|
2b44a6454f | ||
| 53257a16ff | |||
| 8ce7c254fb | |||
| f4be12d00b | |||
| 8d8550a835 | |||
| b0b99ed921 | |||
|
|
89557c89b7 | ||
|
|
67cd06444f | ||
|
|
53156e2330 | ||
|
|
c1c4b414c3 | ||
|
|
ce67ecc23d | ||
|
|
06a50a6662 | ||
|
|
b31bee8731 | ||
|
|
a5abdfd002 | ||
|
|
3f6b362d5a | ||
|
|
9f7a4df61b | ||
|
|
0f24afd07c | ||
|
|
48e43edcf4 | ||
|
|
122eaad1d1 | ||
|
|
6aa535bade | ||
|
|
1e8a77ec3f | ||
|
|
833cf649f3 | ||
|
|
2c9821d753 | ||
|
|
d5cf6d31ca | ||
|
|
00cbd3c871 | ||
| 06c2d35580 | |||
| 1f44b7bf8e | |||
|
|
c0171412c1 | ||
|
|
4089ad9050 | ||
|
|
80f935db26 | ||
|
|
934eb2e9ee | ||
|
|
4f270a9ca0 | ||
|
|
c405cfef54 | ||
|
|
86e7b5a56d | ||
|
|
3ab4a17d5a | ||
|
|
1327650492 | ||
|
|
036958b279 | ||
|
|
9ab84fffda | ||
|
|
e9e7b60f22 | ||
|
|
ef367bd1db | ||
|
|
d2ad209d13 | ||
|
|
861568069c | ||
|
|
fa6e5afa9c | ||
|
|
4e6121e618 | ||
|
|
45d47924e9 | ||
|
|
2eb664fc79 | ||
|
|
13e0c26eef | ||
|
|
d1044ee6c8 | ||
|
|
3b490e9995 | ||
|
|
21603dc856 | ||
|
|
07d751700f | ||
| a2fbbf0715 | |||
| 83f69ff007 | |||
| aa19c3b3a7 | |||
| 23165c0b3f | |||
| 3ebd3d3dce | |||
| 8c0a002020 | |||
|
|
5cfec8bb9d | ||
|
|
f17931a014 | ||
|
|
ee15c65988 | ||
|
|
68f20268b8 | ||
|
|
563c45bc27 | ||
|
|
2764972abd | ||
|
|
2406d2b81a | ||
|
|
052b5007c2 | ||
|
|
e060ff3aaa | ||
|
|
ea278a4892 | ||
|
|
4a98e79414 | ||
|
|
6d26da1817 | ||
| 34216c2708 | |||
| 9174338621 | |||
| 2cc5c81df6 | |||
| ace222749a | |||
| 1aa266ad71 | |||
| 20dd638133 | |||
| f6c1e35180 | |||
| bf0670c285 | |||
| 7e08e2d628 | |||
| 4b926c9ece | |||
| a7f4f9af27 | |||
| ec1685dfa8 | |||
| f171853c22 | |||
| c1f0a1b7f3 | |||
| 55b3552786 | |||
| 4f9b4ad3cf | |||
| ef035d3c44 | |||
| 7684fde8e5 | |||
| 6dc4c9080e | |||
| 8268efe625 | |||
| a30dc2a419 | |||
| 1d714d1e5a | |||
| 4f054a0437 | |||
| dde8fe1cbe | |||
|
|
ed6ac2ac01 | ||
|
|
fe5f9d2124 | ||
|
|
58b424a873 | ||
|
|
e481b6a812 | ||
|
|
f5768044e8 | ||
|
|
1e5c1faa5b | ||
|
|
e86f956b03 | ||
|
|
f31823803d | ||
|
|
88e42cb87a | ||
|
|
99df5bb8c1 | ||
|
|
c38f5c8819 | ||
|
|
a5ae46f5c3 | ||
|
|
1c95244850 | ||
|
|
dd9edf160e | ||
|
|
d67c1e77bd | ||
|
|
c6a2bfe4b2 | ||
|
|
b25e3a38f8 | ||
|
|
543a7a6eb3 | ||
| fd17e11e87 | |||
| 51a7107802 | |||
|
|
aea808dae1 | ||
|
|
7b7895728b | ||
|
|
f785a111d8 | ||
|
|
15b84a9b18 | ||
|
|
09f0436f98 | ||
| 3256c62a39 | |||
| 827eb716d7 | |||
| a6a84e4b29 | |||
| ec80aa8bcc | |||
| 262efa0380 | |||
| 5fc3bca01a | |||
|
|
8431e52e0a | ||
| 171529497d | |||
|
|
eeb14786e8 | ||
| 9b9d5bd528 | |||
| 6633994940 | |||
| b4b6643581 | |||
| f059d6f662 | |||
| ab8fd9dd83 | |||
| efb5dfcd3e | |||
| a84441078c | |||
| 1e0a76757a | |||
| 267b2df2d7 | |||
| 368b6a29aa | |||
| fa59d71717 | |||
|
|
6e380c5f5c | ||
|
|
37cb4a8f56 | ||
|
|
7aa47b08ad | ||
|
|
cab623b3f7 | ||
|
|
4051c04c91 | ||
|
|
55e1a00e67 | ||
|
|
39c4d8f134 | ||
|
|
096dbdb2e6 | ||
|
|
e6582983f2 | ||
|
|
062b01f784 | ||
|
|
40927ac286 | ||
| 131ffe42e7 | |||
| 706ef44491 | |||
|
|
09648f0b2d | ||
|
|
a7ab191ed1 | ||
|
|
6e5f99391f | ||
|
|
14fbf40ac8 | ||
|
|
52e773bc7e | ||
|
|
3b91f9693e | ||
|
|
0c08672dcd | ||
|
|
0dff0c91e1 |
15
.babelrc
Normal file
15
.babelrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
modules: false,
|
||||
targets: {
|
||||
browsers: ["IE 11", "> 5%"]
|
||||
},
|
||||
useBuiltIns: "usage",
|
||||
corejs: "3"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
13
.drone.yml
13
.drone.yml
@@ -9,10 +9,17 @@ platform:
|
||||
|
||||
steps:
|
||||
- name: frontend_install
|
||||
image: node:13.6.0
|
||||
image: node:14
|
||||
commands:
|
||||
- node -v
|
||||
- yarn --version
|
||||
- name: backend_build
|
||||
image: node:14
|
||||
commands:
|
||||
- node -v
|
||||
- yarn --version
|
||||
- yarn
|
||||
- yarn build
|
||||
- name: deploy
|
||||
image: appleboy/drone-ssh
|
||||
pull: true
|
||||
@@ -26,13 +33,13 @@ steps:
|
||||
- drone-test
|
||||
status: success
|
||||
settings:
|
||||
host: 10.0.0.114
|
||||
host: 10.0.0.52
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
command_timeout: 600s
|
||||
script:
|
||||
- /home/kevin/deploy/vinlottis.sh
|
||||
- /home/kevin/deploy.sh
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
|
||||
17
README.md
17
README.md
@@ -1,9 +1,19 @@
|
||||
# vinlattis
|
||||
<h1 align="center">
|
||||
Vinlottis 🍾
|
||||
</h1>
|
||||
|
||||
[](https://drone.kevinmidboe.com/KevinMidboe/vinlottis)
|
||||
<div align="center">
|
||||
|
||||
[](https://drone.schleppe.cloud/KevinMidboe/vinlottis)
|
||||
|
||||
Prerequisits
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
[**Vinlottis**](https://vinlottis.no) is the unofficial website for Knowit's wine-lottery, usually happening every friday at around 15:00.
|
||||
|
||||
|
||||
### Prerequisites
|
||||
```
|
||||
mongodb
|
||||
nodejs
|
||||
@@ -12,7 +22,6 @@ npm
|
||||
|
||||
|
||||
### Run dev
|
||||
|
||||
Since the backend and API runs separate from the Vue-on-save-compiler, when running the dev-server, the backend needs to be run separate
|
||||
|
||||
```
|
||||
|
||||
40
api/chat.js
40
api/chat.js
@@ -1,22 +1,46 @@
|
||||
const path = require("path");
|
||||
const { addMessage } = require(path.join(__dirname + "/redis.js"));
|
||||
|
||||
const validateUsername = (username) => {
|
||||
let error = undefined;
|
||||
const illegalChars = /\W/;
|
||||
const minLength = 3;
|
||||
const maxLength = 15;
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
error = 'Ugyldig brukernavn.';
|
||||
} else if (username.length === 0) {
|
||||
error = 'Vennligst oppgi brukernavn.';
|
||||
} else if (username.length < minLength || username.length > maxLength) {
|
||||
error = `Brukernavn må være mellom ${minLength}-${maxLength} karaktere.`
|
||||
} else if (illegalChars.test(username)) {
|
||||
error = 'Brukernavn kan bare inneholde tall og bokstaver.'
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
const io = (io) => {
|
||||
io.on("connection", socket => {
|
||||
let username = null;
|
||||
|
||||
socket.on("username", msg => {
|
||||
if (msg.username == null) {
|
||||
const usernameValidationError = validateUsername(msg.username);
|
||||
if (usernameValidationError) {
|
||||
username = null;
|
||||
socket.emit("accept_username", false);
|
||||
return;
|
||||
}
|
||||
if (msg.username.length > 3 && msg.username.length < 30) {
|
||||
socket.emit("accept_username", {
|
||||
reason: usernameValidationError,
|
||||
success: false,
|
||||
username: undefined
|
||||
});
|
||||
} else {
|
||||
username = msg.username;
|
||||
socket.emit("accept_username", true);
|
||||
return;
|
||||
socket.emit("accept_username", {
|
||||
reason: undefined,
|
||||
success: true,
|
||||
username: msg.username
|
||||
});
|
||||
}
|
||||
socket.emit("accept_username", false);
|
||||
});
|
||||
|
||||
socket.on("chat", msg => {
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
|
||||
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
const getAllHistory = (req, res) => {
|
||||
let { page, limit } = req.query;
|
||||
page = !isNaN(page) ? Number(page) : undefined;
|
||||
limit = !isNaN(limit) ? Number(limit) : undefined;
|
||||
|
||||
router.route("/chat/history").get(async (req, res) => {
|
||||
let { skip, take } = req.query;
|
||||
skip = !isNaN(skip) ? Number(skip) : undefined;
|
||||
take = !isNaN(take) ? Number(take) : undefined;
|
||||
return history(page, limit)
|
||||
.then(messages => res.json(messages))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
};
|
||||
|
||||
try {
|
||||
const messages = await history(skip, take);
|
||||
res.json(messages)
|
||||
} catch(error) {
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
const deleteHistory = (req, res) => {
|
||||
return clearHistory()
|
||||
.then(message => res.json(message))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
};
|
||||
|
||||
router.route("/chat/history").delete(async (req, res) => {
|
||||
try {
|
||||
const messages = await clearHistory();
|
||||
res.json(messages)
|
||||
} catch(error) {
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = {
|
||||
getAllHistory,
|
||||
deleteHistory
|
||||
};
|
||||
|
||||
33
api/github.js
Normal file
33
api/github.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
class Github {
|
||||
constructor(apiToken) {
|
||||
this.apiToken = apiToken;
|
||||
this.hostname = "https://api.github.com"
|
||||
}
|
||||
|
||||
listRepositoryContributors() {
|
||||
const headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": `token ${ this.apiToken }`
|
||||
};
|
||||
|
||||
const url = `${ this.hostname }/repos/KevinMidboe/vinlottis/contributors`
|
||||
return fetch(url, { headers })
|
||||
.then(resp => resp.json())
|
||||
.then(contributors =>
|
||||
contributors.map(contributor => new Contributor(contributor))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Contributor {
|
||||
constructor(contributorObject) {
|
||||
this.name = contributorObject.login;
|
||||
this.avatarUrl = contributorObject.avatar_url;
|
||||
this.profileUrl = contributorObject.html_url;
|
||||
this.projectContributions = contributorObject.contributions;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Github;
|
||||
59
api/login.js
59
api/login.js
@@ -1,59 +0,0 @@
|
||||
const passport = require("passport");
|
||||
const path = require("path");
|
||||
const User = require(path.join(__dirname + "/../schemas/User"));
|
||||
const router = require("express").Router();
|
||||
|
||||
router.get("/", function(req, res) {
|
||||
res.sendFile(path.join(__dirname + "/../public/index.html"));
|
||||
});
|
||||
|
||||
router.get("/register", function(req, res) {
|
||||
res.sendFile(path.join(__dirname + "/../public/index.html"));
|
||||
});
|
||||
|
||||
// router.post("/register", function(req, res, next) {
|
||||
// User.register(
|
||||
// new User({ username: req.body.username }),
|
||||
// req.body.password,
|
||||
// function(err) {
|
||||
// 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);
|
||||
// }
|
||||
|
||||
// return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
|
||||
router.get("/login", function(req, res) {
|
||||
res.sendFile(path.join(__dirname + "/../public/index.html"));
|
||||
});
|
||||
|
||||
router.post("/login", function(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);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
router.get("/logout", function(req, res) {
|
||||
req.logout();
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
132
api/lottery.js
Normal file
132
api/lottery.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const path = require('path');
|
||||
|
||||
const Highscore = require(path.join(__dirname, '/schemas/Highscore'));
|
||||
const Wine = require(path.join(__dirname, '/schemas/Wine'));
|
||||
|
||||
// Utils
|
||||
const epochToDateString = date => new Date(parseInt(date)).toDateString();
|
||||
|
||||
const sortNewestFirst = (lotteries) => {
|
||||
return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1)
|
||||
}
|
||||
|
||||
const groupHighscoreByDate = async (highscore=undefined) => {
|
||||
if (highscore == undefined)
|
||||
highscore = await Highscore.find();
|
||||
|
||||
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}`,
|
||||
date,
|
||||
winners: lottery.winners
|
||||
}))
|
||||
}
|
||||
|
||||
const byName = (req, res) => {
|
||||
const { name } = req.params;
|
||||
const regexName = new RegExp(name, "i"); // lowercase regex of the name
|
||||
|
||||
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.`
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(highscore => resolveWineReferences(highscore, "wins"))
|
||||
.then(highscore => res.send({
|
||||
message: `Lottery winnings for name: ${ name }.`,
|
||||
name: highscore.name,
|
||||
highscore: sortNewestFirst(highscore.wins)
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
all,
|
||||
latest,
|
||||
byEpochDate,
|
||||
byName
|
||||
};
|
||||
131
api/message.js
Normal file
131
api/message.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
|
||||
|
||||
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)
|
||||
|
||||
return `${da}-${mo}-${ye}`
|
||||
}
|
||||
|
||||
async function sendWineSelectMessage(winnerObject) {
|
||||
winnerObject.timestamp_sent = new Date().getTime();
|
||||
winnerObject.timestamp_limit = new Date().getTime() * 600000;
|
||||
await winnerObject.save();
|
||||
|
||||
let url = new URL(`/#/winner/${winnerObject.id}`, "https://lottis.vin");
|
||||
|
||||
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.)`
|
||||
)
|
||||
}
|
||||
|
||||
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!`)
|
||||
}
|
||||
|
||||
async function sendLastWinnerMessage(winnerObject, wineObject) {
|
||||
console.log(`User ${winnerObject.id} is only one left, chosing wine for him/her.`);
|
||||
winnerObject.timestamp_sent = new Date().getTime();
|
||||
winnerObject.timestamp_limit = new Date().getTime();
|
||||
await winnerObject.save();
|
||||
|
||||
return sendMessageToUser(
|
||||
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!`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendWineSelectMessageTooLate(winnerObject) {
|
||||
return sendMessageToUser(
|
||||
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.`
|
||||
);
|
||||
}
|
||||
|
||||
async function sendMessageToUser(phoneNumber, message) {
|
||||
console.log(`Attempting to send message to ${ phoneNumber }.`)
|
||||
|
||||
const body = {
|
||||
sender: "Vinlottis",
|
||||
message: message,
|
||||
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 }`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
console.log(`statusCode: ${ res.statusCode }`);
|
||||
console.log(`statusMessage: ${ res.statusMessage }`);
|
||||
|
||||
res.setEncoding('utf8');
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
res.on("data", (data) => {
|
||||
console.log("Response from message gateway:", data)
|
||||
|
||||
resolve(JSON.parse(data))
|
||||
});
|
||||
} else {
|
||||
res.on("data", (data) => {
|
||||
data = JSON.parse(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.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendWineSelectMessage,
|
||||
sendWineConfirmation,
|
||||
sendLastWinnerMessage,
|
||||
sendWineSelectMessageTooLate,
|
||||
sendInitialMessageToWinners
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
const mustBeAuthenticated = (req, res, next) => {
|
||||
console.log(req.isAuthenticated());
|
||||
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,
|
||||
6
api/middleware/setAdminHeaderIfAuthenticated.js
Normal file
6
api/middleware/setAdminHeaderIfAuthenticated.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const setAdminHeaderIfAuthenticated = (req, res, next) => {
|
||||
res.set("Vinlottis-Admin", req.isAuthenticated());
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = setAdminHeaderIfAuthenticated;
|
||||
6
api/middleware/setupCORS.js
Normal file
6
api/middleware/setupCORS.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const openCORS = (req, res, next) => {
|
||||
res.set("Access-Control-Allow-Origin", "*")
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = openCORS;
|
||||
37
api/middleware/setupHeaders.js
Normal file
37
api/middleware/setupHeaders.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const camelToKebabCase = str => str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
||||
|
||||
const mapFeaturePolicyToString = (features) => {
|
||||
return Object.entries(features).map(([key, value]) => {
|
||||
key = camelToKebabCase(key)
|
||||
value = value == "*" ? value : `'${ value }'`
|
||||
return `${key} ${value}`
|
||||
}).join("; ")
|
||||
}
|
||||
|
||||
const setupHeaders = (req, res, next) => {
|
||||
res.set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
// Security
|
||||
res.set("X-Content-Type-Options", "nosniff");
|
||||
res.set("X-XSS-Protection", "1; mode=block");
|
||||
res.set("X-Frame-Options", "SAMEORIGIN");
|
||||
res.set("X-DNS-Prefetch-Control", "off");
|
||||
res.set("X-Download-Options", "noopen");
|
||||
res.set("Strict-Transport-Security", "max-age=15552000; includeSubDomains")
|
||||
|
||||
// Feature policy
|
||||
const features = {
|
||||
fullscreen: "*",
|
||||
payment: "none",
|
||||
microphone: "none",
|
||||
camera: "self",
|
||||
speaker: "*",
|
||||
syncXhr: "self"
|
||||
}
|
||||
const featureString = mapFeaturePolicyToString(features);
|
||||
res.set("Feature-Policy", featureString)
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = setupHeaders;
|
||||
35
api/person.js
Normal file
35
api/person.js
Normal file
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
93
api/redis.js
93
api/redis.js
@@ -1,29 +1,40 @@
|
||||
const { promisify } = require("util"); // from node
|
||||
|
||||
let client;
|
||||
let llenAsync;
|
||||
let lrangeAsync;
|
||||
try {
|
||||
const redis = require("redis");
|
||||
console.log("trying to create");
|
||||
console.log("Trying to connect with redis..");
|
||||
client = redis.createClient();
|
||||
|
||||
client.zcount = promisify(client.zcount).bind(client);
|
||||
client.zadd = promisify(client.zadd).bind(client);
|
||||
client.zrevrange = promisify(client.zrevrange).bind(client);
|
||||
client.del = promisify(client.del).bind(client);
|
||||
|
||||
client.on("connect", () => console.log("Redis connection established!"));
|
||||
|
||||
client.on("error", function(err) {
|
||||
client.quit();
|
||||
console.error("Missing redis-configurations..");
|
||||
console.error("Unable to connect to redis, setting up redis-mock.");
|
||||
|
||||
client = {
|
||||
rpush: function() {
|
||||
console.log("redis-dummy lpush", arguments);
|
||||
if (typeof arguments[arguments.length - 1] == "function") {
|
||||
arguments[arguments.length - 1](null);
|
||||
}
|
||||
zcount: function() {
|
||||
console.log("redis-dummy zcount", arguments);
|
||||
return Promise.resolve()
|
||||
},
|
||||
lrange: function() {
|
||||
console.log("redis-dummy lrange", arguments);
|
||||
if (typeof arguments[arguments.length - 1] == "function") {
|
||||
arguments[arguments.length - 1](null);
|
||||
}
|
||||
zadd: function() {
|
||||
console.log("redis-dummy zadd", arguments);
|
||||
return Promise.resolve();
|
||||
},
|
||||
zrevrange: function() {
|
||||
console.log("redis-dummy zrevrange", arguments);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
del: function() {
|
||||
console.log("redis-dummy del", arguments);
|
||||
if (typeof arguments[arguments.length - 1] == "function") {
|
||||
arguments[arguments.length - 1](null);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -31,36 +42,46 @@ try {
|
||||
|
||||
const addMessage = message => {
|
||||
const json = JSON.stringify(message);
|
||||
client.rpush("messages", json);
|
||||
|
||||
return message;
|
||||
return client.zadd("messages", message.timestamp, json)
|
||||
.then(position => {
|
||||
return {
|
||||
success: true
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const history = (skip = 0, take = 20) => {
|
||||
skip = (1 + skip) * -1; // negate to get FIFO
|
||||
return new Promise((resolve, reject) =>
|
||||
client.lrange("messages", skip * take, skip, (err, data) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
}
|
||||
const history = (page=1, limit=10) => {
|
||||
const start = (page - 1) * limit;
|
||||
const stop = (limit * page) - 1;
|
||||
|
||||
data = data.map(data => JSON.parse(data));
|
||||
resolve(data);
|
||||
const getTotalCount = client.zcount("messages", '-inf', '+inf');
|
||||
const getMessages = client.zrevrange("messages", start, stop);
|
||||
|
||||
return Promise.all([getTotalCount, getMessages])
|
||||
.then(([totalCount, messages]) => {
|
||||
if (messages) {
|
||||
return {
|
||||
messages: messages.map(entry => JSON.parse(entry)).reverse(),
|
||||
count: messages.length,
|
||||
total: totalCount
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
messages: [],
|
||||
count: 0,
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const clearHistory = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
client.del("messages", (err, success) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
return client.del("messages")
|
||||
.then(success => {
|
||||
return {
|
||||
success: success == 1 ? true : false
|
||||
}
|
||||
resolve(success == 1 ? true : false);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
69
api/request.js
Normal file
69
api/request.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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 deleteRequestedWineById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
if(id == null){
|
||||
return res.json({
|
||||
message: "Id er ikke definert",
|
||||
success: false
|
||||
})
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
rating: null,
|
||||
occurences: null,
|
||||
image: wine.image,
|
||||
id: wine.id
|
||||
});
|
||||
await thisWineIsLOKO.save()
|
||||
}
|
||||
|
||||
let requestedWine = await RequestedWine.findOne({ "wineId": wine.id})
|
||||
|
||||
if(requestedWine == undefined){
|
||||
requestedWine = new RequestedWine({
|
||||
count: 1,
|
||||
wineId: wine.id,
|
||||
wine: thisWineIsLOKO
|
||||
})
|
||||
} else {
|
||||
requestedWine.count += 1;
|
||||
}
|
||||
await requestedWine.save()
|
||||
|
||||
return res.send(requestedWine);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requestNewWine,
|
||||
getAllRequestedWines,
|
||||
deleteRequestedWineById
|
||||
};
|
||||
@@ -1,35 +1,25 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const mongoose = require("mongoose");
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
|
||||
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 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"
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
router.route("/wines/prelottery").get(async (req, res) => {
|
||||
const prelotteryWines = async (req, res) => {
|
||||
let wines = await PreLotteryWine.find();
|
||||
res.json(wines);
|
||||
});
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
router.route("/purchase/statistics").get(async (req, res) => {
|
||||
const allPurchase = async (req, res) => {
|
||||
let purchases = await Purchase.find()
|
||||
.populate("wines")
|
||||
.sort({ date: 1 });
|
||||
res.json(purchases);
|
||||
});
|
||||
return res.json(purchases);
|
||||
};
|
||||
|
||||
router.route("/purchase/statistics/color").get(async (req, res) => {
|
||||
const purchaseByColor = async (req, res) => {
|
||||
const countColor = await Purchase.find();
|
||||
let red = 0;
|
||||
let blue = 0;
|
||||
@@ -75,7 +65,7 @@ router.route("/purchase/statistics/color").get(async (req, res) => {
|
||||
|
||||
const total = red + yellow + blue + green;
|
||||
|
||||
res.json({
|
||||
return res.json({
|
||||
red: {
|
||||
total: red,
|
||||
win: redWin
|
||||
@@ -95,21 +85,21 @@ router.route("/purchase/statistics/color").get(async (req, res) => {
|
||||
stolen: stolen,
|
||||
total: total
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
router.route("/highscore/statistics").get(async (req, res) => {
|
||||
const highscore = async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
|
||||
res.json(highscore);
|
||||
});
|
||||
return res.json(highscore);
|
||||
};
|
||||
|
||||
router.route("/wines/statistics").get(async (req, res) => {
|
||||
const allWines = async (req, res) => {
|
||||
const wines = await Wine.find();
|
||||
|
||||
res.json(wines);
|
||||
});
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
router.route("/wines/statistics/overall").get(async (req, res) => {
|
||||
const allWinesSummary = async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
let wines = {};
|
||||
|
||||
@@ -149,7 +139,16 @@ router.route("/wines/statistics/overall").get(async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
res.json(Object.values(wines));
|
||||
});
|
||||
wines = Object.values(wines).reverse()
|
||||
|
||||
module.exports = router;
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
prelotteryWines,
|
||||
allPurchase,
|
||||
purchaseByColor,
|
||||
highscore,
|
||||
allWines,
|
||||
allWinesSummary
|
||||
};
|
||||
|
||||
73
api/router.js
Normal file
73
api/router.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const express = require("express");
|
||||
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 githubController = require(path.join(__dirname, "/controllers/githubController"));
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/wineinfo/search", wineinfo.wineSearch);
|
||||
|
||||
router.get("/request/all", setAdminHeaderIfAuthenticated, request.getAllRequestedWines);
|
||||
router.post("/request/new-wine", request.requestNewWine);
|
||||
router.delete("/request/:id", request.deleteRequestedWineById);
|
||||
|
||||
router.get("/wineinfo/schema", mustBeAuthenticated, update.schema);
|
||||
router.get("/wineinfo/:ean", wineinfo.byEAN);
|
||||
|
||||
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("/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("/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.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.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinnerById);
|
||||
router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById);
|
||||
router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection);
|
||||
|
||||
router.get('/chat/history', chatHistoryApi.getAllHistory)
|
||||
router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory)
|
||||
|
||||
router.get("/project/contributors", githubController.getProjectContributors);
|
||||
|
||||
router.post('/login', userApi.login);
|
||||
router.post('/register', mustBeAuthenticated, userApi.register);
|
||||
router.get('/logout', userApi.logout);
|
||||
|
||||
module.exports = router;
|
||||
@@ -6,7 +6,9 @@ const PreLotteryWine = new Schema({
|
||||
vivinoLink: String,
|
||||
rating: Number,
|
||||
id: String,
|
||||
image: String
|
||||
image: String,
|
||||
price: String,
|
||||
country: String
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);
|
||||
13
api/schemas/RequestedWine.js
Normal file
13
api/schemas/RequestedWine.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const RequestedWine = new Schema({
|
||||
count: Number,
|
||||
wineId: String,
|
||||
wine: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Wine"
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("RequestedWine", RequestedWine);
|
||||
@@ -8,7 +8,11 @@ const VirtualWinner = new Schema({
|
||||
green: Number,
|
||||
blue: Number,
|
||||
red: Number,
|
||||
yellow: Number
|
||||
yellow: Number,
|
||||
id: String,
|
||||
timestamp_drawn: Number,
|
||||
timestamp_sent: Number,
|
||||
timestamp_limit: Number
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("VirtualWinner", VirtualWinner);
|
||||
@@ -2,19 +2,14 @@ const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const webpush = require("web-push"); //requiring the web-push module
|
||||
const mongoose = require("mongoose");
|
||||
const schedule = require("node-schedule");
|
||||
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
|
||||
const mustBeAuthenticated = require(path.join(
|
||||
__dirname + "/../middleware/mustBeAuthenticated"
|
||||
__dirname, "/middleware/mustBeAuthenticated"
|
||||
));
|
||||
|
||||
const config = require(path.join(__dirname + "/../config/defaults/push"));
|
||||
const Subscription = require(path.join(__dirname + "/../schemas/Subscription"));
|
||||
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
|
||||
const lotteryConfig = require(path.join(
|
||||
__dirname + "/../config/defaults/lottery"
|
||||
));
|
||||
|
||||
202
api/update.js
202
api/update.js
@@ -1,32 +1,17 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const mongoose = require("mongoose");
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
|
||||
const sub = require(path.join(__dirname + "/../api/subscriptions"));
|
||||
const mustBeAuthenticated = require(path.join(
|
||||
__dirname + "/../middleware/mustBeAuthenticated"
|
||||
));
|
||||
const sub = require(path.join(__dirname, "/subscriptions"));
|
||||
|
||||
const Subscription = require(path.join(__dirname + "/../schemas/Subscription"));
|
||||
const Purchase = require(path.join(__dirname + "/../schemas/Purchase"));
|
||||
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
|
||||
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"
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
const VirtualWinner = require(path.join(
|
||||
__dirname + "/../schemas/VirtualWinner"
|
||||
));
|
||||
const Highscore = require(path.join(__dirname + "/../schemas/Highscore"));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
router.route("/log/wines").post(mustBeAuthenticated, async (req, res) => {
|
||||
const submitWines = async (req, res) => {
|
||||
const wines = req.body;
|
||||
for (let i = 0; i < wines.length; i++) {
|
||||
let wine = wines[i];
|
||||
@@ -43,112 +28,115 @@ router.route("/log/wines").post(mustBeAuthenticated, async (req, res) => {
|
||||
}
|
||||
|
||||
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"
|
||||
});
|
||||
sub.sendNotification(subscription, message);
|
||||
|
||||
try {
|
||||
sub.sendNotification(subscription, message);
|
||||
} catch (error) {
|
||||
console.error("Error when trying to send push notification to subscriber.");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
res.send(true);
|
||||
});
|
||||
return res.send({
|
||||
message: "Submitted and notified push subscribers of new wines!",
|
||||
success: true
|
||||
});
|
||||
};
|
||||
|
||||
router.route("/log/schema").get(mustBeAuthenticated, async (req, res) => {
|
||||
const schema = async (req, res) => {
|
||||
let schema = { ...PreLotteryWine.schema.obj };
|
||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||
accumulator[current] = "";
|
||||
return accumulator;
|
||||
return accumulator
|
||||
}, {});
|
||||
|
||||
res.send(nulledSchema);
|
||||
});
|
||||
return res.send(nulledSchema);
|
||||
}
|
||||
|
||||
router.route("/log").post(mustBeAuthenticated, async (req, res) => {
|
||||
await PreLotteryWine.deleteMany();
|
||||
// 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)))
|
||||
|
||||
const purchaseBody = req.body.purchase;
|
||||
const winnersBody = req.body.winners;
|
||||
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 }));
|
||||
}
|
||||
|
||||
const date = purchaseBody.date;
|
||||
const blue = purchaseBody.blue;
|
||||
const red = purchaseBody.red;
|
||||
const yellow = purchaseBody.yellow;
|
||||
const green = purchaseBody.green;
|
||||
/**
|
||||
* @apiParam (Request body) {Array} winners List of winners
|
||||
*/
|
||||
const submitWinnersToLottery = async (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
const { winners, date } = lottery;
|
||||
|
||||
const bought = purchaseBody.bought;
|
||||
const stolen = purchaseBody.stolen;
|
||||
|
||||
const winesThisDate = [];
|
||||
|
||||
for (let i = 0; i < winnersBody.length; i++) {
|
||||
let currentWinner = winnersBody[i];
|
||||
|
||||
let wonWine = await Wine.findOne({ name: currentWinner.wine.name });
|
||||
if (wonWine == undefined) {
|
||||
let newWonWine = new Wine({
|
||||
name: currentWinner.wine.name,
|
||||
vivinoLink: currentWinner.wine.vivinoLink,
|
||||
rating: currentWinner.wine.rating,
|
||||
occurences: 1,
|
||||
image: currentWinner.wine.image,
|
||||
id: currentWinner.wine.id
|
||||
});
|
||||
await newWonWine.save();
|
||||
wonWine = newWonWine;
|
||||
} else {
|
||||
wonWine.occurences += 1;
|
||||
wonWine.image = currentWinner.wine.image;
|
||||
wonWine.id = currentWinner.wine.id;
|
||||
await wonWine.save();
|
||||
}
|
||||
|
||||
winesThisDate.push(wonWine);
|
||||
|
||||
const person = await Highscore.findOne({
|
||||
name: currentWinner.name
|
||||
});
|
||||
|
||||
if (person == undefined) {
|
||||
let newPerson = new Highscore({
|
||||
name: currentWinner.name,
|
||||
wins: [
|
||||
{
|
||||
color: currentWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await newPerson.save();
|
||||
} else {
|
||||
person.wins.push({
|
||||
color: currentWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
});
|
||||
person.markModified("wins");
|
||||
await person.save();
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
let purchase = new Purchase({
|
||||
date: date,
|
||||
blue: blue,
|
||||
yellow: yellow,
|
||||
red: red,
|
||||
green: green,
|
||||
wines: winesThisDate,
|
||||
bought: bought,
|
||||
stolen: stolen
|
||||
});
|
||||
return res.json(true);
|
||||
}
|
||||
|
||||
await purchase.save();
|
||||
/**
|
||||
* @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
|
||||
|
||||
res.send(true);
|
||||
});
|
||||
const { date,
|
||||
blue,
|
||||
red,
|
||||
yellow,
|
||||
green,
|
||||
bought,
|
||||
stolen } = lottery;
|
||||
|
||||
module.exports = router;
|
||||
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
|
||||
};
|
||||
|
||||
51
api/user.js
Normal file
51
api/user.js
Normal file
@@ -0,0 +1,51 @@
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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("/");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
logout
|
||||
};
|
||||
@@ -1,37 +1,16 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const mongoose = require("mongoose");
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
let io;
|
||||
const mustBeAuthenticated = require(path.join(
|
||||
__dirname + "/../middleware/mustBeAuthenticated"
|
||||
));
|
||||
const crypto = require("crypto");
|
||||
|
||||
const Attendee = require(path.join(__dirname + "/../schemas/Attendee"));
|
||||
const VirtualWinner = require(path.join(
|
||||
__dirname + "/../schemas/VirtualWinner"
|
||||
));
|
||||
const config = require(path.join(__dirname, "/../config/defaults/lottery"));
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration"));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
|
||||
router.route("/winners").delete(mustBeAuthenticated, async (req, res) => {
|
||||
await VirtualWinner.deleteMany();
|
||||
io.emit("refresh_data", {});
|
||||
res.json(true);
|
||||
});
|
||||
|
||||
router.route("/attendees").delete(mustBeAuthenticated, async (req, res) => {
|
||||
await Attendee.deleteMany();
|
||||
io.emit("refresh_data", {});
|
||||
res.json(true);
|
||||
});
|
||||
|
||||
router.route("/winners").get(async (req, res) => {
|
||||
const winners = async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
let winnersRedacted = [];
|
||||
let winner;
|
||||
@@ -43,41 +22,104 @@ router.route("/winners").get(async (req, res) => {
|
||||
});
|
||||
}
|
||||
res.json(winnersRedacted);
|
||||
});
|
||||
};
|
||||
|
||||
router.route("/winners/secure").get(mustBeAuthenticated, async (req, res) => {
|
||||
const winnersSecure = async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
|
||||
res.json(winners);
|
||||
});
|
||||
return res.json(winners);
|
||||
};
|
||||
|
||||
router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
||||
let allContestants = await Attendee.find({ winner: false });
|
||||
if (allContestants.length == 0) {
|
||||
res.json(false);
|
||||
return;
|
||||
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
|
||||
});
|
||||
}
|
||||
let ballotColors = [];
|
||||
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++) {
|
||||
ballotColors.push("blue");
|
||||
raffleColors.push("blue");
|
||||
}
|
||||
for (let red = 0; red < currentContestant.red; red++) {
|
||||
ballotColors.push("red");
|
||||
raffleColors.push("red");
|
||||
}
|
||||
for (let green = 0; green < currentContestant.green; green++) {
|
||||
ballotColors.push("green");
|
||||
raffleColors.push("green");
|
||||
}
|
||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||
ballotColors.push("yellow");
|
||||
raffleColors.push("yellow");
|
||||
}
|
||||
}
|
||||
|
||||
ballotColors = shuffle(ballotColors);
|
||||
raffleColors = shuffle(raffleColors);
|
||||
|
||||
let colorToChooseFrom =
|
||||
ballotColors[Math.floor(Math.random() * ballotColors.length)];
|
||||
raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||
let findObject = { winner: false };
|
||||
|
||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||
@@ -124,7 +166,16 @@ router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
||||
Math.floor(Math.random() * attendeeListDemocratic.length)
|
||||
];
|
||||
|
||||
io.emit("winner", { color: colorToChooseFrom, name: winner.name });
|
||||
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,
|
||||
@@ -133,7 +184,9 @@ router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
||||
red: winner.red,
|
||||
blue: winner.blue,
|
||||
green: winner.green,
|
||||
yellow: winner.yellow
|
||||
yellow: winner.yellow,
|
||||
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||
timestamp_drawn: new Date().getTime()
|
||||
});
|
||||
|
||||
await Attendee.update(
|
||||
@@ -142,52 +195,57 @@ router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
||||
);
|
||||
|
||||
await newWinnerElement.save();
|
||||
res.json(winner);
|
||||
});
|
||||
return res.json({
|
||||
success: true,
|
||||
winner
|
||||
});
|
||||
};
|
||||
|
||||
router.route("/attendees").get(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,
|
||||
ballots: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||
red: attendee.red,
|
||||
blue: attendee.blue,
|
||||
green: attendee.green,
|
||||
yellow: attendee.yellow
|
||||
const finish = async (req, res) => {
|
||||
if (!config.gatewayToken) {
|
||||
return res.json({
|
||||
message: "Missing api token for sms gateway.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
res.json(attendeesRedacted);
|
||||
});
|
||||
|
||||
router.route("/attendees/secure").get(mustBeAuthenticated, async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
|
||||
res.json(attendees);
|
||||
});
|
||||
|
||||
router.route("/attendee").post(mustBeAuthenticated, 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
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
await newAttendee.save();
|
||||
|
||||
io.emit("new_attendee", {});
|
||||
if (winners.length == 0) {
|
||||
return res.json({
|
||||
message: "No winners to draw from.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
|
||||
res.send(true);
|
||||
});
|
||||
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,
|
||||
@@ -209,7 +267,15 @@ function shuffle(array) {
|
||||
return array;
|
||||
}
|
||||
|
||||
module.exports = function(_io) {
|
||||
io = _io;
|
||||
return router;
|
||||
};
|
||||
module.exports = {
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
winners,
|
||||
winnersSecure,
|
||||
drawWinner,
|
||||
finish,
|
||||
attendees,
|
||||
attendeesSecure,
|
||||
addAttendee
|
||||
}
|
||||
|
||||
|
||||
200
api/virtualRegistration.js
Normal file
200
api/virtualRegistration.js
Normal file
@@ -0,0 +1,200 @@
|
||||
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
|
||||
};
|
||||
27
api/wine.js
Normal file
27
api/wine.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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,
|
||||
occurences: 1,
|
||||
image: prelotteryWine.image,
|
||||
id: prelotteryWine.id
|
||||
});
|
||||
await newWonWine.save();
|
||||
wonWine = newWonWine;
|
||||
} else {
|
||||
wonWine.occurences += 1;
|
||||
wonWine.image = prelotteryWine.image;
|
||||
wonWine.id = prelotteryWine.id;
|
||||
await wonWine.save();
|
||||
}
|
||||
|
||||
return wonWine;
|
||||
}
|
||||
|
||||
module.exports.findSaveWine = findSaveWine;
|
||||
@@ -1,15 +1,53 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const fetch = require('node-fetch')
|
||||
const path = require('path')
|
||||
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
||||
|
||||
const mustBeAuthenticated = require(path.join(__dirname + "/../middleware/mustBeAuthenticated"))
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
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)
|
||||
|
||||
router.route("/wineinfo/:ean").get(async (req, res) => {
|
||||
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())
|
||||
|
||||
@@ -25,7 +63,10 @@ router.route("/wineinfo/:ean").get(async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
res.send(vinmonopoletResponse);
|
||||
});
|
||||
return res.send(vinmonopoletResponse);
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
module.exports = {
|
||||
byEAN,
|
||||
wineSearch
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ try {
|
||||
module.exports = require("../env/lottery.config");
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"You haven't defined lottery-configs, you sure you want to continue without them?"
|
||||
"⚠️ You haven't defined lottery-configs, you sure you want to continue without them?\n"
|
||||
);
|
||||
module.exports = {
|
||||
name: "NAME MISSING",
|
||||
@@ -11,6 +11,6 @@ try {
|
||||
message: "INSERT MESSAGE",
|
||||
date: 5,
|
||||
hours: 15,
|
||||
apiUrl: "https://lottis.vin"
|
||||
gatewayToken: "asd"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ try {
|
||||
module.exports = require("../env/push.config");
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"You haven't defined push-parameters, you sure you want to continue without them?"
|
||||
"⚠️ You haven't defined push-parameters, you sure you want to continue without them?\n"
|
||||
);
|
||||
module.exports = { publicKey: false, privateKey: false, mailto: false };
|
||||
}
|
||||
|
||||
7
config/env/lottery.config.example.js
vendored
7
config/env/lottery.config.example.js
vendored
@@ -5,5 +5,8 @@ module.exports = {
|
||||
message: "VINLOTTERI",
|
||||
date: 5,
|
||||
hours: 15,
|
||||
apiUrl: undefined
|
||||
};
|
||||
gatewayToken: undefined,
|
||||
vinmonopoletToken: undefined,
|
||||
googleanalytics_trackingId: undefined,
|
||||
googleanalytics_cookieLifetime: 60 * 60 * 24 * 14
|
||||
};
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
const webpack = require("webpack");
|
||||
const helpers = require("./helpers");
|
||||
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
const ServiceWorkerConfig = {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue"]
|
||||
},
|
||||
entry: {
|
||||
serviceWorker: [helpers.root("src/service-worker", "service-worker")]
|
||||
serviceWorker: [helpers.root("frontend/service-worker", "service-worker")]
|
||||
},
|
||||
optimization: {
|
||||
minimizer: []
|
||||
@@ -19,7 +19,7 @@ const ServiceWorkerConfig = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: "babel-loader",
|
||||
include: [helpers.root("src/service-worker", "service-worker")]
|
||||
include: [helpers.root("frontend/service-worker", "service-worker")]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -31,11 +31,10 @@ const ServiceWorkerConfig = {
|
||||
//filename: "js/[name].bundle.js"
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new UglifyJSPlugin({
|
||||
cache: true,
|
||||
parallel: false,
|
||||
sourceMap: false
|
||||
new TerserPlugin({
|
||||
test: /\.js(\?.*)?$/i,
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const helpers = require("./helpers");
|
||||
|
||||
const VinlottisConfig = {
|
||||
entry: {
|
||||
vinlottis: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new HtmlWebpackPlugin({
|
||||
chunks: ["vinlottis"],
|
||||
filename: "../index.html",
|
||||
template: "./src/templates/Create.html",
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: false,
|
||||
preserveLineBreaks: true,
|
||||
removeAttributeQuotes: true
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = VinlottisConfig;
|
||||
@@ -11,10 +11,16 @@ const webpackConfig = function(isDev) {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue"],
|
||||
alias: {
|
||||
vue$: isDev ? "vue/dist/vue.min.js" : "vue/dist/vue.min.js",
|
||||
"@": helpers.root("src")
|
||||
vue$: "vue/dist/vue.min.js",
|
||||
"@": helpers.root("frontend")
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
vinlottis: helpers.root("frontend", "vinlottis-init")
|
||||
},
|
||||
externals: {
|
||||
moment: 'moment' // comes with chart.js
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -33,35 +39,31 @@ const webpackConfig = function(isDev) {
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: "babel-loader",
|
||||
include: [helpers.root("src")]
|
||||
use: [ "babel-loader" ],
|
||||
include: [helpers.root("frontend")]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
||||
MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
||||
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
use: [
|
||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
||||
MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
||||
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.woff(2)?(\?[a-z0-9]+)?$/,
|
||||
loader: "url-loader?limit=10000&mimetype=application/font-woff"
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: "application/font-woff"
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/,
|
||||
@@ -72,14 +74,16 @@ const webpackConfig = function(isDev) {
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__ENV__: JSON.stringify(process.env.NODE_ENV),
|
||||
__NAME__: JSON.stringify(env.name),
|
||||
__PHONE__: JSON.stringify(env.phone),
|
||||
__PRICE__: env.price,
|
||||
__MESSAGE__: JSON.stringify(env.message),
|
||||
__DATE__: env.date,
|
||||
__HOURS__: env.hours,
|
||||
__APIURL__: JSON.stringify(env.apiUrl),
|
||||
__PUSHENABLED__: JSON.stringify(require("./defaults/push") != false)
|
||||
__PUSHENABLED__: JSON.stringify(require("./defaults/push") != false),
|
||||
__GA_TRACKINGID__: JSON.stringify(env.googleanalytics_trackingId),
|
||||
__GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,52 +1,63 @@
|
||||
"use strict";
|
||||
|
||||
const webpack = require("webpack");
|
||||
const merge = require("webpack-merge");
|
||||
const { merge } = require("webpack-merge");
|
||||
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
|
||||
const HtmlPlugin = require("html-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const helpers = require("./helpers");
|
||||
const commonConfig = require("./webpack.config.common");
|
||||
const environment = require("./env/dev.env");
|
||||
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
let webpackConfig = merge(commonConfig(true), {
|
||||
mode: "development",
|
||||
devtool: "cheap-module-eval-source-map",
|
||||
devtool: "eval-cheap-module-source-map",
|
||||
output: {
|
||||
path: helpers.root("dist"),
|
||||
publicPath: "/",
|
||||
filename: "js/[name].bundle.js",
|
||||
chunkFilename: "js/[id].chunk.js"
|
||||
filename: "js/[name].bundle.js"
|
||||
},
|
||||
optimization: {
|
||||
runtimeChunk: "single",
|
||||
concatenateModules: true,
|
||||
splitChunks: {
|
||||
chunks: "all"
|
||||
chunks: "initial"
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin(environment),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new FriendlyErrorsPlugin()
|
||||
new FriendlyErrorsPlugin(),
|
||||
new MiniCSSExtractPlugin({
|
||||
filename: "css/[name].css"
|
||||
})
|
||||
],
|
||||
devServer: {
|
||||
compress: true,
|
||||
historyApiFallback: true,
|
||||
host: "0.0.0.0",
|
||||
hot: true,
|
||||
overlay: true,
|
||||
stats: {
|
||||
normal: true
|
||||
}
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:30030",
|
||||
changeOrigin: true
|
||||
},
|
||||
"/socket.io": {
|
||||
target: "ws://localhost:30030",
|
||||
changeOrigin: false,
|
||||
ws: true
|
||||
}
|
||||
},
|
||||
writeToDisk: false
|
||||
}
|
||||
});
|
||||
|
||||
webpackConfig = merge(webpackConfig, {
|
||||
entry: {
|
||||
main: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
|
||||
},
|
||||
plugins: [
|
||||
new HtmlPlugin({
|
||||
template: "src/templates/Create.html",
|
||||
chunksSortMode: "dependency"
|
||||
new HtmlWebpackPlugin({
|
||||
template: "frontend/templates/Index.html"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const merge = require("webpack-merge");
|
||||
const { merge } = require("webpack-merge");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
|
||||
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
const helpers = require("./helpers");
|
||||
const commonConfig = require("./webpack.config.common");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const environment = isProd
|
||||
? require("./env/prod.env")
|
||||
@@ -16,11 +19,11 @@ const environment = isProd
|
||||
|
||||
const webpackConfig = merge(commonConfig(false), {
|
||||
mode: "production",
|
||||
stats: { children: false },
|
||||
output: {
|
||||
path: helpers.root("public/dist"),
|
||||
publicPath: "/dist/",
|
||||
filename: "js/[name].bundle.[hash:7].js"
|
||||
//filename: "js/[name].bundle.js"
|
||||
publicPath: "/public/dist/",
|
||||
filename: "js/[name].bundle.[fullhash:7].js"
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
@@ -33,37 +36,47 @@ const webpackConfig = merge(commonConfig(false), {
|
||||
}
|
||||
}
|
||||
},
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new HtmlWebpackPlugin({
|
||||
chunks: ["vinlottis"],
|
||||
filename: "index.html",
|
||||
template: "./frontend/templates/Index.html",
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: false,
|
||||
preserveLineBreaks: true,
|
||||
removeAttributeQuotes: true
|
||||
}
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ["default", { discardComments: { removeAll: true } }]
|
||||
}
|
||||
}),
|
||||
new UglifyJSPlugin({
|
||||
cache: true,
|
||||
parallel: false,
|
||||
sourceMap: !isProd
|
||||
new TerserPlugin({
|
||||
test: /\.js(\?.*)?$/i,
|
||||
})
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new CleanWebpackPlugin(), // clean output folder
|
||||
new webpack.EnvironmentPlugin(environment),
|
||||
new MiniCSSExtractPlugin({
|
||||
filename: "css/[name].[hash:7].css"
|
||||
//filename: "css/[name].css"
|
||||
filename: "css/[name].[fullhash:7].css"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if (!isProd) {
|
||||
webpackConfig.devtool = "source-map";
|
||||
}
|
||||
|
||||
if (process.env.npm_config_report) {
|
||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||
.BundleAnalyzerPlugin;
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
|
||||
}
|
||||
if (process.env.BUILD_REPORT) {
|
||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||
.BundleAnalyzerPlugin;
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
|
||||
}
|
||||
|
||||
module.exports = webpackConfig;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<banner />
|
||||
<banner :routes="routes"/>
|
||||
<router-view />
|
||||
<Footer />
|
||||
<UpdateToast
|
||||
v-if="showToast"
|
||||
:text="toastText"
|
||||
@@ -14,17 +15,48 @@
|
||||
<script>
|
||||
import ServiceWorkerMixin from "@/mixins/ServiceWorkerMixin";
|
||||
import banner from "@/ui/Banner";
|
||||
import Footer from "@/ui/Footer";
|
||||
import UpdateToast from "@/ui/UpdateToast";
|
||||
|
||||
export default {
|
||||
name: "vinlottis",
|
||||
components: { banner, UpdateToast },
|
||||
components: { banner, UpdateToast, Footer },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
showToast: false,
|
||||
toastText: null,
|
||||
refreshToast: false
|
||||
refreshToast: false,
|
||||
routes: [
|
||||
{
|
||||
name: "Virtuelt lotteri",
|
||||
route: "/lottery"
|
||||
},
|
||||
{
|
||||
name: "Dagens viner",
|
||||
route: "/dagens/"
|
||||
},
|
||||
{
|
||||
name: "Highscore",
|
||||
route: "/highscore"
|
||||
},
|
||||
{
|
||||
name: "Historie",
|
||||
route: "/history/"
|
||||
},
|
||||
{
|
||||
name: "Foreslå vin",
|
||||
route: "/request"
|
||||
},
|
||||
{
|
||||
name: "Foreslåtte viner",
|
||||
route: "/requested-wines"
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
route: "/login"
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -45,37 +77,31 @@ export default {
|
||||
methods: {
|
||||
closeToast: function() {
|
||||
this.showToast = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./styles/global.scss";
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 600;
|
||||
src: url("/../public/assets/fonts/bold.woff"),
|
||||
url("/../public/assets/fonts/bold.woff") format("woff"), local("Arial");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 300;
|
||||
src: url("/../public/assets/fonts/regular.eot"),
|
||||
url("/../public/assets/fonts/regular.woff") format("woff"), local("Arial");
|
||||
font-display: swap;
|
||||
}
|
||||
@import "styles/global.scss";
|
||||
@import "styles/positioning.scss";
|
||||
@import "styles/vinlottis-icons";
|
||||
|
||||
body {
|
||||
background-color: #dbeede;
|
||||
background-color: $primary;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
background-color: white;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: 80px auto 100px;
|
||||
|
||||
.main-container{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
375
frontend/api.js
Normal file
375
frontend/api.js
Normal file
@@ -0,0 +1,375 @@
|
||||
import fetch from "node-fetch";
|
||||
|
||||
const BASE_URL = window.location.origin;
|
||||
|
||||
const statistics = () => {
|
||||
return fetch("/api/purchase/statistics")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const colorStatistics = () => {
|
||||
return fetch("/api/purchase/statistics/color")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const highscoreStatistics = () => {
|
||||
return fetch("/api/highscore/statistics")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const overallWineStatistics = () => {
|
||||
return fetch("/api/wines/statistics/overall")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const allRequestedWines = () => {;
|
||||
return fetch("/api/request/all")
|
||||
.then(resp => {
|
||||
const isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||
return Promise.all([resp.json(), isAdmin]);
|
||||
});
|
||||
};
|
||||
|
||||
const chartWinsByColor = () => {
|
||||
return fetch("/api/purchase/statistics/color")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const chartPurchaseByColor = () => {
|
||||
return fetch("/api/purchase/statistics")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const prelottery = () => {
|
||||
return fetch("/api/wines/prelottery")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const sendLottery = sendObject => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
};
|
||||
|
||||
return fetch("/api/lottery", options)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const sendLotteryWinners = sendObject => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
};
|
||||
|
||||
return fetch("/api/lottery/winners", options)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const addAttendee = sendObject => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
};
|
||||
|
||||
return fetch("/api/virtual/attendee/add", options)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const getVirtualWinner = () => {
|
||||
return fetch("/api/virtual/winner/draw")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const attendeesSecure = () => {
|
||||
return fetch("/api/virtual/attendee/all/secure")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const winnersSecure = () => {
|
||||
return fetch("/api/virtual/winner/all/secure")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const winners = () => {
|
||||
return fetch("/api/virtual/winner/all")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const deleteRequestedWine = wineToBeDeleted => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(wineToBeDeleted)
|
||||
};
|
||||
|
||||
return fetch("api/request/" + wineToBeDeleted.id, options)
|
||||
.then(resp => resp.json());
|
||||
}
|
||||
|
||||
const deleteWinners = () => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch("/api/virtual/winner/all", options)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const deleteAttendees = () => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch("/api/virtual/attendee/all", options)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const attendees = () => {
|
||||
return fetch("/api/virtual/attendee/all")
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const requestNewWine = (wine) => {
|
||||
const options = {
|
||||
body: JSON.stringify({
|
||||
wine: wine
|
||||
}),
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "post"
|
||||
}
|
||||
|
||||
return fetch("/api/request/new-wine", options)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const logWines = wines => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(wines)
|
||||
};
|
||||
|
||||
return fetch("/api/log/wines", options)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const wineSchema = () => {
|
||||
const url = new URL("/api/wineinfo/schema", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const barcodeToVinmonopolet = id => {
|
||||
return fetch("/api/wineinfo/")
|
||||
.then(async resp => {
|
||||
if (!resp.ok) {
|
||||
if (resp.status == 404) {
|
||||
throw await resp.json();
|
||||
}
|
||||
} else {
|
||||
return resp.json();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const searchForWine = searchString => {
|
||||
return fetch("/api/wineinfo/search?query=" + searchString)
|
||||
.then(async resp => {
|
||||
if (!resp.ok) {
|
||||
if (resp.status == 404) {
|
||||
throw await resp.json();
|
||||
}
|
||||
} else {
|
||||
return resp.json();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleErrors = async resp => {
|
||||
if ([400, 409].includes(resp.status)) {
|
||||
throw await resp.json();
|
||||
} else {
|
||||
console.error("Unexpected error occured when login/register user:", resp);
|
||||
throw await resp.json();
|
||||
}
|
||||
};
|
||||
|
||||
const login = (username, password) => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
return fetch("/api/login", options)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const register = (username, password) => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
return fetch("/api/register", options)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getChatHistory = (page=1, limit=10) => {
|
||||
const url = new URL("/api/chat/history", BASE_URL);
|
||||
if (!isNaN(page)) url.searchParams.append("page", page);
|
||||
if (!isNaN(limit)) url.searchParams.append("limit", limit);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const finishedDraw = () => {
|
||||
const options = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
return fetch("/api/virtual/finish", options)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const getAmIWinner = id => {
|
||||
return fetch(`/api/winner/${id}`)
|
||||
.then(resp => resp.json());
|
||||
};
|
||||
|
||||
const postWineChosen = (id, wineName) => {
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ wineName: wineName })
|
||||
};
|
||||
|
||||
return fetch(`/api/winner/${id}`, options)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const historyAll = () => {
|
||||
return fetch(`/api/lottery/all`)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const historyByDate = (date) => {
|
||||
return fetch(`/api/lottery/by-date/${ date }`)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getWinnerByName = (name) => {
|
||||
const encodedName = encodeURIComponent(name)
|
||||
|
||||
return fetch(`/api/lottery/by-name/${name}`)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const projectContributors = () => {
|
||||
return fetch("/api/project/contributors")
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
export {
|
||||
statistics,
|
||||
colorStatistics,
|
||||
highscoreStatistics,
|
||||
overallWineStatistics,
|
||||
chartWinsByColor,
|
||||
chartPurchaseByColor,
|
||||
prelottery,
|
||||
sendLottery,
|
||||
sendLotteryWinners,
|
||||
logWines,
|
||||
wineSchema,
|
||||
barcodeToVinmonopolet,
|
||||
searchForWine,
|
||||
requestNewWine,
|
||||
allRequestedWines,
|
||||
login,
|
||||
register,
|
||||
addAttendee,
|
||||
getVirtualWinner,
|
||||
attendeesSecure,
|
||||
attendees,
|
||||
winners,
|
||||
winnersSecure,
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
deleteRequestedWine,
|
||||
getChatHistory,
|
||||
finishedDraw,
|
||||
getAmIWinner,
|
||||
postWineChosen,
|
||||
historyAll,
|
||||
historyByDate,
|
||||
getWinnerByName,
|
||||
projectContributors
|
||||
};
|
||||
135
frontend/components/AboutPage.vue
Normal file
135
frontend/components/AboutPage.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="header-chin">
|
||||
<h1>Om oss</h1>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="github">
|
||||
<h2>Project contributors</h2>
|
||||
|
||||
<div class="contributors">
|
||||
<a class="contributor" v-for="contributor in contributors" :href="contributor.profileUrl">
|
||||
<img :src="contributor.avatarUrl" />
|
||||
<span class="name">{{ contributor.name }}</span>
|
||||
|
||||
<span>Contributions: {{ contributor.projectContributions }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<h2>Project guidelines</h2>
|
||||
<p>Lorem ipsum</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { projectContributors } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
contributors: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchContributors()
|
||||
},
|
||||
methods: {
|
||||
fetchContributors() {
|
||||
projectContributors()
|
||||
.then(contributors => contributors.contributors)
|
||||
.then(contributors => this.filterOutBots(contributors))
|
||||
.then(contributors => this.contributors = contributors);
|
||||
},
|
||||
filterOutBots(contributors) {
|
||||
return contributors.filter(contributor => !contributor.name.includes('[bot]'))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.header-chin {
|
||||
padding: 3rem 0 4rem;
|
||||
margin-bottom: 4rem;
|
||||
background-color: $primary;
|
||||
|
||||
h1 {
|
||||
font-family: 'knowit';
|
||||
font-weight: 400;
|
||||
font-size: 3.5rem;
|
||||
display: block;
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 10vw 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
@include mobile {
|
||||
margin: 0 5vw 1rem;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.github {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.contributors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
@include mobile {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
// grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.contributor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
margin-right: 1rem;
|
||||
// min-width: 125px;
|
||||
color: $matte-text-color;
|
||||
|
||||
-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);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0.5rem;
|
||||
|
||||
&:first-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
width: max-content;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
frontend/components/AllRequestedWines.vue
Normal file
64
frontend/components/AllRequestedWines.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<main class="container">
|
||||
<h1>Alle foreslåtte viner</h1>
|
||||
|
||||
<section class="requested-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"/>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { allRequestedWines } from "@/api";
|
||||
import RequestedWineCard from "@/ui/RequestedWineCard";
|
||||
export default {
|
||||
components: {
|
||||
RequestedWineCard
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
wines: undefined,
|
||||
canRequest: true,
|
||||
isAdmin: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterOutDeletedWine(wine){
|
||||
this.wines = this.wines.filter(item => item.wine._id !== wine._id)
|
||||
},
|
||||
async refreshData(){
|
||||
[this.wines, this.isAdmin] = await allRequestedWines() || [[], false]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.refreshData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.container {
|
||||
width: 90vw;
|
||||
margin: 3rem auto;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-family: "knowit";
|
||||
color: $matte-text-color;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.requested-wines-container{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
}
|
||||
</style>
|
||||
130
frontend/components/AllWinesPage.vue
Normal file
130
frontend/components/AllWinesPage.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1 class="">Alle viner</h1>
|
||||
|
||||
<div id="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>
|
||||
<span class="raffle-element red-raffle">{{ wine.red == null ? 0 : wine.red }}</span>
|
||||
<span class="raffle-element green-raffle">{{ wine.green == null ? 0 : wine.green }}</span>
|
||||
<span class="raffle-element yellow-raffle">{{ wine.yellow == null ? 0 : wine.yellow }}</span>
|
||||
</div>
|
||||
|
||||
<div class="name-wins">
|
||||
<span class="label">Vunnet av:</span>
|
||||
<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>
|
||||
</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
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wines: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
winDateUrl(date) {
|
||||
const timestamp = new Date(date).getTime();
|
||||
return `/history/${timestamp}`
|
||||
},
|
||||
dateString: dateString
|
||||
},
|
||||
async mounted() {
|
||||
this.wines = await overallWineStatistics();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries";
|
||||
@import "@/styles/variables";
|
||||
|
||||
.container {
|
||||
width: 90vw;
|
||||
margin: 3rem auto;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-family: "knowit";
|
||||
font-weight: normal;
|
||||
|
||||
font-family: knowit, Arial;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.label {
|
||||
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;
|
||||
|
||||
a {
|
||||
font-weight: normal;
|
||||
&:not(:hover) {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
padding-left: 1.5rem;
|
||||
list-style: none;
|
||||
|
||||
&:before {
|
||||
content: "- ";
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
padding: 1rem;
|
||||
font-size: 1.3rem;
|
||||
margin: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,15 +5,14 @@
|
||||
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
||||
</p>
|
||||
|
||||
<RaffleGenerator @numberOfBallots="val => this.numberOfBallots = val" />
|
||||
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" />
|
||||
|
||||
<Vipps class="vipps" :amount="numberOfBallots" />
|
||||
<Vipps class="vipps" :amount="numberOfRaffles" />
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { page, event } from "vue-analytics";
|
||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||
import Vipps from "@/ui/Vipps";
|
||||
import Countdown from "@/ui/Countdown";
|
||||
@@ -27,7 +26,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hardStart: false,
|
||||
numberOfBallots: null
|
||||
numberOfRaffles: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -44,7 +43,7 @@ export default {
|
||||
this.hardStart = true;
|
||||
},
|
||||
track() {
|
||||
this.$ga.page("/lottery/generate");
|
||||
window.ga('send', 'pageview', '/lottery/generate');
|
||||
}
|
||||
}
|
||||
};
|
||||
177
frontend/components/HighscorePage.vue
Normal file
177
frontend/components/HighscorePage.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Vinlottis highscore</h1>
|
||||
|
||||
<div class="filter flex el-spacing">
|
||||
<input type="text" v-model="filterInput" placeholder="Filtrer på navn" />
|
||||
<button v-if="filterInput" @click="resetFilter" class="vin-button auto-height margin-left-sm">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<b>{{ person.rank }}.</b> {{ person.name }} - {{ person.wins.length }}
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="center desktop-only">
|
||||
👈 Se dine vin(n), trykk på navnet ditt
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highscoreStatistics } from "@/api";
|
||||
import { humanReadableDate, daysAgo } from "@/utils";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: { Wine },
|
||||
data() {
|
||||
return {
|
||||
highscore: [],
|
||||
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);
|
||||
},
|
||||
computed: {
|
||||
filteredResults() {
|
||||
let highscore = this.highscore;
|
||||
let val = this.filterInput;
|
||||
|
||||
if (val.length) {
|
||||
val = val.toLowerCase()
|
||||
const nameIncludesString = (person) => person.name.toLowerCase().includes(val);
|
||||
highscore = highscore.filter(nameIncludesString)
|
||||
}
|
||||
|
||||
return highscore
|
||||
}
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
resetFilter() {
|
||||
this.filterInput = '';
|
||||
document.getElementsByTagName('input')[0].focus();
|
||||
},
|
||||
selectWinner(winner) {
|
||||
const path = "/highscore/" + encodeURIComponent(winner.name)
|
||||
this.$router.push(path);
|
||||
},
|
||||
humanReadableDate: humanReadableDate,
|
||||
daysAgo: daysAgo
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
$elementSpacing: 3.5rem;
|
||||
|
||||
.el-spacing {
|
||||
margin-bottom: $elementSpacing;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90vw;
|
||||
margin: 3rem auto;
|
||||
max-width: 1200px;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 3rem;
|
||||
|
||||
@include desktop {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-family: "knowit";
|
||||
color: $matte-text-color;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.filter input {
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
border-color: black;
|
||||
border-width: 1.5px;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
@include desktop {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.highscore-header {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.3rem;
|
||||
color: $matte-text-color;
|
||||
}
|
||||
|
||||
.highscore-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
width: fit-content;
|
||||
display: inline-block;
|
||||
margin-bottom: calc(1rem - 2px);
|
||||
font-size: 1.25rem;
|
||||
color: $matte-text-color;
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 2px solid transparent;
|
||||
&:hover, &:focus {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
right: 10vw;
|
||||
max-width: 50vw;
|
||||
|
||||
font-size: 2.5rem;
|
||||
background-color: $primary;
|
||||
padding: 1rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-style: italic;
|
||||
|
||||
@include widescreen {
|
||||
right: 20vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
frontend/components/HistoryPage.vue
Normal file
44
frontend/components/HistoryPage.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<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) }`" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { historyByDate, historyAll } from '@/api'
|
||||
import { humanReadableDate } from "@/utils";
|
||||
import Winners from '@/ui/Winners'
|
||||
|
||||
export default {
|
||||
name: 'History page of prev lotteries',
|
||||
components: { Winners },
|
||||
data() {
|
||||
return {
|
||||
lotteries: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
humanReadableDate: humanReadableDate
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -11,9 +11,7 @@ import VirtualLotteryPage from "@/components/VirtualLotteryPage";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tabs,
|
||||
GeneratePage,
|
||||
VirtualLotteryPage
|
||||
Tabs
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
273
frontend/components/PersonalHighscorePage.vue
Normal file
273
frontend/components/PersonalHighscorePage.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Vinlottis highscore</h1>
|
||||
|
||||
<div class="backdrop">
|
||||
<a @click="navigateBack" @keydown.enter="navigateBack" tabindex="0">
|
||||
⬅ <span class="vin-link navigate-back">Tilbake til {{ previousRoute.name }}</span>
|
||||
</a>
|
||||
|
||||
<section v-if="winner">
|
||||
<h2 class="name">{{ winner.name }}</h2>
|
||||
|
||||
<p class="win-count el-spacing">{{ numberOfWins }} vinn</p>
|
||||
|
||||
<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">
|
||||
{{ occurences }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="el-spacing">Flasker vunnet:</h4>
|
||||
|
||||
<div v-for="win in winner.highscore" :key="win._id">
|
||||
<router-link :to="winDateUrl(win.date)" class="days-ago">
|
||||
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
|
||||
</router-link>
|
||||
|
||||
<div class="won-wine">
|
||||
<img :src="smallerWineImage(win.wine.image)">
|
||||
|
||||
<div class="won-wine-details">
|
||||
<h3>{{ win.wine.name }}</h3>
|
||||
<a :href="win.wine.vivinoLink" class="vin-link no-margin">
|
||||
Les mer på vinmonopolet.no
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="raffle-element small" :class="win.color + `-raffle`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 v-else-if="error" class="error">
|
||||
{{ error }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getWinnerByName } from "@/api";
|
||||
import { humanReadableDate, daysAgo } from "@/utils";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
winner: undefined,
|
||||
error: undefined,
|
||||
previousRoute: {
|
||||
default: true,
|
||||
name: "topplisten",
|
||||
path: "/highscore"
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
next(vm => {
|
||||
if (from.name != null)
|
||||
vm.previousRoute = from
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
numberOfWins() {
|
||||
return this.winner.highscore.length
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const nameFromURL = this.$route.params.name;
|
||||
getWinnerByName(nameFromURL)
|
||||
.then(winner => this.setWinner(winner))
|
||||
.catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`)
|
||||
},
|
||||
methods: {
|
||||
setWinner(winner) {
|
||||
this.winner = {
|
||||
name: winner.name,
|
||||
highscore: [],
|
||||
...winner
|
||||
}
|
||||
this.winningColors = this.findWinningColors()
|
||||
},
|
||||
smallerWineImage(image) {
|
||||
if (image && image.includes(`515x515`))
|
||||
return image.replace(`515x515`, `175x175`)
|
||||
return image
|
||||
},
|
||||
findWinningColors() {
|
||||
const colors = this.winner.highscore.map(win => win.color)
|
||||
const colorOccurences = {}
|
||||
colors.forEach(color => {
|
||||
if (colorOccurences[color] == undefined) {
|
||||
colorOccurences[color] = 1
|
||||
} else {
|
||||
colorOccurences[color] += 1
|
||||
}
|
||||
})
|
||||
return colorOccurences
|
||||
},
|
||||
winDateUrl(date) {
|
||||
const timestamp = new Date(date).getTime();
|
||||
return `/history/${timestamp}`
|
||||
},
|
||||
navigateBack() {
|
||||
if (this.previousRoute.default) {
|
||||
this.$router.push({ path: this.previousRoute.path });
|
||||
} else {
|
||||
this.$router.go(-1);
|
||||
}
|
||||
},
|
||||
humanReadableDate: humanReadableDate,
|
||||
daysAgo: daysAgo
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables";
|
||||
@import "@/styles/media-queries";
|
||||
|
||||
$elementSpacing: 3rem;
|
||||
|
||||
.el-spacing {
|
||||
margin-bottom: $elementSpacing;
|
||||
}
|
||||
|
||||
.navigate-back {
|
||||
font-weight: normal;
|
||||
font-size: 1.2rem;
|
||||
border-width: 2px;
|
||||
border-color: gray;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90vw;
|
||||
margin: 3rem auto;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 3rem;
|
||||
max-width: 1200px;
|
||||
|
||||
@include desktop {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-family: "knowit";
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
font-size: 3.5rem;
|
||||
font-family: "knowit";
|
||||
font-weight: normal;
|
||||
margin: 2rem 0 1rem 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.win-count {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.raffle-container {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
|
||||
div:not(:last-of-type) {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
.raffle-element {
|
||||
width: 5rem;
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
margin-top: 0;
|
||||
|
||||
&.small {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.days-ago {
|
||||
color: $matte-text-color;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.won-wine {
|
||||
--spacing: 1rem;
|
||||
background-color: white;
|
||||
margin: var(--spacing) 0 3rem 0;
|
||||
padding: var(--spacing);
|
||||
|
||||
position: relative;
|
||||
|
||||
@include desktop {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0 3rem;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
&-details {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
|
||||
@include tablet {
|
||||
width: calc(100% - 160px - 80px);
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
color: $matte-text-color;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 1.2rem;
|
||||
border-width: 2px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
position: absolute;
|
||||
top: calc(var(--spacing) * 2);
|
||||
right: calc(var(--spacing) * 2);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.backdrop {
|
||||
$background: rgb(244,244,244);
|
||||
|
||||
--padding: 2rem;
|
||||
@include desktop {
|
||||
--padding: 5rem;
|
||||
}
|
||||
background-color: $background;
|
||||
padding: var(--padding);
|
||||
}
|
||||
</style>
|
||||
@@ -78,9 +78,14 @@
|
||||
</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" :inlineSlot="true">
|
||||
<wine v-for="winner in winners" :key="winner" :wine="winner.wine">
|
||||
<div class="winner-element">
|
||||
<div class="color-selector">
|
||||
<div class="label-div">
|
||||
@@ -107,16 +112,30 @@
|
||||
@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">
|
||||
<button class="vin-button" @click="sendInfo">Send inn vinnere</button>
|
||||
<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>
|
||||
@@ -126,7 +145,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { prelottery, log, logWines, wineSchema } from "@/api";
|
||||
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";
|
||||
@@ -137,6 +166,7 @@ export default {
|
||||
return {
|
||||
payed: undefined,
|
||||
winners: [],
|
||||
fetchedWinners: [],
|
||||
wines: [],
|
||||
pushMessage: "",
|
||||
pushLink: "/",
|
||||
@@ -160,8 +190,67 @@ export default {
|
||||
},
|
||||
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;
|
||||
},
|
||||
@@ -173,6 +262,7 @@ export default {
|
||||
this.winners.push({
|
||||
name: "",
|
||||
color: "",
|
||||
potentialWinner: "",
|
||||
wine: {
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
@@ -222,7 +312,7 @@ export default {
|
||||
},
|
||||
sendWines: async function() {
|
||||
let response = await logWines(this.wines);
|
||||
if (response == true) {
|
||||
if (response.success == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
@@ -240,7 +330,7 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
sendInfo: async function(event) {
|
||||
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,
|
||||
@@ -249,48 +339,63 @@ export default {
|
||||
};
|
||||
|
||||
let sendObject = {
|
||||
purchase: {
|
||||
date: new Date(),
|
||||
lottery: {
|
||||
date: dateString(new Date()),
|
||||
...colors
|
||||
},
|
||||
winners: this.winners
|
||||
}
|
||||
};
|
||||
|
||||
if (sendObject.purchase.red == undefined) {
|
||||
if (sendObject.lottery.red == undefined) {
|
||||
alert("Rød må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.purchase.green == undefined) {
|
||||
if (sendObject.lottery.green == undefined) {
|
||||
alert("Grønn må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.purchase.yellow == undefined) {
|
||||
if (sendObject.lottery.yellow == undefined) {
|
||||
alert("Gul må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.purchase.blue == undefined) {
|
||||
if (sendObject.lottery.blue == undefined) {
|
||||
alert("Blå må defineres");
|
||||
return;
|
||||
}
|
||||
|
||||
sendObject.purchase.bought =
|
||||
sendObject.lottery.bought =
|
||||
parseInt(colors.blue) +
|
||||
parseInt(colors.red) +
|
||||
parseInt(colors.green) +
|
||||
parseInt(colors.yellow);
|
||||
const stolen = sendObject.purchase.bought - parseInt(this.payed) / 10;
|
||||
const stolen = sendObject.lottery.bought - parseInt(this.payed) / 10;
|
||||
if (isNaN(stolen) || stolen == undefined) {
|
||||
alert("Betalt må registreres");
|
||||
return;
|
||||
}
|
||||
sendObject.purchase.stolen = stolen;
|
||||
sendObject.lottery.stolen = stolen;
|
||||
|
||||
if (sendObject.winners.length == 0) {
|
||||
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.winners.length; i++) {
|
||||
let currentWinner = sendObject.winners[i];
|
||||
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");
|
||||
@@ -302,7 +407,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
let response = await log(sendObject);
|
||||
let response = await sendLotteryWinners(sendObject);
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
@@ -341,7 +446,6 @@ export default {
|
||||
}
|
||||
},
|
||||
setWinnerdataToStorage() {
|
||||
console.log("saving localstorage");
|
||||
localStorage.setItem("winners", JSON.stringify(this.winners));
|
||||
localStorage.setItem(
|
||||
"colorValues",
|
||||
@@ -362,7 +466,13 @@ export default {
|
||||
<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;
|
||||
@@ -376,12 +486,24 @@ h2 {
|
||||
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;
|
||||
|
||||
@@ -391,8 +513,7 @@ hr {
|
||||
}
|
||||
}
|
||||
.winner-container {
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
@@ -405,7 +526,13 @@ hr {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .wine {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
.edit {
|
||||
width: 100%;
|
||||
@@ -419,10 +546,10 @@ hr {
|
||||
}
|
||||
.winner-element {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
|
||||
@include desktop {
|
||||
margin-top: 1.5rem;
|
||||
> div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
@@ -516,7 +643,7 @@ hr {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
margin: 3rem auto 0;
|
||||
margin: 3rem auto 1rem;
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
@@ -532,9 +659,9 @@ hr {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
237
frontend/components/RequestWine.vue
Normal file
237
frontend/components/RequestWine.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<section class="main-container">
|
||||
<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 }"
|
||||
/>
|
||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||
<section class="wine-info">
|
||||
<h2 v-if="wine.name">{{ wine.name }}</h2>
|
||||
<h2 v-else>(no name)</h2>
|
||||
<div class="details">
|
||||
<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>
|
||||
</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>
|
||||
</section>
|
||||
<p v-if="this.wines && this.wines.length == 0">
|
||||
Fant ingen viner med det navnet!
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { searchForWine, requestNewWine } from "@/api";
|
||||
import Wine from "@/ui/Wine";
|
||||
import Modal from "@/ui/Modal";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Wine,
|
||||
Modal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchString: undefined,
|
||||
wines: undefined,
|
||||
showModal: false,
|
||||
modalButtons: [
|
||||
{
|
||||
text: "Legg til flere viner",
|
||||
action: "stay"
|
||||
},
|
||||
{
|
||||
text: "Se alle viner",
|
||||
action: "move"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchWineFromVin(){
|
||||
if(this.searchString){
|
||||
this.wines = []
|
||||
let localSearchString = this.searchString.replace(/ /g,"_");
|
||||
searchForWine(localSearchString)
|
||||
.then(res => this.wines = res)
|
||||
}
|
||||
},
|
||||
request(wine){
|
||||
requestNewWine(wine)
|
||||
.then(() => this.showModal = true)
|
||||
},
|
||||
emitFromModalButton(action){
|
||||
if(action == "stay"){
|
||||
this.showModal = false
|
||||
} else {
|
||||
this.$router.push("/requested-wines");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries";
|
||||
@import "@/styles/global";
|
||||
@import "@/styles/variables";
|
||||
|
||||
|
||||
h1{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-container{
|
||||
margin: auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 90%;
|
||||
color: black;
|
||||
border-radius: 4px;
|
||||
padding: 1rem 1rem;
|
||||
border: 1px solid black;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
|
||||
.search-container{
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.search-section{
|
||||
display: grid;
|
||||
grid: 1fr / 1fr .2fr;
|
||||
|
||||
@include mobile{
|
||||
.vin-button{
|
||||
display: none;
|
||||
}
|
||||
.search-input-field{
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.single-result{
|
||||
margin-top: 1rem;
|
||||
display: grid;
|
||||
grid: 1fr / .5fr 2fr .5fr .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);
|
||||
|
||||
@include mobile{
|
||||
|
||||
grid: 1fr .5fr / .5fr 1fr;
|
||||
grid-template-areas: "picture details"
|
||||
"button-left button-right";
|
||||
grid-gap: .5em;
|
||||
|
||||
.vin-button{
|
||||
grid-area: button-right;
|
||||
padding: .5em;
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.wine-link{
|
||||
grid-area: button-left;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-size: 1em;
|
||||
max-width: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.wine-image {
|
||||
height: 100px;
|
||||
grid-area: picture;
|
||||
}
|
||||
|
||||
.wine-placeholder {
|
||||
height: 100px;
|
||||
width: 70px;
|
||||
grid-area: picture;
|
||||
}
|
||||
|
||||
.wine-info{
|
||||
grid-area: details;
|
||||
width: 100%;
|
||||
|
||||
h2{
|
||||
margin: 0;
|
||||
}
|
||||
.details{
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.wine-link {
|
||||
grid-area: button-left;
|
||||
color: #333333;
|
||||
font-family: Arial;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $link-color;
|
||||
height: 1.2em;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.vin-button{
|
||||
grid-area: button-right;
|
||||
}
|
||||
|
||||
@include tablet{
|
||||
h2{
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
@include desktop{
|
||||
h2{
|
||||
font-size: 1.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,16 +1,14 @@
|
||||
<template>
|
||||
<div class="outer">
|
||||
<div class="container">
|
||||
<h1 class="title">Dagens viner</h1>
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" :fullscreen="true" :inlineSlot="true" />
|
||||
</div>
|
||||
<div class="container">
|
||||
<h1 class="title">Dagens viner</h1>
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { page, event } from "vue-analytics";
|
||||
import { prelottery } from "@/api";
|
||||
import Banner from "@/ui/Banner";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
@@ -25,14 +23,14 @@ export default {
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
const _wines = await fetch("/api/wines/prelottery");
|
||||
this.wines = await _wines.json();
|
||||
prelottery().then(wines => this.wines = wines);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/styles/media-queries";
|
||||
@import "@/styles/media-queries";
|
||||
@import "@/styles/variables";
|
||||
|
||||
.wine-image {
|
||||
height: 250px;
|
||||
@@ -46,7 +44,7 @@ h1 {
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: space-evenly;
|
||||
margin: 0 2rem;
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
@@ -110,7 +108,7 @@ a:visited {
|
||||
font-family: Arial;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ff5fff;
|
||||
border-bottom: 1px solid $link-color;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
382
frontend/components/VinlottisPage.vue
Normal file
382
frontend/components/VinlottisPage.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<main class="main-container">
|
||||
|
||||
<section class="top-container">
|
||||
|
||||
<div class="want-to-win">
|
||||
<h1>
|
||||
Vil du også vinne?
|
||||
</h1>
|
||||
<img
|
||||
src="/public/assets/images/notification.svg"
|
||||
alt="Notification-bell"
|
||||
@click="requestNotificationAccess"
|
||||
class="notification-request-button"
|
||||
role="button"
|
||||
v-if="notificationAllowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<router-link to="/lottery" class="participate-button">
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
<p>Trykk her for å delta</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/generate" class="see-details-link">
|
||||
Se vipps detaljer og QR-kode
|
||||
</router-link>
|
||||
|
||||
<div class="icons-container">
|
||||
<i class="icon icon--heart-sparks"></i>
|
||||
<i class="icon icon--face-1"></i>
|
||||
<i class="icon icon--face-3"></i>
|
||||
<i class="icon icon--ballon"></i>
|
||||
|
||||
<i class="icon icon--bottle"></i>
|
||||
<i class="icon icon--bottle"></i>
|
||||
<i class="icon icon--bottle"></i>
|
||||
<i class="icon icon--bottle"></i>
|
||||
<i class="icon icon--bottle"></i>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="content-container">
|
||||
|
||||
<div class="scroll-info">
|
||||
<i class ="icon icon--arrow-long-right"></i>
|
||||
<p>Scroll for å se vinnere og annen gøy statistikk</p>
|
||||
</div>
|
||||
|
||||
<Highscore class="highscore"/>
|
||||
<TotalBought class="total-bought" />
|
||||
|
||||
<section class="chart-container">
|
||||
<PurchaseGraph class="purchase" />
|
||||
<WinGraph class="win" />
|
||||
</section>
|
||||
|
||||
<Wines class="wines-container" />
|
||||
|
||||
</section>
|
||||
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PurchaseGraph from "@/ui/PurchaseGraph";
|
||||
import TotalBought from "@/ui/TotalBought";
|
||||
import Highscore from "@/ui/Highscore";
|
||||
import WinGraph from "@/ui/WinGraph";
|
||||
import Wines from "@/ui/Wines";
|
||||
import Vipps from "@/ui/Vipps";
|
||||
import Countdown from "@/ui/Countdown";
|
||||
import { prelottery } from "@/api";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PurchaseGraph,
|
||||
TotalBought,
|
||||
Highscore,
|
||||
WinGraph,
|
||||
Wines,
|
||||
Vipps,
|
||||
Countdown
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hardStart: false,
|
||||
pushAllowed: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
notificationAllowed: function() {
|
||||
if (!("PushManager" in window)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
Notification.permission !== "granted" ||
|
||||
!this.pushAllowed ||
|
||||
localStorage.getItem("push") == null
|
||||
);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.$on("push-allowed", () => {
|
||||
this.pushAllowed = true;
|
||||
});
|
||||
if (window.location.hostname == "localhost") {
|
||||
return;
|
||||
}
|
||||
this.track();
|
||||
},
|
||||
methods: {
|
||||
requestNotificationAccess() {
|
||||
this.$root.$children[0].registerServiceWorkerPushNotification();
|
||||
},
|
||||
changeEnabled(way) {
|
||||
this.hardStart = way;
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/');
|
||||
},
|
||||
startCountdown() {
|
||||
this.hardStart = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "../styles/variables.scss";
|
||||
|
||||
.top-container {
|
||||
height: 30em;
|
||||
background-color: $primary;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-template-rows: repeat(12, 1fr);
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
|
||||
@include mobile{
|
||||
padding-bottom: 2em;
|
||||
height: 15em;
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
}
|
||||
|
||||
.want-to-win {
|
||||
grid-row: 2 / 4;
|
||||
grid-column: 2 / -1;
|
||||
display: flex;
|
||||
|
||||
h1{
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
h1{
|
||||
font-size: 3em;
|
||||
}
|
||||
grid-row: 2 / 4;
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-request-button{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.participate-button {
|
||||
grid-row: 4 / 6;
|
||||
grid-column: 2 / -1;
|
||||
|
||||
background: inherit;
|
||||
border: 4px solid black;
|
||||
padding: 0 1em 0 1em;
|
||||
display: flex;
|
||||
width: 12.5em;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
|
||||
i {
|
||||
color: $link-color;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
grid-row: 4 / 6;
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
}
|
||||
|
||||
.see-details-link {
|
||||
grid-row: 6 / 8;
|
||||
grid-column: 2 / -1;
|
||||
|
||||
@include tablet {
|
||||
grid-row: 6 / 8;
|
||||
grid-column: 2 / 10;
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
font-weight: 200;
|
||||
font-size: 1.3em;
|
||||
|
||||
text-decoration: underline;
|
||||
text-decoration-color: $link-color;
|
||||
text-underline-position: under;
|
||||
}
|
||||
|
||||
.icons-container {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 7 / -1;
|
||||
@include mobile{
|
||||
margin-top: 2em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
grid-row: 6 / -1;
|
||||
grid-column: 7 / -1;
|
||||
}
|
||||
|
||||
@include desktop{
|
||||
grid-row: 4 / -3;
|
||||
grid-column: 7 / 11;
|
||||
}
|
||||
|
||||
@include widescreen {
|
||||
grid-column: 6 / 10;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
min-width: 375px;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid: repeat(6, 1fr) / repeat(12, 1fr);
|
||||
|
||||
i {
|
||||
font-size: 5em;
|
||||
|
||||
&.icon--heart-sparks{
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 2 / 4;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
|
||||
}
|
||||
&.icon--face-1{
|
||||
grid-column: 4 / 7;
|
||||
grid-row: 2 / 4;
|
||||
justify-self: center;
|
||||
|
||||
}
|
||||
&.icon--face-3{
|
||||
grid-column: 7 / 10;
|
||||
grid-row: 1 / 4;
|
||||
align-self: center;
|
||||
}
|
||||
&.icon--ballon{
|
||||
grid-column: 9 / 11;
|
||||
grid-row: 3 / 5;
|
||||
|
||||
}
|
||||
&.icon--bottle{
|
||||
grid-row: 4 / -1;
|
||||
|
||||
&:nth-of-type(5) {
|
||||
grid-column: 4 / 5;
|
||||
align-self: center;
|
||||
}
|
||||
&:nth-of-type(6) {
|
||||
grid-column: 5 / 6;
|
||||
}
|
||||
&:nth-of-type(7) {
|
||||
grid-column: 6 / 7;
|
||||
align-self: center;
|
||||
}
|
||||
&:nth-of-type(8) {
|
||||
grid-column: 7 / 8;
|
||||
}
|
||||
&:nth-of-type(9){
|
||||
grid-column: 8 / 9;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-family: "knowit";
|
||||
}
|
||||
|
||||
.to-lottery{
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
row-gap: 5em;
|
||||
|
||||
.scroll-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
.total-bought {
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.highscore {
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
|
||||
.icon--arrow-long-right {
|
||||
transform: rotate(90deg);
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
|
||||
.scroll-info{
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
grid-column: 3 / -3;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.total-bought {
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
|
||||
.highscore {
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
grid-column: 3 / -3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
380
frontend/components/VirtualLotteryPage.vue
Normal file
380
frontend/components/VirtualLotteryPage.vue
Normal file
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div>
|
||||
<header ref="header">
|
||||
<div class="container">
|
||||
<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>Send vipps med melding "Vinlotteri" for å bli registrert til lotteriet.</li>
|
||||
<li>Send gjerne melding om fargeønske også.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<Vipps :amount="1" class="vipps-qr desktop-only" />
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container" ref="content">
|
||||
<WinnerDraw
|
||||
:currentWinnerDrawn="currentWinnerDrawn"
|
||||
:currentWinner="currentWinner"
|
||||
:attendees="attendees"
|
||||
/>
|
||||
|
||||
<div class="todays-raffles">
|
||||
<h2>Liste av lodd kjøpt i dag</h2>
|
||||
|
||||
<div class="raffle-container">
|
||||
<div v-for="color in Object.keys(ticketsBought)" :class="color + '-raffle raffle-element'" :key="color">
|
||||
<span>{{ ticketsBought[color] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Winners :winners="winners" class="winners" :drawing="currentWinner" />
|
||||
|
||||
<div class="container-attendees">
|
||||
<h2>Deltakere ({{ attendees.length }})</h2>
|
||||
<Attendees :attendees="attendees" class="attendees" />
|
||||
</div>
|
||||
|
||||
<div class="container-chat">
|
||||
<h2>Chat</h2>
|
||||
<Chat class="chat" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container wines-container">
|
||||
<h2>Dagens fangst ({{ wines.length }})</h2>
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||
</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";
|
||||
import Attendees from "@/ui/Attendees";
|
||||
import Wine from "@/ui/Wine";
|
||||
import Winners from "@/ui/Winners";
|
||||
import WinnerDraw from "@/ui/WinnerDraw";
|
||||
import io from "socket.io-client";
|
||||
|
||||
export default {
|
||||
components: { Chat, Attendees, Winners, WinnerDraw, Vipps, VippsPill, Wine },
|
||||
data() {
|
||||
return {
|
||||
attendees: [],
|
||||
winners: [],
|
||||
wines: [],
|
||||
currentWinnerDrawn: false,
|
||||
currentWinner: null,
|
||||
socket: null,
|
||||
attendeesFetched: false,
|
||||
wasDisconnected: false,
|
||||
ticketsBought: {
|
||||
"red": 0,
|
||||
"blue": 0,
|
||||
"green": 0,
|
||||
"yellow": 0
|
||||
}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.track();
|
||||
this.getAttendees();
|
||||
this.getTodaysWines();
|
||||
this.getWinners();
|
||||
this.socket = io(window.location.origin);
|
||||
this.socket.on("color_winner", msg => {});
|
||||
|
||||
this.socket.on("disconnect", msg => {
|
||||
this.wasDisconnected = true;
|
||||
});
|
||||
|
||||
this.socket.on("winner", async msg => {
|
||||
this.currentWinnerDrawn = true;
|
||||
this.currentWinner = {
|
||||
name: msg.name,
|
||||
color: msg.color,
|
||||
winnerCount: msg.winner_count
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
this.currentWinner = null;
|
||||
this.currentWinnerDrawn = false;
|
||||
}, 19250);
|
||||
});
|
||||
this.socket.on("refresh_data", async msg => {
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
});
|
||||
this.socket.on("new_attendee", async msg => {
|
||||
this.getAttendees();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
},
|
||||
methods: {
|
||||
getWinners: async function() {
|
||||
let response = await winners();
|
||||
if (response) {
|
||||
this.winners = response;
|
||||
}
|
||||
},
|
||||
getTodaysWines() {
|
||||
prelottery()
|
||||
.then(wines => {
|
||||
this.wines = wines;
|
||||
this.todayExists = wines.length > 0;
|
||||
})
|
||||
.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);
|
||||
|
||||
this.ticketsBought = {
|
||||
red: addValueOfListObjectByKey(response, "red"),
|
||||
blue: addValueOfListObjectByKey(response, "blue"),
|
||||
green: addValueOfListObjectByKey(response, "green"),
|
||||
yellow: addValueOfListObjectByKey(response, "yellow")
|
||||
};
|
||||
}
|
||||
this.attendeesFetched = true;
|
||||
},
|
||||
scrollToContent() {
|
||||
console.log(window.scrollY)
|
||||
const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50;
|
||||
const { scrollY } = window;
|
||||
let scrollHeight = intersectingHeaderHeight;
|
||||
if (scrollY > 0) {
|
||||
scrollHeight = intersectingHeaderHeight + scrollY;
|
||||
}
|
||||
|
||||
window.scrollTo({
|
||||
top: scrollHeight,
|
||||
behavior: "smooth"
|
||||
});
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/lottery/game');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.container {
|
||||
width: 80vw;
|
||||
padding: 0 10vw;
|
||||
|
||||
@include mobile {
|
||||
width: 90vw;
|
||||
padding: 0 5vw;
|
||||
}
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
> div, > section {
|
||||
@include mobile {
|
||||
grid-column: span 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
header {
|
||||
h1 {
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
font-size: 3rem;
|
||||
margin: 4rem 0 2rem;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 1rem;
|
||||
font-size: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
background-color: $primary;
|
||||
padding-bottom: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
.instructions {
|
||||
grid-column: 1 / 4;
|
||||
|
||||
@include mobile {
|
||||
grid-column: span 5;
|
||||
}
|
||||
}
|
||||
|
||||
.vipps-qr {
|
||||
grid-column: 4;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.vipps-pill {
|
||||
margin: 0 auto 2rem;
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
.call-to-action {
|
||||
grid-column: span 5;
|
||||
}
|
||||
|
||||
ol {
|
||||
font-size: 1.4rem;
|
||||
line-height: 3rem;
|
||||
color: $matte-text-color;
|
||||
|
||||
@include mobile {
|
||||
line-height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.4rem;
|
||||
line-height: 2rem;
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
|
||||
.vin-link {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
bottom: 3px;
|
||||
color: $link-color;
|
||||
margin-left: 0.5rem;
|
||||
display: inline-block;
|
||||
transform: rotate(-90deg);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.vin-link {
|
||||
font-weight: 400;
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.todays-raffles {
|
||||
grid-column: 1;
|
||||
|
||||
@include mobile {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-container {
|
||||
width: 165px;
|
||||
height: 175px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
font-size: 1.6rem;
|
||||
color: $matte-text-color;
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.winners {
|
||||
grid-column: 2 / 5;
|
||||
|
||||
@include mobile {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.container-attendees {
|
||||
grid-column: 1 / 3;
|
||||
margin-right: 1rem;
|
||||
margin-top: 2rem;
|
||||
|
||||
@include mobile {
|
||||
margin-right: 0;
|
||||
order: 4;
|
||||
}
|
||||
|
||||
> div {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.container-chat {
|
||||
grid-column: 3 / 5;
|
||||
margin-left: 1rem;
|
||||
margin-top: 2rem;
|
||||
|
||||
@include mobile {
|
||||
margin-left: 0;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
> div {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.wines-container {
|
||||
margin-bottom: 4rem;
|
||||
|
||||
h2 {
|
||||
grid-column: 1 / 5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@
|
||||
<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 + '-ballot'" class="ballot-element">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">
|
||||
<span>{{ winner.name }}</span>
|
||||
<span>{{ winner.phoneNumber }}</span>
|
||||
<span>Rød: {{ winner.red }}</span>
|
||||
@@ -47,11 +47,11 @@
|
||||
<span class="name">{{ attendee.name }}</span>
|
||||
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
||||
</div>
|
||||
<div class="ballots-container">
|
||||
<div class="red-ballot ballot-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-ballot ballot-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-ballot ballot-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</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>
|
||||
@@ -106,6 +106,8 @@
|
||||
</div>
|
||||
<br />
|
||||
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
||||
|
||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -118,13 +120,17 @@ import {
|
||||
attendees,
|
||||
winnersSecure,
|
||||
deleteWinners,
|
||||
deleteAttendees
|
||||
deleteAttendees,
|
||||
finishedDraw,
|
||||
prelottery
|
||||
} from "@/api";
|
||||
import TextToast from "@/ui/TextToast";
|
||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RaffleGenerator
|
||||
RaffleGenerator,
|
||||
TextToast
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -134,7 +140,7 @@ export default {
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
ballots: 0,
|
||||
raffles: 0,
|
||||
randomColors: false,
|
||||
attendees: [],
|
||||
winners: [],
|
||||
@@ -143,7 +149,9 @@ export default {
|
||||
drawTime: 20,
|
||||
currentWinners: 1,
|
||||
numberOfWinners: 4,
|
||||
socket: null
|
||||
socket: null,
|
||||
toastText: undefined,
|
||||
showToast: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -165,12 +173,23 @@ export default {
|
||||
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,
|
||||
@@ -178,12 +197,17 @@ export default {
|
||||
blue: this.blue,
|
||||
green: this.green,
|
||||
yellow: this.yellow,
|
||||
ballots: this.ballots
|
||||
raffles: this.raffles
|
||||
});
|
||||
|
||||
if (response == true) {
|
||||
alert("Sendt inn deltaker!");
|
||||
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;
|
||||
|
||||
@@ -201,19 +225,29 @@ export default {
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
drawWinner: async function() {
|
||||
this.drawingWinner = true;
|
||||
let response = await getVirtualWinner();
|
||||
if (response) {
|
||||
if (this.currentWinners < this.numberOfWinners) {
|
||||
this.countdown();
|
||||
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"]);
|
||||
}
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
alert("Noe gikk galt under trekningen..!");
|
||||
}
|
||||
},
|
||||
countdown: function() {
|
||||
@@ -236,19 +270,23 @@ export default {
|
||||
}, 1000);
|
||||
},
|
||||
deleteAllWinners: async function() {
|
||||
let response = await deleteWinners();
|
||||
if (response) {
|
||||
this.getWinners();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
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() {
|
||||
let response = await deleteAttendees();
|
||||
if (response) {
|
||||
this.getAttendees();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
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() {
|
||||
@@ -317,13 +355,13 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.ballot-element {
|
||||
.raffle-element {
|
||||
width: 140px;
|
||||
height: 150px;
|
||||
margin: 20px 0;
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
@@ -341,19 +379,19 @@ hr {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.green-ballot {
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-ballot {
|
||||
&.blue-raffle {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-ballot {
|
||||
&.yellow-raffle {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-ballot {
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
@@ -385,7 +423,7 @@ button {
|
||||
margin: 0 auto;
|
||||
|
||||
& .name-and-phone,
|
||||
& .ballots-container {
|
||||
& .raffles-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -394,7 +432,7 @@ button {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .ballots-container {
|
||||
& .raffles-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
100
frontend/components/WinnerPage.vue
Normal file
100
frontend/components/WinnerPage.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div v-if="!posted">
|
||||
<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" 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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</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: [],
|
||||
posted: false
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.id = this.$router.currentRoute.params.id;
|
||||
|
||||
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();
|
||||
},
|
||||
methods: {
|
||||
chooseWine: async function(name) {
|
||||
let posted = await postWineChosen(this.id, name);
|
||||
console.log("response", posted);
|
||||
if (posted.success) {
|
||||
this.posted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/global";
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
.sent-container {
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.select-wine {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
2
frontend/mixins/EventBus.js
Normal file
2
frontend/mixins/EventBus.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Vue from "vue";
|
||||
export default new Vue();
|
||||
@@ -9,8 +9,12 @@ var serviceWorkerRegistrationMixin = {
|
||||
localStorage.removeItem("push");
|
||||
}
|
||||
}
|
||||
this.registerPushListener();
|
||||
this.registerServiceWorker();
|
||||
if (window.location.href.includes('localhost')) {
|
||||
console.info("Service worker manually disabled while on localhost.")
|
||||
} else {
|
||||
this.registerPushListener();
|
||||
this.registerServiceWorker();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
registerPushListener: function() {
|
||||
@@ -92,4 +96,4 @@ var serviceWorkerRegistrationMixin = {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = serviceWorkerRegistrationMixin;
|
||||
export default serviceWorkerRegistrationMixin;
|
||||
132
frontend/router.js
Normal file
132
frontend/router.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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 AboutPage = () => import(
|
||||
/* webpackChunkName: "sub-pages" */
|
||||
"@/components/AboutPage");
|
||||
|
||||
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 RequestWine = () => import(
|
||||
/* webpackChunkName: "request" */
|
||||
"@/components/RequestWine");
|
||||
const AllRequestedWines = () => import(
|
||||
/* webpackChunkName: "request" */
|
||||
"@/components/AllRequestedWines");
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "*",
|
||||
name: "Hjem",
|
||||
component: VinlottisPage
|
||||
},
|
||||
{
|
||||
path: "/lottery",
|
||||
name: "Lotteri",
|
||||
component: VirtualLotteryPage
|
||||
},
|
||||
{
|
||||
path: "/dagens",
|
||||
name: "Dagens vin",
|
||||
component: TodaysPage
|
||||
},
|
||||
{
|
||||
path: "/viner",
|
||||
name: "All viner",
|
||||
component: AllWinesPage
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: LoginPage
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
name: "Registrer",
|
||||
component: CreatePage
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "Admin side",
|
||||
component: AdminPage
|
||||
},
|
||||
{
|
||||
path: "/generate/",
|
||||
component: GeneratePage
|
||||
},
|
||||
{
|
||||
path: "/winner/:id",
|
||||
component: WinnerPage
|
||||
},
|
||||
{
|
||||
path: "/history/:date",
|
||||
name: "Historie for dato",
|
||||
component: HistoryPage
|
||||
},
|
||||
{
|
||||
path: "/history",
|
||||
name: "Historie",
|
||||
component: HistoryPage
|
||||
},
|
||||
{
|
||||
path: "/highscore/:name",
|
||||
name: "Personlig topplisten",
|
||||
component: PersonalHighscorePage
|
||||
},
|
||||
{
|
||||
path: "/highscore",
|
||||
name: "Topplisten",
|
||||
component: HighscorePage
|
||||
},
|
||||
{
|
||||
path: "/request",
|
||||
name: "Etterspør vin",
|
||||
component: RequestWine
|
||||
},
|
||||
{
|
||||
path: "/requested-wines",
|
||||
name: "Etterspurte vin",
|
||||
component: AllRequestedWines
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "Om oss",
|
||||
component: AboutPage
|
||||
}
|
||||
];
|
||||
|
||||
export { routes };
|
||||
241
frontend/styles/banner.scss
Normal file
241
frontend/styles/banner.scss
Normal file
@@ -0,0 +1,241 @@
|
||||
@import "./media-queries.scss";
|
||||
@import "./variables.scss";
|
||||
|
||||
.top-banner {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 1fr 0.5fr;
|
||||
grid-template-areas: "menu logo clock";
|
||||
grid-gap: 1em;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
background-color: $primary;
|
||||
|
||||
// ios homescreen app whitespace above header fix.
|
||||
&::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
position: absolute;
|
||||
top: -3rem;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.company-logo {
|
||||
grid-area: logo;
|
||||
}
|
||||
|
||||
.menu-toggle-container {
|
||||
grid-area: menu;
|
||||
color: #1e1e1e;
|
||||
border-radius: 50% 50%;
|
||||
z-index: 3;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
height: 3px;
|
||||
width: 18px;
|
||||
background: #111;
|
||||
z-index: 1;
|
||||
transform-origin: 4px 0px;
|
||||
transition:
|
||||
transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
|
||||
background 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
|
||||
opacity 0.55s ease;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
transform-origin: 0% 0%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
span:nth-last-child(2) {
|
||||
transform-origin: 0% 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&.open {
|
||||
span{
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg) translate(2px, -2px);
|
||||
background: #232323;
|
||||
}
|
||||
|
||||
span:nth-last-child(2) {
|
||||
opacity: 0;
|
||||
transform: rotate(0deg) scale(0.2, 0.2);
|
||||
}
|
||||
|
||||
span:nth-last-child(3) {
|
||||
transform: rotate(45deg) translate(3.5px, -2px);
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background-color: $primary;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-out;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
row-gap: 3em;
|
||||
|
||||
&.collapsed {
|
||||
max-height: 0%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
opacity: 1;
|
||||
right: -2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
right: 0;
|
||||
color: $link-color;
|
||||
font-size: 1.4rem;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
}
|
||||
|
||||
.single-route {
|
||||
font-size: 3em;
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
color: #1e1e1e;
|
||||
border-bottom: 4px solid transparent;
|
||||
display: block;
|
||||
|
||||
&.open {
|
||||
-webkit-animation: fadeInFromNone 3s ease-out;
|
||||
-moz-animation: fadeInFromNone 3s ease-out;
|
||||
-o-animation: fadeInFromNone 3s ease-out;
|
||||
animation: fadeInFromNone 3s ease-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-color: $link-color;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.clock {
|
||||
grid-area: clock;
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
font-family: Arial;
|
||||
@include mobile {
|
||||
font-size: 0.8em;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
h2 {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 600;
|
||||
src: url("/../../public/assets/fonts/bold.woff");
|
||||
src: url("/public/assets/fonts/bold.woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 300;
|
||||
src: url("/../../public/assets/fonts/regular.eot");
|
||||
src: url("/public/assets/fonts/regular.woff");
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -18,6 +18,10 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
@@ -74,6 +78,16 @@ body {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin-right: unset;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
&:not(.row) {
|
||||
flex-direction: column;
|
||||
@@ -98,16 +112,16 @@ textarea {
|
||||
|
||||
.vin-button {
|
||||
font-family: Arial;
|
||||
$color: #b7debd;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background: $color;
|
||||
background: $primary;
|
||||
color: #333;
|
||||
padding: 10px 30px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
width: fit-content;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.3rem;
|
||||
height: 4rem;
|
||||
max-height: 4rem;
|
||||
cursor: pointer;
|
||||
@@ -118,6 +132,15 @@ textarea {
|
||||
// disable-dbl-tap-zoom
|
||||
touch-action: manipulation;
|
||||
|
||||
&.auto-height {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: $red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -133,41 +156,175 @@ textarea {
|
||||
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
transform: scale(1.02) translateZ(0);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:disabled{
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: min-content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vin-link {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $link-color;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
text-decoration: none;
|
||||
color: $matte-text-color;
|
||||
|
||||
&:focus, &:hover {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.margin-top {
|
||||
&-md {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.margin-left {
|
||||
&-md {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.margin-right {
|
||||
&-md {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.margin-bottom {
|
||||
&-md {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.width {
|
||||
&-100 {
|
||||
width: 100%;
|
||||
}
|
||||
&-75 {
|
||||
width: 75%;
|
||||
}
|
||||
&-50 {
|
||||
width: 50%;
|
||||
}
|
||||
&-25 {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ballot-element {
|
||||
.raffle-element {
|
||||
margin: 20px 0;
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
|
||||
&.green-ballot {
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-ballot {
|
||||
&.blue-raffle {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-ballot {
|
||||
&.yellow-raffle {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-ballot {
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin raffle {
|
||||
padding-bottom: 50px;
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 25px;
|
||||
height: 50px;
|
||||
background: radial-gradient(closest-side, #fff, #fff 50%, transparent 50%);
|
||||
background-size: 50px 50px;
|
||||
background-position: 0 25px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
&::after{
|
||||
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
|
||||
background-size: 50px 50px;
|
||||
background-position: 25px -25px;
|
||||
bottom: -25px
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
@include mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
@include desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -68,4 +68,5 @@ form {
|
||||
width: calc(100% - 5rem);
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
40
frontend/styles/media-queries.scss
Normal file
40
frontend/styles/media-queries.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
$mobile-width: 768px;
|
||||
$tablet-max: 1200px;
|
||||
$desktop-max: 2004px;
|
||||
|
||||
|
||||
@mixin mobile {
|
||||
@media (max-width: #{$mobile-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tablet {
|
||||
@media (min-width: #{$mobile-width + 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: #{$tablet-max + 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin widescreen {
|
||||
@media (min-width: #{$desktop-max + 1px}){
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
@include mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
@include tablet {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
46
frontend/styles/positioning.scss
Normal file
46
frontend/styles/positioning.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.flex {
|
||||
display: flex;
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
&.justify-space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
&.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
&.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.float {
|
||||
&-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&-right {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
$primary: #dbeede;
|
||||
$primary: #b7debd;
|
||||
|
||||
$light-green: #c8f9df;
|
||||
$green: #0be881;
|
||||
@@ -15,3 +15,7 @@ $dark-yellow: #ecc31d;
|
||||
$light-red: #fbd7de;
|
||||
$red: #ef5878;
|
||||
$dark-red: #ec3b61;
|
||||
|
||||
$link-color: #ff5fff;
|
||||
|
||||
$matte-text-color: #333333;
|
||||
122
frontend/styles/vinlottis-icons.css
Normal file
122
frontend/styles/vinlottis-icons.css
Normal file
@@ -0,0 +1,122 @@
|
||||
@font-face {
|
||||
font-family: 'vinlottis-icons';
|
||||
src:
|
||||
url('/public/assets/fonts/vinlottis-icons/vinlottis-icons.ttf?95xu5r') format('truetype'),
|
||||
url('/public/assets/fonts/vinlottis-icons/vinlottis-icons.woff?95xu5r') format('woff'),
|
||||
url('/public/assets/fonts/vinlottis-icons/vinlottis-icons.svg?95xu5r#vinlottis-icons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'vinlottis-icons' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon--arrow-long-right:before {
|
||||
content: "\e907";
|
||||
}
|
||||
.icon--arrow-long-left:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.icon--arrow-right:before {
|
||||
content: "\e909";
|
||||
}
|
||||
.icon--arrow-left:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon--ballon:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon--bars:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon--bottle:before {
|
||||
content: "\e90d";
|
||||
}
|
||||
.icon--cake-chart:before {
|
||||
content: "\e90f";
|
||||
}
|
||||
.icon--stopwatch:before {
|
||||
content: "\e911";
|
||||
}
|
||||
.icon--cloud:before {
|
||||
content: "\e912";
|
||||
}
|
||||
.icon--dart:before {
|
||||
content: "\e914";
|
||||
}
|
||||
.icon--eye-1:before {
|
||||
content: "\e919";
|
||||
}
|
||||
.icon--eye-2:before {
|
||||
content: "\e91a";
|
||||
}
|
||||
.icon--eye-3:before {
|
||||
content: "\e91b";
|
||||
}
|
||||
.icon--eye-4:before {
|
||||
content: "\e91c";
|
||||
}
|
||||
.icon--eye-5:before {
|
||||
content: "\e91d";
|
||||
}
|
||||
.icon--eye-6:before {
|
||||
content: "\e91e";
|
||||
}
|
||||
.icon--eye-7:before {
|
||||
content: "\e91f";
|
||||
}
|
||||
.icon--eye-8:before {
|
||||
content: "\e920";
|
||||
}
|
||||
.icon--face-1:before {
|
||||
content: "\e922";
|
||||
}
|
||||
.icon--face-2:before {
|
||||
content: "\e923";
|
||||
}
|
||||
.icon--face-3:before {
|
||||
content: "\e924";
|
||||
}
|
||||
.icon--heart-sparks:before {
|
||||
content: "\e928";
|
||||
}
|
||||
.icon--heart:before {
|
||||
content: "\e929";
|
||||
}
|
||||
.icon--medal:before {
|
||||
content: "\e936";
|
||||
}
|
||||
.icon--megaphone:before {
|
||||
content: "\e937";
|
||||
}
|
||||
.icon--phone:before {
|
||||
content: "\e93a";
|
||||
}
|
||||
.icon--plus:before {
|
||||
content: "\e93e";
|
||||
}
|
||||
.icon--spark:before {
|
||||
content: "\e946";
|
||||
}
|
||||
.icon--tag:before {
|
||||
content: "\e949";
|
||||
}
|
||||
.icon--talk:before {
|
||||
content: "\e94b";
|
||||
}
|
||||
.icon--cross:before {
|
||||
content: "\e952";
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
color="#23101f"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#dbeede" />
|
||||
<meta name="theme-color" content="#b7debd" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
@@ -55,6 +55,7 @@
|
||||
<div id="app"></div>
|
||||
|
||||
<noscript>Du trenger vin, jeg trenger javascript!</noscript>
|
||||
<script src="/public/analytics.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -65,7 +66,7 @@
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 100vh;
|
||||
background-color: #dbeede;
|
||||
background-color: #b7debd;
|
||||
font-size: 1.5rem;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<div class="attendees" v-if="attendees.length > 0">
|
||||
<h2>Deltakere ({{ attendees.length }})</h2>
|
||||
<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-ballot ballot-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-ballot ballot-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-ballot ballot-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div>
|
||||
<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>
|
||||
@@ -42,11 +41,17 @@ export default {
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.attendee-name {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.ballot-element {
|
||||
hr {
|
||||
border: 2px solid black;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
font-size: 0.75rem;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
@@ -56,20 +61,24 @@ export default {
|
||||
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;
|
||||
width: 65%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.attendees-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
max-height: 550px;
|
||||
}
|
||||
|
||||
.attendee {
|
||||
@@ -78,5 +87,9 @@ export default {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 2px solid #d7d8d7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +1,55 @@
|
||||
<template>
|
||||
<router-link to="/" class="link">
|
||||
<div class="top-banner">
|
||||
<header class="top-banner">
|
||||
<!-- Mobile -->
|
||||
<router-link to="/" class="company-logo">
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
||||
<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(seconds) }}</span>
|
||||
</h2>
|
||||
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<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>
|
||||
<i class="icon icon--arrow-right"></i>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<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(seconds) }}</span>
|
||||
</h2>
|
||||
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
||||
</div>
|
||||
</router-link>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
nextLottery: null,
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
distance: 0,
|
||||
enabled: false,
|
||||
code: "38384040373937396665",
|
||||
codeDone: "",
|
||||
interval: null
|
||||
interval: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
routes: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initialize(), this.countdown();
|
||||
},
|
||||
@@ -49,25 +68,15 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMenu(){
|
||||
this.isOpen = this.isOpen ? false : true;
|
||||
},
|
||||
pad: function(num) {
|
||||
if (num < 10) {
|
||||
return `0${num}`;
|
||||
}
|
||||
return num;
|
||||
},
|
||||
listenerFunction: function(event) {
|
||||
this.codeDone += event.keyCode;
|
||||
if (this.code.substring(0, this.codeDone.length) == this.codeDone) {
|
||||
if (this.code == this.codeDone && !this.enabled) {
|
||||
this.enabled = true;
|
||||
this.initialize();
|
||||
this.countdown();
|
||||
this.codeDone = "";
|
||||
}
|
||||
} else {
|
||||
this.codeDone = "";
|
||||
}
|
||||
},
|
||||
initialize: function() {
|
||||
let d = new Date();
|
||||
let dayOfLottery = __DATE__;
|
||||
@@ -79,8 +88,17 @@ export default {
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setHours(__HOURS__));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setMinutes(0));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setSeconds(0));
|
||||
let nowDate = new Date();
|
||||
let now = nowDate.getTime();
|
||||
if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) {
|
||||
let _diff =
|
||||
(nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) *
|
||||
60 *
|
||||
-1;
|
||||
nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff);
|
||||
}
|
||||
this.nextLottery = nextDayOfLottery;
|
||||
let now = new Date().getTime();
|
||||
|
||||
this.distance = new Date(this.nextLottery).getTime() - now;
|
||||
},
|
||||
countdown: function() {
|
||||
@@ -106,49 +124,11 @@ export default {
|
||||
this.initialize();
|
||||
}
|
||||
this.interval = setTimeout(this.countdown, 500);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "../styles/variables.scss";
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-banner {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: calc(100% - 80px);
|
||||
margin-top: 0px;
|
||||
padding: 0px 40px;
|
||||
background-color: $primary;
|
||||
-webkit-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
|
||||
-moz-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
|
||||
box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
|
||||
|
||||
@include mobile {
|
||||
padding: 0px 40px;
|
||||
|
||||
> img {
|
||||
height: 23px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clock {
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
font-family: Arial;
|
||||
h2 {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@import "../styles/banner.scss";
|
||||
</style>
|
||||
347
frontend/ui/Chat.vue
Normal file
347
frontend/ui/Chat.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<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>
|
||||
|
||||
<div class="history" ref="history" v-if="chatHistory.length > 0">
|
||||
<div class="opaque-skirt"></div>
|
||||
<div v-if="hasMorePages" class="fetch-older-history">
|
||||
<button @click="loadMoreHistory">Hent eldre meldinger</button>
|
||||
</div>
|
||||
|
||||
<div class="history-message"
|
||||
v-for="(history, index) in chatHistory"
|
||||
:key="`${history.username}-${history.timestamp}-${index}`"
|
||||
>
|
||||
<div>
|
||||
<span class="username">{{ history.username }}</span>
|
||||
<span class="timestamp">{{ getTime(history.timestamp) }}</span>
|
||||
</div>
|
||||
<span class="message">{{ history.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="username" class="user-actions">
|
||||
<input @keyup.enter="sendMessage" type="text" v-model="message" placeholder="Melding.." />
|
||||
<button @click="sendMessage">Send</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="username-dialog">
|
||||
<input
|
||||
type="text"
|
||||
@keyup.enter="setUsername"
|
||||
v-model="temporaryUsername"
|
||||
maxlength="30"
|
||||
placeholder="Ditt navn.."
|
||||
/>
|
||||
|
||||
<div class="validation-error" v-if="validationError">
|
||||
{{ validationError }}
|
||||
</div>
|
||||
<button @click="setUsername">Lagre brukernavn</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getChatHistory } from "@/api";
|
||||
import io from "socket.io-client";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
socket: null,
|
||||
chatHistory: [],
|
||||
hasMorePages: true,
|
||||
message: "",
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
temporaryUsername: null,
|
||||
username: null,
|
||||
validationError: undefined
|
||||
};
|
||||
},
|
||||
created() {
|
||||
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;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chatHistory: {
|
||||
handler: function(newVal, oldVal) {
|
||||
if (oldVal.length == 0) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
else if (newVal && newVal.length == oldVal.length) {
|
||||
if (this.isScrollPositionAtBottom()) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
} else {
|
||||
const prevOldestMessage = oldVal[0];
|
||||
this.scrollToMessageElement(prevOldestMessage);
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
},
|
||||
mounted() {
|
||||
this.socket = io(window.location.origin);
|
||||
this.socket.on("chat", msg => {
|
||||
this.chatHistory.push(msg);
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", msg => {
|
||||
this.wasDisconnected = true;
|
||||
});
|
||||
|
||||
this.socket.on("connect", msg => {
|
||||
if (
|
||||
this.emitUsernameOnConnect ||
|
||||
(this.wasDisconnected && this.username != null)
|
||||
) {
|
||||
this.setUsername(this.username);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("accept_username", msg => {
|
||||
const { reason, success, username } = msg;
|
||||
this.usernameAccepted = success;
|
||||
|
||||
if (success !== true) {
|
||||
this.username = null;
|
||||
this.validationError = reason;
|
||||
} else {
|
||||
this.usernameAllowed = true;
|
||||
this.username = username;
|
||||
this.validationError = null;
|
||||
window.localStorage.setItem("username", username);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
loadMoreHistory() {
|
||||
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;
|
||||
});
|
||||
},
|
||||
pad(num) {
|
||||
if (num > 9) return num;
|
||||
return `0${num}`;
|
||||
},
|
||||
getTime(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
||||
date.getMinutes()
|
||||
)}:${this.pad(date.getSeconds())}`;
|
||||
|
||||
if (date.getDate() == new Date().getDate()) {
|
||||
return timeString;
|
||||
}
|
||||
return `${date.toLocaleDateString()} ${timeString}`;
|
||||
},
|
||||
sendMessage() {
|
||||
const message = { message: this.message };
|
||||
this.socket.emit("chat", message);
|
||||
this.message = '';
|
||||
this.scrollToBottomOfHistory();
|
||||
},
|
||||
setUsername(username=undefined) {
|
||||
if (this.temporaryUsername) {
|
||||
username = this.temporaryUsername;
|
||||
}
|
||||
const message = { username: username };
|
||||
this.socket.emit("username", message);
|
||||
},
|
||||
removeUsername() {
|
||||
this.username = null;
|
||||
this.temporaryUsername = null;
|
||||
window.localStorage.removeItem("username");
|
||||
},
|
||||
isScrollPositionAtBottom() {
|
||||
const { history } = this.$refs;
|
||||
if (history) {
|
||||
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
|
||||
}
|
||||
return false
|
||||
},
|
||||
scrollToBottomOfHistory() {
|
||||
setTimeout(() => {
|
||||
const { history } = this.$refs;
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}, 1);
|
||||
},
|
||||
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;
|
||||
|
||||
setTimeout(() => {
|
||||
const { history } = self.$refs;
|
||||
const childrenElements = Array.from(history.getElementsByClassName('history-message'));
|
||||
|
||||
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
|
||||
history.scrollTop = elemInNewList.offsetTop - 70
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.chat-container {
|
||||
position: relative;
|
||||
transform: translate3d(0,0,0);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 3.25rem;
|
||||
}
|
||||
|
||||
.logged-in-username {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
left: 1rem;
|
||||
color: $matte-text-color;
|
||||
width: calc(100% - 2rem);
|
||||
|
||||
button {
|
||||
width: unset;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
right: 0rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
border-bottom: 2px solid $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.history {
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
position: relative;
|
||||
max-height: 550px;
|
||||
margin-top: 2rem;
|
||||
|
||||
&-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.35rem 0;
|
||||
position: relative;
|
||||
|
||||
.username {
|
||||
font-weight: bold;
|
||||
font-size: 1.05rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.9rem;
|
||||
top: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&-message:nth-of-type(2) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
& .opaque-skirt {
|
||||
width: calc(100% - 2rem);
|
||||
position: fixed;
|
||||
height: 2rem;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
white,
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
}
|
||||
|
||||
& .fetch-older-history {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.username-dialog {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
.validation-error {
|
||||
position: absolute;
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
top: -3.5rem;
|
||||
left: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2.1rem;
|
||||
left: 2rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
transform: rotate(45deg);
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background: #b7debd;
|
||||
color: #333;
|
||||
padding: 10px 30px;
|
||||
border: 0;
|
||||
width: fit-content;
|
||||
font-size: 1rem;
|
||||
/* height: 1.5rem; */
|
||||
/* max-height: 1.5rem; */
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: transform 0.5s ease;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
touch-action: manipulation;
|
||||
|
||||
@include mobile {
|
||||
padding: 10px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
97
frontend/ui/Footer.vue
Normal file
97
frontend/ui/Footer.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<footer>
|
||||
<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">
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="mailto:questions@vinlottis.no" class="mail">
|
||||
<span class="vin-link">questions@vinlottis.no</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<router-link to="/" class="company-logo">
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo">
|
||||
</router-link>
|
||||
</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: 100px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f4f4f4;
|
||||
|
||||
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>
|
||||
120
frontend/ui/Highscore.vue
Normal file
120
frontend/ui/Highscore.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="highscores" v-if="highscore.length > 0">
|
||||
|
||||
<section class="heading">
|
||||
<h3>
|
||||
Topp 5 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>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highscoreStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { highscore: [] };
|
||||
},
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:visited {
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: none;
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.winner-list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr));
|
||||
gap: 5%;
|
||||
|
||||
.single-winner {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: $primary;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
|
||||
i {
|
||||
font-size: 3em;
|
||||
width: max-content;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.placement {
|
||||
grid-row: 1;
|
||||
grid-column: 1 / 3;
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.winner-name {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
|
||||
.winner-icon {
|
||||
grid-row: 1;
|
||||
grid-column: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
frontend/ui/Modal.vue
Normal file
101
frontend/ui/Modal.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<main class="modal-backdrop">
|
||||
<section class="modal">
|
||||
<header class="modal-header" v-if="headerText">
|
||||
{{headerText}}
|
||||
</header>
|
||||
<section class="modal-body">
|
||||
<p>
|
||||
{{modalText}}
|
||||
</p>
|
||||
<section class="button-container">
|
||||
<button v-for="(button, index) in buttons" :key="index" @click="modalButtonClicked(button.action)" class="vin-button">
|
||||
{{button.text}}
|
||||
</button>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerText: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
modalText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
buttons: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
methods:{
|
||||
modalButtonClicked(action){
|
||||
this.$emit('click', action)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
|
||||
.modal-fade-enter,
|
||||
.modal-fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity .5s ease
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #FFFFFF;
|
||||
-webkit-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
|
||||
-moz-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
|
||||
box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
color: #4AAE9B;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
position: relative;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -141,13 +141,6 @@ export default {
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
|
||||
@include mobile {
|
||||
position: relative;
|
||||
width: 90vw !important;
|
||||
max-height: unset;
|
||||
height: 30vh;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -35,11 +35,10 @@
|
||||
type="number"
|
||||
placeholder="Antall lodd"
|
||||
@keyup.enter="generateColors"
|
||||
v-model="numberOfBallots"
|
||||
v-model="numberOfRaffles"
|
||||
/>
|
||||
<button class="vin-button" @click="generateColors">Generer</button>
|
||||
</div>
|
||||
|
||||
<div class="colors">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
@@ -69,7 +68,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
numberOfBallots: 4,
|
||||
numberOfRaffles: 4,
|
||||
colors: [],
|
||||
blue: 0,
|
||||
red: 0,
|
||||
@@ -85,14 +84,14 @@ export default {
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
this.$emit("numberOfBallots", this.numberOfBallots);
|
||||
this.$emit("numberOfRaffles", this.numberOfRaffles);
|
||||
if (this.generateOnInit) {
|
||||
this.generateColors();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
numberOfBallots: function() {
|
||||
this.$emit("numberOfBallots", this.numberOfBallots);
|
||||
numberOfRaffles: function() {
|
||||
this.$emit("numberOfRaffles", this.numberOfRaffles);
|
||||
this.generateColors();
|
||||
}
|
||||
},
|
||||
@@ -102,7 +101,7 @@ export default {
|
||||
if (time == 5) {
|
||||
this.generating = false;
|
||||
this.generated = true;
|
||||
if (this.numberOfBallots > 1 &&
|
||||
if (this.numberOfRaffles > 1 &&
|
||||
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) {
|
||||
return
|
||||
}
|
||||
@@ -113,13 +112,11 @@ export default {
|
||||
|
||||
this.emitColors()
|
||||
|
||||
if (window.location.hostname == "localhost") {
|
||||
return;
|
||||
}
|
||||
this.$ga.event({
|
||||
eventCategory: "Ballots",
|
||||
window.ga('send', {
|
||||
hitType: "event",
|
||||
eventCategory: "Raffles",
|
||||
eventAction: "Generate",
|
||||
eventValue: JSON.stringify(this.colors)
|
||||
eventLabel: JSON.stringify(this.colors)
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -148,8 +145,8 @@ export default {
|
||||
alert("Du må velge MINST 1 farge");
|
||||
return;
|
||||
}
|
||||
if (this.numberOfBallots > 0) {
|
||||
for (let i = 0; i < this.numberOfBallots; i++) {
|
||||
if (this.numberOfRaffles > 0) {
|
||||
for (let i = 0; i < this.numberOfRaffles; i++) {
|
||||
let color =
|
||||
randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||
this.colors.push(color);
|
||||
@@ -293,9 +290,9 @@ label .text {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
124
frontend/ui/RequestedWineCard.vue
Normal file
124
frontend/ui/RequestedWineCard.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<Wine :wine="wine">
|
||||
<template v-slot:top>
|
||||
<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 }" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:default>
|
||||
<button @click="deleteWine(wine)" v-if="showDeleteButton == true" class="vin-button small danger width-100">
|
||||
Slett vinen
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<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' }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</Wine>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deleteRequestedWine, requestNewWine } from "@/api";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Wine
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
wine: this.requestedElement.wine,
|
||||
locallyRequested: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
requestedElement: {
|
||||
required: true,
|
||||
type: Object
|
||||
},
|
||||
showDeleteButton: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
request(){
|
||||
if (this.locallyRequested)
|
||||
return
|
||||
console.log("requesting", this.wine)
|
||||
this.locallyRequested = true
|
||||
this.requestedElement.count = this.requestedElement.count +1
|
||||
requestNewWine(this.wine)
|
||||
},
|
||||
async deleteWine() {
|
||||
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);
|
||||
} else {
|
||||
alert("Klarte ikke slette vinen");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables";
|
||||
|
||||
.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{
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
&.link {
|
||||
border-color: $link-color
|
||||
}
|
||||
|
||||
&.icon--heart {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.request {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 1.5rem;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -94,8 +94,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/styles/variables";
|
||||
@import "./src/styles/global";
|
||||
@import "@/styles/variables";
|
||||
@import "@/styles/global";
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
@@ -7,22 +7,16 @@
|
||||
:key="index"
|
||||
@click="changeTab(index)"
|
||||
:class="chosenTab == index ? 'active' : null"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
>{{ tab.name }}</div>
|
||||
</div>
|
||||
<div class="tab-elements">
|
||||
<component
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
:is="tab.component"
|
||||
:class="chosenTab == index ? null : 'hide'"
|
||||
/>
|
||||
<component :is="tabs[chosenTab].component" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventBus from "@/mixins/EventBus";
|
||||
export default {
|
||||
props: {
|
||||
tabs: {
|
||||
@@ -45,6 +39,7 @@ export default {
|
||||
changeTab: function(num) {
|
||||
this.chosenTab = num;
|
||||
this.$emit("tabChange", num);
|
||||
eventBus.$emit("tab-change");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -54,9 +49,6 @@ export default {
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
@@ -1,6 +1,16 @@
|
||||
<template>
|
||||
<div class="outer-bought">
|
||||
<section class="outer-bought">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bought-container">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
@@ -8,34 +18,19 @@
|
||||
color.name +
|
||||
'-container ' +
|
||||
color.name +
|
||||
'-ballot inner-bought-container ballot-element'
|
||||
'-raffle raffle-element-local'
|
||||
"
|
||||
:key="color.name"
|
||||
>
|
||||
<div class="number-container">
|
||||
<span class="color-total bought-number-span">
|
||||
{{ color.total }}
|
||||
</span>
|
||||
<span>kjøpte</span>
|
||||
</div>
|
||||
<div class="inner-text-container">
|
||||
<div>{{ color.win }} vinn</div>
|
||||
<div>{{ color.totalPercentage }}% vinn</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inner-bought-container total-ballots">
|
||||
<div class="total-container">
|
||||
Totalt
|
||||
<div>
|
||||
<span class="total">{{ total }}</span> kjøpte
|
||||
</div>
|
||||
<div>{{ totalWin }} vinn</div>
|
||||
<div>{{ stolen }} stjålet</div>
|
||||
</div>
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
import { colorStatistics } from "@/api";
|
||||
@@ -60,11 +55,13 @@ export default {
|
||||
},
|
||||
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;
|
||||
@@ -114,119 +111,106 @@ export default {
|
||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||
},
|
||||
methods: {
|
||||
translate(color){
|
||||
switch(color) {
|
||||
case "blue":
|
||||
return "Blå"
|
||||
break;
|
||||
case "red":
|
||||
return "Rød"
|
||||
break;
|
||||
case "green":
|
||||
return "Grønn"
|
||||
break;
|
||||
case "yellow":
|
||||
return "Gul"
|
||||
break;
|
||||
break;
|
||||
}
|
||||
},
|
||||
getPercentage: function(win, total) {
|
||||
return this.round(win == 0 ? 0 : (win / total) * 100);
|
||||
},
|
||||
round: function(number) {
|
||||
return Math.round(number * 100) / 100;
|
||||
|
||||
//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;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "../styles/global.scss";
|
||||
|
||||
.inner-bought-container {
|
||||
@include mobile{
|
||||
section {
|
||||
margin-top: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
.total-raffles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ballot-element {
|
||||
width: 140px;
|
||||
height: 150px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.number-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
& span:last-child {
|
||||
padding-bottom: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.inner-text-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
// TODO fix styling for displaying in columns
|
||||
@include mobile {
|
||||
& div {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.total-ballots {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.total-container {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.total-container {
|
||||
> div:nth-of-type(2) {
|
||||
margin-top: auto;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bought-number-span {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.outer-bought {
|
||||
@include mobile {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.bought-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
padding-bottom: 3rem;
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
justify-content: space-between;
|
||||
font-family: Arial;
|
||||
margin-top: 2em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 50px;
|
||||
|
||||
@include mobile {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.raffle-element-local {
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@include raffle;
|
||||
|
||||
.color-total,
|
||||
.total {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.win-percentage {
|
||||
margin-left: 30px;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
display: inline-block;
|
||||
}
|
||||
p {
|
||||
margin-left: 30px;
|
||||
&.winner-chance {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.bought-container {
|
||||
flex-wrap: wrap;
|
||||
&.total-bought-color{
|
||||
font-weight: bold;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
&.amount-of-wins {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-raffle {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-raffle {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
frontend/ui/VippsPill.vue
Normal file
81
frontend/ui/VippsPill.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div aria-label="button" role="button" @click="openVipps" tabindex="0">
|
||||
<img src="public/assets/images/vipps-pay_with_vipps_pill.png" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
amount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
phone: __PHONE__,
|
||||
name: __NAME__,
|
||||
price: __PRICE__,
|
||||
message: __MESSAGE__
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMobile: function() {
|
||||
return this.isMobileFunction();
|
||||
},
|
||||
priceToPay: function() {
|
||||
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
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openVipps() {
|
||||
if (!this.isMobileFunction()) {
|
||||
return;
|
||||
}
|
||||
window.location.assign(this.vippsUrlBasedOnUserAgent);
|
||||
},
|
||||
isMobileFunction() {
|
||||
if (
|
||||
navigator.userAgent.match(/Android/i) ||
|
||||
navigator.userAgent.match(/webOS/i) ||
|
||||
navigator.userAgent.match(/iPhone/i) ||
|
||||
navigator.userAgent.match(/iPad/i) ||
|
||||
navigator.userAgent.match(/iPod/i) ||
|
||||
navigator.userAgent.match(/BlackBerry/i) ||
|
||||
navigator.userAgent.match(/Windows Phone/i)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -107,13 +107,6 @@ export default {
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
|
||||
@include mobile {
|
||||
position: relative;
|
||||
width: 90vw !important;
|
||||
max-height: unset;
|
||||
height: 30vh;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
138
frontend/ui/Wine.vue
Normal file
138
frontend/ui/Wine.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="wine">
|
||||
<slot name="top"></slot>
|
||||
<div class="wine-image">
|
||||
<img
|
||||
v-if="wine.image && loadImage"
|
||||
:src="wine.image"
|
||||
/>
|
||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||
</div>
|
||||
|
||||
<div class="wine-details">
|
||||
<span v-if="wine.name" class="wine-name">{{ wine.name }}</span>
|
||||
<span v-if="wine.rating"><b>Rating:</b> {{ wine.rating }}</span>
|
||||
<span v-if="wine.price"><b>Pris:</b> {{ wine.price }} NOK</span>
|
||||
<span v-if="wine.country"><b>Land:</b> {{ wine.country }}</span>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<div class="bottom-section">
|
||||
<slot name="bottom"></slot>
|
||||
<a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="link float-right">
|
||||
Les mer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
wine: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadImage: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setImage(entries) {
|
||||
const { target, isIntersecting } = entries[0];
|
||||
if (!isIntersecting) return;
|
||||
|
||||
this.loadImage = true;
|
||||
this.observer.unobserve(target);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.observer = new IntersectionObserver(this.setImage, {
|
||||
root: this.$el,
|
||||
threshold: 0
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
this.observer.observe(this.$el);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries";
|
||||
@import "@/styles/variables";
|
||||
|
||||
.wine {
|
||||
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);
|
||||
|
||||
@include tablet {
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wine-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
height: 250px;
|
||||
@include mobile {
|
||||
object-fit: cover;
|
||||
max-width: 90px;
|
||||
}
|
||||
}
|
||||
.wine-placeholder {
|
||||
height: 250px;
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.wine-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> span {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.wine-name{
|
||||
font-size: 20px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.wine-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bottom-section {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
|
||||
.link {
|
||||
color: $matte-text-color;
|
||||
font-family: Arial;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
border-bottom: 2px solid $matte-text-color;
|
||||
|
||||
&:hover {
|
||||
font-weight: normal;
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
166
frontend/ui/Wines.vue
Normal file
166
frontend/ui/Wines.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div v-if="wines.length > 0" class="wines-main-container">
|
||||
<div class="info-and-link">
|
||||
<h3>
|
||||
Topp 5 viner
|
||||
</h3>
|
||||
<router-link to="viner">
|
||||
<span class="vin-link">Se alle viner </span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="wine-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>
|
||||
<i class="icon icon--heart" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Wine>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Wine from "@/ui/Wine";
|
||||
import { overallWineStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Wine
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
clickedWine: null,
|
||||
};
|
||||
},
|
||||
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);
|
||||
},
|
||||
methods: {
|
||||
predicate: function() {
|
||||
var fields = [],
|
||||
n_fields = arguments.length,
|
||||
field,
|
||||
name,
|
||||
cmp;
|
||||
|
||||
var default_cmp = function(a, b) {
|
||||
if (a == undefined) a = 0;
|
||||
if (b == undefined) b = 0;
|
||||
if (a === b) return 0;
|
||||
return a < b ? -1 : 1;
|
||||
},
|
||||
getCmpFunc = function(primer, reverse) {
|
||||
var dfc = default_cmp,
|
||||
// closer in scope
|
||||
cmp = default_cmp;
|
||||
if (primer) {
|
||||
cmp = function(a, b) {
|
||||
return dfc(primer(a), primer(b));
|
||||
};
|
||||
}
|
||||
if (reverse) {
|
||||
return function(a, b) {
|
||||
return -1 * cmp(a, b);
|
||||
};
|
||||
}
|
||||
return cmp;
|
||||
};
|
||||
|
||||
// preprocess sorting options
|
||||
for (var i = 0; i < n_fields; i++) {
|
||||
field = arguments[i];
|
||||
if (typeof field === "string") {
|
||||
name = field;
|
||||
cmp = default_cmp;
|
||||
} else {
|
||||
name = field.name;
|
||||
cmp = getCmpFunc(field.primer, field.reverse);
|
||||
}
|
||||
fields.push({
|
||||
name: name,
|
||||
cmp: cmp
|
||||
});
|
||||
}
|
||||
|
||||
// final comparison function
|
||||
return function(A, B) {
|
||||
var name, result;
|
||||
for (var i = 0; i < n_fields; i++) {
|
||||
result = 0;
|
||||
field = fields[i];
|
||||
name = field.name;
|
||||
|
||||
result = field.cmp(A[name], B[name]);
|
||||
if (result !== 0) break;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
@import "@/styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.wines-main-container {
|
||||
margin-bottom: 10em;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
span {
|
||||
padding-right: 0.5rem;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.icon--heart{
|
||||
font-size: 1.5rem;
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,39 +1,23 @@
|
||||
<template>
|
||||
<div class="current-drawn-container">
|
||||
<div class="current-draw" v-if="drawing">
|
||||
<h2>TREKKER</h2>
|
||||
<div
|
||||
:class="currentColor + '-ballot'"
|
||||
class="ballot-element center-new-winner"
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
>
|
||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
<div class="current-draw" v-if="drawingDone">
|
||||
<h2>VINNER</h2>
|
||||
<div
|
||||
:class="currentColor + '-ballot'"
|
||||
class="ballot-element center-new-winner"
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
>
|
||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div class="current-drawn-container" v-if="drawing">
|
||||
<h2 v-if="winnersNameDrawn !== true">TREKKER {{ ordinalNumber() }} VINNER</h2>
|
||||
<h2 v-else>VINNER</h2>
|
||||
|
||||
<div
|
||||
:class="currentColor + '-raffle'"
|
||||
class="raffle-element"
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
>
|
||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import confetti from "canvas-confetti";
|
||||
export default {
|
||||
props: {
|
||||
currentWinner: {
|
||||
@@ -59,14 +43,13 @@ export default {
|
||||
nameTimeout: null,
|
||||
colorDone: false,
|
||||
drawing: false,
|
||||
drawingDone: false,
|
||||
winnersNameDrawn: false,
|
||||
winnerQueue: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
currentWinner: function(currentWinner) {
|
||||
if (currentWinner == null) {
|
||||
this.drawingDone = false;
|
||||
return;
|
||||
}
|
||||
if (this.drawing) {
|
||||
@@ -74,6 +57,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.drawing = true;
|
||||
this.winnersNameDrawn = false;
|
||||
this.currentName = null;
|
||||
this.currentColor = null;
|
||||
this.nameRounds = 0;
|
||||
@@ -97,8 +81,8 @@ export default {
|
||||
this.drawColor(this.currentWinnerLocal.color);
|
||||
return;
|
||||
}
|
||||
this.drawing = false;
|
||||
this.drawingDone = true;
|
||||
this.winnersNameDrawn = true;
|
||||
this.startConfetti(this.currentName);
|
||||
return;
|
||||
}
|
||||
this.currentName = this.attendees[
|
||||
@@ -111,7 +95,7 @@ export default {
|
||||
}, 50);
|
||||
},
|
||||
drawColor: function(winnerColor) {
|
||||
this.drawingDone = false;
|
||||
this.winnersNameDrawn = false;
|
||||
if (this.colorRounds == 100) {
|
||||
this.currentColor = winnerColor;
|
||||
this.colorDone = true;
|
||||
@@ -126,7 +110,7 @@ export default {
|
||||
clearTimeout(this.colorTimeout);
|
||||
this.colorTimeout = setTimeout(() => {
|
||||
this.drawColor(winnerColor);
|
||||
}, 50);
|
||||
}, 70);
|
||||
},
|
||||
getRotation: function() {
|
||||
if (this.colorDone) {
|
||||
@@ -147,9 +131,63 @@ export default {
|
||||
case 3:
|
||||
return "yellow";
|
||||
}
|
||||
},
|
||||
startConfetti(currentName) {
|
||||
//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}
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
var interval = setInterval(function() {
|
||||
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});
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
},
|
||||
ordinalNumber(number=this.currentWinnerLocal.winnerCount) {
|
||||
const dictonary = {
|
||||
1: "første",
|
||||
2: "andre",
|
||||
3: "tredje",
|
||||
4: "fjerde",
|
||||
5: "femte",
|
||||
6: "sjette",
|
||||
7: "syvende",
|
||||
8: "åttende",
|
||||
9: "niende",
|
||||
10: "tiende",
|
||||
11: "ellevte",
|
||||
12: "tolvte"
|
||||
};
|
||||
return number in dictonary ? dictonary[number] : number;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -159,22 +197,27 @@ export default {
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.current-drawn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-column: 1 / 5;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ballot-element {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
font-size: 1.2rem;
|
||||
.raffle-element {
|
||||
width: 280px;
|
||||
height: 300px;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
|
||||
-webkit-mask-size: cover;
|
||||
-moz-mask-size: cover;
|
||||
mask-size: cover;
|
||||
}
|
||||
</style>
|
||||
100
frontend/ui/Winners.vue
Normal file
100
frontend/ui/Winners.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<section>
|
||||
<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) }`">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="drawing" class="container">
|
||||
<h3>Trekningen er igang!</h3>
|
||||
</div>
|
||||
|
||||
<div v-else class="container">
|
||||
<h3>Trekningen har ikke startet enda <button>⏰</button></h3>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
winners: {
|
||||
type: Array
|
||||
},
|
||||
drawing: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.6rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: auto;
|
||||
color: $matte-text-color;
|
||||
font-size: 1.6rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.winning-raffles {
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
font-size: 1rem;
|
||||
width: 145px;
|
||||
height: 145px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
button {
|
||||
-webkit-appearance: unset;
|
||||
background-color: unset;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
border: unset;
|
||||
height: auto;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
frontend/utils.js
Normal file
27
frontend/utils.js
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
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)
|
||||
|
||||
return `${ye}-${mo}-${da}`
|
||||
}
|
||||
|
||||
function humanReadableDate(date) {
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
return new Date(date).toLocaleDateString(undefined, options);
|
||||
}
|
||||
|
||||
function daysAgo(date) {
|
||||
const day = 24 * 60 * 60 * 1000;
|
||||
return Math.round(Math.abs((new Date() - new Date(date)) / day));
|
||||
}
|
||||
|
||||
export {
|
||||
dateString,
|
||||
humanReadableDate,
|
||||
daysAgo
|
||||
}
|
||||
54
frontend/vinlottis-init.js
Normal file
54
frontend/vinlottis-init.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import { routes } from "@/router.js";
|
||||
import Vinlottis from "@/Vinlottis";
|
||||
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { Vue as VueIntegration } from "@sentry/integrations";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
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 })
|
||||
],
|
||||
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);
|
||||
};
|
||||
ga.l = 1 * new Date();
|
||||
|
||||
// Initiate
|
||||
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');
|
||||
|
||||
if (ENV == 'development')
|
||||
window[`ga-disable-${__GA_TRACKINGID__}`] = true;
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: routes
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
router,
|
||||
components: { Vinlottis },
|
||||
template: "<Vinlottis/>",
|
||||
render: h => h(Vinlottis)
|
||||
});
|
||||
5075
package-lock.json
generated
5075
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@@ -4,76 +4,64 @@
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"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",
|
||||
"start": "node server.js",
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||
"start-noauth": "cross-env NODE_ENV=development node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "~7.2",
|
||||
"@zxing/library": "^0.15.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"@sentry/browser": "^5.28.0",
|
||||
"@sentry/integrations": "^5.28.0",
|
||||
"@zxing/library": "^0.18.3",
|
||||
"canvas-confetti": "^1.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"chart.js": "^2.9.3",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"connect-mongo": "^3.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"feature-policy": "^0.4.0",
|
||||
"helmet": "^3.21.2",
|
||||
"moment": "^2.24.0",
|
||||
"mongoose": "^5.8.7",
|
||||
"mongoose": "^5.11.4",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"node-sass": "^5.0.0",
|
||||
"node-schedule": "^1.3.2",
|
||||
"passport": "^0.4.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-local-mongoose": "^6.0.1",
|
||||
"qrcode": "^1.4.4",
|
||||
"referrer-policy": "^1.2.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"socket.io": "^3.0.3",
|
||||
"socket.io-client": "^3.0.3",
|
||||
"vue": "~2.6",
|
||||
"vue-analytics": "^5.22.1",
|
||||
"vue-router": "~3.0",
|
||||
"vuex": "^3.1.1",
|
||||
"vue-router": "~3.4.9",
|
||||
"vuex": "^3.6.0",
|
||||
"web-push": "^3.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.2",
|
||||
"@babel/plugin-proposal-class-properties": "~7.3",
|
||||
"@babel/plugin-proposal-decorators": "~7.3",
|
||||
"@babel/plugin-proposal-json-strings": "~7.2",
|
||||
"@babel/plugin-syntax-dynamic-import": "~7.2",
|
||||
"@babel/plugin-syntax-import-meta": "~7.2",
|
||||
"@babel/preset-env": "~7.3",
|
||||
"babel-loader": "~8.0",
|
||||
"compression-webpack-plugin": "^3.1.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^3.2.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"@babel/core": "~7.12",
|
||||
"@babel/preset-env": "~7.12",
|
||||
"babel-loader": "~8.2.2",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"core-js": "3.8.1",
|
||||
"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": "~3.2",
|
||||
"mini-css-extract-plugin": "~0.5",
|
||||
"optimize-css-assets-webpack-plugin": "~3.2",
|
||||
"pm2": "^4.2.3",
|
||||
"html-webpack-plugin": "5.0.0-alpha.15",
|
||||
"mini-css-extract-plugin": "~1.3.2",
|
||||
"optimize-css-assets-webpack-plugin": "~5.0.4",
|
||||
"redis": "^3.0.2",
|
||||
"sass-loader": "~7.1",
|
||||
"uglifyjs-webpack-plugin": "~1.2",
|
||||
"url-loader": "^2.2.0",
|
||||
"vue-loader": "~15.6",
|
||||
"sass-loader": "~10.1.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "~15.9.5",
|
||||
"vue-style-loader": "~4.1",
|
||||
"vue-template-compiler": "~2.6",
|
||||
"webpack": "~4.41.5",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "~3.2",
|
||||
"webpack-dev-server": "~3.1",
|
||||
"webpack-hot-middleware": "~2.24",
|
||||
"webpack-merge": "~4.2"
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "~5.10.0",
|
||||
"webpack-bundle-analyzer": "^4.2.0",
|
||||
"webpack-cli": "~4.2.0",
|
||||
"webpack-dev-server": "~3.11",
|
||||
"webpack-merge": "~5.4"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user