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:
|
steps:
|
||||||
- name: frontend_install
|
- name: frontend_install
|
||||||
image: node:13.6.0
|
image: node:14
|
||||||
commands:
|
commands:
|
||||||
- node -v
|
- node -v
|
||||||
- yarn --version
|
- yarn --version
|
||||||
|
- name: backend_build
|
||||||
|
image: node:14
|
||||||
|
commands:
|
||||||
|
- node -v
|
||||||
|
- yarn --version
|
||||||
|
- yarn
|
||||||
|
- yarn build
|
||||||
- name: deploy
|
- name: deploy
|
||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
pull: true
|
pull: true
|
||||||
@@ -26,13 +33,13 @@ steps:
|
|||||||
- drone-test
|
- drone-test
|
||||||
status: success
|
status: success
|
||||||
settings:
|
settings:
|
||||||
host: 10.0.0.114
|
host: 10.0.0.52
|
||||||
username: root
|
username: root
|
||||||
key:
|
key:
|
||||||
from_secret: ssh_key
|
from_secret: ssh_key
|
||||||
command_timeout: 600s
|
command_timeout: 600s
|
||||||
script:
|
script:
|
||||||
- /home/kevin/deploy/vinlottis.sh
|
- /home/kevin/deploy.sh
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
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">
|
||||||
|
|
||||||
Prerequisits
|
[](https://drone.schleppe.cloud/KevinMidboe/vinlottis)
|
||||||
|
|
||||||
|
</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
|
mongodb
|
||||||
nodejs
|
nodejs
|
||||||
@@ -12,7 +22,6 @@ npm
|
|||||||
|
|
||||||
|
|
||||||
### Run dev
|
### 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
|
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 path = require("path");
|
||||||
const { addMessage } = require(path.join(__dirname + "/redis.js"));
|
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) => {
|
const io = (io) => {
|
||||||
io.on("connection", socket => {
|
io.on("connection", socket => {
|
||||||
let username = null;
|
let username = null;
|
||||||
|
|
||||||
socket.on("username", msg => {
|
socket.on("username", msg => {
|
||||||
if (msg.username == null) {
|
const usernameValidationError = validateUsername(msg.username);
|
||||||
|
if (usernameValidationError) {
|
||||||
username = null;
|
username = null;
|
||||||
socket.emit("accept_username", false);
|
socket.emit("accept_username", {
|
||||||
return;
|
reason: usernameValidationError,
|
||||||
}
|
success: false,
|
||||||
if (msg.username.length > 3 && msg.username.length < 30) {
|
username: undefined
|
||||||
|
});
|
||||||
|
} else {
|
||||||
username = msg.username;
|
username = msg.username;
|
||||||
socket.emit("accept_username", true);
|
socket.emit("accept_username", {
|
||||||
return;
|
reason: undefined,
|
||||||
|
success: true,
|
||||||
|
username: msg.username
|
||||||
|
});
|
||||||
}
|
}
|
||||||
socket.emit("accept_username", false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("chat", msg => {
|
socket.on("chat", msg => {
|
||||||
|
|||||||
@@ -1,33 +1,29 @@
|
|||||||
const express = require("express");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
|
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
const getAllHistory = (req, res) => {
|
||||||
next();
|
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) => {
|
return history(page, limit)
|
||||||
let { skip, take } = req.query;
|
.then(messages => res.json(messages))
|
||||||
skip = !isNaN(skip) ? Number(skip) : undefined;
|
.catch(error => res.status(500).json({
|
||||||
take = !isNaN(take) ? Number(take) : undefined;
|
message: error.message,
|
||||||
|
success: false
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
const deleteHistory = (req, res) => {
|
||||||
const messages = await history(skip, take);
|
return clearHistory()
|
||||||
res.json(messages)
|
.then(message => res.json(message))
|
||||||
} catch(error) {
|
.catch(error => res.status(500).json({
|
||||||
res.status(500).send(error);
|
message: error.message,
|
||||||
}
|
success: false
|
||||||
});
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
router.route("/chat/history").delete(async (req, res) => {
|
module.exports = {
|
||||||
try {
|
getAllHistory,
|
||||||
const messages = await clearHistory();
|
deleteHistory
|
||||||
res.json(messages)
|
};
|
||||||
} catch(error) {
|
|
||||||
res.status(500).send(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
|
|||||||
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) => {
|
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()) {
|
if (!req.isAuthenticated()) {
|
||||||
return res.status(401).send({
|
return res.status(401).send({
|
||||||
success: false,
|
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 client;
|
||||||
|
let llenAsync;
|
||||||
|
let lrangeAsync;
|
||||||
try {
|
try {
|
||||||
const redis = require("redis");
|
const redis = require("redis");
|
||||||
console.log("trying to create");
|
console.log("Trying to connect with redis..");
|
||||||
client = redis.createClient();
|
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.on("error", function(err) {
|
||||||
client.quit();
|
client.quit();
|
||||||
console.error("Missing redis-configurations..");
|
console.error("Unable to connect to redis, setting up redis-mock.");
|
||||||
|
|
||||||
client = {
|
client = {
|
||||||
rpush: function() {
|
zcount: function() {
|
||||||
console.log("redis-dummy lpush", arguments);
|
console.log("redis-dummy zcount", arguments);
|
||||||
if (typeof arguments[arguments.length - 1] == "function") {
|
return Promise.resolve()
|
||||||
arguments[arguments.length - 1](null);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
lrange: function() {
|
zadd: function() {
|
||||||
console.log("redis-dummy lrange", arguments);
|
console.log("redis-dummy zadd", arguments);
|
||||||
if (typeof arguments[arguments.length - 1] == "function") {
|
return Promise.resolve();
|
||||||
arguments[arguments.length - 1](null);
|
},
|
||||||
}
|
zrevrange: function() {
|
||||||
|
console.log("redis-dummy zrevrange", arguments);
|
||||||
|
return Promise.resolve(null);
|
||||||
},
|
},
|
||||||
del: function() {
|
del: function() {
|
||||||
console.log("redis-dummy del", arguments);
|
console.log("redis-dummy del", arguments);
|
||||||
if (typeof arguments[arguments.length - 1] == "function") {
|
return Promise.resolve();
|
||||||
arguments[arguments.length - 1](null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -31,36 +42,46 @@ try {
|
|||||||
|
|
||||||
const addMessage = message => {
|
const addMessage = message => {
|
||||||
const json = JSON.stringify(message);
|
const json = JSON.stringify(message);
|
||||||
client.rpush("messages", json);
|
return client.zadd("messages", message.timestamp, json)
|
||||||
|
.then(position => {
|
||||||
return message;
|
return {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const history = (skip = 0, take = 20) => {
|
const history = (page=1, limit=10) => {
|
||||||
skip = (1 + skip) * -1; // negate to get FIFO
|
const start = (page - 1) * limit;
|
||||||
return new Promise((resolve, reject) =>
|
const stop = (limit * page) - 1;
|
||||||
client.lrange("messages", skip * take, skip, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
data = data.map(data => JSON.parse(data));
|
const getTotalCount = client.zcount("messages", '-inf', '+inf');
|
||||||
resolve(data);
|
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 = () => {
|
const clearHistory = () => {
|
||||||
return new Promise((resolve, reject) =>
|
return client.del("messages")
|
||||||
client.del("messages", (err, success) => {
|
.then(success => {
|
||||||
if (err) {
|
return {
|
||||||
console.log(err);
|
success: success == 1 ? true : false
|
||||||
reject(err);
|
|
||||||
}
|
}
|
||||||
resolve(success == 1 ? true : false);
|
|
||||||
})
|
})
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
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 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 Purchase = require(path.join(__dirname, "/schemas/Purchase"));
|
||||||
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
|
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||||
const Highscore = require(path.join(__dirname + "/../schemas/Highscore"));
|
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
|
||||||
const PreLotteryWine = require(path.join(
|
const PreLotteryWine = require(path.join(
|
||||||
__dirname + "/../schemas/PreLotteryWine"
|
__dirname, "/schemas/PreLotteryWine"
|
||||||
));
|
));
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
const prelotteryWines = async (req, res) => {
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.route("/wines/prelottery").get(async (req, res) => {
|
|
||||||
let wines = await PreLotteryWine.find();
|
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()
|
let purchases = await Purchase.find()
|
||||||
.populate("wines")
|
.populate("wines")
|
||||||
.sort({ date: 1 });
|
.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();
|
const countColor = await Purchase.find();
|
||||||
let red = 0;
|
let red = 0;
|
||||||
let blue = 0;
|
let blue = 0;
|
||||||
@@ -75,7 +65,7 @@ router.route("/purchase/statistics/color").get(async (req, res) => {
|
|||||||
|
|
||||||
const total = red + yellow + blue + green;
|
const total = red + yellow + blue + green;
|
||||||
|
|
||||||
res.json({
|
return res.json({
|
||||||
red: {
|
red: {
|
||||||
total: red,
|
total: red,
|
||||||
win: redWin
|
win: redWin
|
||||||
@@ -95,21 +85,21 @@ router.route("/purchase/statistics/color").get(async (req, res) => {
|
|||||||
stolen: stolen,
|
stolen: stolen,
|
||||||
total: total
|
total: total
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
router.route("/highscore/statistics").get(async (req, res) => {
|
const highscore = async (req, res) => {
|
||||||
const highscore = await Highscore.find().populate("wins.wine");
|
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();
|
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");
|
const highscore = await Highscore.find().populate("wins.wine");
|
||||||
let wines = {};
|
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,
|
vivinoLink: String,
|
||||||
rating: Number,
|
rating: Number,
|
||||||
id: String,
|
id: String,
|
||||||
image: String
|
image: String,
|
||||||
|
price: String,
|
||||||
|
country: String
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);
|
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,
|
green: Number,
|
||||||
blue: Number,
|
blue: Number,
|
||||||
red: Number,
|
red: Number,
|
||||||
yellow: Number
|
yellow: Number,
|
||||||
|
id: String,
|
||||||
|
timestamp_drawn: Number,
|
||||||
|
timestamp_sent: Number,
|
||||||
|
timestamp_limit: Number
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = mongoose.model("VirtualWinner", VirtualWinner);
|
module.exports = mongoose.model("VirtualWinner", VirtualWinner);
|
||||||
@@ -2,19 +2,14 @@ const express = require("express");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const webpush = require("web-push"); //requiring the web-push module
|
const webpush = require("web-push"); //requiring the web-push module
|
||||||
const mongoose = require("mongoose");
|
|
||||||
const schedule = require("node-schedule");
|
const schedule = require("node-schedule");
|
||||||
|
|
||||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
|
||||||
useNewUrlParser: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const mustBeAuthenticated = require(path.join(
|
const mustBeAuthenticated = require(path.join(
|
||||||
__dirname + "/../middleware/mustBeAuthenticated"
|
__dirname, "/middleware/mustBeAuthenticated"
|
||||||
));
|
));
|
||||||
|
|
||||||
const config = require(path.join(__dirname + "/../config/defaults/push"));
|
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(
|
const lotteryConfig = require(path.join(
|
||||||
__dirname + "/../config/defaults/lottery"
|
__dirname + "/../config/defaults/lottery"
|
||||||
));
|
));
|
||||||
|
|||||||
188
api/update.js
188
api/update.js
@@ -1,32 +1,17 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const path = require("path");
|
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 sub = require(path.join(__dirname, "/subscriptions"));
|
||||||
const mustBeAuthenticated = require(path.join(
|
|
||||||
__dirname + "/../middleware/mustBeAuthenticated"
|
|
||||||
));
|
|
||||||
|
|
||||||
const Subscription = require(path.join(__dirname + "/../schemas/Subscription"));
|
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
||||||
const Purchase = require(path.join(__dirname + "/../schemas/Purchase"));
|
const _personFunctions = require(path.join(__dirname, "/person"));
|
||||||
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
|
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
|
||||||
|
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
||||||
const PreLotteryWine = require(path.join(
|
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) => {
|
const submitWines = async (req, res) => {
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.route("/log/wines").post(mustBeAuthenticated, async (req, res) => {
|
|
||||||
const wines = req.body;
|
const wines = req.body;
|
||||||
for (let i = 0; i < wines.length; i++) {
|
for (let i = 0; i < wines.length; i++) {
|
||||||
let wine = wines[i];
|
let wine = wines[i];
|
||||||
@@ -43,112 +28,115 @@ router.route("/log/wines").post(mustBeAuthenticated, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let subs = await Subscription.find();
|
let subs = await Subscription.find();
|
||||||
|
console.log("Sending new wines w/ push notification to all subscribers.")
|
||||||
for (let i = 0; i < subs.length; i++) {
|
for (let i = 0; i < subs.length; i++) {
|
||||||
let subscription = subs[i]; //get subscription from your databse here.
|
let subscription = subs[i]; //get subscription from your databse here.
|
||||||
|
|
||||||
const message = JSON.stringify({
|
const message = JSON.stringify({
|
||||||
message: "Dagens vin er lagt til, se den på lottis.vin/dagens!",
|
message: "Dagens vin er lagt til, se den på lottis.vin/dagens!",
|
||||||
title: "Ny vin!",
|
title: "Ny vin!",
|
||||||
link: "/#/dagens"
|
link: "/#/dagens"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
sub.sendNotification(subscription, message);
|
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 schema = { ...PreLotteryWine.schema.obj };
|
||||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||||
accumulator[current] = "";
|
accumulator[current] = "";
|
||||||
return accumulator;
|
return accumulator
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
res.send(nulledSchema);
|
return res.send(nulledSchema);
|
||||||
});
|
}
|
||||||
|
|
||||||
router.route("/log").post(mustBeAuthenticated, async (req, res) => {
|
// TODO IMPLEMENT WITH FRONTEND (unused)
|
||||||
await PreLotteryWine.deleteMany();
|
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;
|
return Lottery.findOneAndUpdate({ date: date }, {
|
||||||
const winnersBody = req.body.winners;
|
|
||||||
|
|
||||||
const date = purchaseBody.date;
|
|
||||||
const blue = purchaseBody.blue;
|
|
||||||
const red = purchaseBody.red;
|
|
||||||
const yellow = purchaseBody.yellow;
|
|
||||||
const green = purchaseBody.green;
|
|
||||||
|
|
||||||
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,
|
date: date,
|
||||||
wine: wonWine
|
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 }));
|
||||||
|
}
|
||||||
|
|
||||||
await newPerson.save();
|
/**
|
||||||
} else {
|
* @apiParam (Request body) {Array} winners List of winners
|
||||||
person.wins.push({
|
*/
|
||||||
color: currentWinner.color,
|
const submitWinnersToLottery = async (req, res) => {
|
||||||
date: date,
|
const { lottery } = req.body;
|
||||||
wine: wonWine
|
const { winners, date } = lottery;
|
||||||
});
|
|
||||||
person.markModified("wins");
|
for (let i = 0; i < winners.length; i++) {
|
||||||
await person.save();
|
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({
|
return res.json(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiParam (Request body) {Date} date Date of lottery
|
||||||
|
* @apiParam (Request body) {Number} blue Number of blue tickets
|
||||||
|
* @apiParam (Request body) {Number} red Number of red tickets
|
||||||
|
* @apiParam (Request body) {Number} green Number of green tickets
|
||||||
|
* @apiParam (Request body) {Number} yellow Number of yellow tickets
|
||||||
|
* @apiParam (Request body) {Number} bought Number of tickets bought
|
||||||
|
* @apiParam (Request body) {Number} stolen Number of tickets stolen
|
||||||
|
*/
|
||||||
|
const submitLottery = async (req, res) => {
|
||||||
|
const { lottery } = req.body
|
||||||
|
|
||||||
|
const { date,
|
||||||
|
blue,
|
||||||
|
red,
|
||||||
|
yellow,
|
||||||
|
green,
|
||||||
|
bought,
|
||||||
|
stolen } = lottery;
|
||||||
|
|
||||||
|
return Lottery.findOneAndUpdate({ date: date }, {
|
||||||
date: date,
|
date: date,
|
||||||
blue: blue,
|
blue: blue,
|
||||||
yellow: yellow,
|
yellow: yellow,
|
||||||
red: red,
|
red: red,
|
||||||
green: green,
|
green: green,
|
||||||
wines: winesThisDate,
|
|
||||||
bought: bought,
|
bought: bought,
|
||||||
stolen: stolen
|
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 }));
|
||||||
|
|
||||||
await purchase.save();
|
return res.send(true);
|
||||||
|
};
|
||||||
|
|
||||||
res.send(true);
|
module.exports = {
|
||||||
});
|
submitWines,
|
||||||
|
schema,
|
||||||
module.exports = router;
|
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 path = require("path");
|
||||||
const router = express.Router();
|
const crypto = require("crypto");
|
||||||
const mongoose = require("mongoose");
|
|
||||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
|
||||||
useNewUrlParser: true
|
|
||||||
});
|
|
||||||
let io;
|
|
||||||
const mustBeAuthenticated = require(path.join(
|
|
||||||
__dirname + "/../middleware/mustBeAuthenticated"
|
|
||||||
));
|
|
||||||
|
|
||||||
const Attendee = require(path.join(__dirname + "/../schemas/Attendee"));
|
const config = require(path.join(__dirname, "/../config/defaults/lottery"));
|
||||||
const VirtualWinner = require(path.join(
|
const Message = require(path.join(__dirname, "/message"));
|
||||||
__dirname + "/../schemas/VirtualWinner"
|
const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration"));
|
||||||
));
|
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||||
next();
|
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) => {
|
const winners = async (req, res) => {
|
||||||
await Attendee.deleteMany();
|
|
||||||
io.emit("refresh_data", {});
|
|
||||||
res.json(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.route("/winners").get(async (req, res) => {
|
|
||||||
let winners = await VirtualWinner.find();
|
let winners = await VirtualWinner.find();
|
||||||
let winnersRedacted = [];
|
let winnersRedacted = [];
|
||||||
let winner;
|
let winner;
|
||||||
@@ -43,41 +22,104 @@ router.route("/winners").get(async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.json(winnersRedacted);
|
res.json(winnersRedacted);
|
||||||
});
|
};
|
||||||
|
|
||||||
router.route("/winners/secure").get(mustBeAuthenticated, async (req, res) => {
|
const winnersSecure = async (req, res) => {
|
||||||
let winners = await VirtualWinner.find();
|
let winners = await VirtualWinner.find();
|
||||||
|
|
||||||
res.json(winners);
|
return res.json(winners);
|
||||||
});
|
};
|
||||||
|
|
||||||
router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
const deleteWinners = async (req, res) => {
|
||||||
let allContestants = await Attendee.find({ winner: false });
|
await VirtualWinner.deleteMany();
|
||||||
if (allContestants.length == 0) {
|
var io = req.app.get('socketio');
|
||||||
res.json(false);
|
io.emit("refresh_data", {});
|
||||||
return;
|
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++) {
|
for (let i = 0; i < allContestants.length; i++) {
|
||||||
let currentContestant = allContestants[i];
|
let currentContestant = allContestants[i];
|
||||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||||
ballotColors.push("blue");
|
raffleColors.push("blue");
|
||||||
}
|
}
|
||||||
for (let red = 0; red < currentContestant.red; red++) {
|
for (let red = 0; red < currentContestant.red; red++) {
|
||||||
ballotColors.push("red");
|
raffleColors.push("red");
|
||||||
}
|
}
|
||||||
for (let green = 0; green < currentContestant.green; green++) {
|
for (let green = 0; green < currentContestant.green; green++) {
|
||||||
ballotColors.push("green");
|
raffleColors.push("green");
|
||||||
}
|
}
|
||||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||||
ballotColors.push("yellow");
|
raffleColors.push("yellow");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ballotColors = shuffle(ballotColors);
|
raffleColors = shuffle(raffleColors);
|
||||||
|
|
||||||
let colorToChooseFrom =
|
let colorToChooseFrom =
|
||||||
ballotColors[Math.floor(Math.random() * ballotColors.length)];
|
raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||||
let findObject = { winner: false };
|
let findObject = { winner: false };
|
||||||
|
|
||||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||||
@@ -124,7 +166,16 @@ router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
|||||||
Math.floor(Math.random() * attendeeListDemocratic.length)
|
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({
|
let newWinnerElement = new VirtualWinner({
|
||||||
name: winner.name,
|
name: winner.name,
|
||||||
@@ -133,7 +184,9 @@ router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
|||||||
red: winner.red,
|
red: winner.red,
|
||||||
blue: winner.blue,
|
blue: winner.blue,
|
||||||
green: winner.green,
|
green: winner.green,
|
||||||
yellow: winner.yellow
|
yellow: winner.yellow,
|
||||||
|
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||||
|
timestamp_drawn: new Date().getTime()
|
||||||
});
|
});
|
||||||
|
|
||||||
await Attendee.update(
|
await Attendee.update(
|
||||||
@@ -142,52 +195,57 @@ router.route("/winner").get(mustBeAuthenticated, async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await newWinnerElement.save();
|
await newWinnerElement.save();
|
||||||
res.json(winner);
|
return res.json({
|
||||||
});
|
success: true,
|
||||||
|
winner
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
router.route("/attendees").get(async (req, res) => {
|
const finish = async (req, res) => {
|
||||||
let attendees = await Attendee.find();
|
if (!config.gatewayToken) {
|
||||||
let attendeesRedacted = [];
|
return res.json({
|
||||||
let attendee;
|
message: "Missing api token for sms gateway.",
|
||||||
for (let i = 0; i < attendees.length; i++) {
|
success: false
|
||||||
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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.json(attendeesRedacted);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.route("/attendees/secure").get(mustBeAuthenticated, async (req, res) => {
|
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||||
let attendees = await Attendee.find();
|
timestamp_drawn: 1
|
||||||
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
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) {
|
function shuffle(array) {
|
||||||
let currentIndex = array.length,
|
let currentIndex = array.length,
|
||||||
@@ -209,7 +267,15 @@ function shuffle(array) {
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function(_io) {
|
module.exports = {
|
||||||
io = _io;
|
deleteWinners,
|
||||||
return router;
|
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 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) => {
|
const wineSearch = async (req, res) => {
|
||||||
next();
|
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)
|
||||||
|
|
||||||
router.route("/wineinfo/:ean").get(async (req, res) => {
|
const vinmonopoletResponse = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
|
||||||
|
|
||||||
|
if (vinmonopoletResponse.errors != null) {
|
||||||
|
return vinmonopoletResponse.errors.map(error => {
|
||||||
|
if (error.type == "UnknownProductError") {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean)
|
||||||
|
|
||||||
|
return res.send(winesConverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byEAN = async (req, res) => {
|
||||||
const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean)
|
const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean)
|
||||||
.then(resp => resp.json())
|
.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");
|
module.exports = require("../env/lottery.config");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
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 = {
|
module.exports = {
|
||||||
name: "NAME MISSING",
|
name: "NAME MISSING",
|
||||||
@@ -11,6 +11,6 @@ try {
|
|||||||
message: "INSERT MESSAGE",
|
message: "INSERT MESSAGE",
|
||||||
date: 5,
|
date: 5,
|
||||||
hours: 15,
|
hours: 15,
|
||||||
apiUrl: "https://lottis.vin"
|
gatewayToken: "asd"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ try {
|
|||||||
module.exports = require("../env/push.config");
|
module.exports = require("../env/push.config");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
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 };
|
module.exports = { publicKey: false, privateKey: false, mailto: false };
|
||||||
}
|
}
|
||||||
|
|||||||
5
config/env/lottery.config.example.js
vendored
5
config/env/lottery.config.example.js
vendored
@@ -5,5 +5,8 @@ module.exports = {
|
|||||||
message: "VINLOTTERI",
|
message: "VINLOTTERI",
|
||||||
date: 5,
|
date: 5,
|
||||||
hours: 15,
|
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 webpack = require("webpack");
|
||||||
const helpers = require("./helpers");
|
const helpers = require("./helpers");
|
||||||
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
|
|
||||||
const ServiceWorkerConfig = {
|
const ServiceWorkerConfig = {
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".js", ".vue"]
|
extensions: [".js", ".vue"]
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
serviceWorker: [helpers.root("src/service-worker", "service-worker")]
|
serviceWorker: [helpers.root("frontend/service-worker", "service-worker")]
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimizer: []
|
minimizer: []
|
||||||
@@ -19,7 +19,7 @@ const ServiceWorkerConfig = {
|
|||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: "babel-loader",
|
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"
|
//filename: "js/[name].bundle.js"
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new UglifyJSPlugin({
|
new TerserPlugin({
|
||||||
cache: true,
|
test: /\.js(\?.*)?$/i,
|
||||||
parallel: false,
|
|
||||||
sourceMap: false
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: {
|
resolve: {
|
||||||
extensions: [".js", ".vue"],
|
extensions: [".js", ".vue"],
|
||||||
alias: {
|
alias: {
|
||||||
vue$: isDev ? "vue/dist/vue.min.js" : "vue/dist/vue.min.js",
|
vue$: "vue/dist/vue.min.js",
|
||||||
"@": helpers.root("src")
|
"@": helpers.root("frontend")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
entry: {
|
||||||
|
vinlottis: helpers.root("frontend", "vinlottis-init")
|
||||||
|
},
|
||||||
|
externals: {
|
||||||
|
moment: 'moment' // comes with chart.js
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
@@ -33,35 +39,31 @@ const webpackConfig = function(isDev) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: "babel-loader",
|
use: [ "babel-loader" ],
|
||||||
include: [helpers.root("src")]
|
include: [helpers.root("frontend")]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: [
|
use: [
|
||||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
MiniCSSExtractPlugin.loader,
|
||||||
{ loader: "css-loader", options: { sourceMap: isDev } }
|
{ loader: "css-loader", options: { sourceMap: isDev } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
use: [
|
use: [
|
||||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
MiniCSSExtractPlugin.loader,
|
||||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
|
||||||
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.sass$/,
|
|
||||||
use: [
|
|
||||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
|
||||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
{ loader: "css-loader", options: { sourceMap: isDev } },
|
||||||
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.woff(2)?(\?[a-z0-9]+)?$/,
|
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]+)?$/,
|
test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/,
|
||||||
@@ -72,14 +74,16 @@ const webpackConfig = function(isDev) {
|
|||||||
plugins: [
|
plugins: [
|
||||||
new VueLoaderPlugin(),
|
new VueLoaderPlugin(),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
|
__ENV__: JSON.stringify(process.env.NODE_ENV),
|
||||||
__NAME__: JSON.stringify(env.name),
|
__NAME__: JSON.stringify(env.name),
|
||||||
__PHONE__: JSON.stringify(env.phone),
|
__PHONE__: JSON.stringify(env.phone),
|
||||||
__PRICE__: env.price,
|
__PRICE__: env.price,
|
||||||
__MESSAGE__: JSON.stringify(env.message),
|
__MESSAGE__: JSON.stringify(env.message),
|
||||||
__DATE__: env.date,
|
__DATE__: env.date,
|
||||||
__HOURS__: env.hours,
|
__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";
|
"use strict";
|
||||||
|
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const merge = require("webpack-merge");
|
const { merge } = require("webpack-merge");
|
||||||
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
|
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
|
||||||
const HtmlPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
const helpers = require("./helpers");
|
const helpers = require("./helpers");
|
||||||
const commonConfig = require("./webpack.config.common");
|
const commonConfig = require("./webpack.config.common");
|
||||||
const environment = require("./env/dev.env");
|
const environment = require("./env/dev.env");
|
||||||
|
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
|
||||||
let webpackConfig = merge(commonConfig(true), {
|
let webpackConfig = merge(commonConfig(true), {
|
||||||
mode: "development",
|
mode: "development",
|
||||||
devtool: "cheap-module-eval-source-map",
|
devtool: "eval-cheap-module-source-map",
|
||||||
output: {
|
output: {
|
||||||
path: helpers.root("dist"),
|
path: helpers.root("dist"),
|
||||||
publicPath: "/",
|
publicPath: "/",
|
||||||
filename: "js/[name].bundle.js",
|
filename: "js/[name].bundle.js"
|
||||||
chunkFilename: "js/[id].chunk.js"
|
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
runtimeChunk: "single",
|
concatenateModules: true,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
chunks: "all"
|
chunks: "initial"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.EnvironmentPlugin(environment),
|
new webpack.EnvironmentPlugin(environment),
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new FriendlyErrorsPlugin(),
|
||||||
new FriendlyErrorsPlugin()
|
new MiniCSSExtractPlugin({
|
||||||
|
filename: "css/[name].css"
|
||||||
|
})
|
||||||
],
|
],
|
||||||
devServer: {
|
devServer: {
|
||||||
compress: true,
|
compress: true,
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
|
host: "0.0.0.0",
|
||||||
hot: true,
|
hot: true,
|
||||||
overlay: true,
|
overlay: true,
|
||||||
stats: {
|
stats: {
|
||||||
normal: true
|
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, {
|
webpackConfig = merge(webpackConfig, {
|
||||||
entry: {
|
|
||||||
main: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new HtmlPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: "src/templates/Create.html",
|
template: "frontend/templates/Index.html"
|
||||||
chunksSortMode: "dependency"
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const webpack = require("webpack");
|
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 OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||||
const MiniCSSExtractPlugin = require("mini-css-extract-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 helpers = require("./helpers");
|
||||||
const commonConfig = require("./webpack.config.common");
|
const commonConfig = require("./webpack.config.common");
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === "production";
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
const environment = isProd
|
const environment = isProd
|
||||||
? require("./env/prod.env")
|
? require("./env/prod.env")
|
||||||
@@ -16,11 +19,11 @@ const environment = isProd
|
|||||||
|
|
||||||
const webpackConfig = merge(commonConfig(false), {
|
const webpackConfig = merge(commonConfig(false), {
|
||||||
mode: "production",
|
mode: "production",
|
||||||
|
stats: { children: false },
|
||||||
output: {
|
output: {
|
||||||
path: helpers.root("public/dist"),
|
path: helpers.root("public/dist"),
|
||||||
publicPath: "/dist/",
|
publicPath: "/public/dist/",
|
||||||
filename: "js/[name].bundle.[hash:7].js"
|
filename: "js/[name].bundle.[fullhash:7].js"
|
||||||
//filename: "js/[name].bundle.js"
|
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
@@ -33,37 +36,47 @@ const webpackConfig = merge(commonConfig(false), {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
minimize: true,
|
||||||
minimizer: [
|
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({
|
new OptimizeCSSAssetsPlugin({
|
||||||
cssProcessorPluginOptions: {
|
cssProcessorPluginOptions: {
|
||||||
preset: ["default", { discardComments: { removeAll: true } }]
|
preset: ["default", { discardComments: { removeAll: true } }]
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new UglifyJSPlugin({
|
new TerserPlugin({
|
||||||
cache: true,
|
test: /\.js(\?.*)?$/i,
|
||||||
parallel: false,
|
|
||||||
sourceMap: !isProd
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin(), // clean output folder
|
||||||
new webpack.EnvironmentPlugin(environment),
|
new webpack.EnvironmentPlugin(environment),
|
||||||
new MiniCSSExtractPlugin({
|
new MiniCSSExtractPlugin({
|
||||||
filename: "css/[name].[hash:7].css"
|
filename: "css/[name].[fullhash:7].css"
|
||||||
//filename: "css/[name].css"
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isProd) {
|
if (!isProd) {
|
||||||
webpackConfig.devtool = "source-map";
|
webpackConfig.devtool = "source-map";
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.npm_config_report) {
|
if (process.env.BUILD_REPORT) {
|
||||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||||
.BundleAnalyzerPlugin;
|
.BundleAnalyzerPlugin;
|
||||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
|
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = webpackConfig;
|
module.exports = webpackConfig;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<banner />
|
<banner :routes="routes"/>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<Footer />
|
||||||
<UpdateToast
|
<UpdateToast
|
||||||
v-if="showToast"
|
v-if="showToast"
|
||||||
:text="toastText"
|
:text="toastText"
|
||||||
@@ -14,17 +15,48 @@
|
|||||||
<script>
|
<script>
|
||||||
import ServiceWorkerMixin from "@/mixins/ServiceWorkerMixin";
|
import ServiceWorkerMixin from "@/mixins/ServiceWorkerMixin";
|
||||||
import banner from "@/ui/Banner";
|
import banner from "@/ui/Banner";
|
||||||
|
import Footer from "@/ui/Footer";
|
||||||
import UpdateToast from "@/ui/UpdateToast";
|
import UpdateToast from "@/ui/UpdateToast";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "vinlottis",
|
name: "vinlottis",
|
||||||
components: { banner, UpdateToast },
|
components: { banner, UpdateToast, Footer },
|
||||||
props: {},
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showToast: false,
|
showToast: false,
|
||||||
toastText: null,
|
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() {
|
mounted() {
|
||||||
@@ -45,37 +77,31 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
closeToast: function() {
|
closeToast: function() {
|
||||||
this.showToast = false;
|
this.showToast = false;
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "./styles/global.scss";
|
@import "styles/global.scss";
|
||||||
@font-face {
|
@import "styles/positioning.scss";
|
||||||
font-family: "knowit";
|
@import "styles/vinlottis-icons";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #dbeede;
|
background-color: $primary;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.app-container {
|
.app-container {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 80px auto 100px;
|
||||||
|
|
||||||
|
.main-container{
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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'
|
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
||||||
</p>
|
</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" />
|
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { page, event } from "vue-analytics";
|
|
||||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||||
import Vipps from "@/ui/Vipps";
|
import Vipps from "@/ui/Vipps";
|
||||||
import Countdown from "@/ui/Countdown";
|
import Countdown from "@/ui/Countdown";
|
||||||
@@ -27,7 +26,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hardStart: false,
|
hardStart: false,
|
||||||
numberOfBallots: null
|
numberOfRaffles: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -44,7 +43,7 @@ export default {
|
|||||||
this.hardStart = true;
|
this.hardStart = true;
|
||||||
},
|
},
|
||||||
track() {
|
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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Tabs,
|
Tabs
|
||||||
GeneratePage,
|
|
||||||
VirtualLotteryPage
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
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>
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button class="vin-button" @click="submitLottery">Send inn lotteri</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Vinnere</h3>
|
<h3>Vinnere</h3>
|
||||||
|
<a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a>
|
||||||
<div class="winner-container" v-if="winners.length > 0">
|
<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="winner-element">
|
||||||
<div class="color-selector">
|
<div class="color-selector">
|
||||||
<div class="label-div">
|
<div class="label-div">
|
||||||
@@ -107,16 +112,30 @@
|
|||||||
@click="winner.color = 'yellow'"
|
@click="winner.color = 'yellow'"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="label-div">
|
<div class="label-div">
|
||||||
<label for="winner-name">Navn vinner</label>
|
<label for="winner-name">Navn vinner</label>
|
||||||
<input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" />
|
<input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</wine>
|
||||||
|
|
||||||
<div class="button-container">
|
<div class="button-container column">
|
||||||
<button class="vin-button" @click="sendInfo">Send inn vinnere</button>
|
<button class="vin-button" @click="submitLotteryWinners">Send inn vinnere</button>
|
||||||
<button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button>
|
<button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +145,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 TextToast from "@/ui/TextToast";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
||||||
@@ -137,6 +166,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
payed: undefined,
|
payed: undefined,
|
||||||
winners: [],
|
winners: [],
|
||||||
|
fetchedWinners: [],
|
||||||
wines: [],
|
wines: [],
|
||||||
pushMessage: "",
|
pushMessage: "",
|
||||||
pushLink: "/",
|
pushLink: "/",
|
||||||
@@ -160,8 +190,67 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.setWinnerdataToStorage();
|
this.setWinnerdataToStorage();
|
||||||
|
eventBus.$off("tab-change", () => {
|
||||||
|
this.fetchColorsAndWinners();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchColorsAndWinners();
|
||||||
|
|
||||||
|
eventBus.$on("tab-change", () => {
|
||||||
|
this.fetchColorsAndWinners();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
amIBeingEdited(wine) {
|
||||||
return this.editWine.id == wine.id && this.editWine.name == wine.name;
|
return this.editWine.id == wine.id && this.editWine.name == wine.name;
|
||||||
},
|
},
|
||||||
@@ -173,6 +262,7 @@ export default {
|
|||||||
this.winners.push({
|
this.winners.push({
|
||||||
name: "",
|
name: "",
|
||||||
color: "",
|
color: "",
|
||||||
|
potentialWinner: "",
|
||||||
wine: {
|
wine: {
|
||||||
name: wine.name,
|
name: wine.name,
|
||||||
vivinoLink: wine.vivinoLink,
|
vivinoLink: wine.vivinoLink,
|
||||||
@@ -222,7 +312,7 @@ export default {
|
|||||||
},
|
},
|
||||||
sendWines: async function() {
|
sendWines: async function() {
|
||||||
let response = await logWines(this.wines);
|
let response = await logWines(this.wines);
|
||||||
if (response == true) {
|
if (response.success == true) {
|
||||||
alert("Sendt!");
|
alert("Sendt!");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -240,7 +330,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendInfo: async function(event) {
|
submitLottery: async function(event) {
|
||||||
const colors = {
|
const colors = {
|
||||||
red: this.lotteryColors.filter(c => c.css == "red")[0].value,
|
red: this.lotteryColors.filter(c => c.css == "red")[0].value,
|
||||||
green: this.lotteryColors.filter(c => c.css == "green")[0].value,
|
green: this.lotteryColors.filter(c => c.css == "green")[0].value,
|
||||||
@@ -249,48 +339,63 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let sendObject = {
|
let sendObject = {
|
||||||
purchase: {
|
lottery: {
|
||||||
date: new Date(),
|
date: dateString(new Date()),
|
||||||
...colors
|
...colors
|
||||||
},
|
}
|
||||||
winners: this.winners
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (sendObject.purchase.red == undefined) {
|
if (sendObject.lottery.red == undefined) {
|
||||||
alert("Rød må defineres");
|
alert("Rød må defineres");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sendObject.purchase.green == undefined) {
|
if (sendObject.lottery.green == undefined) {
|
||||||
alert("Grønn må defineres");
|
alert("Grønn må defineres");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sendObject.purchase.yellow == undefined) {
|
if (sendObject.lottery.yellow == undefined) {
|
||||||
alert("Gul må defineres");
|
alert("Gul må defineres");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sendObject.purchase.blue == undefined) {
|
if (sendObject.lottery.blue == undefined) {
|
||||||
alert("Blå må defineres");
|
alert("Blå må defineres");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendObject.purchase.bought =
|
sendObject.lottery.bought =
|
||||||
parseInt(colors.blue) +
|
parseInt(colors.blue) +
|
||||||
parseInt(colors.red) +
|
parseInt(colors.red) +
|
||||||
parseInt(colors.green) +
|
parseInt(colors.green) +
|
||||||
parseInt(colors.yellow);
|
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) {
|
if (isNaN(stolen) || stolen == undefined) {
|
||||||
alert("Betalt må registreres");
|
alert("Betalt må registreres");
|
||||||
return;
|
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");
|
alert("Det må være med vinnere");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < sendObject.winners.length; i++) {
|
for (let i = 0; i < sendObject.lottery.winners.length; i++) {
|
||||||
let currentWinner = sendObject.winners[i];
|
let currentWinner = sendObject.lottery.winners[i];
|
||||||
|
|
||||||
if (currentWinner.name == undefined || currentWinner.name == "") {
|
if (currentWinner.name == undefined || currentWinner.name == "") {
|
||||||
alert("Navn må defineres");
|
alert("Navn må defineres");
|
||||||
@@ -302,7 +407,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = await log(sendObject);
|
let response = await sendLotteryWinners(sendObject);
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
alert("Sendt!");
|
alert("Sendt!");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -341,7 +446,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setWinnerdataToStorage() {
|
setWinnerdataToStorage() {
|
||||||
console.log("saving localstorage");
|
|
||||||
localStorage.setItem("winners", JSON.stringify(this.winners));
|
localStorage.setItem("winners", JSON.stringify(this.winners));
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"colorValues",
|
"colorValues",
|
||||||
@@ -362,7 +466,13 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/global.scss";
|
@import "../styles/global.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "../styles/media-queries.scss";
|
||||||
|
select {
|
||||||
|
margin: 0 0 auto;
|
||||||
|
height: 2rem;
|
||||||
|
min-width: 0;
|
||||||
|
width: 98%;
|
||||||
|
padding: 1%;
|
||||||
|
}
|
||||||
h1 {
|
h1 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -376,12 +486,24 @@ h2 {
|
|||||||
font-family: knowit, Arial;
|
font-family: knowit, Arial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wine-link {
|
||||||
|
color: #333333;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid $link-color;
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.page-container {
|
.page-container {
|
||||||
padding: 0 1.5rem 3rem;
|
padding: 0 1.5rem 3rem;
|
||||||
|
|
||||||
@@ -391,8 +513,7 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.winner-container {
|
.winner-container {
|
||||||
width: max-content;
|
width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -405,7 +526,13 @@ hr {
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
> .wine {
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.edit {
|
.edit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -419,10 +546,10 @@ hr {
|
|||||||
}
|
}
|
||||||
.winner-element {
|
.winner-element {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
|
|
||||||
@include desktop {
|
> div {
|
||||||
margin-top: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
@@ -516,7 +643,7 @@ hr {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 3rem auto 0;
|
margin: 3rem auto 1rem;
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
margin: 1.8rem auto 0;
|
margin: 1.8rem auto 0;
|
||||||
@@ -532,9 +659,9 @@ hr {
|
|||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
mask-image: url(/public/assets/images/lodd.svg);
|
||||||
-webkit-mask-repeat: no-repeat;
|
-webkit-mask-repeat: no-repeat;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
|
|
||||||
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>
|
<template>
|
||||||
<div class="outer">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title">Dagens viner</h1>
|
<h1 class="title">Dagens viner</h1>
|
||||||
<div class="wines-container">
|
<div class="wines-container">
|
||||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" :fullscreen="true" :inlineSlot="true" />
|
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { page, event } from "vue-analytics";
|
import { prelottery } from "@/api";
|
||||||
import Banner from "@/ui/Banner";
|
import Banner from "@/ui/Banner";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
|
|
||||||
@@ -25,14 +23,14 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const _wines = await fetch("/api/wines/prelottery");
|
prelottery().then(wines => this.wines = wines);
|
||||||
this.wines = await _wines.json();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "./src/styles/media-queries";
|
@import "@/styles/media-queries";
|
||||||
|
@import "@/styles/variables";
|
||||||
|
|
||||||
.wine-image {
|
.wine-image {
|
||||||
height: 250px;
|
height: 250px;
|
||||||
@@ -46,7 +44,7 @@ h1 {
|
|||||||
.wines-container {
|
.wines-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-evenly;
|
||||||
margin: 0 2rem;
|
margin: 0 2rem;
|
||||||
|
|
||||||
@media (min-width: 1500px) {
|
@media (min-width: 1500px) {
|
||||||
@@ -110,7 +108,7 @@ a:visited {
|
|||||||
font-family: Arial;
|
font-family: Arial;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-bottom: 1px solid #ff5fff;
|
border-bottom: 1px solid $link-color;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||||
<div class="winners" v-if="winners.length > 0">
|
<div class="winners" v-if="winners.length > 0">
|
||||||
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
<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.name }}</span>
|
||||||
<span>{{ winner.phoneNumber }}</span>
|
<span>{{ winner.phoneNumber }}</span>
|
||||||
<span>Rød: {{ winner.red }}</span>
|
<span>Rød: {{ winner.red }}</span>
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
<span class="name">{{ attendee.name }}</span>
|
<span class="name">{{ attendee.name }}</span>
|
||||||
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ballots-container">
|
<div class="raffles-container">
|
||||||
<div class="red-ballot ballot-element small">{{ attendee.red }}</div>
|
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||||
<div class="blue-ballot ballot-element small">{{ attendee.blue }}</div>
|
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||||
<div class="green-ballot ballot-element small">{{ attendee.green }}</div>
|
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||||
<div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div>
|
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +106,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
||||||
|
|
||||||
|
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -118,13 +120,17 @@ import {
|
|||||||
attendees,
|
attendees,
|
||||||
winnersSecure,
|
winnersSecure,
|
||||||
deleteWinners,
|
deleteWinners,
|
||||||
deleteAttendees
|
deleteAttendees,
|
||||||
|
finishedDraw,
|
||||||
|
prelottery
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
|
import TextToast from "@/ui/TextToast";
|
||||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
RaffleGenerator
|
RaffleGenerator,
|
||||||
|
TextToast
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -134,7 +140,7 @@ export default {
|
|||||||
blue: 0,
|
blue: 0,
|
||||||
green: 0,
|
green: 0,
|
||||||
yellow: 0,
|
yellow: 0,
|
||||||
ballots: 0,
|
raffles: 0,
|
||||||
randomColors: false,
|
randomColors: false,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
winners: [],
|
winners: [],
|
||||||
@@ -143,7 +149,9 @@ export default {
|
|||||||
drawTime: 20,
|
drawTime: 20,
|
||||||
currentWinners: 1,
|
currentWinners: 1,
|
||||||
numberOfWinners: 4,
|
numberOfWinners: 4,
|
||||||
socket: null
|
socket: null,
|
||||||
|
toastText: undefined,
|
||||||
|
showToast: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -165,12 +173,23 @@ export default {
|
|||||||
this.socket.on("new_attendee", async msg => {
|
this.socket.on("new_attendee", async msg => {
|
||||||
this.getAttendees();
|
this.getAttendees();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.finishedDraw = finishedDraw;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setWithRandomColors(colors) {
|
setWithRandomColors(colors) {
|
||||||
Object.keys(colors).forEach(color => (this[color] = colors[color]));
|
Object.keys(colors).forEach(color => (this[color] = colors[color]));
|
||||||
},
|
},
|
||||||
sendAttendee: async function() {
|
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({
|
let response = await addAttendee({
|
||||||
name: this.name,
|
name: this.name,
|
||||||
phoneNumber: this.phoneNumber,
|
phoneNumber: this.phoneNumber,
|
||||||
@@ -178,12 +197,17 @@ export default {
|
|||||||
blue: this.blue,
|
blue: this.blue,
|
||||||
green: this.green,
|
green: this.green,
|
||||||
yellow: this.yellow,
|
yellow: this.yellow,
|
||||||
ballots: this.ballots
|
raffles: this.raffles
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
alert("Sendt inn deltaker!");
|
this.toastText = `Sendt inn deltaker: ${this.name}`;
|
||||||
|
this.showToast = true;
|
||||||
|
|
||||||
this.name = null;
|
this.name = null;
|
||||||
this.phoneNumber = null;
|
this.phoneNumber = null;
|
||||||
|
this.yellow = 0;
|
||||||
|
this.green = 0;
|
||||||
this.red = 0;
|
this.red = 0;
|
||||||
this.blue = 0;
|
this.blue = 0;
|
||||||
|
|
||||||
@@ -201,19 +225,29 @@ export default {
|
|||||||
this.secondsLeft = this.drawTime;
|
this.secondsLeft = this.drawTime;
|
||||||
},
|
},
|
||||||
drawWinner: async function() {
|
drawWinner: async function() {
|
||||||
|
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||||
this.drawingWinner = true;
|
this.drawingWinner = true;
|
||||||
let response = await getVirtualWinner();
|
let response = await getVirtualWinner();
|
||||||
if (response) {
|
|
||||||
|
if (response.success) {
|
||||||
|
console.log("Winner:", response.winner);
|
||||||
if (this.currentWinners < this.numberOfWinners) {
|
if (this.currentWinners < this.numberOfWinners) {
|
||||||
this.countdown();
|
this.countdown();
|
||||||
} else {
|
} else {
|
||||||
this.drawingWinner = false;
|
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.getWinners();
|
||||||
this.getAttendees();
|
this.getAttendees();
|
||||||
} else {
|
} else {
|
||||||
this.drawingWinner = false;
|
this.drawingWinner = false;
|
||||||
alert("Noe gikk galt under trekningen..!");
|
alert("Noe gikk galt under trekningen..! " + response["message"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
countdown: function() {
|
countdown: function() {
|
||||||
@@ -236,20 +270,24 @@ export default {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
deleteAllWinners: async function() {
|
deleteAllWinners: async function() {
|
||||||
|
if (window.confirm("Er du sikker på at du vil slette vinnere?")) {
|
||||||
let response = await deleteWinners();
|
let response = await deleteWinners();
|
||||||
if (response) {
|
if (response) {
|
||||||
this.getWinners();
|
this.getWinners();
|
||||||
} else {
|
} else {
|
||||||
alert("Klarte ikke hente ut vinnere");
|
alert("Klarte ikke hente ut vinnere");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteAllAttendees: async function() {
|
deleteAllAttendees: async function() {
|
||||||
|
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
|
||||||
let response = await deleteAttendees();
|
let response = await deleteAttendees();
|
||||||
if (response) {
|
if (response) {
|
||||||
this.getAttendees();
|
this.getAttendees();
|
||||||
} else {
|
} else {
|
||||||
alert("Klarte ikke hente ut vinnere");
|
alert("Klarte ikke hente ut vinnere");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getWinners: async function() {
|
getWinners: async function() {
|
||||||
let response = await winnersSecure();
|
let response = await winnersSecure();
|
||||||
@@ -317,13 +355,13 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
margin: 20px 0;
|
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;
|
background-repeat: no-repeat;
|
||||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
mask-image: url(/public/assets/images/lodd.svg);
|
||||||
-webkit-mask-repeat: no-repeat;
|
-webkit-mask-repeat: no-repeat;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
@@ -341,19 +379,19 @@ hr {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.green-ballot {
|
&.green-raffle {
|
||||||
background-color: $light-green;
|
background-color: $light-green;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.blue-ballot {
|
&.blue-raffle {
|
||||||
background-color: $light-blue;
|
background-color: $light-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.yellow-ballot {
|
&.yellow-raffle {
|
||||||
background-color: $light-yellow;
|
background-color: $light-yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red-ballot {
|
&.red-raffle {
|
||||||
background-color: $light-red;
|
background-color: $light-red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,7 +423,7 @@ button {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
& .name-and-phone,
|
& .name-and-phone,
|
||||||
& .ballots-container {
|
& .raffles-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -394,7 +432,7 @@ button {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ballots-container {
|
& .raffles-container {
|
||||||
flex-direction: row;
|
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");
|
localStorage.removeItem("push");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (window.location.href.includes('localhost')) {
|
||||||
|
console.info("Service worker manually disabled while on localhost.")
|
||||||
|
} else {
|
||||||
this.registerPushListener();
|
this.registerPushListener();
|
||||||
this.registerServiceWorker();
|
this.registerServiceWorker();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
registerPushListener: function() {
|
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-face {
|
||||||
font-family: "knowit";
|
font-family: "knowit";
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url("/../../public/assets/fonts/bold.woff");
|
src: url("/public/assets/fonts/bold.woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "knowit";
|
font-family: "knowit";
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
src: url("/../../public/assets/fonts/regular.eot");
|
src: url("/public/assets/fonts/regular.woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -18,6 +18,10 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
@@ -74,6 +78,16 @@ body {
|
|||||||
margin-right: 2rem;
|
margin-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.column {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-right: unset;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
&:not(.row) {
|
&:not(.row) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -98,16 +112,16 @@ textarea {
|
|||||||
|
|
||||||
.vin-button {
|
.vin-button {
|
||||||
font-family: Arial;
|
font-family: Arial;
|
||||||
$color: #b7debd;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: $color;
|
background: $primary;
|
||||||
color: #333;
|
color: #333;
|
||||||
padding: 10px 30px;
|
padding: 10px 30px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
|
line-height: 1.3rem;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
max-height: 4rem;
|
max-height: 4rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -118,6 +132,15 @@ textarea {
|
|||||||
// disable-dbl-tap-zoom
|
// disable-dbl-tap-zoom
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
|
|
||||||
|
&.auto-height {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background-color: $red;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
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);
|
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);
|
transform: scale(1.02) translateZ(0);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
opacity: 1;
|
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 {
|
.no-margin {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
margin: 20px 0;
|
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;
|
background-repeat: no-repeat;
|
||||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
mask-image: url(/public/assets/images/lodd.svg);
|
||||||
-webkit-mask-repeat: no-repeat;
|
-webkit-mask-repeat: no-repeat;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
|
|
||||||
&.green-ballot {
|
&.green-raffle {
|
||||||
background-color: $light-green;
|
background-color: $light-green;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.blue-ballot {
|
&.blue-raffle {
|
||||||
background-color: $light-blue;
|
background-color: $light-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.yellow-ballot {
|
&.yellow-raffle {
|
||||||
background-color: $light-yellow;
|
background-color: $light-yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red-ballot {
|
&.red-raffle {
|
||||||
background-color: $light-red;
|
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);
|
width: calc(100% - 5rem);
|
||||||
background-color: $light-red;
|
background-color: $light-red;
|
||||||
color: $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;
|
$light-green: #c8f9df;
|
||||||
$green: #0be881;
|
$green: #0be881;
|
||||||
@@ -15,3 +15,7 @@ $dark-yellow: #ecc31d;
|
|||||||
$light-red: #fbd7de;
|
$light-red: #fbd7de;
|
||||||
$red: #ef5878;
|
$red: #ef5878;
|
||||||
$dark-red: #ec3b61;
|
$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"
|
color="#23101f"
|
||||||
/>
|
/>
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#dbeede" />
|
<meta name="theme-color" content="#b7debd" />
|
||||||
<meta
|
<meta
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
content="black-translucent"
|
content="black-translucent"
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<noscript>Du trenger vin, jeg trenger javascript!</noscript>
|
<noscript>Du trenger vin, jeg trenger javascript!</noscript>
|
||||||
|
<script src="/public/analytics.js" async></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #dbeede;
|
background-color: #b7debd;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="attendees" v-if="attendees.length > 0">
|
<div class="attendees" v-if="attendees.length > 0">
|
||||||
<h2>Deltakere ({{ attendees.length }})</h2>
|
|
||||||
<div class="attendees-container" ref="attendees">
|
<div class="attendees-container" ref="attendees">
|
||||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||||
<span class="attendee-name">{{ attendee.name }}</span>
|
<span class="attendee-name">{{ attendee.name }}</span>
|
||||||
<div class="red-ballot ballot-element small">{{ attendee.red }}</div>
|
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||||
<div class="blue-ballot ballot-element small">{{ attendee.blue }}</div>
|
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||||
<div class="green-ballot ballot-element small">{{ attendee.green }}</div>
|
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||||
<div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div>
|
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,11 +41,17 @@ export default {
|
|||||||
@import "../styles/global.scss";
|
@import "../styles/global.scss";
|
||||||
@import "../styles/variables.scss";
|
@import "../styles/variables.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "../styles/media-queries.scss";
|
||||||
|
|
||||||
.attendee-name {
|
.attendee-name {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
hr {
|
||||||
|
border: 2px solid black;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raffle-element {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
@@ -56,20 +61,24 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attendees {
|
.attendees {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 65%;
|
height: auto;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attendees-container {
|
.attendees-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
max-height: 550px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attendee {
|
.attendee {
|
||||||
@@ -78,5 +87,9 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
border-bottom: 2px solid #d7d8d7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,9 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link to="/" class="link">
|
<header class="top-banner">
|
||||||
<div class="top-banner">
|
<!-- Mobile -->
|
||||||
|
<router-link to="/" class="company-logo">
|
||||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<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">
|
<div class="clock">
|
||||||
<h2 v-if="!fiveMinutesLeft && !tenMinutesOver">
|
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
|
||||||
<span v-if="days > 0">{{ pad(days) }}:</span>
|
<span v-if="days > 0">{{ pad(days) }}:</span>
|
||||||
<span>{{ pad(hours) }}</span>:
|
<span>{{ pad(hours) }}</span>:
|
||||||
<span>{{ pad(minutes) }}</span>:
|
<span>{{ pad(minutes) }}</span>:
|
||||||
@@ -11,26 +27,29 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</router-link>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
isOpen: false,
|
||||||
nextLottery: null,
|
nextLottery: null,
|
||||||
days: 0,
|
days: 0,
|
||||||
hours: 0,
|
hours: 0,
|
||||||
minutes: 0,
|
minutes: 0,
|
||||||
seconds: 0,
|
seconds: 0,
|
||||||
distance: 0,
|
distance: 0,
|
||||||
enabled: false,
|
interval: null,
|
||||||
code: "38384040373937396665",
|
|
||||||
codeDone: "",
|
|
||||||
interval: null
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
routes: {
|
||||||
|
required: true,
|
||||||
|
type: Array
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initialize(), this.countdown();
|
this.initialize(), this.countdown();
|
||||||
},
|
},
|
||||||
@@ -49,25 +68,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleMenu(){
|
||||||
|
this.isOpen = this.isOpen ? false : true;
|
||||||
|
},
|
||||||
pad: function(num) {
|
pad: function(num) {
|
||||||
if (num < 10) {
|
if (num < 10) {
|
||||||
return `0${num}`;
|
return `0${num}`;
|
||||||
}
|
}
|
||||||
return 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() {
|
initialize: function() {
|
||||||
let d = new Date();
|
let d = new Date();
|
||||||
let dayOfLottery = __DATE__;
|
let dayOfLottery = __DATE__;
|
||||||
@@ -79,8 +88,17 @@ export default {
|
|||||||
nextDayOfLottery = new Date(nextDayOfLottery.setHours(__HOURS__));
|
nextDayOfLottery = new Date(nextDayOfLottery.setHours(__HOURS__));
|
||||||
nextDayOfLottery = new Date(nextDayOfLottery.setMinutes(0));
|
nextDayOfLottery = new Date(nextDayOfLottery.setMinutes(0));
|
||||||
nextDayOfLottery = new Date(nextDayOfLottery.setSeconds(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;
|
this.nextLottery = nextDayOfLottery;
|
||||||
let now = new Date().getTime();
|
|
||||||
this.distance = new Date(this.nextLottery).getTime() - now;
|
this.distance = new Date(this.nextLottery).getTime() - now;
|
||||||
},
|
},
|
||||||
countdown: function() {
|
countdown: function() {
|
||||||
@@ -106,49 +124,11 @@ export default {
|
|||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
this.interval = setTimeout(this.countdown, 500);
|
this.interval = setTimeout(this.countdown, 500);
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/media-queries.scss";
|
@import "../styles/banner.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</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 {
|
.chart {
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
|
width: 100%;
|
||||||
@include mobile {
|
|
||||||
position: relative;
|
|
||||||
width: 90vw !important;
|
|
||||||
max-height: unset;
|
|
||||||
height: 30vh;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -35,11 +35,10 @@
|
|||||||
type="number"
|
type="number"
|
||||||
placeholder="Antall lodd"
|
placeholder="Antall lodd"
|
||||||
@keyup.enter="generateColors"
|
@keyup.enter="generateColors"
|
||||||
v-model="numberOfBallots"
|
v-model="numberOfRaffles"
|
||||||
/>
|
/>
|
||||||
<button class="vin-button" @click="generateColors">Generer</button>
|
<button class="vin-button" @click="generateColors">Generer</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="colors">
|
<div class="colors">
|
||||||
<div
|
<div
|
||||||
v-for="color in colors"
|
v-for="color in colors"
|
||||||
@@ -69,7 +68,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
numberOfBallots: 4,
|
numberOfRaffles: 4,
|
||||||
colors: [],
|
colors: [],
|
||||||
blue: 0,
|
blue: 0,
|
||||||
red: 0,
|
red: 0,
|
||||||
@@ -85,14 +84,14 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.$emit("numberOfBallots", this.numberOfBallots);
|
this.$emit("numberOfRaffles", this.numberOfRaffles);
|
||||||
if (this.generateOnInit) {
|
if (this.generateOnInit) {
|
||||||
this.generateColors();
|
this.generateColors();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
numberOfBallots: function() {
|
numberOfRaffles: function() {
|
||||||
this.$emit("numberOfBallots", this.numberOfBallots);
|
this.$emit("numberOfRaffles", this.numberOfRaffles);
|
||||||
this.generateColors();
|
this.generateColors();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -102,7 +101,7 @@ export default {
|
|||||||
if (time == 5) {
|
if (time == 5) {
|
||||||
this.generating = false;
|
this.generating = false;
|
||||||
this.generated = true;
|
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) {
|
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -113,13 +112,11 @@ export default {
|
|||||||
|
|
||||||
this.emitColors()
|
this.emitColors()
|
||||||
|
|
||||||
if (window.location.hostname == "localhost") {
|
window.ga('send', {
|
||||||
return;
|
hitType: "event",
|
||||||
}
|
eventCategory: "Raffles",
|
||||||
this.$ga.event({
|
|
||||||
eventCategory: "Ballots",
|
|
||||||
eventAction: "Generate",
|
eventAction: "Generate",
|
||||||
eventValue: JSON.stringify(this.colors)
|
eventLabel: JSON.stringify(this.colors)
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,8 +145,8 @@ export default {
|
|||||||
alert("Du må velge MINST 1 farge");
|
alert("Du må velge MINST 1 farge");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.numberOfBallots > 0) {
|
if (this.numberOfRaffles > 0) {
|
||||||
for (let i = 0; i < this.numberOfBallots; i++) {
|
for (let i = 0; i < this.numberOfRaffles; i++) {
|
||||||
let color =
|
let color =
|
||||||
randomArray[Math.floor(Math.random() * randomArray.length)];
|
randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||||
this.colors.push(color);
|
this.colors.push(color);
|
||||||
@@ -293,9 +290,9 @@ label .text {
|
|||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
mask-image: url(/public/assets/images/lodd.svg);
|
||||||
-webkit-mask-repeat: no-repeat;
|
-webkit-mask-repeat: no-repeat;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "./src/styles/variables";
|
@import "@/styles/variables";
|
||||||
@import "./src/styles/global";
|
@import "@/styles/global";
|
||||||
|
|
||||||
video {
|
video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -7,22 +7,16 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
@click="changeTab(index)"
|
@click="changeTab(index)"
|
||||||
:class="chosenTab == index ? 'active' : null"
|
:class="chosenTab == index ? 'active' : null"
|
||||||
>
|
>{{ tab.name }}</div>
|
||||||
{{ tab.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-elements">
|
<div class="tab-elements">
|
||||||
<component
|
<component :is="tabs[chosenTab].component" />
|
||||||
v-for="(tab, index) in tabs"
|
|
||||||
:key="index"
|
|
||||||
:is="tab.component"
|
|
||||||
:class="chosenTab == index ? null : 'hide'"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import eventBus from "@/mixins/EventBus";
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -45,6 +39,7 @@ export default {
|
|||||||
changeTab: function(num) {
|
changeTab: function(num) {
|
||||||
this.chosenTab = num;
|
this.chosenTab = num;
|
||||||
this.$emit("tabChange", num);
|
this.$emit("tabChange", num);
|
||||||
|
eventBus.$emit("tab-change");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,9 +49,6 @@ export default {
|
|||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-container {
|
.tab-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="outer-bought">
|
<section class="outer-bought">
|
||||||
<h3>Loddstatistikk</h3>
|
<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 class="bought-container">
|
||||||
<div
|
<div
|
||||||
v-for="color in colors"
|
v-for="color in colors"
|
||||||
@@ -8,34 +18,19 @@
|
|||||||
color.name +
|
color.name +
|
||||||
'-container ' +
|
'-container ' +
|
||||||
color.name +
|
color.name +
|
||||||
'-ballot inner-bought-container ballot-element'
|
'-raffle raffle-element-local'
|
||||||
"
|
"
|
||||||
:key="color.name"
|
:key="color.name"
|
||||||
>
|
>
|
||||||
<div class="number-container">
|
<p class="winner-chance">
|
||||||
<span class="color-total bought-number-span">
|
{{translate(color.name)}} vinnersjanse
|
||||||
{{ color.total }}
|
</p>
|
||||||
</span>
|
<span class="win-percentage">{{ color.totalPercentage }}% </span>
|
||||||
<span>kjøpte</span>
|
<p class="total-bought-color">{{ color.total }} kjøpte</p>
|
||||||
</div>
|
<p class="amount-of-wins"> {{ color.win }} vinn </p>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { colorStatistics } from "@/api";
|
import { colorStatistics } from "@/api";
|
||||||
@@ -60,11 +55,13 @@ export default {
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let response = await colorStatistics();
|
let response = await colorStatistics();
|
||||||
|
|
||||||
this.red = response.red;
|
this.red = response.red;
|
||||||
this.blue = response.blue;
|
this.blue = response.blue;
|
||||||
this.green = response.green;
|
this.green = response.green;
|
||||||
this.yellow = response.yellow;
|
this.yellow = response.yellow;
|
||||||
this.total = response.total;
|
this.total = response.total;
|
||||||
|
|
||||||
this.totalWin =
|
this.totalWin =
|
||||||
this.red.win + this.yellow.win + this.blue.win + this.green.win;
|
this.red.win + this.yellow.win + this.blue.win + this.green.win;
|
||||||
this.stolen = response.stolen;
|
this.stolen = response.stolen;
|
||||||
@@ -114,119 +111,106 @@ export default {
|
|||||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
getPercentage: function(win, total) {
|
||||||
return this.round(win == 0 ? 0 : (win / total) * 100);
|
return this.round(win == 0 ? 0 : (win / total) * 100);
|
||||||
},
|
},
|
||||||
round: function(number) {
|
round: function(number) {
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/global.scss";
|
|
||||||
@import "../styles/variables.scss";
|
@import "../styles/variables.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "../styles/media-queries.scss";
|
||||||
|
@import "../styles/global.scss";
|
||||||
|
|
||||||
.inner-bought-container {
|
@include mobile{
|
||||||
|
section {
|
||||||
|
margin-top: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-raffles {
|
||||||
display: flex;
|
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 {
|
.bought-container {
|
||||||
display: flex;
|
margin-top: 2em;
|
||||||
flex-direction: row;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
grid-gap: 50px;
|
||||||
|
|
||||||
|
.raffle-element-local {
|
||||||
|
height: 250px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 3rem;
|
display: flex;
|
||||||
max-width: 1400px;
|
flex-direction: column;
|
||||||
margin: auto;
|
position: relative;
|
||||||
justify-content: space-between;
|
@include raffle;
|
||||||
font-family: Arial;
|
|
||||||
|
|
||||||
@include mobile {
|
.win-percentage {
|
||||||
padding-bottom: 0px;
|
margin-left: 30px;
|
||||||
|
font-size: 50px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.color-total,
|
p {
|
||||||
.total {
|
margin-left: 30px;
|
||||||
font-size: 2rem;
|
&.winner-chance {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.total-bought-color{
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.small {
|
&.amount-of-wins {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.25rem;
|
}
|
||||||
display: inline-block;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
h3 {
|
||||||
.bought-container {
|
margin-left: 15px;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
|
|
||||||
|
&.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>
|
</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 {
|
.chart {
|
||||||
height: 40vh;
|
height: 40vh;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
|
width: 100%;
|
||||||
@include mobile {
|
|
||||||
position: relative;
|
|
||||||
width: 90vw !important;
|
|
||||||
max-height: unset;
|
|
||||||
height: 30vh;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<div class="current-drawn-container">
|
<div class="current-drawn-container" v-if="drawing">
|
||||||
<div class="current-draw" v-if="drawing">
|
<h2 v-if="winnersNameDrawn !== true">TREKKER {{ ordinalNumber() }} VINNER</h2>
|
||||||
<h2>TREKKER</h2>
|
<h2 v-else>VINNER</h2>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="currentColor + '-ballot'"
|
:class="currentColor + '-raffle'"
|
||||||
class="ballot-element center-new-winner"
|
class="raffle-element"
|
||||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||||
>
|
>
|
||||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import confetti from "canvas-confetti";
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
currentWinner: {
|
currentWinner: {
|
||||||
@@ -59,14 +43,13 @@ export default {
|
|||||||
nameTimeout: null,
|
nameTimeout: null,
|
||||||
colorDone: false,
|
colorDone: false,
|
||||||
drawing: false,
|
drawing: false,
|
||||||
drawingDone: false,
|
winnersNameDrawn: false,
|
||||||
winnerQueue: []
|
winnerQueue: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentWinner: function(currentWinner) {
|
currentWinner: function(currentWinner) {
|
||||||
if (currentWinner == null) {
|
if (currentWinner == null) {
|
||||||
this.drawingDone = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.drawing) {
|
if (this.drawing) {
|
||||||
@@ -74,6 +57,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.drawing = true;
|
this.drawing = true;
|
||||||
|
this.winnersNameDrawn = false;
|
||||||
this.currentName = null;
|
this.currentName = null;
|
||||||
this.currentColor = null;
|
this.currentColor = null;
|
||||||
this.nameRounds = 0;
|
this.nameRounds = 0;
|
||||||
@@ -97,8 +81,8 @@ export default {
|
|||||||
this.drawColor(this.currentWinnerLocal.color);
|
this.drawColor(this.currentWinnerLocal.color);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.drawing = false;
|
this.winnersNameDrawn = true;
|
||||||
this.drawingDone = true;
|
this.startConfetti(this.currentName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentName = this.attendees[
|
this.currentName = this.attendees[
|
||||||
@@ -111,7 +95,7 @@ export default {
|
|||||||
}, 50);
|
}, 50);
|
||||||
},
|
},
|
||||||
drawColor: function(winnerColor) {
|
drawColor: function(winnerColor) {
|
||||||
this.drawingDone = false;
|
this.winnersNameDrawn = false;
|
||||||
if (this.colorRounds == 100) {
|
if (this.colorRounds == 100) {
|
||||||
this.currentColor = winnerColor;
|
this.currentColor = winnerColor;
|
||||||
this.colorDone = true;
|
this.colorDone = true;
|
||||||
@@ -126,7 +110,7 @@ export default {
|
|||||||
clearTimeout(this.colorTimeout);
|
clearTimeout(this.colorTimeout);
|
||||||
this.colorTimeout = setTimeout(() => {
|
this.colorTimeout = setTimeout(() => {
|
||||||
this.drawColor(winnerColor);
|
this.drawColor(winnerColor);
|
||||||
}, 50);
|
}, 70);
|
||||||
},
|
},
|
||||||
getRotation: function() {
|
getRotation: function() {
|
||||||
if (this.colorDone) {
|
if (this.colorDone) {
|
||||||
@@ -147,9 +131,63 @@ export default {
|
|||||||
case 3:
|
case 3:
|
||||||
return "yellow";
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -159,22 +197,27 @@ export default {
|
|||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-drawn-container {
|
.current-drawn-container {
|
||||||
display: flex;
|
grid-column: 1 / 5;
|
||||||
justify-content: center;
|
display: grid;
|
||||||
align-items: center;
|
place-items: center;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
width: 140px;
|
width: 280px;
|
||||||
height: 140px;
|
height: 300px;
|
||||||
font-size: 1.2rem;
|
font-size: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
-webkit-mask-size: cover;
|
||||||
|
-moz-mask-size: cover;
|
||||||
|
mask-size: cover;
|
||||||
}
|
}
|
||||||
</style>
|
</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)
|
||||||
|
});
|
||||||
5047
package-lock.json
generated
5047
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": "",
|
"description": "",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"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",
|
"start": "node server.js",
|
||||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress",
|
"start-noauth": "cross-env NODE_ENV=development node server.js",
|
||||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/polyfill": "~7.2",
|
"@sentry/browser": "^5.28.0",
|
||||||
"@zxing/library": "^0.15.2",
|
"@sentry/integrations": "^5.28.0",
|
||||||
"body-parser": "^1.19.0",
|
"@zxing/library": "^0.18.3",
|
||||||
|
"canvas-confetti": "^1.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"chart.js": "^2.9.3",
|
"chart.js": "^2.9.3",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
|
||||||
"compression": "^1.7.4",
|
|
||||||
"connect-mongo": "^3.2.0",
|
"connect-mongo": "^3.2.0",
|
||||||
"cors": "^2.8.5",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.0",
|
"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",
|
"moment": "^2.24.0",
|
||||||
"mongoose": "^5.8.7",
|
"mongoose": "^5.11.4",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"node-sass": "^4.13.0",
|
"node-sass": "^5.0.0",
|
||||||
"node-schedule": "^1.3.2",
|
"node-schedule": "^1.3.2",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-local-mongoose": "^6.0.1",
|
"passport-local-mongoose": "^6.0.1",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"referrer-policy": "^1.2.0",
|
"socket.io": "^3.0.3",
|
||||||
"socket.io": "^2.3.0",
|
"socket.io-client": "^3.0.3",
|
||||||
"socket.io-client": "^2.3.0",
|
|
||||||
"vue": "~2.6",
|
"vue": "~2.6",
|
||||||
"vue-analytics": "^5.22.1",
|
"vue-router": "~3.4.9",
|
||||||
"vue-router": "~3.0",
|
"vuex": "^3.6.0",
|
||||||
"vuex": "^3.1.1",
|
|
||||||
"web-push": "^3.4.3"
|
"web-push": "^3.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "~7.2",
|
"@babel/core": "~7.12",
|
||||||
"@babel/plugin-proposal-class-properties": "~7.3",
|
"@babel/preset-env": "~7.12",
|
||||||
"@babel/plugin-proposal-decorators": "~7.3",
|
"babel-loader": "~8.2.2",
|
||||||
"@babel/plugin-proposal-json-strings": "~7.2",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"@babel/plugin-syntax-dynamic-import": "~7.2",
|
"core-js": "3.8.1",
|
||||||
"@babel/plugin-syntax-import-meta": "~7.2",
|
"css-loader": "^5.0.1",
|
||||||
"@babel/preset-env": "~7.3",
|
"file-loader": "^6.2.0",
|
||||||
"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",
|
|
||||||
"friendly-errors-webpack-plugin": "~1.7",
|
"friendly-errors-webpack-plugin": "~1.7",
|
||||||
"google-maps-api-loader": "^1.1.1",
|
"google-maps-api-loader": "^1.1.1",
|
||||||
"html-webpack-plugin": "~3.2",
|
"html-webpack-plugin": "5.0.0-alpha.15",
|
||||||
"mini-css-extract-plugin": "~0.5",
|
"mini-css-extract-plugin": "~1.3.2",
|
||||||
"optimize-css-assets-webpack-plugin": "~3.2",
|
"optimize-css-assets-webpack-plugin": "~5.0.4",
|
||||||
"pm2": "^4.2.3",
|
|
||||||
"redis": "^3.0.2",
|
"redis": "^3.0.2",
|
||||||
"sass-loader": "~7.1",
|
"sass-loader": "~10.1.0",
|
||||||
"uglifyjs-webpack-plugin": "~1.2",
|
"url-loader": "^4.1.1",
|
||||||
"url-loader": "^2.2.0",
|
"vue-loader": "~15.9.5",
|
||||||
"vue-loader": "~15.6",
|
|
||||||
"vue-style-loader": "~4.1",
|
"vue-style-loader": "~4.1",
|
||||||
"vue-template-compiler": "~2.6",
|
"vue-template-compiler": "^2.6.12",
|
||||||
"webpack": "~4.41.5",
|
"webpack": "~5.10.0",
|
||||||
"webpack-bundle-analyzer": "^3.6.0",
|
"webpack-bundle-analyzer": "^4.2.0",
|
||||||
"webpack-cli": "~3.2",
|
"webpack-cli": "~4.2.0",
|
||||||
"webpack-dev-server": "~3.1",
|
"webpack-dev-server": "~3.11",
|
||||||
"webpack-hot-middleware": "~2.24",
|
"webpack-merge": "~5.4"
|
||||||
"webpack-merge": "~4.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user