Compare commits
1 Commits
feat/contr
...
refactor/w
| Author | SHA1 | Date | |
|---|---|---|---|
| bf06559298 |
15
.babelrc
15
.babelrc
@@ -1,15 +0,0 @@
|
||||
{
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
modules: false,
|
||||
targets: {
|
||||
browsers: ["IE 11", "> 5%"]
|
||||
},
|
||||
useBuiltIns: "usage",
|
||||
corejs: "3"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
13
.drone.yml
13
.drone.yml
@@ -9,17 +9,10 @@ platform:
|
||||
|
||||
steps:
|
||||
- name: frontend_install
|
||||
image: node:14
|
||||
image: node:13.6.0
|
||||
commands:
|
||||
- node -v
|
||||
- yarn --version
|
||||
- name: backend_build
|
||||
image: node:14
|
||||
commands:
|
||||
- node -v
|
||||
- yarn --version
|
||||
- yarn
|
||||
- yarn build
|
||||
- name: deploy
|
||||
image: appleboy/drone-ssh
|
||||
pull: true
|
||||
@@ -33,13 +26,13 @@ steps:
|
||||
- drone-test
|
||||
status: success
|
||||
settings:
|
||||
host: 10.0.0.52
|
||||
host: 10.0.0.114
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
command_timeout: 600s
|
||||
script:
|
||||
- /home/kevin/deploy.sh
|
||||
- /home/kevin/deploy/vinlottis.sh
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
|
||||
17
README.md
17
README.md
@@ -1,19 +1,9 @@
|
||||
<h1 align="center">
|
||||
Vinlottis 🍾
|
||||
</h1>
|
||||
# vinlattis
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://drone.schleppe.cloud/KevinMidboe/vinlottis)
|
||||
[](https://drone.kevinmidboe.com/KevinMidboe/vinlottis)
|
||||
|
||||
</div>
|
||||
Prerequisits
|
||||
|
||||
<br/>
|
||||
|
||||
[**Vinlottis**](https://vinlottis.no) is the unofficial website for Knowit's wine-lottery, usually happening every friday at around 15:00.
|
||||
|
||||
|
||||
### Prerequisites
|
||||
```
|
||||
mongodb
|
||||
nodejs
|
||||
@@ -22,6 +12,7 @@ npm
|
||||
|
||||
|
||||
### Run dev
|
||||
|
||||
Since the backend and API runs separate from the Vue-on-save-compiler, when running the dev-server, the backend needs to be run separate
|
||||
|
||||
```
|
||||
|
||||
42
api/chat.js
42
api/chat.js
@@ -1,46 +1,22 @@
|
||||
const path = require("path");
|
||||
const { addMessage } = require(path.join(__dirname + "/redis.js"));
|
||||
|
||||
const validateUsername = (username) => {
|
||||
let error = undefined;
|
||||
const illegalChars = /\W/;
|
||||
const minLength = 3;
|
||||
const maxLength = 15;
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
error = 'Ugyldig brukernavn.';
|
||||
} else if (username.length === 0) {
|
||||
error = 'Vennligst oppgi brukernavn.';
|
||||
} else if (username.length < minLength || username.length > maxLength) {
|
||||
error = `Brukernavn må være mellom ${minLength}-${maxLength} karaktere.`
|
||||
} else if (illegalChars.test(username)) {
|
||||
error = 'Brukernavn kan bare inneholde tall og bokstaver.'
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
const io = (io) => {
|
||||
io.on("connection", socket => {
|
||||
let username = null;
|
||||
|
||||
socket.on("username", msg => {
|
||||
const usernameValidationError = validateUsername(msg.username);
|
||||
if (usernameValidationError) {
|
||||
if (msg.username == null) {
|
||||
username = null;
|
||||
socket.emit("accept_username", {
|
||||
reason: usernameValidationError,
|
||||
success: false,
|
||||
username: undefined
|
||||
});
|
||||
} else {
|
||||
username = msg.username;
|
||||
socket.emit("accept_username", {
|
||||
reason: undefined,
|
||||
success: true,
|
||||
username: msg.username
|
||||
});
|
||||
socket.emit("accept_username", false);
|
||||
return;
|
||||
}
|
||||
if (msg.username.length > 3 && msg.username.length < 30) {
|
||||
username = msg.username;
|
||||
socket.emit("accept_username", true);
|
||||
return;
|
||||
}
|
||||
socket.emit("accept_username", false);
|
||||
});
|
||||
|
||||
socket.on("chat", msg => {
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
|
||||
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
|
||||
|
||||
const getAllHistory = (req, res) => {
|
||||
let { page, limit } = req.query;
|
||||
page = !isNaN(page) ? Number(page) : undefined;
|
||||
limit = !isNaN(limit) ? Number(limit) : undefined;
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
return history(page, limit)
|
||||
.then(messages => res.json(messages))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
};
|
||||
router.route("/chat/history").get(async (req, res) => {
|
||||
let { skip, take } = req.query;
|
||||
skip = !isNaN(skip) ? Number(skip) : undefined;
|
||||
take = !isNaN(take) ? Number(take) : undefined;
|
||||
|
||||
const deleteHistory = (req, res) => {
|
||||
return clearHistory()
|
||||
.then(message => res.json(message))
|
||||
.catch(error => res.status(500).json({
|
||||
message: error.message,
|
||||
success: false
|
||||
}));
|
||||
};
|
||||
try {
|
||||
const messages = await history(skip, take);
|
||||
res.json(messages)
|
||||
} catch(error) {
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getAllHistory,
|
||||
deleteHistory
|
||||
};
|
||||
router.route("/chat/history").delete(async (req, res) => {
|
||||
try {
|
||||
const messages = await clearHistory();
|
||||
res.json(messages)
|
||||
} catch(error) {
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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
Normal file
59
api/login.js
Normal file
@@ -0,0 +1,59 @@
|
||||
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
132
api/lottery.js
@@ -1,132 +0,0 @@
|
||||
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
131
api/message.js
@@ -1,131 +0,0 @@
|
||||
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,6 +0,0 @@
|
||||
const setAdminHeaderIfAuthenticated = (req, res, next) => {
|
||||
res.set("Vinlottis-Admin", req.isAuthenticated());
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = setAdminHeaderIfAuthenticated;
|
||||
@@ -1,6 +0,0 @@
|
||||
const openCORS = (req, res, next) => {
|
||||
res.set("Access-Control-Allow-Origin", "*")
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = openCORS;
|
||||
@@ -1,37 +0,0 @@
|
||||
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;
|
||||
@@ -1,35 +0,0 @@
|
||||
const path = require("path");
|
||||
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
|
||||
|
||||
async function findSavePerson(foundWinner, wonWine, date) {
|
||||
let person = await Highscore.findOne({
|
||||
name: foundWinner.name
|
||||
});
|
||||
|
||||
if (person == undefined) {
|
||||
let newPerson = new Highscore({
|
||||
name: foundWinner.name,
|
||||
wins: [
|
||||
{
|
||||
color: foundWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await newPerson.save();
|
||||
} else {
|
||||
person.wins.push({
|
||||
color: foundWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
});
|
||||
person.markModified("wins");
|
||||
await person.save();
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
module.exports.findSavePerson = findSavePerson;
|
||||
93
api/redis.js
93
api/redis.js
@@ -1,40 +1,29 @@
|
||||
const { promisify } = require("util"); // from node
|
||||
|
||||
let client;
|
||||
let llenAsync;
|
||||
let lrangeAsync;
|
||||
try {
|
||||
const redis = require("redis");
|
||||
console.log("Trying to connect with redis..");
|
||||
console.log("trying to create");
|
||||
client = redis.createClient();
|
||||
|
||||
client.zcount = promisify(client.zcount).bind(client);
|
||||
client.zadd = promisify(client.zadd).bind(client);
|
||||
client.zrevrange = promisify(client.zrevrange).bind(client);
|
||||
client.del = promisify(client.del).bind(client);
|
||||
|
||||
client.on("connect", () => console.log("Redis connection established!"));
|
||||
|
||||
client.on("error", function(err) {
|
||||
client.quit();
|
||||
console.error("Unable to connect to redis, setting up redis-mock.");
|
||||
|
||||
console.error("Missing redis-configurations..");
|
||||
client = {
|
||||
zcount: function() {
|
||||
console.log("redis-dummy zcount", arguments);
|
||||
return Promise.resolve()
|
||||
rpush: function() {
|
||||
console.log("redis-dummy lpush", arguments);
|
||||
if (typeof arguments[arguments.length - 1] == "function") {
|
||||
arguments[arguments.length - 1](null);
|
||||
}
|
||||
},
|
||||
zadd: function() {
|
||||
console.log("redis-dummy zadd", arguments);
|
||||
return Promise.resolve();
|
||||
},
|
||||
zrevrange: function() {
|
||||
console.log("redis-dummy zrevrange", arguments);
|
||||
return Promise.resolve(null);
|
||||
lrange: function() {
|
||||
console.log("redis-dummy lrange", arguments);
|
||||
if (typeof arguments[arguments.length - 1] == "function") {
|
||||
arguments[arguments.length - 1](null);
|
||||
}
|
||||
},
|
||||
del: function() {
|
||||
console.log("redis-dummy del", arguments);
|
||||
return Promise.resolve();
|
||||
if (typeof arguments[arguments.length - 1] == "function") {
|
||||
arguments[arguments.length - 1](null);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -42,46 +31,36 @@ try {
|
||||
|
||||
const addMessage = message => {
|
||||
const json = JSON.stringify(message);
|
||||
return client.zadd("messages", message.timestamp, json)
|
||||
.then(position => {
|
||||
return {
|
||||
success: true
|
||||
}
|
||||
})
|
||||
client.rpush("messages", json);
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const history = (page=1, limit=10) => {
|
||||
const start = (page - 1) * limit;
|
||||
const stop = (limit * page) - 1;
|
||||
|
||||
const getTotalCount = client.zcount("messages", '-inf', '+inf');
|
||||
const getMessages = client.zrevrange("messages", start, stop);
|
||||
|
||||
return Promise.all([getTotalCount, getMessages])
|
||||
.then(([totalCount, messages]) => {
|
||||
if (messages) {
|
||||
return {
|
||||
messages: messages.map(entry => JSON.parse(entry)).reverse(),
|
||||
count: messages.length,
|
||||
total: totalCount
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
messages: [],
|
||||
count: 0,
|
||||
total: 0
|
||||
}
|
||||
const history = (skip = 0, take = 20) => {
|
||||
skip = (1 + skip) * -1; // negate to get FIFO
|
||||
return new Promise((resolve, reject) =>
|
||||
client.lrange("messages", skip * take, skip, (err, data) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
data = data.map(data => JSON.parse(data));
|
||||
resolve(data);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const clearHistory = () => {
|
||||
return client.del("messages")
|
||||
.then(success => {
|
||||
return {
|
||||
success: success == 1 ? true : false
|
||||
return new Promise((resolve, reject) =>
|
||||
client.del("messages", (err, success) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
}
|
||||
resolve(success == 1 ? true : false);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
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,25 +1,35 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const mongoose = require("mongoose");
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
|
||||
const Purchase = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
const Wine = require(path.join(__dirname, "/schemas/Wine"));
|
||||
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
|
||||
const Purchase = require(path.join(__dirname + "/../schemas/Purchase"));
|
||||
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
|
||||
const Highscore = require(path.join(__dirname + "/../schemas/Highscore"));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
__dirname + "/../schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
const prelotteryWines = async (req, res) => {
|
||||
let wines = await PreLotteryWine.find();
|
||||
return res.json(wines);
|
||||
};
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
const allPurchase = async (req, res) => {
|
||||
router.route("/wines/prelottery").get(async (req, res) => {
|
||||
let wines = await PreLotteryWine.find();
|
||||
res.json(wines);
|
||||
});
|
||||
|
||||
router.route("/purchase/statistics").get(async (req, res) => {
|
||||
let purchases = await Purchase.find()
|
||||
.populate("wines")
|
||||
.sort({ date: 1 });
|
||||
return res.json(purchases);
|
||||
};
|
||||
res.json(purchases);
|
||||
});
|
||||
|
||||
const purchaseByColor = async (req, res) => {
|
||||
router.route("/purchase/statistics/color").get(async (req, res) => {
|
||||
const countColor = await Purchase.find();
|
||||
let red = 0;
|
||||
let blue = 0;
|
||||
@@ -65,7 +75,7 @@ const purchaseByColor = async (req, res) => {
|
||||
|
||||
const total = red + yellow + blue + green;
|
||||
|
||||
return res.json({
|
||||
res.json({
|
||||
red: {
|
||||
total: red,
|
||||
win: redWin
|
||||
@@ -85,21 +95,21 @@ const purchaseByColor = async (req, res) => {
|
||||
stolen: stolen,
|
||||
total: total
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const highscore = async (req, res) => {
|
||||
router.route("/highscore/statistics").get(async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
|
||||
return res.json(highscore);
|
||||
};
|
||||
res.json(highscore);
|
||||
});
|
||||
|
||||
const allWines = async (req, res) => {
|
||||
router.route("/wines/statistics").get(async (req, res) => {
|
||||
const wines = await Wine.find();
|
||||
|
||||
return res.json(wines);
|
||||
};
|
||||
res.json(wines);
|
||||
});
|
||||
|
||||
const allWinesSummary = async (req, res) => {
|
||||
router.route("/wines/statistics/overall").get(async (req, res) => {
|
||||
const highscore = await Highscore.find().populate("wins.wine");
|
||||
let wines = {};
|
||||
|
||||
@@ -114,7 +124,7 @@ const allWinesSummary = async (req, res) => {
|
||||
wines[wine._id] = {
|
||||
name: wine.name,
|
||||
occurences: wine.occurences,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
link: wine.link,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
id: wine.id,
|
||||
@@ -139,16 +149,7 @@ const allWinesSummary = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
wines = Object.values(wines).reverse()
|
||||
res.json(Object.values(wines));
|
||||
});
|
||||
|
||||
return res.json(wines);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
prelotteryWines,
|
||||
allPurchase,
|
||||
purchaseByColor,
|
||||
highscore,
|
||||
allWines,
|
||||
allWinesSummary
|
||||
};
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
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;
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
@@ -2,14 +2,19 @@ const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const webpush = require("web-push"); //requiring the web-push module
|
||||
const mongoose = require("mongoose");
|
||||
const schedule = require("node-schedule");
|
||||
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
|
||||
const mustBeAuthenticated = require(path.join(
|
||||
__dirname, "/middleware/mustBeAuthenticated"
|
||||
__dirname + "/../middleware/mustBeAuthenticated"
|
||||
));
|
||||
|
||||
const config = require(path.join(__dirname + "/../config/defaults/push"));
|
||||
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
|
||||
const Subscription = require(path.join(__dirname + "/../schemas/Subscription"));
|
||||
const lotteryConfig = require(path.join(
|
||||
__dirname + "/../config/defaults/lottery"
|
||||
));
|
||||
|
||||
208
api/update.js
208
api/update.js
@@ -1,23 +1,38 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const mongoose = require("mongoose");
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
|
||||
const sub = require(path.join(__dirname, "/subscriptions"));
|
||||
|
||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
||||
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
|
||||
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
const sub = require(path.join(__dirname + "/../api/subscriptions"));
|
||||
const mustBeAuthenticated = require(path.join(
|
||||
__dirname + "/../middleware/mustBeAuthenticated"
|
||||
));
|
||||
|
||||
const submitWines = async (req, res) => {
|
||||
const Subscription = require(path.join(__dirname + "/../schemas/Subscription"));
|
||||
const Purchase = require(path.join(__dirname + "/../schemas/Purchase"));
|
||||
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname + "/../schemas/PreLotteryWine"
|
||||
));
|
||||
const VirtualWinner = require(path.join(
|
||||
__dirname + "/../schemas/VirtualWinner"
|
||||
));
|
||||
const Highscore = require(path.join(__dirname + "/../schemas/Highscore"));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
router.route("/log/wines").post(mustBeAuthenticated, async (req, res) => {
|
||||
const wines = req.body;
|
||||
for (let i = 0; i < wines.length; i++) {
|
||||
let wine = wines[i];
|
||||
let newWonWine = new PreLotteryWine({
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
link: wine.link,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
price: wine.price,
|
||||
@@ -28,115 +43,112 @@ const submitWines = async (req, res) => {
|
||||
}
|
||||
|
||||
let subs = await Subscription.find();
|
||||
console.log("Sending new wines w/ push notification to all subscribers.")
|
||||
for (let i = 0; i < subs.length; i++) {
|
||||
let subscription = subs[i]; //get subscription from your databse here.
|
||||
|
||||
const message = JSON.stringify({
|
||||
message: "Dagens vin er lagt til, se den på lottis.vin/dagens!",
|
||||
title: "Ny vin!",
|
||||
link: "/#/dagens"
|
||||
});
|
||||
|
||||
try {
|
||||
sub.sendNotification(subscription, message);
|
||||
} catch (error) {
|
||||
console.error("Error when trying to send push notification to subscriber.");
|
||||
console.error(error);
|
||||
}
|
||||
sub.sendNotification(subscription, message);
|
||||
}
|
||||
|
||||
return res.send({
|
||||
message: "Submitted and notified push subscribers of new wines!",
|
||||
success: true
|
||||
});
|
||||
};
|
||||
res.send(true);
|
||||
});
|
||||
|
||||
const schema = async (req, res) => {
|
||||
router.route("/log/schema").get(mustBeAuthenticated, async (req, res) => {
|
||||
let schema = { ...PreLotteryWine.schema.obj };
|
||||
let nulledSchema = Object.keys(schema).reduce((accumulator, current) => {
|
||||
accumulator[current] = "";
|
||||
return accumulator
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
return res.send(nulledSchema);
|
||||
}
|
||||
res.send(nulledSchema);
|
||||
});
|
||||
|
||||
// TODO IMPLEMENT WITH FRONTEND (unused)
|
||||
const submitWinesToLottery = async (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
const { date, wines } = lottery;
|
||||
const wineObjects = await Promise.all(wines.map(async (wine) => await _wineFunctions.findSaveWine(wine)))
|
||||
router.route("/log").post(mustBeAuthenticated, async (req, res) => {
|
||||
await PreLotteryWine.deleteMany();
|
||||
|
||||
return Lottery.findOneAndUpdate({ date: date }, {
|
||||
date: date,
|
||||
wines: wineObjects
|
||||
}, {
|
||||
upsert: true
|
||||
}).then(_ => res.send(true))
|
||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving wine to lottery.',
|
||||
success: false,
|
||||
exception: err.message }));
|
||||
}
|
||||
const purchaseBody = req.body.purchase;
|
||||
const winnersBody = req.body.winners;
|
||||
|
||||
/**
|
||||
* @apiParam (Request body) {Array} winners List of winners
|
||||
*/
|
||||
const submitWinnersToLottery = async (req, res) => {
|
||||
const { lottery } = req.body;
|
||||
const { winners, date } = lottery;
|
||||
const date = purchaseBody.date;
|
||||
const blue = purchaseBody.blue;
|
||||
const red = purchaseBody.red;
|
||||
const yellow = purchaseBody.yellow;
|
||||
const green = purchaseBody.green;
|
||||
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
let currentWinner = winners[i];
|
||||
let wonWine = await _wineFunctions.findSaveWine(currentWinner.wine); // TODO rename to findAndSaveWineToLottery
|
||||
await _personFunctions.findSavePerson(currentWinner, wonWine, date); // TODO rename to findAndSaveWineToPerson
|
||||
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,
|
||||
link: currentWinner.wine.link,
|
||||
rating: currentWinner.wine.rating,
|
||||
occurences: 1,
|
||||
image: currentWinner.wine.image,
|
||||
id: currentWinner.wine.id
|
||||
});
|
||||
await newWonWine.save();
|
||||
wonWine = newWonWine;
|
||||
} else {
|
||||
wonWine.occurences += 1;
|
||||
wonWine.image = currentWinner.wine.image;
|
||||
wonWine.id = currentWinner.wine.id;
|
||||
await wonWine.save();
|
||||
}
|
||||
|
||||
winesThisDate.push(wonWine);
|
||||
|
||||
const person = await Highscore.findOne({
|
||||
name: currentWinner.name
|
||||
});
|
||||
|
||||
if (person == undefined) {
|
||||
let newPerson = new Highscore({
|
||||
name: currentWinner.name,
|
||||
wins: [
|
||||
{
|
||||
color: currentWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await newPerson.save();
|
||||
} else {
|
||||
person.wins.push({
|
||||
color: currentWinner.color,
|
||||
date: date,
|
||||
wine: wonWine
|
||||
});
|
||||
person.markModified("wins");
|
||||
await person.save();
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(true);
|
||||
}
|
||||
let purchase = new Purchase({
|
||||
date: date,
|
||||
blue: blue,
|
||||
yellow: yellow,
|
||||
red: red,
|
||||
green: green,
|
||||
wines: winesThisDate,
|
||||
bought: bought,
|
||||
stolen: stolen
|
||||
});
|
||||
|
||||
/**
|
||||
* @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
|
||||
await purchase.save();
|
||||
|
||||
const { date,
|
||||
blue,
|
||||
red,
|
||||
yellow,
|
||||
green,
|
||||
bought,
|
||||
stolen } = lottery;
|
||||
res.send(true);
|
||||
});
|
||||
|
||||
return Lottery.findOneAndUpdate({ date: date }, {
|
||||
date: date,
|
||||
blue: blue,
|
||||
yellow: yellow,
|
||||
red: red,
|
||||
green: green,
|
||||
bought: bought,
|
||||
stolen: stolen
|
||||
}, {
|
||||
upsert: true
|
||||
}).then(_ => res.send(true))
|
||||
.catch(err => res.status(500).send({ message: 'Unexpected error while updating/saving lottery.',
|
||||
success: false,
|
||||
exception: err.message }));
|
||||
|
||||
return res.send(true);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
submitWines,
|
||||
schema,
|
||||
submitLottery,
|
||||
submitWinnersToLottery,
|
||||
submitWinesToLottery
|
||||
};
|
||||
module.exports = router;
|
||||
|
||||
51
api/user.js
51
api/user.js
@@ -1,51 +0,0 @@
|
||||
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,16 +1,37 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const router = express.Router();
|
||||
const mongoose = require("mongoose");
|
||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
let io;
|
||||
const mustBeAuthenticated = require(path.join(
|
||||
__dirname + "/../middleware/mustBeAuthenticated"
|
||||
));
|
||||
|
||||
const config = require(path.join(__dirname, "/../config/defaults/lottery"));
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration"));
|
||||
const Attendee = require(path.join(__dirname + "/../schemas/Attendee"));
|
||||
const VirtualWinner = require(path.join(
|
||||
__dirname + "/../schemas/VirtualWinner"
|
||||
));
|
||||
|
||||
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
|
||||
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
|
||||
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
router.route("/winners").delete(mustBeAuthenticated, async (req, res) => {
|
||||
await VirtualWinner.deleteMany();
|
||||
io.emit("refresh_data", {});
|
||||
res.json(true);
|
||||
});
|
||||
|
||||
const winners = async (req, res) => {
|
||||
router.route("/attendees").delete(mustBeAuthenticated, async (req, res) => {
|
||||
await Attendee.deleteMany();
|
||||
io.emit("refresh_data", {});
|
||||
res.json(true);
|
||||
});
|
||||
|
||||
router.route("/winners").get(async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
let winnersRedacted = [];
|
||||
let winner;
|
||||
@@ -22,104 +43,41 @@ const winners = async (req, res) => {
|
||||
});
|
||||
}
|
||||
res.json(winnersRedacted);
|
||||
};
|
||||
});
|
||||
|
||||
const winnersSecure = async (req, res) => {
|
||||
router.route("/winners/secure").get(mustBeAuthenticated, async (req, res) => {
|
||||
let winners = await VirtualWinner.find();
|
||||
|
||||
return res.json(winners);
|
||||
};
|
||||
res.json(winners);
|
||||
});
|
||||
|
||||
const deleteWinners = async (req, res) => {
|
||||
await VirtualWinner.deleteMany();
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("refresh_data", {});
|
||||
return res.json(true);
|
||||
};
|
||||
|
||||
const attendees = async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
let attendeesRedacted = [];
|
||||
let attendee;
|
||||
for (let i = 0; i < attendees.length; i++) {
|
||||
attendee = attendees[i];
|
||||
attendeesRedacted.push({
|
||||
name: attendee.name,
|
||||
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||
red: attendee.red,
|
||||
blue: attendee.blue,
|
||||
green: attendee.green,
|
||||
yellow: attendee.yellow
|
||||
});
|
||||
}
|
||||
return res.json(attendeesRedacted);
|
||||
};
|
||||
|
||||
const attendeesSecure = async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
|
||||
return res.json(attendees);
|
||||
};
|
||||
|
||||
const addAttendee = async (req, res) => {
|
||||
const attendee = req.body;
|
||||
const { red, blue, yellow, green } = attendee;
|
||||
|
||||
let newAttendee = new Attendee({
|
||||
name: attendee.name,
|
||||
red,
|
||||
blue,
|
||||
green,
|
||||
yellow,
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
winner: false
|
||||
});
|
||||
await newAttendee.save();
|
||||
|
||||
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("new_attendee", {});
|
||||
|
||||
return res.send(true);
|
||||
};
|
||||
|
||||
const deleteAttendees = async (req, res) => {
|
||||
await Attendee.deleteMany();
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("refresh_data", {});
|
||||
return res.json(true);
|
||||
};
|
||||
|
||||
const drawWinner = async (req, res) => {
|
||||
router.route("/winner").get(mustBeAuthenticated, 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."
|
||||
});
|
||||
res.json(false);
|
||||
return;
|
||||
}
|
||||
let raffleColors = [];
|
||||
let ballotColors = [];
|
||||
for (let i = 0; i < allContestants.length; i++) {
|
||||
let currentContestant = allContestants[i];
|
||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||
raffleColors.push("blue");
|
||||
ballotColors.push("blue");
|
||||
}
|
||||
for (let red = 0; red < currentContestant.red; red++) {
|
||||
raffleColors.push("red");
|
||||
ballotColors.push("red");
|
||||
}
|
||||
for (let green = 0; green < currentContestant.green; green++) {
|
||||
raffleColors.push("green");
|
||||
ballotColors.push("green");
|
||||
}
|
||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||
raffleColors.push("yellow");
|
||||
ballotColors.push("yellow");
|
||||
}
|
||||
}
|
||||
|
||||
raffleColors = shuffle(raffleColors);
|
||||
ballotColors = shuffle(ballotColors);
|
||||
|
||||
let colorToChooseFrom =
|
||||
raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||
ballotColors[Math.floor(Math.random() * ballotColors.length)];
|
||||
let findObject = { winner: false };
|
||||
|
||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||
@@ -166,16 +124,7 @@ const drawWinner = async (req, res) => {
|
||||
Math.floor(Math.random() * attendeeListDemocratic.length)
|
||||
];
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
});
|
||||
|
||||
var io = req.app.get('socketio');
|
||||
io.emit("winner", {
|
||||
color: colorToChooseFrom,
|
||||
name: winner.name,
|
||||
winner_count: winners.length + 1
|
||||
});
|
||||
io.emit("winner", { color: colorToChooseFrom, name: winner.name });
|
||||
|
||||
let newWinnerElement = new VirtualWinner({
|
||||
name: winner.name,
|
||||
@@ -184,9 +133,7 @@ const drawWinner = async (req, res) => {
|
||||
red: winner.red,
|
||||
blue: winner.blue,
|
||||
green: winner.green,
|
||||
yellow: winner.yellow,
|
||||
id: sha512(winner.phoneNumber, genRandomString(10)),
|
||||
timestamp_drawn: new Date().getTime()
|
||||
yellow: winner.yellow
|
||||
});
|
||||
|
||||
await Attendee.update(
|
||||
@@ -195,57 +142,52 @@ const drawWinner = async (req, res) => {
|
||||
);
|
||||
|
||||
await newWinnerElement.save();
|
||||
return res.json({
|
||||
success: true,
|
||||
winner
|
||||
});
|
||||
};
|
||||
res.json(winner);
|
||||
});
|
||||
|
||||
const finish = async (req, res) => {
|
||||
if (!config.gatewayToken) {
|
||||
return res.json({
|
||||
message: "Missing api token for sms gateway.",
|
||||
success: false
|
||||
router.route("/attendees").get(async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
let attendeesRedacted = [];
|
||||
let attendee;
|
||||
for (let i = 0; i < attendees.length; i++) {
|
||||
attendee = attendees[i];
|
||||
attendeesRedacted.push({
|
||||
name: attendee.name,
|
||||
ballots: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||
red: attendee.red,
|
||||
blue: attendee.blue,
|
||||
green: attendee.green,
|
||||
yellow: attendee.yellow
|
||||
});
|
||||
}
|
||||
res.json(attendeesRedacted);
|
||||
});
|
||||
|
||||
let winners = await VirtualWinner.find({ timestamp_sent: undefined }).sort({
|
||||
timestamp_drawn: 1
|
||||
router.route("/attendees/secure").get(mustBeAuthenticated, async (req, res) => {
|
||||
let attendees = await Attendee.find();
|
||||
|
||||
res.json(attendees);
|
||||
});
|
||||
|
||||
router.route("/attendee").post(mustBeAuthenticated, async (req, res) => {
|
||||
const attendee = req.body;
|
||||
const { red, blue, yellow, green } = attendee;
|
||||
|
||||
let newAttendee = new Attendee({
|
||||
name: attendee.name,
|
||||
red,
|
||||
blue,
|
||||
green,
|
||||
yellow,
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
winner: false
|
||||
});
|
||||
await newAttendee.save();
|
||||
|
||||
if (winners.length == 0) {
|
||||
return res.json({
|
||||
message: "No winners to draw from.",
|
||||
success: false
|
||||
});
|
||||
}
|
||||
io.emit("new_attendee", {});
|
||||
|
||||
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;
|
||||
};
|
||||
res.send(true);
|
||||
});
|
||||
|
||||
function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
@@ -267,15 +209,7 @@ function shuffle(array) {
|
||||
return array;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
winners,
|
||||
winnersSecure,
|
||||
drawWinner,
|
||||
finish,
|
||||
attendees,
|
||||
attendeesSecure,
|
||||
addAttendee
|
||||
}
|
||||
|
||||
module.exports = function(_io) {
|
||||
io = _io;
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const _wineFunctions = require(path.join(__dirname, "/wine"));
|
||||
const _personFunctions = require(path.join(__dirname, "/person"));
|
||||
const Message = require(path.join(__dirname, "/message"));
|
||||
const VirtualWinner = require(path.join(
|
||||
__dirname, "/schemas/VirtualWinner"
|
||||
));
|
||||
const PreLotteryWine = require(path.join(
|
||||
__dirname, "/schemas/PreLotteryWine"
|
||||
));
|
||||
|
||||
|
||||
const getWinesToWinnerById = async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No winner with this id.",
|
||||
existing: false,
|
||||
turn: false
|
||||
});
|
||||
}
|
||||
|
||||
let allWinners = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
if (
|
||||
allWinners[0].id != foundWinner.id ||
|
||||
foundWinner.timestamp_limit == undefined ||
|
||||
foundWinner.timestamp_sent == undefined
|
||||
) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Not the winner next in line!",
|
||||
existing: true,
|
||||
turn: false
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
existing: true,
|
||||
turn: true,
|
||||
name: foundWinner.name,
|
||||
color: foundWinner.color
|
||||
});
|
||||
};
|
||||
|
||||
const registerWinnerSelection = async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let wineName = req.body.wineName;
|
||||
let foundWinner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!foundWinner) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No winner with this id."
|
||||
})
|
||||
} else if (foundWinner.timestamp_limit < new Date().getTime()) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Timelimit expired, you will receive a wine after other users have chosen.",
|
||||
limit: true
|
||||
})
|
||||
}
|
||||
|
||||
let date = new Date();
|
||||
date.setHours(5, 0, 0, 0);
|
||||
let prelotteryWine = await PreLotteryWine.findOne({ name: wineName });
|
||||
|
||||
if (!prelotteryWine) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "No wine with this name.",
|
||||
wine: false
|
||||
});
|
||||
}
|
||||
|
||||
let wonWine = await _wineFunctions.findSaveWine(prelotteryWine);
|
||||
await prelotteryWine.delete();
|
||||
await _personFunctions.findSavePerson(foundWinner, wonWine, date);
|
||||
await Message.sendWineConfirmation(foundWinner, wonWine, date);
|
||||
|
||||
await foundWinner.delete();
|
||||
console.info("Saved winners choice.");
|
||||
|
||||
return findAndNotifyNextWinner()
|
||||
.then(() => res.json({
|
||||
message: "Choice saved and next in line notified.",
|
||||
success: true
|
||||
}))
|
||||
.catch(error => res.json({
|
||||
message: error["message"] || "Error when notifing next winner.",
|
||||
success: false
|
||||
}))
|
||||
};
|
||||
|
||||
const chooseLastWineForUser = (winner, preLotteryWine) => {
|
||||
let date = new Date();
|
||||
date.setHours(5, 0, 0, 0);
|
||||
|
||||
return _wineFunctions.findSaveWine(preLotteryWine)
|
||||
.then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date))
|
||||
.then(() => preLotteryWine.delete())
|
||||
.then(() => Message.sendLastWinnerMessage(winner, preLotteryWine))
|
||||
.then(() => winner.delete())
|
||||
.catch(err => {
|
||||
console.log("Error thrown from chooseLastWineForUser: " + err);
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
|
||||
const findAndNotifyNextWinner = async () => {
|
||||
let nextWinner = undefined;
|
||||
|
||||
let winnersLeft = await VirtualWinner.find().sort({ timestamp_drawn: 1 });
|
||||
let winesLeft = await PreLotteryWine.find();
|
||||
|
||||
if (winnersLeft.length > 1) {
|
||||
console.log("multiple winners left, choose next in line")
|
||||
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
||||
console.log("one winner left, but multiple wines")
|
||||
nextWinner = winnersLeft[0] // one winner left, but multiple wines
|
||||
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
||||
console.log("one winner and one wine left, choose for user")
|
||||
nextWinner = winnersLeft[0] // one winner and one wine left, choose for user
|
||||
wine = winesLeft[0]
|
||||
return chooseLastWineForUser(nextWinner, wine);
|
||||
}
|
||||
|
||||
if (nextWinner) {
|
||||
return Message.sendWineSelectMessage(nextWinner)
|
||||
.then(messageResponse => startTimeout(nextWinner.id))
|
||||
} else {
|
||||
console.info("All winners notified. Could start cleanup here.");
|
||||
return Promise.resolve({
|
||||
message: "All winners notified."
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotificationToWinnerById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
let winner = await VirtualWinner.findOne({ id: id });
|
||||
|
||||
if (!winner) {
|
||||
return res.json({
|
||||
message: "No winner with this id.",
|
||||
success: false
|
||||
})
|
||||
}
|
||||
|
||||
return Message.sendWineSelectMessage(winner)
|
||||
.then(success => res.json({
|
||||
success: success,
|
||||
message: `Message sent to winner ${id} successfully!`
|
||||
}))
|
||||
.catch(err => res.json({
|
||||
success: false,
|
||||
message: "Error while trying to send sms.",
|
||||
error: err
|
||||
}))
|
||||
}
|
||||
|
||||
function startTimeout(id) {
|
||||
const minute = 60000;
|
||||
const minutesForTimeout = 10;
|
||||
|
||||
console.log(`Starting timeout for user ${id}.`);
|
||||
console.log(`Timeout duration: ${ minutesForTimeout * minute }`)
|
||||
setTimeout(async () => {
|
||||
let virtualWinner = await VirtualWinner.findOne({ id: id });
|
||||
if (!virtualWinner) {
|
||||
console.log(`Timeout done for user ${id}, but user has already sent data.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Timeout done for user ${id}, sending update to user.`);
|
||||
|
||||
Message.sendWineSelectMessageTooLate(virtualWinner);
|
||||
|
||||
virtualWinner.timestamp_drawn = new Date().getTime();
|
||||
virtualWinner.timestamp_limit = null;
|
||||
virtualWinner.timestamp_sent = null;
|
||||
await virtualWinner.save();
|
||||
|
||||
findAndNotifyNextWinner();
|
||||
}, minutesForTimeout * minute);
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWinesToWinnerById,
|
||||
registerWinnerSelection,
|
||||
findAndNotifyNextWinner,
|
||||
|
||||
sendNotificationToWinnerById
|
||||
};
|
||||
27
api/wine.js
27
api/wine.js
@@ -1,27 +0,0 @@
|
||||
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,53 +1,15 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const router = express.Router();
|
||||
const fetch = require('node-fetch')
|
||||
const path = require('path')
|
||||
const config = require(path.join(__dirname + "/../config/env/lottery.config"));
|
||||
|
||||
const convertToOurWineObject = wine => {
|
||||
if(wine.basic.ageLimit === "18"){
|
||||
return {
|
||||
name: wine.basic.productShortName,
|
||||
vivinoLink: "https://www.vinmonopolet.no/p/" + wine.basic.productId,
|
||||
rating: wine.basic.alcoholContent,
|
||||
occurences: 0,
|
||||
id: wine.basic.productId,
|
||||
image: `https://bilder.vinmonopolet.no/cache/500x500-0/${wine.basic.productId}-1.jpg`,
|
||||
price: wine.prices[0].salesPrice.toString(),
|
||||
country: wine.origins.origin.country
|
||||
}
|
||||
}
|
||||
}
|
||||
const mustBeAuthenticated = require(path.join(__dirname + "/../middleware/mustBeAuthenticated"))
|
||||
|
||||
const wineSearch = async (req, res) => {
|
||||
const {query} = req.query
|
||||
let url = new URL(`https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=test&maxResults=15`)
|
||||
url.searchParams.set('productShortNameContains', query)
|
||||
|
||||
const vinmonopoletResponse = await fetch(url, {
|
||||
headers: {
|
||||
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
|
||||
}
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(err => console.error(err))
|
||||
|
||||
|
||||
if (vinmonopoletResponse.errors != null) {
|
||||
return vinmonopoletResponse.errors.map(error => {
|
||||
if (error.type == "UnknownProductError") {
|
||||
return res.status(404).json({
|
||||
message: error.message
|
||||
})
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
})
|
||||
}
|
||||
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean)
|
||||
router.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
return res.send(winesConverted);
|
||||
}
|
||||
|
||||
const byEAN = async (req, res) => {
|
||||
router.route("/wineinfo/:ean").get(async (req, res) => {
|
||||
const vinmonopoletResponse = await fetch("https://app.vinmonopolet.no/vmpws/v2/vmp/products/barCodeSearch/" + req.params.ean)
|
||||
.then(resp => resp.json())
|
||||
|
||||
@@ -63,10 +25,7 @@ const byEAN = async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
return res.send(vinmonopoletResponse);
|
||||
};
|
||||
res.send(vinmonopoletResponse);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
byEAN,
|
||||
wineSearch
|
||||
};
|
||||
module.exports = router;
|
||||
|
||||
@@ -2,7 +2,7 @@ try {
|
||||
module.exports = require("../env/lottery.config");
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"⚠️ You haven't defined lottery-configs, you sure you want to continue without them?\n"
|
||||
"You haven't defined lottery-configs, you sure you want to continue without them?"
|
||||
);
|
||||
module.exports = {
|
||||
name: "NAME MISSING",
|
||||
@@ -11,6 +11,6 @@ try {
|
||||
message: "INSERT MESSAGE",
|
||||
date: 5,
|
||||
hours: 15,
|
||||
gatewayToken: "asd"
|
||||
apiUrl: "https://lottis.vin"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ try {
|
||||
module.exports = require("../env/push.config");
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"⚠️ You haven't defined push-parameters, you sure you want to continue without them?\n"
|
||||
"You haven't defined push-parameters, you sure you want to continue without them?"
|
||||
);
|
||||
module.exports = { publicKey: false, privateKey: false, mailto: false };
|
||||
}
|
||||
|
||||
7
config/env/lottery.config.example.js
vendored
7
config/env/lottery.config.example.js
vendored
@@ -5,8 +5,5 @@ module.exports = {
|
||||
message: "VINLOTTERI",
|
||||
date: 5,
|
||||
hours: 15,
|
||||
gatewayToken: undefined,
|
||||
vinmonopoletToken: undefined,
|
||||
googleanalytics_trackingId: undefined,
|
||||
googleanalytics_cookieLifetime: 60 * 60 * 24 * 14
|
||||
};
|
||||
apiUrl: undefined
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
const webpack = require("webpack");
|
||||
const helpers = require("./helpers");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
|
||||
|
||||
const ServiceWorkerConfig = {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue"]
|
||||
},
|
||||
entry: {
|
||||
serviceWorker: [helpers.root("frontend/service-worker", "service-worker")]
|
||||
serviceWorker: [helpers.root("src/service-worker", "service-worker")]
|
||||
},
|
||||
optimization: {
|
||||
minimizer: []
|
||||
@@ -19,7 +19,7 @@ const ServiceWorkerConfig = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: "babel-loader",
|
||||
include: [helpers.root("frontend/service-worker", "service-worker")]
|
||||
include: [helpers.root("src/service-worker", "service-worker")]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -31,10 +31,11 @@ const ServiceWorkerConfig = {
|
||||
//filename: "js/[name].bundle.js"
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
test: /\.js(\?.*)?$/i,
|
||||
new UglifyJSPlugin({
|
||||
cache: true,
|
||||
parallel: false,
|
||||
sourceMap: false
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
28
config/vinlottis.config.js
Normal file
28
config/vinlottis.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
"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,16 +11,10 @@ const webpackConfig = function(isDev) {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue"],
|
||||
alias: {
|
||||
vue$: "vue/dist/vue.min.js",
|
||||
"@": helpers.root("frontend")
|
||||
vue$: isDev ? "vue/dist/vue.min.js" : "vue/dist/vue.min.js",
|
||||
"@": helpers.root("src")
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
vinlottis: helpers.root("frontend", "vinlottis-init")
|
||||
},
|
||||
externals: {
|
||||
moment: 'moment' // comes with chart.js
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -39,31 +33,35 @@ const webpackConfig = function(isDev) {
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: [ "babel-loader" ],
|
||||
include: [helpers.root("frontend")]
|
||||
loader: "babel-loader",
|
||||
include: [helpers.root("src")]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
MiniCSSExtractPlugin.loader,
|
||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
MiniCSSExtractPlugin.loader,
|
||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
||||
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
use: [
|
||||
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
|
||||
{ loader: "css-loader", options: { sourceMap: isDev } },
|
||||
{ loader: "sass-loader", options: { sourceMap: isDev } }
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.woff(2)?(\?[a-z0-9]+)?$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 10000,
|
||||
mimetype: "application/font-woff"
|
||||
}
|
||||
loader: "url-loader?limit=10000&mimetype=application/font-woff"
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/,
|
||||
@@ -74,16 +72,14 @@ const webpackConfig = function(isDev) {
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__ENV__: JSON.stringify(process.env.NODE_ENV),
|
||||
__NAME__: JSON.stringify(env.name),
|
||||
__PHONE__: JSON.stringify(env.phone),
|
||||
__PRICE__: env.price,
|
||||
__MESSAGE__: JSON.stringify(env.message),
|
||||
__DATE__: env.date,
|
||||
__HOURS__: env.hours,
|
||||
__PUSHENABLED__: JSON.stringify(require("./defaults/push") != false),
|
||||
__GA_TRACKINGID__: JSON.stringify(env.googleanalytics_trackingId),
|
||||
__GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime
|
||||
__APIURL__: JSON.stringify(env.apiUrl),
|
||||
__PUSHENABLED__: JSON.stringify(require("./defaults/push") != false)
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,63 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
const merge = require("webpack-merge");
|
||||
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const HtmlPlugin = require("html-webpack-plugin");
|
||||
const helpers = require("./helpers");
|
||||
const commonConfig = require("./webpack.config.common");
|
||||
const environment = require("./env/dev.env");
|
||||
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
let webpackConfig = merge(commonConfig(true), {
|
||||
mode: "development",
|
||||
devtool: "eval-cheap-module-source-map",
|
||||
devtool: "cheap-module-eval-source-map",
|
||||
output: {
|
||||
path: helpers.root("dist"),
|
||||
publicPath: "/",
|
||||
filename: "js/[name].bundle.js"
|
||||
filename: "js/[name].bundle.js",
|
||||
chunkFilename: "js/[id].chunk.js"
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: true,
|
||||
runtimeChunk: "single",
|
||||
splitChunks: {
|
||||
chunks: "initial"
|
||||
chunks: "all"
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin(environment),
|
||||
new FriendlyErrorsPlugin(),
|
||||
new MiniCSSExtractPlugin({
|
||||
filename: "css/[name].css"
|
||||
})
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new FriendlyErrorsPlugin()
|
||||
],
|
||||
devServer: {
|
||||
compress: true,
|
||||
historyApiFallback: true,
|
||||
host: "0.0.0.0",
|
||||
hot: true,
|
||||
overlay: true,
|
||||
stats: {
|
||||
normal: true
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:30030",
|
||||
changeOrigin: true
|
||||
},
|
||||
"/socket.io": {
|
||||
target: "ws://localhost:30030",
|
||||
changeOrigin: false,
|
||||
ws: true
|
||||
}
|
||||
},
|
||||
writeToDisk: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
webpackConfig = merge(webpackConfig, {
|
||||
entry: {
|
||||
main: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: "frontend/templates/Index.html"
|
||||
new HtmlPlugin({
|
||||
template: "src/templates/Create.html",
|
||||
chunksSortMode: "dependency"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -3,15 +3,12 @@
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const merge = require("webpack-merge");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
|
||||
const helpers = require("./helpers");
|
||||
const commonConfig = require("./webpack.config.common");
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const environment = isProd
|
||||
? require("./env/prod.env")
|
||||
@@ -19,11 +16,11 @@ const environment = isProd
|
||||
|
||||
const webpackConfig = merge(commonConfig(false), {
|
||||
mode: "production",
|
||||
stats: { children: false },
|
||||
output: {
|
||||
path: helpers.root("public/dist"),
|
||||
publicPath: "/public/dist/",
|
||||
filename: "js/[name].bundle.[fullhash:7].js"
|
||||
publicPath: "/dist/",
|
||||
filename: "js/[name].bundle.[hash:7].js"
|
||||
//filename: "js/[name].bundle.js"
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
@@ -36,47 +33,37 @@ const webpackConfig = merge(commonConfig(false), {
|
||||
}
|
||||
}
|
||||
},
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new HtmlWebpackPlugin({
|
||||
chunks: ["vinlottis"],
|
||||
filename: "index.html",
|
||||
template: "./frontend/templates/Index.html",
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: false,
|
||||
preserveLineBreaks: true,
|
||||
removeAttributeQuotes: true
|
||||
}
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ["default", { discardComments: { removeAll: true } }]
|
||||
}
|
||||
}),
|
||||
new TerserPlugin({
|
||||
test: /\.js(\?.*)?$/i,
|
||||
new UglifyJSPlugin({
|
||||
cache: true,
|
||||
parallel: false,
|
||||
sourceMap: !isProd
|
||||
})
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(), // clean output folder
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.EnvironmentPlugin(environment),
|
||||
new MiniCSSExtractPlugin({
|
||||
filename: "css/[name].[fullhash:7].css"
|
||||
filename: "css/[name].[hash:7].css"
|
||||
//filename: "css/[name].css"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
if (!isProd) {
|
||||
webpackConfig.devtool = "source-map";
|
||||
}
|
||||
|
||||
if (process.env.BUILD_REPORT) {
|
||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||
.BundleAnalyzerPlugin;
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
|
||||
if (process.env.npm_config_report) {
|
||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
|
||||
.BundleAnalyzerPlugin;
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = webpackConfig;
|
||||
|
||||
375
frontend/api.js
375
frontend/api.js
@@ -1,375 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
<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>
|
||||
@@ -1,64 +0,0 @@
|
||||
<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>
|
||||
@@ -1,130 +0,0 @@
|
||||
<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>
|
||||
@@ -1,177 +0,0 @@
|
||||
<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>
|
||||
@@ -1,44 +0,0 @@
|
||||
<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>
|
||||
@@ -1,273 +0,0 @@
|
||||
<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>
|
||||
@@ -1,237 +0,0 @@
|
||||
<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,382 +0,0 @@
|
||||
<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>
|
||||
@@ -1,380 +0,0 @@
|
||||
<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>
|
||||
@@ -1,100 +0,0 @@
|
||||
<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>
|
||||
@@ -1,2 +0,0 @@
|
||||
import Vue from "vue";
|
||||
export default new Vue();
|
||||
@@ -1,132 +0,0 @@
|
||||
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 };
|
||||
@@ -1,241 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
.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,122 +0,0 @@
|
||||
@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";
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
<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>
|
||||
@@ -1,97 +0,0 @@
|
||||
<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>
|
||||
@@ -1,120 +0,0 @@
|
||||
<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>
|
||||
@@ -1,101 +0,0 @@
|
||||
<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>
|
||||
@@ -1,124 +0,0 @@
|
||||
<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>
|
||||
@@ -1,81 +0,0 @@
|
||||
<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>
|
||||
@@ -1,138 +0,0 @@
|
||||
<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>
|
||||
@@ -1,166 +0,0 @@
|
||||
<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,100 +0,0 @@
|
||||
<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>
|
||||
@@ -1,27 +0,0 @@
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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)
|
||||
});
|
||||
@@ -1,10 +1,5 @@
|
||||
const mustBeAuthenticated = (req, res, next) => {
|
||||
if (process.env.NODE_ENV == "development") {
|
||||
console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
|
||||
req.isAuthenticated = () => true;
|
||||
return next();
|
||||
}
|
||||
|
||||
console.log(req.isAuthenticated());
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
5065
package-lock.json
generated
5065
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@@ -4,64 +4,76 @@
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --progress",
|
||||
"build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress",
|
||||
"dev": "yarn webpack serve --mode development --env development",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js",
|
||||
"start-noauth": "cross-env NODE_ENV=development node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --progress",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^5.28.0",
|
||||
"@sentry/integrations": "^5.28.0",
|
||||
"@zxing/library": "^0.18.3",
|
||||
"canvas-confetti": "^1.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"@babel/polyfill": "~7.2",
|
||||
"@zxing/library": "^0.15.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"connect-mongo": "^3.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"feature-policy": "^0.4.0",
|
||||
"helmet": "^3.21.2",
|
||||
"moment": "^2.24.0",
|
||||
"mongoose": "^5.11.4",
|
||||
"mongoose": "^5.8.7",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-sass": "^5.0.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"node-schedule": "^1.3.2",
|
||||
"passport": "^0.4.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-local-mongoose": "^6.0.1",
|
||||
"qrcode": "^1.4.4",
|
||||
"socket.io": "^3.0.3",
|
||||
"socket.io-client": "^3.0.3",
|
||||
"referrer-policy": "^1.2.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"vue": "~2.6",
|
||||
"vue-router": "~3.4.9",
|
||||
"vuex": "^3.6.0",
|
||||
"vue-analytics": "^5.22.1",
|
||||
"vue-router": "~3.0",
|
||||
"vuex": "^3.1.1",
|
||||
"web-push": "^3.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.12",
|
||||
"@babel/preset-env": "~7.12",
|
||||
"babel-loader": "~8.2.2",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"core-js": "3.8.1",
|
||||
"css-loader": "^5.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"@babel/core": "~7.2",
|
||||
"@babel/plugin-proposal-class-properties": "~7.3",
|
||||
"@babel/plugin-proposal-decorators": "~7.3",
|
||||
"@babel/plugin-proposal-json-strings": "~7.2",
|
||||
"@babel/plugin-syntax-dynamic-import": "~7.2",
|
||||
"@babel/plugin-syntax-import-meta": "~7.2",
|
||||
"@babel/preset-env": "~7.3",
|
||||
"babel-loader": "~8.0",
|
||||
"compression-webpack-plugin": "^3.1.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^3.2.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"friendly-errors-webpack-plugin": "~1.7",
|
||||
"google-maps-api-loader": "^1.1.1",
|
||||
"html-webpack-plugin": "5.0.0-alpha.15",
|
||||
"mini-css-extract-plugin": "~1.3.2",
|
||||
"optimize-css-assets-webpack-plugin": "~5.0.4",
|
||||
"html-webpack-plugin": "~3.2",
|
||||
"mini-css-extract-plugin": "~0.5",
|
||||
"optimize-css-assets-webpack-plugin": "~3.2",
|
||||
"pm2": "^4.2.3",
|
||||
"redis": "^3.0.2",
|
||||
"sass-loader": "~10.1.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "~15.9.5",
|
||||
"sass-loader": "~7.1",
|
||||
"uglifyjs-webpack-plugin": "~1.2",
|
||||
"url-loader": "^2.2.0",
|
||||
"vue-loader": "~15.6",
|
||||
"vue-style-loader": "~4.1",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "~5.10.0",
|
||||
"webpack-bundle-analyzer": "^4.2.0",
|
||||
"webpack-cli": "~4.2.0",
|
||||
"webpack-dev-server": "~3.11",
|
||||
"webpack-merge": "~5.4"
|
||||
"vue-template-compiler": "~2.6",
|
||||
"webpack": "~4.41.5",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "~3.2",
|
||||
"webpack-dev-server": "~3.1",
|
||||
"webpack-hot-middleware": "~2.24",
|
||||
"webpack-merge": "~4.2"
|
||||
}
|
||||
}
|
||||
|
||||
12
pm2.json
Normal file
12
pm2.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"name": "vinlottis",
|
||||
"script": "./server.js",
|
||||
"watch": true,
|
||||
"instances": "max",
|
||||
"exec_mode": "cluster",
|
||||
"ignore_watch": ["./node_modules", "./public/assets/"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// https://www.google-analytics.com/analytics.js - 24.11.2020
|
||||
(function(){/*
|
||||
|
||||
Copyright The Closure Library Authors.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
var l=this||self,m=function(a,b){a=a.split(".");var c=l;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c=c[d]&&c[d]!==Object.prototype[d]?c[d]:c[d]={}:c[d]=b};var q=function(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c])},r=function(a){for(var b in a)if(a.hasOwnProperty(b))return!0;return!1};var t=/^(?:(?:https?|mailto|ftp):|[^:/?#]*(?:[/?#]|$))/i;var u=window,v=document,w=function(a,b){v.addEventListener?v.addEventListener(a,b,!1):v.attachEvent&&v.attachEvent("on"+a,b)};var x={},y=function(){x.TAGGING=x.TAGGING||[];x.TAGGING[1]=!0};var z=/:[0-9]+$/,A=function(a,b,c){a=a.split("&");for(var d=0;d<a.length;d++){var e=a[d].split("=");if(decodeURIComponent(e[0]).replace(/\+/g," ")===b)return b=e.slice(1).join("="),c?b:decodeURIComponent(b).replace(/\+/g," ")}},D=function(a,b){b&&(b=String(b).toLowerCase());if("protocol"===b||"port"===b)a.protocol=B(a.protocol)||B(u.location.protocol);"port"===b?a.port=String(Number(a.hostname?a.port:u.location.port)||("http"==a.protocol?80:"https"==a.protocol?443:"")):"host"===b&&(a.hostname=(a.hostname||
|
||||
u.location.hostname).replace(z,"").toLowerCase());return C(a,b,void 0,void 0,void 0)},C=function(a,b,c,d,e){var f=B(a.protocol);b&&(b=String(b).toLowerCase());switch(b){case "url_no_fragment":d="";a&&a.href&&(d=a.href.indexOf("#"),d=0>d?a.href:a.href.substr(0,d));a=d;break;case "protocol":a=f;break;case "host":a=a.hostname.replace(z,"").toLowerCase();c&&(d=/^www\d*\./.exec(a))&&d[0]&&(a=a.substr(d[0].length));break;case "port":a=String(Number(a.port)||("http"==f?80:"https"==f?443:""));break;case "path":a.pathname||
|
||||
a.hostname||y();a="/"==a.pathname.substr(0,1)?a.pathname:"/"+a.pathname;a=a.split("/");a:if(d=d||[],c=a[a.length-1],Array.prototype.indexOf)d=d.indexOf(c),d="number"==typeof d?d:-1;else{for(e=0;e<d.length;e++)if(d[e]===c){d=e;break a}d=-1}0<=d&&(a[a.length-1]="");a=a.join("/");break;case "query":a=a.search.replace("?","");e&&(a=A(a,e,void 0));break;case "extension":a=a.pathname.split(".");a=1<a.length?a[a.length-1]:"";a=a.split("/")[0];break;case "fragment":a=a.hash.replace("#","");break;default:a=
|
||||
a&&a.href}return a},B=function(a){return a?a.replace(":","").toLowerCase():""},E=function(a){var b=v.createElement("a");a&&(b.href=a);var c=b.pathname;"/"!==c[0]&&(a||y(),c="/"+c);a=b.hostname.replace(z,"");return{href:b.href,protocol:b.protocol,host:b.host,hostname:a,pathname:c,search:b.search,hash:b.hash,port:b.port}};function F(){for(var a=G,b={},c=0;c<a.length;++c)b[a[c]]=c;return b}function H(){var a="ABCDEFGHIJKLMNOPQRSTUVWXYZ";a+=a.toLowerCase()+"0123456789-_";return a+"."}var G,I;function J(a){G=G||H();I=I||F();for(var b=[],c=0;c<a.length;c+=3){var d=c+1<a.length,e=c+2<a.length,f=a.charCodeAt(c),g=d?a.charCodeAt(c+1):0,h=e?a.charCodeAt(c+2):0,k=f>>2;f=(f&3)<<4|g>>4;g=(g&15)<<2|h>>6;h&=63;e||(h=64,d||(g=64));b.push(G[k],G[f],G[g],G[h])}return b.join("")}
|
||||
function K(a){function b(k){for(;d<a.length;){var n=a.charAt(d++),p=I[n];if(null!=p)return p;if(!/^[\s\xa0]*$/.test(n))throw Error("Unknown base64 encoding at char: "+n);}return k}G=G||H();I=I||F();for(var c="",d=0;;){var e=b(-1),f=b(0),g=b(64),h=b(64);if(64===h&&-1===e)return c;c+=String.fromCharCode(e<<2|f>>4);64!=g&&(c+=String.fromCharCode(f<<4&240|g>>2),64!=h&&(c+=String.fromCharCode(g<<6&192|h)))}};var L;var N=function(){var a=aa,b=ba,c=M(),d=function(g){a(g.target||g.srcElement||{})},e=function(g){b(g.target||g.srcElement||{})};if(!c.init){w("mousedown",d);w("keyup",d);w("submit",e);var f=HTMLFormElement.prototype.submit;HTMLFormElement.prototype.submit=function(){b(this);f.call(this)};c.init=!0}},O=function(a,b,c,d,e){a={callback:a,domains:b,fragment:2===c,placement:c,forms:d,sameHost:e};M().decorators.push(a)},P=function(a,b,c){for(var d=M().decorators,e={},f=0;f<d.length;++f){var g=d[f],h;if(h=
|
||||
!c||g.forms)a:{h=g.domains;var k=a,n=!!g.sameHost;if(h&&(n||k!==v.location.hostname))for(var p=0;p<h.length;p++)if(h[p]instanceof RegExp){if(h[p].test(k)){h=!0;break a}}else if(0<=k.indexOf(h[p])||n&&0<=h[p].indexOf(k)){h=!0;break a}h=!1}h&&(h=g.placement,void 0==h&&(h=g.fragment?2:1),h===b&&q(e,g.callback()))}return e},M=function(){var a={};var b=u.google_tag_data;u.google_tag_data=void 0===b?a:b;a=u.google_tag_data;b=a.gl;b&&b.decorators||(b={decorators:[]},a.gl=b);return b};var ca=/(.*?)\*(.*?)\*(.*)/,da=/([^?#]+)(\?[^#]*)?(#.*)?/;function Q(a){return new RegExp("(.*?)(^|&)"+a+"=([^&]*)&?(.*)")}
|
||||
var S=function(a){var b=[],c;for(c in a)if(a.hasOwnProperty(c)){var d=a[c];void 0!==d&&d===d&&null!==d&&"[object Object]"!==d.toString()&&(b.push(c),b.push(J(String(d))))}a=b.join("*");return["1",R(a),a].join("*")},R=function(a,b){a=[window.navigator.userAgent,(new Date).getTimezoneOffset(),window.navigator.userLanguage||window.navigator.language,Math.floor((new Date).getTime()/60/1E3)-(void 0===b?0:b),a].join("*");if(!(b=L)){b=Array(256);for(var c=0;256>c;c++){for(var d=c,e=0;8>e;e++)d=d&1?d>>>1^
|
||||
3988292384:d>>>1;b[c]=d}}L=b;b=4294967295;for(c=0;c<a.length;c++)b=b>>>8^L[(b^a.charCodeAt(c))&255];return((b^-1)>>>0).toString(36)},fa=function(a){return function(b){var c=E(u.location.href),d=c.search.replace("?","");var e=A(d,"_gl",!0);b.query=T(e||"")||{};e=D(c,"fragment");var f=e.match(Q("_gl"));b.fragment=T(f&&f[3]||"")||{};a&&ea(c,d,e)}};function U(a,b){if(a=Q(a).exec(b)){var c=a[2],d=a[4];b=a[1];d&&(b=b+c+d)}return b}
|
||||
var ea=function(a,b,c){function d(f,g){f=U("_gl",f);f.length&&(f=g+f);return f}if(u.history&&u.history.replaceState){var e=Q("_gl");if(e.test(b)||e.test(c))a=D(a,"path"),b=d(b,"?"),c=d(c,"#"),u.history.replaceState({},void 0,""+a+b+c)}},T=function(a){var b=void 0===b?3:b;try{if(a){a:{for(var c=0;3>c;++c){var d=ca.exec(a);if(d){var e=d;break a}a=decodeURIComponent(a)}e=void 0}if(e&&"1"===e[1]){var f=e[2],g=e[3];a:{for(e=0;e<b;++e)if(f===R(g,e)){var h=!0;break a}h=!1}if(h){b={};var k=g?g.split("*"):
|
||||
[];for(g=0;g<k.length;g+=2)b[k[g]]=K(k[g+1]);return b}}}}catch(n){}};function V(a,b,c,d){function e(k){k=U(a,k);var n=k.charAt(k.length-1);k&&"&"!==n&&(k+="&");return k+h}d=void 0===d?!1:d;var f=da.exec(c);if(!f)return"";c=f[1];var g=f[2]||"";f=f[3]||"";var h=a+"="+b;d?f="#"+e(f.substring(1)):g="?"+e(g.substring(1));return""+c+g+f}
|
||||
function W(a,b){var c="FORM"===(a.tagName||"").toUpperCase(),d=P(b,1,c),e=P(b,2,c);b=P(b,3,c);r(d)&&(d=S(d),c?X("_gl",d,a):Y("_gl",d,a,!1));!c&&r(e)&&(c=S(e),Y("_gl",c,a,!0));for(var f in b)b.hasOwnProperty(f)&&Z(f,b[f],a)}function Z(a,b,c,d){if(c.tagName){if("a"===c.tagName.toLowerCase())return Y(a,b,c,d);if("form"===c.tagName.toLowerCase())return X(a,b,c)}if("string"==typeof c)return V(a,b,c,d)}function Y(a,b,c,d){c.href&&(a=V(a,b,c.href,void 0===d?!1:d),t.test(a)&&(c.href=a))}
|
||||
function X(a,b,c){if(c&&c.action){var d=(c.method||"").toLowerCase();if("get"===d){d=c.childNodes||[];for(var e=!1,f=0;f<d.length;f++){var g=d[f];if(g.name===a){g.setAttribute("value",b);e=!0;break}}e||(d=v.createElement("input"),d.setAttribute("type","hidden"),d.setAttribute("name",a),d.setAttribute("value",b),c.appendChild(d))}else"post"===d&&(a=V(a,b,c.action),t.test(a)&&(c.action=a))}}
|
||||
var aa=function(a){try{a:{for(var b=100;a&&0<b;){if(a.href&&a.nodeName.match(/^a(?:rea)?$/i)){var c=a;break a}a=a.parentNode;b--}c=null}if(c){var d=c.protocol;"http:"!==d&&"https:"!==d||W(c,c.hostname)}}catch(e){}},ba=function(a){try{if(a.action){var b=D(E(a.action),"host");W(a,b)}}catch(c){}};m("google_tag_data.glBridge.auto",function(a,b,c,d){N();O(a,b,"fragment"===c?2:1,!!d,!1)});m("google_tag_data.glBridge.passthrough",function(a,b,c){N();O(a,[C(u.location,"host",!0)],b,!!c,!0)});m("google_tag_data.glBridge.decorate",function(a,b,c){a=S(a);return Z("_gl",a,b,!!c)});m("google_tag_data.glBridge.generate",S);m("google_tag_data.glBridge.get",function(a,b){var c=fa(!!b);b=M();b.data||(b.data={query:{},fragment:{}},c(b.data));c={};if(b=b.data)q(c,b.query),a&&q(c,b.fragment);return c});})(window);
|
||||
(function(){function La(a){var b=1,c;if(a)for(b=0,c=a.length-1;0<=c;c--){var d=a.charCodeAt(c);b=(b<<6&268435455)+d+(d<<14);d=b&266338304;b=0!=d?b^d>>21:b}return b};/*
|
||||
|
||||
Copyright The Closure Library Authors.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
var $c=function(a){this.C=a||[]};$c.prototype.set=function(a){this.C[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0;b<this.C.length;b++)this.C[b]&&(a[Math.floor(b/6)]^=1<<b%6);for(b=0;b<a.length;b++)a[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(a[b]||0);return a.join("")+"~"};var ha=window.GoogleAnalyticsObject,wa;if(wa=void 0!=ha)wa=-1<(ha.constructor+"").indexOf("String");var ne;if(ne=wa){var Ee=window.GoogleAnalyticsObject;ne=Ee?Ee.replace(/^[\s\xa0]+|[\s\xa0]+$/g,""):""}var gb=ne||"ga",jd=/^(?:utma\.)?\d+\.\d+$/,kd=/^amp-[\w.-]{22,64}$/,Ba=!1;var vd=new $c;function J(a){vd.set(a)}var Td=function(a){a=Dd(a);a=new $c(a);for(var b=vd.C.slice(),c=0;c<a.C.length;c++)b[c]=b[c]||a.C[c];return(new $c(b)).encode()},Dd=function(a){a=a.get(Gd);ka(a)||(a=[]);return a};var ea=function(a){return"function"==typeof a},ka=function(a){return"[object Array]"==Object.prototype.toString.call(Object(a))},qa=function(a){return void 0!=a&&-1<(a.constructor+"").indexOf("String")},D=function(a,b){return 0==a.indexOf(b)},sa=function(a){return a?a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,""):""},ra=function(){for(var a=O.navigator.userAgent+(M.cookie?M.cookie:"")+(M.referrer?M.referrer:""),b=a.length,c=O.history.length;0<c;)a+=c--^b++;return[hd()^La(a)&2147483647,Math.round((new Date).getTime()/
|
||||
1E3)].join(".")},ta=function(a){var b=M.createElement("img");b.width=1;b.height=1;b.src=a;return b},ua=function(){},K=function(a){if(encodeURIComponent instanceof Function)return encodeURIComponent(a);J(28);return a},L=function(a,b,c,d){try{a.addEventListener?a.addEventListener(b,c,!!d):a.attachEvent&&a.attachEvent("on"+b,c)}catch(e){J(27)}},f=/^[\w\-:/.?=&%!\[\]]+$/,Nd=/^[\w+/_-]+[=]{0,2}$/,Id=function(a,b,c){if(a){var d=M.querySelector&&M.querySelector("script[nonce]")||null;d=d?d.nonce||d.getAttribute&&
|
||||
d.getAttribute("nonce")||"":"";if(c){var e=c="";b&&f.test(b)&&(c=' id="'+b+'"');d&&Nd.test(d)&&(e=' nonce="'+d+'"');f.test(a)&&M.write("<script"+c+e+' src="'+a+'">\x3c/script>')}else c=M.createElement("script"),c.type="text/javascript",c.async=!0,c.src=a,b&&(c.id=b),d&&c.setAttribute("nonce",d),a=M.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)}},be=function(a,b){return E(M.location[b?"href":"search"],a)},E=function(a,b){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,
|
||||
"\\$1")+"=([^&#]*)"))&&2==a.length?a[1]:""},xa=function(){var a=""+M.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},de=function(a,b){var c=a.indexOf(b);if(5==c||6==c)if(a=a.charAt(c+b.length),"/"==a||"?"==a||""==a||":"==a)return!0;return!1},ya=function(a,b){var c=M.referrer;if(/^(https?|android-app):\/\//i.test(c)){if(a)return c;a="//"+M.location.hostname;if(!de(c,a))return b&&(b=a.replace(/\./g,"-")+".cdn.ampproject.org",de(c,b))?void 0:c}},za=function(a,b){if(1==b.length&&null!=
|
||||
b[0]&&"object"===typeof b[0])return b[0];for(var c={},d=Math.min(a.length+1,b.length),e=0;e<d;e++)if("object"===typeof b[e]){for(var g in b[e])b[e].hasOwnProperty(g)&&(c[g]=b[e][g]);break}else e<a.length&&(c[a[e]]=b[e]);return c};var ee=function(){this.b=[];this.ea={};this.m={}};ee.prototype.set=function(a,b,c){this.b.push(a);c?this.m[":"+a]=b:this.ea[":"+a]=b};ee.prototype.get=function(a){return this.m.hasOwnProperty(":"+a)?this.m[":"+a]:this.ea[":"+a]};ee.prototype.map=function(a){for(var b=0;b<this.b.length;b++){var c=this.b[b],d=this.get(c);d&&a(c,d)}};var O=window,M=document,va=function(a,b){return setTimeout(a,b)};var Qa=window,Za=document,G=function(a){var b=Qa._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===Qa["ga-disable-"+a])return!0;try{var c=Qa.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(g){}a=[];b=String(Za.cookie).split(";");for(c=0;c<b.length;c++){var d=b[c].split("="),e=d[0].replace(/^\s*|\s*$/g,"");e&&"AMP_TOKEN"==e&&((d=d.slice(1).join("=").replace(/^\s*|\s*$/g,""))&&(d=decodeURIComponent(d)),a.push(d))}for(b=0;b<a.length;b++)if("$OPT_OUT"==a[b])return!0;return Za.getElementById("__gaOptOutExtension")?
|
||||
!0:!1};var Ca=function(a){var b=[],c=M.cookie.split(";");a=new RegExp("^\\s*"+a+"=\\s*(.*?)\\s*$");for(var d=0;d<c.length;d++){var e=c[d].match(a);e&&b.push(e[1])}return b},zc=function(a,b,c,d,e,g,ca){e=G(e)?!1:eb.test(M.location.hostname)||"/"==c&&vc.test(d)?!1:!0;if(!e)return!1;b&&1200<b.length&&(b=b.substring(0,1200));c=a+"="+b+"; path="+c+"; ";g&&(c+="expires="+(new Date((new Date).getTime()+g)).toGMTString()+"; ");d&&"none"!==d&&(c+="domain="+d+";");ca&&(c+=ca+";");d=M.cookie;M.cookie=c;if(!(d=d!=M.cookie))a:{a=
|
||||
Ca(a);for(d=0;d<a.length;d++)if(b==a[d]){d=!0;break a}d=!1}return d},Cc=function(a){return encodeURIComponent?encodeURIComponent(a).replace(/\(/g,"%28").replace(/\)/g,"%29"):a},vc=/^(www\.)?google(\.com?)?(\.[a-z]{2})?$/,eb=/(^|\.)doubleclick\.net$/i;var Fa,Ga,fb,Ab,ja=/^https?:\/\/[^/]*cdn\.ampproject\.org\//,Ue=/^(?:www\.|m\.|amp\.)+/,Ub=[],da=function(a){if(ye(a[Kd])){if(void 0===Ab){var b;if(b=(b=De.get())&&b._ga||void 0)Ab=b,J(81)}if(void 0!==Ab)return a[Q]||(a[Q]=Ab),!1}if(a[Kd]){J(67);if(a[ac]&&"cookie"!=a[ac])return!1;if(void 0!==Ab)a[Q]||(a[Q]=Ab);else{a:{b=String(a[W]||xa());var c=String(a[Yb]||"/"),d=Ca(String(a[U]||"_ga"));b=na(d,b,c);if(!b||jd.test(b))b=!0;else if(b=Ca("AMP_TOKEN"),0==b.length)b=!0;else{if(1==b.length&&(b=decodeURIComponent(b[0]),
|
||||
"$RETRIEVING"==b||"$OPT_OUT"==b||"$ERROR"==b||"$NOT_FOUND"==b)){b=!0;break a}b=!1}}if(b&&tc(ic,String(a[Na])))return!0}}return!1},ic=function(){Z.D([ua])},tc=function(a,b){var c=Ca("AMP_TOKEN");if(1<c.length)return J(55),!1;c=decodeURIComponent(c[0]||"");if("$OPT_OUT"==c||"$ERROR"==c||G(b))return J(62),!1;if(!ja.test(M.referrer)&&"$NOT_FOUND"==c)return J(68),!1;if(void 0!==Ab)return J(56),va(function(){a(Ab)},0),!0;if(Fa)return Ub.push(a),!0;if("$RETRIEVING"==c)return J(57),va(function(){tc(a,b)},
|
||||
1E4),!0;Fa=!0;c&&"$"!=c[0]||(xc("$RETRIEVING",3E4),setTimeout(Mc,3E4),c="");return Pc(c,b)?(Ub.push(a),!0):!1},Pc=function(a,b,c){if(!window.JSON)return J(58),!1;var d=O.XMLHttpRequest;if(!d)return J(59),!1;var e=new d;if(!("withCredentials"in e))return J(60),!1;e.open("POST",(c||"https://ampcid.google.com/v1/publisher:getClientId")+"?key=AIzaSyA65lEHUEizIsNtlbNo-l2K18dT680nsaM",!0);e.withCredentials=!0;e.setRequestHeader("Content-Type","text/plain");e.onload=function(){Fa=!1;if(4==e.readyState){try{200!=
|
||||
e.status&&(J(61),Qc("","$ERROR",3E4));var g=JSON.parse(e.responseText);g.optOut?(J(63),Qc("","$OPT_OUT",31536E6)):g.clientId?Qc(g.clientId,g.securityToken,31536E6):!c&&g.alternateUrl?(Ga&&clearTimeout(Ga),Fa=!0,Pc(a,b,g.alternateUrl)):(J(64),Qc("","$NOT_FOUND",36E5))}catch(ca){J(65),Qc("","$ERROR",3E4)}e=null}};d={originScope:"AMP_ECID_GOOGLE"};a&&(d.securityToken=a);e.send(JSON.stringify(d));Ga=va(function(){J(66);Qc("","$ERROR",3E4)},1E4);return!0},Mc=function(){Fa=!1},xc=function(a,b){if(void 0===
|
||||
fb){fb="";for(var c=id(),d=0;d<c.length;d++){var e=c[d];if(zc("AMP_TOKEN",encodeURIComponent(a),"/",e,"",b)){fb=e;return}}}zc("AMP_TOKEN",encodeURIComponent(a),"/",fb,"",b)},Qc=function(a,b,c){Ga&&clearTimeout(Ga);b&&xc(b,c);Ab=a;b=Ub;Ub=[];for(c=0;c<b.length;c++)b[c](a)},ye=function(a){a:{if(ja.test(M.referrer)){var b=M.location.hostname.replace(Ue,"");b:{var c=M.referrer;c=c.replace(/^https?:\/\//,"");var d=c.replace(/^[^/]+/,"").split("/"),e=d[2];d=(d="s"==e?d[3]:e)?decodeURIComponent(d):d;if(!d){if(0==
|
||||
c.indexOf("xn--")){c="";break b}(c=c.match(/(.*)\.cdn\.ampproject\.org\/?$/))&&2==c.length&&(d=c[1].replace(/-/g,".").replace(/\.\./g,"-"))}c=d?d.replace(Ue,""):""}(d=b===c)||(c="."+c,d=b.substring(b.length-c.length,b.length)===c);if(d){b=!0;break a}else J(78)}b=!1}return b&&!1!==a};var bd=function(a){return(a?"https:":Ba||"https:"==M.location.protocol?"https:":"http:")+"//www.google-analytics.com"},Ge=function(a){switch(a){default:case 1:return"https://www.google-analytics.com/gtm/js?id=";case 2:return"https://www.googletagmanager.com/gtag/js?id="}},Da=function(a){this.name="len";this.message=a+"-8192"},ba=function(a,b,c){c=c||ua;if(2036>=b.length)wc(a,b,c);else if(8192>=b.length)x(a,b,c)||wd(a,b,c)||wc(a,b,c);else throw ge("len",b.length),new Da(b.length);},pe=function(a,b,
|
||||
c,d){d=d||ua;wd(a+"?"+b,"",d,c)},wc=function(a,b,c){var d=ta(a+"?"+b);d.onload=d.onerror=function(){d.onload=null;d.onerror=null;c()}},wd=function(a,b,c,d){var e=O.XMLHttpRequest;if(!e)return!1;var g=new e;if(!("withCredentials"in g))return!1;a=a.replace(/^http:/,"https:");g.open("POST",a,!0);g.withCredentials=!0;g.setRequestHeader("Content-Type","text/plain");g.onreadystatechange=function(){if(4==g.readyState){if(d&&"text/plain"===g.getResponseHeader("Content-Type"))try{Ea(d,g.responseText,c)}catch(ca){ge("xhr",
|
||||
"rsp"),c()}else c();g=null}};g.send(b);return!0},Ea=function(a,b,c){if(1>b.length)ge("xhr","ver","0"),c();else if(3<a.count++)ge("xhr","tmr",""+a.count),c();else{var d=b.charAt(0);if("1"===d)oc(a,b.substring(1),c);else if(a.V&&"2"===d){var e=b.substring(1).split(","),g=0;b=function(){++g===e.length&&c()};for(d=0;d<e.length;d++)oc(a,e[d],b)}else ge("xhr","ver",String(b.length)),c()}},oc=function(a,b,c){if(0===b.length)c();else{var d=b.charAt(0);switch(d){case "d":pe("https://stats.g.doubleclick.net/j/collect",
|
||||
a.U,a,c);break;case "g":wc("https://www.google.%/ads/ga-audiences".replace("%","com"),a.google,c);(b=b.substring(1))&&(/^[a-z.]{1,6}$/.test(b)?wc("https://www.google.%/ads/ga-audiences".replace("%",b),a.google,ua):ge("tld","bcc",b));break;case "G":if(a.V){a.V("G-"+b.substring(1));c();break}case "x":if(a.V){a.V();c();break}default:ge("xhr","brc",d),c()}}},x=function(a,b,c){return O.navigator.sendBeacon?O.navigator.sendBeacon(a,b)?(c(),!0):!1:!1},ge=function(a,b,c){1<=100*Math.random()||G("?")||(a=
|
||||
["t=error","_e="+a,"_v=j87","sr=1"],b&&a.push("_f="+b),c&&a.push("_m="+K(c.substring(0,100))),a.push("aip=1"),a.push("z="+hd()),wc(bd(!0)+"/u/d",a.join("&"),ua))};var qc=function(){return O.gaData=O.gaData||{}},h=function(a){var b=qc();return b[a]=b[a]||{}};var Ha=function(){this.M=[]};Ha.prototype.add=function(a){this.M.push(a)};Ha.prototype.D=function(a){try{for(var b=0;b<this.M.length;b++){var c=a.get(this.M[b]);c&&ea(c)&&c.call(O,a)}}catch(d){}b=a.get(Ia);b!=ua&&ea(b)&&(a.set(Ia,ua,!0),setTimeout(b,10))};function Ja(a){if(100!=a.get(Ka)&&La(P(a,Q))%1E4>=100*R(a,Ka))throw"abort";}function Ma(a){if(G(P(a,Na)))throw"abort";}function Oa(){var a=M.location.protocol;if("http:"!=a&&"https:"!=a)throw"abort";}
|
||||
function Pa(a){try{O.navigator.sendBeacon?J(42):O.XMLHttpRequest&&"withCredentials"in new O.XMLHttpRequest&&J(40)}catch(c){}a.set(ld,Td(a),!0);a.set(Ac,R(a,Ac)+1);var b=[];ue.map(function(c,d){d.F&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.F+"="+K(""+c))))});!1===a.get(xe)&&b.push("npa=1");b.push("z="+Bd());a.set(Ra,b.join("&"),!0)}
|
||||
function Sa(a){var b=P(a,fa);!b&&a.get(Vd)&&(b="beacon");var c=P(a,gd),d=P(a,oe),e=c||(d||bd(!1)+"")+"/collect";switch(P(a,ad)){case "d":e=c||(d||bd(!1)+"")+"/j/collect";b=a.get(qe)||void 0;pe(e,P(a,Ra),b,a.Z(Ia));break;default:b?(c=P(a,Ra),d=(d=a.Z(Ia))||ua,"image"==b?wc(e,c,d):"xhr"==b&&wd(e,c,d)||"beacon"==b&&x(e,c,d)||ba(e,c,d)):ba(e,P(a,Ra),a.Z(Ia))}e=P(a,Na);e=h(e);b=e.hitcount;e.hitcount=b?b+1:1;e.first_hit||(e.first_hit=(new Date).getTime());e=P(a,Na);delete h(e).pending_experiments;a.set(Ia,
|
||||
ua,!0)}function Hc(a){qc().expId&&a.set(Nc,qc().expId);qc().expVar&&a.set(Oc,qc().expVar);var b=P(a,Na);if(b=h(b).pending_experiments){var c=[];for(d in b)b.hasOwnProperty(d)&&b[d]&&c.push(encodeURIComponent(d)+"."+encodeURIComponent(b[d]));var d=c.join("!")}else d=void 0;d&&((b=a.get(m))&&(d=b+"!"+d),a.set(m,d,!0))}function cd(){if(O.navigator&&"preview"==O.navigator.loadPurpose)throw"abort";}
|
||||
function yd(a){var b=O.gaDevIds||[];if(ka(b)){var c=a.get("&did");qa(c)&&0<c.length&&(b=b.concat(c.split(",")));c=[];for(var d=0;d<b.length;d++){var e;a:{for(e=0;e<c.length;e++)if(b[d]==c[e]){e=!0;break a}e=!1}e||c.push(b[d])}0!=c.length&&a.set("&did",c.join(","),!0)}}function vb(a){if(!a.get(Na))throw"abort";};var hd=function(){return Math.round(2147483647*Math.random())},Bd=function(){try{var a=new Uint32Array(1);O.crypto.getRandomValues(a);return a[0]&2147483647}catch(b){return hd()}};function Ta(a){var b=R(a,Ua);500<=b&&J(15);var c=P(a,Va);if("transaction"!=c&&"item"!=c){c=R(a,Wa);var d=(new Date).getTime(),e=R(a,Xa);0==e&&a.set(Xa,d);e=Math.round(2*(d-e)/1E3);0<e&&(c=Math.min(c+e,20),a.set(Xa,d));if(0>=c)throw"abort";a.set(Wa,--c)}a.set(Ua,++b)};var Ya=function(){this.data=new ee};Ya.prototype.get=function(a){var b=$a(a),c=this.data.get(a);b&&void 0==c&&(c=ea(b.defaultValue)?b.defaultValue():b.defaultValue);return b&&b.Z?b.Z(this,a,c):c};var P=function(a,b){a=a.get(b);return void 0==a?"":""+a},R=function(a,b){a=a.get(b);return void 0==a||""===a?0:Number(a)};Ya.prototype.Z=function(a){return(a=this.get(a))&&ea(a)?a:ua};
|
||||
Ya.prototype.set=function(a,b,c){if(a)if("object"==typeof a)for(var d in a)a.hasOwnProperty(d)&&ab(this,d,a[d],c);else ab(this,a,b,c)};var ab=function(a,b,c,d){if(void 0!=c)switch(b){case Na:wb.test(c)}var e=$a(b);e&&e.o?e.o(a,b,c,d):a.data.set(b,c,d)};var ue=new ee,ve=[],bb=function(a,b,c,d,e){this.name=a;this.F=b;this.Z=d;this.o=e;this.defaultValue=c},$a=function(a){var b=ue.get(a);if(!b)for(var c=0;c<ve.length;c++){var d=ve[c],e=d[0].exec(a);if(e){b=d[1](e);ue.set(b.name,b);break}}return b},yc=function(a){var b;ue.map(function(c,d){d.F==a&&(b=d)});return b&&b.name},S=function(a,b,c,d,e){a=new bb(a,b,c,d,e);ue.set(a.name,a);return a.name},cb=function(a,b){ve.push([new RegExp("^"+a+"$"),b])},T=function(a,b,c){return S(a,b,c,void 0,db)},db=function(){};var hb=T("apiVersion","v"),ib=T("clientVersion","_v");S("anonymizeIp","aip");var jb=S("adSenseId","a"),Va=S("hitType","t"),Ia=S("hitCallback"),Ra=S("hitPayload");S("nonInteraction","ni");S("currencyCode","cu");S("dataSource","ds");var Vd=S("useBeacon",void 0,!1),fa=S("transport");S("sessionControl","sc","");S("sessionGroup","sg");S("queueTime","qt");var Ac=S("_s","_s");S("screenName","cd");var kb=S("location","dl",""),lb=S("referrer","dr"),mb=S("page","dp","");S("hostname","dh");
|
||||
var nb=S("language","ul"),ob=S("encoding","de");S("title","dt",function(){return M.title||void 0});cb("contentGroup([0-9]+)",function(a){return new bb(a[0],"cg"+a[1])});var pb=S("screenColors","sd"),qb=S("screenResolution","sr"),rb=S("viewportSize","vp"),sb=S("javaEnabled","je"),tb=S("flashVersion","fl");S("campaignId","ci");S("campaignName","cn");S("campaignSource","cs");S("campaignMedium","cm");S("campaignKeyword","ck");S("campaignContent","cc");
|
||||
var ub=S("eventCategory","ec"),xb=S("eventAction","ea"),yb=S("eventLabel","el"),zb=S("eventValue","ev"),Bb=S("socialNetwork","sn"),Cb=S("socialAction","sa"),Db=S("socialTarget","st"),Eb=S("l1","plt"),Fb=S("l2","pdt"),Gb=S("l3","dns"),Hb=S("l4","rrt"),Ib=S("l5","srt"),Jb=S("l6","tcp"),Kb=S("l7","dit"),Lb=S("l8","clt"),Ve=S("l9","_gst"),We=S("l10","_gbt"),Xe=S("l11","_cst"),Ye=S("l12","_cbt"),Mb=S("timingCategory","utc"),Nb=S("timingVar","utv"),Ob=S("timingLabel","utl"),Pb=S("timingValue","utt");
|
||||
S("appName","an");S("appVersion","av","");S("appId","aid","");S("appInstallerId","aiid","");S("exDescription","exd");S("exFatal","exf");var Nc=S("expId","xid"),Oc=S("expVar","xvar"),m=S("exp","exp"),Rc=S("_utma","_utma"),Sc=S("_utmz","_utmz"),Tc=S("_utmht","_utmht"),Ua=S("_hc",void 0,0),Xa=S("_ti",void 0,0),Wa=S("_to",void 0,20);cb("dimension([0-9]+)",function(a){return new bb(a[0],"cd"+a[1])});cb("metric([0-9]+)",function(a){return new bb(a[0],"cm"+a[1])});S("linkerParam",void 0,void 0,Bc,db);
|
||||
var Ze=T("_cd2l",void 0,!1),ld=S("usage","_u"),Gd=S("_um");S("forceSSL",void 0,void 0,function(){return Ba},function(a,b,c){J(34);Ba=!!c});var ed=S("_j1","jid"),ia=S("_j2","gjid");cb("\\&(.*)",function(a){var b=new bb(a[0],a[1]),c=yc(a[0].substring(1));c&&(b.Z=function(d){return d.get(c)},b.o=function(d,e,g,ca){d.set(c,g,ca)},b.F=void 0);return b});
|
||||
var Qb=T("_oot"),dd=S("previewTask"),Rb=S("checkProtocolTask"),md=S("validationTask"),Sb=S("checkStorageTask"),Uc=S("historyImportTask"),Tb=S("samplerTask"),Vb=S("_rlt"),Wb=S("buildHitTask"),Xb=S("sendHitTask"),Vc=S("ceTask"),zd=S("devIdTask"),Cd=S("timingTask"),Ld=S("displayFeaturesTask"),oa=S("customTask"),ze=S("fpsCrossDomainTask"),V=T("name"),Q=T("clientId","cid"),n=T("clientIdTime"),xd=T("storedClientId"),Ad=S("userId","uid"),Na=T("trackingId","tid"),U=T("cookieName",void 0,"_ga"),W=T("cookieDomain"),
|
||||
Yb=T("cookiePath",void 0,"/"),Zb=T("cookieExpires",void 0,63072E3),Hd=T("cookieUpdate",void 0,!0),Be=T("cookieFlags",void 0,""),$b=T("legacyCookieDomain"),Wc=T("legacyHistoryImport",void 0,!0),ac=T("storage",void 0,"cookie"),bc=T("allowLinker",void 0,!1),cc=T("allowAnchor",void 0,!0),Ka=T("sampleRate","sf",100),dc=T("siteSpeedSampleRate",void 0,1),ec=T("alwaysSendReferrer",void 0,!1),I=T("_gid","_gid"),la=T("_gcn"),Kd=T("useAmpClientId"),ce=T("_gclid"),fe=T("_gt"),he=T("_ge",void 0,7776E6),ie=T("_gclsrc"),
|
||||
je=T("storeGac",void 0,!0),oe=S("_x_19"),Ae=S("_fplc","_fplc"),F=T("_cs"),Je=T("_useUp",void 0,!1),Le=S("up","up"),gd=S("transportUrl"),Md=S("_r","_r"),Od=S("_slc","_slc"),qe=S("_dp"),ad=S("_jt",void 0,"n"),Ud=S("allowAdFeatures",void 0,!0),xe=S("allowAdPersonalizationSignals",void 0,!0);function X(a,b,c,d){b[a]=function(){try{return d&&J(d),c.apply(this,arguments)}catch(e){throw ge("exc",a,e&&e.name),e;}}};function fc(){var a,b;if((b=(b=O.navigator)?b.plugins:null)&&b.length)for(var c=0;c<b.length&&!a;c++){var d=b[c];-1<d.name.indexOf("Shockwave Flash")&&(a=d.description)}if(!a)try{var e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");a=e.GetVariable("$version")}catch(g){}if(!a)try{e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"),a="WIN 6,0,21,0",e.AllowScriptAccess="always",a=e.GetVariable("$version")}catch(g){}if(!a)try{e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash"),a=e.GetVariable("$version")}catch(g){}a&&
|
||||
(e=a.match(/[\d]+/g))&&3<=e.length&&(a=e[0]+"."+e[1]+" r"+e[2]);return a||void 0};var Ed=function(a){if("cookie"==a.get(ac))return a=Ca("FPLC"),0<a.length?a[0]:void 0},Fe=function(a){var b;if(b=P(a,oe)&&a.get(Ze))b=De.get(a.get(cc)),b=!(b&&b._fplc);b&&a.set(Ae,Ed(a)||"0")};var aa=function(a){var b=Math.min(R(a,dc),100);return La(P(a,Q))%100>=b?!1:!0},gc=function(a){var b={};if(Ec(b)||Fc(b)){var c=b[Eb];void 0==c||Infinity==c||isNaN(c)||(0<c?(Y(b,Gb),Y(b,Jb),Y(b,Ib),Y(b,Fb),Y(b,Hb),Y(b,Kb),Y(b,Lb),Y(b,Ve),Y(b,We),Y(b,Xe),Y(b,Ye),va(function(){a(b)},10)):L(O,"load",function(){gc(a)},!1))}},Ec=function(a){var b=O.performance||O.webkitPerformance;b=b&&b.timing;if(!b)return!1;var c=b.navigationStart;if(0==c)return!1;a[Eb]=b.loadEventStart-c;a[Gb]=b.domainLookupEnd-b.domainLookupStart;
|
||||
a[Jb]=b.connectEnd-b.connectStart;a[Ib]=b.responseStart-b.requestStart;a[Fb]=b.responseEnd-b.responseStart;a[Hb]=b.fetchStart-c;a[Kb]=b.domInteractive-c;a[Lb]=b.domContentLoadedEventStart-c;a[Ve]=N.L-c;a[We]=N.ya-c;O.google_tag_manager&&O.google_tag_manager._li&&(b=O.google_tag_manager._li,a[Xe]=b.cst,a[Ye]=b.cbt);return!0},Fc=function(a){if(O.top!=O)return!1;var b=O.external,c=b&&b.onloadT;b&&!b.isValidLoadTime&&(c=void 0);2147483648<c&&(c=void 0);0<c&&b.setPageReadyTime();if(void 0==c)return!1;
|
||||
a[Eb]=c;return!0},Y=function(a,b){var c=a[b];if(isNaN(c)||Infinity==c||0>c)a[b]=void 0},Fd=function(a){return function(b){if("pageview"==b.get(Va)&&!a.I){a.I=!0;var c=aa(b),d=0<E(P(b,kb),"gclid").length;(c||d)&&gc(function(e){c&&a.send("timing",e);d&&a.send("adtiming",e)})}}};var hc=!1,mc=function(a){if("cookie"==P(a,ac)){if(a.get(Hd)||P(a,xd)!=P(a,Q)){var b=1E3*R(a,Zb);ma(a,Q,U,b);a.data.set(xd,P(a,Q))}(a.get(Hd)||uc(a)!=P(a,I))&&ma(a,I,la,864E5);if(a.get(je)){var c=P(a,ce);if(c){var d=Math.min(R(a,he),1E3*R(a,Zb));d=Math.min(d,1E3*R(a,fe)+d-(new Date).getTime());a.data.set(he,d);b={};var e=P(a,fe),g=P(a,ie),ca=kc(P(a,Yb)),l=lc(P(a,W)),k=P(a,Na);a=P(a,Be);g&&"aw.ds"!=g?b&&(b.ua=!0):(c=["1",e,Cc(c)].join("."),0<d&&(b&&(b.ta=!0),zc("_gac_"+Cc(k),c,ca,l,k,d,a)));le(b)}}else J(75)}},
|
||||
ma=function(a,b,c,d){var e=nd(a,b);if(e){c=P(a,c);var g=kc(P(a,Yb)),ca=lc(P(a,W)),l=P(a,Be),k=P(a,Na);if("auto"!=ca)zc(c,e,g,ca,k,d,l)&&(hc=!0);else{J(32);for(var w=id(),Ce=0;Ce<w.length;Ce++)if(ca=w[Ce],a.data.set(W,ca),e=nd(a,b),zc(c,e,g,ca,k,d,l)){hc=!0;return}a.data.set(W,"auto")}}},uc=function(a){var b=Ca(P(a,la));return Xd(a,b)},nc=function(a){if("cookie"==P(a,ac)&&!hc&&(mc(a),!hc))throw"abort";},Yc=function(a){if(a.get(Wc)){var b=P(a,W),c=P(a,$b)||xa(),d=Xc("__utma",c,b);d&&(J(19),a.set(Tc,
|
||||
(new Date).getTime(),!0),a.set(Rc,d.R),(b=Xc("__utmz",c,b))&&d.hash==b.hash&&a.set(Sc,b.R))}},nd=function(a,b){b=Cc(P(a,b));var c=lc(P(a,W)).split(".").length;a=jc(P(a,Yb));1<a&&(c+="-"+a);return b?["GA1",c,b].join("."):""},Xd=function(a,b){return na(b,P(a,W),P(a,Yb))},na=function(a,b,c){if(!a||1>a.length)J(12);else{for(var d=[],e=0;e<a.length;e++){var g=a[e];var ca=g.split(".");var l=ca.shift();("GA1"==l||"1"==l)&&1<ca.length?(g=ca.shift().split("-"),1==g.length&&(g[1]="1"),g[0]*=1,g[1]*=1,ca={H:g,
|
||||
s:ca.join(".")}):ca=kd.test(g)?{H:[0,0],s:g}:void 0;ca&&d.push(ca)}if(1==d.length)return J(13),d[0].s;if(0==d.length)J(12);else{J(14);d=Gc(d,lc(b).split(".").length,0);if(1==d.length)return d[0].s;d=Gc(d,jc(c),1);1<d.length&&J(41);return d[0]&&d[0].s}}},Gc=function(a,b,c){for(var d=[],e=[],g,ca=0;ca<a.length;ca++){var l=a[ca];l.H[c]==b?d.push(l):void 0==g||l.H[c]<g?(e=[l],g=l.H[c]):l.H[c]==g&&e.push(l)}return 0<d.length?d:e},lc=function(a){return 0==a.indexOf(".")?a.substr(1):a},id=function(){var a=
|
||||
[],b=xa().split(".");if(4==b.length){var c=b[b.length-1];if(parseInt(c,10)==c)return["none"]}for(c=b.length-2;0<=c;c--)a.push(b.slice(c).join("."));b=M.location.hostname;eb.test(b)||vc.test(b)||a.push("none");return a},kc=function(a){if(!a)return"/";1<a.length&&a.lastIndexOf("/")==a.length-1&&(a=a.substr(0,a.length-1));0!=a.indexOf("/")&&(a="/"+a);return a},jc=function(a){a=kc(a);return"/"==a?1:a.split("/").length},le=function(a){a.ta&&J(77);a.na&&J(74);a.pa&&J(73);a.ua&&J(69)};function Xc(a,b,c){"none"==b&&(b="");var d=[],e=Ca(a);a="__utma"==a?6:2;for(var g=0;g<e.length;g++){var ca=(""+e[g]).split(".");ca.length>=a&&d.push({hash:ca[0],R:e[g],O:ca})}if(0!=d.length)return 1==d.length?d[0]:Zc(b,d)||Zc(c,d)||Zc(null,d)||d[0]}function Zc(a,b){if(null==a)var c=a=1;else c=La(a),a=La(D(a,".")?a.substring(1):"."+a);for(var d=0;d<b.length;d++)if(b[d].hash==c||b[d].hash==a)return b[d]};var Jc=new RegExp(/^https?:\/\/([^\/:]+)/),De=O.google_tag_data.glBridge,Kc=/(.*)([?&#])(?:_ga=[^&#]*)(?:&?)(.*)/,od=/(.*)([?&#])(?:_gac=[^&#]*)(?:&?)(.*)/;function Bc(a){if(a.get(Ze))return J(35),De.generate($e(a));var b=P(a,Q),c=P(a,I)||"";b="_ga=2."+K(pa(c+b,0)+"."+c+"-"+b);(a=af(a))?(J(44),a="&_gac=1."+K([pa(a.qa,0),a.timestamp,a.qa].join("."))):a="";return b+a}
|
||||
function Ic(a,b){var c=new Date,d=O.navigator,e=d.plugins||[];a=[a,d.userAgent,c.getTimezoneOffset(),c.getYear(),c.getDate(),c.getHours(),c.getMinutes()+b];for(b=0;b<e.length;++b)a.push(e[b].description);return La(a.join("."))}function pa(a,b){var c=new Date,d=O.navigator,e=c.getHours()+Math.floor((c.getMinutes()+b)/60);return La([a,d.userAgent,d.language||"",c.getTimezoneOffset(),c.getYear(),c.getDate()+Math.floor(e/24),(24+e)%24,(60+c.getMinutes()+b)%60].join("."))}
|
||||
var Dc=function(a){J(48);this.target=a;this.T=!1};Dc.prototype.ca=function(a,b){if(a){if(this.target.get(Ze))return De.decorate($e(this.target),a,b);if(a.tagName){if("a"==a.tagName.toLowerCase()){a.href&&(a.href=qd(this,a.href,b));return}if("form"==a.tagName.toLowerCase())return rd(this,a)}if("string"==typeof a)return qd(this,a,b)}};
|
||||
var qd=function(a,b,c){var d=Kc.exec(b);d&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));(d=od.exec(b))&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));a=a.target.get("linkerParam");var e=b.indexOf("?");d=b.indexOf("#");c?b+=(-1==d?"#":"&")+a:(c=-1==e?"?":"&",b=-1==d?b+(c+a):b.substring(0,d)+c+a+b.substring(d));b=b.replace(/&+_ga=/,"&_ga=");return b=b.replace(/&+_gac=/,"&_gac=")},rd=function(a,b){if(b&&b.action)if("get"==b.method.toLowerCase()){a=a.target.get("linkerParam").split("&");for(var c=0;c<a.length;c++){var d=
|
||||
a[c].split("="),e=d[1];d=d[0];for(var g=b.childNodes||[],ca=!1,l=0;l<g.length;l++)if(g[l].name==d){g[l].setAttribute("value",e);ca=!0;break}ca||(g=M.createElement("input"),g.setAttribute("type","hidden"),g.setAttribute("name",d),g.setAttribute("value",e),b.appendChild(g))}}else"post"==b.method.toLowerCase()&&(b.action=qd(a,b.action))};
|
||||
Dc.prototype.S=function(a,b,c){function d(g){try{g=g||O.event;a:{var ca=g.target||g.srcElement;for(g=100;ca&&0<g;){if(ca.href&&ca.nodeName.match(/^a(?:rea)?$/i)){var l=ca;break a}ca=ca.parentNode;g--}l={}}("http:"==l.protocol||"https:"==l.protocol)&&sd(a,l.hostname||"")&&l.href&&(l.href=qd(e,l.href,b))}catch(k){J(26)}}var e=this;this.target.get(Ze)?De.auto(function(){return $e(e.target)},a,b?"fragment":"",c):(this.T||(this.T=!0,L(M,"mousedown",d,!1),L(M,"keyup",d,!1)),c&&L(M,"submit",function(g){g=
|
||||
g||O.event;if((g=g.target||g.srcElement)&&g.action){var ca=g.action.match(Jc);ca&&sd(a,ca[1])&&rd(e,g)}}))};Dc.prototype.$=function(a){if(a){var b=this,c=b.target.get(F);void 0!==c&&De.passthrough(function(){if(c("analytics_storage"))return{};var d={};return d._ga=b.target.get(Q),d._up="1",d},1,!0)}};function sd(a,b){if(b==M.location.hostname)return!1;for(var c=0;c<a.length;c++)if(a[c]instanceof RegExp){if(a[c].test(b))return!0}else if(0<=b.indexOf(a[c]))return!0;return!1}
|
||||
function ke(a,b){return b!=Ic(a,0)&&b!=Ic(a,-1)&&b!=Ic(a,-2)&&b!=pa(a,0)&&b!=pa(a,-1)&&b!=pa(a,-2)}function $e(a){var b=af(a),c={};c._ga=a.get(Q);c._gid=a.get(I)||void 0;c._gac=b?[b.qa,b.timestamp].join("."):void 0;b=a.get(Ae);a=Ed(a);return c._fplc=b&&"0"!==b?b:a,c}function af(a){function b(e){return void 0==e||""===e?0:Number(e)}var c=a.get(ce);if(c&&a.get(je)){var d=b(a.get(fe));if(1E3*d+b(a.get(he))<=(new Date).getTime())J(76);else return{timestamp:d,qa:c}}};var p=/^(GTM|OPT)-[A-Z0-9]+$/,Ie=/^G-[A-Z0-9]+$/,q=/;_gaexp=[^;]*/g,r=/;((__utma=)|([^;=]+=GAX?\d+\.))[^;]*/g,Aa=/^https?:\/\/[\w\-.]+\.google.com(:\d+)?\/optimize\/opt-launch\.html\?.*$/,t=function(a){function b(d,e){e&&(c+="&"+d+"="+K(e))}var c=Ge(a.type)+K(a.id);"dataLayer"!=a.B&&b("l",a.B);b("cx",a.context);b("t",a.target);b("cid",a.clientId);b("cidt",a.ka);b("gac",a.la);b("aip",a.ia);a.sync&&b("m","sync");b("cycle",a.G);a.qa&&b("gclid",a.qa);Aa.test(M.referrer)&&b("cb",String(hd()));return c},
|
||||
He=function(a,b){var c=(new Date).getTime();O[a.B]=O[a.B]||[];c={"gtm.start":c};a.sync||(c.event="gtm.js");O[a.B].push(c);2===a.type&&function(d,e,g){O[a.B].push(arguments)}("config",a.id,b)},Ke=function(a,b,c,d){c=c||{};var e=1;Ie.test(b)&&(e=2);var g={id:b,type:e,B:c.dataLayer||"dataLayer",G:!1},ca=void 0;a.get(">m")==b&&(g.G=!0);1===e?(g.ia=!!a.get("anonymizeIp"),g.sync=d,b=String(a.get("name")),"t0"!=b&&(g.target=b),G(String(a.get("trackingId")))||(g.clientId=String(a.get(Q)),g.ka=Number(a.get(n)),
|
||||
c=c.palindrome?r:q,c=(c=M.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,g.la=c,g.qa=E(P(a,kb),"gclid"))):2===e&&(g.context="c",ca={allow_google_signals:a.get(Ud),allow_ad_personalization_signals:a.get(xe)});He(g,ca);return t(g)};var H={},Jd=function(a,b){b||(b=(b=P(a,V))&&"t0"!=b?Wd.test(b)?"_gat_"+Cc(P(a,Na)):"_gat_"+Cc(b):"_gat");this.Y=b},Rd=function(a,b){var c=b.get(Wb);b.set(Wb,function(e){Pd(a,e,ed);Pd(a,e,ia);var g=c(e);Qd(a,e);return g});var d=b.get(Xb);b.set(Xb,function(e){var g=d(e);if(se(e)){J(80);var ca={U:re(e,1),google:re(e,2),count:0};pe("https://stats.g.doubleclick.net/j/collect",ca.U,ca);e.set(ed,"",!0)}return g})},Pd=function(a,b,c){!1===b.get(Ud)||b.get(c)||("1"==Ca(a.Y)[0]?b.set(c,"",!0):b.set(c,""+hd(),
|
||||
!0))},Qd=function(a,b){se(b)&&zc(a.Y,"1",P(b,Yb),P(b,W),P(b,Na),6E4,P(b,Be))},se=function(a){return!!a.get(ed)&&!1!==a.get(Ud)},Ne=function(a){return!H[P(a,Na)]&&void 0===a.get(">m")&&void 0===a.get(fa)&&void 0===a.get(gd)&&void 0===a.get(oe)},re=function(a,b){var c=new ee,d=function(g){$a(g).F&&c.set($a(g).F,a.get(g))};d(hb);d(ib);d(Na);d(Q);d(ed);1==b&&(d(Ad),d(ia),d(I));!1===a.get(xe)&&c.set("npa","1");c.set($a(ld).F,Td(a));var e="";c.map(function(g,ca){e+=K(g)+"=";e+=K(""+ca)+"&"});e+="z="+
|
||||
hd();1==b?e="t=dc&aip=1&_r=3&"+e:2==b&&(e="t=sr&aip=1&_r=4&slf_rd=1&"+e);return e},Me=function(a){if(Ne(a))return H[P(a,Na)]=!0,function(b){if(b&&!H[b]){var c=Ke(a,b);Id(c);H[b]=!0}}},Wd=/^gtm\d+$/;var fd=function(a,b){a=a.model;if(!a.get("dcLoaded")){var c=new $c(Dd(a));c.set(29);a.set(Gd,c.C);b=b||{};var d;b[U]&&(d=Cc(b[U]));b=new Jd(a,d);Rd(b,a);a.set("dcLoaded",!0)}};var Sd=function(a){if(!a.get("dcLoaded")&&"cookie"==a.get(ac)){var b=new Jd(a);Pd(b,a,ed);Pd(b,a,ia);Qd(b,a);b=se(a);var c=Ne(a);b&&a.set(Md,1,!0);c&&a.set(Od,1,!0);if(b||c)a.set(ad,"d",!0),J(79),a.set(qe,{U:re(a,1),google:re(a,2),V:Me(a),count:0},!0)}};var Lc=function(){var a=O.gaGlobal=O.gaGlobal||{};return a.hid=a.hid||hd()};var wb=/^(UA|YT|MO|GP)-(\d+)-(\d+)$/,pc=function(a){function b(e,g){d.model.data.set(e,g)}function c(e,g){b(e,g);d.filters.add(e)}var d=this;this.model=new Ya;this.filters=new Ha;b(V,a[V]);b(Na,sa(a[Na]));b(U,a[U]);b(W,a[W]||xa());b(Yb,a[Yb]);b(Zb,a[Zb]);b(Hd,a[Hd]);b(Be,a[Be]);b($b,a[$b]);b(Wc,a[Wc]);b(bc,a[bc]);b(cc,a[cc]);b(Ka,a[Ka]);b(dc,a[dc]);b(ec,a[ec]);b(ac,a[ac]);b(Ad,a[Ad]);b(n,a[n]);b(Kd,a[Kd]);b(je,a[je]);b(Ze,a[Ze]);b(oe,a[oe]);b(Je,a[Je]);b(F,a[F]);b(hb,1);b(ib,"j87");c(Qb,Ma);c(oa,
|
||||
ua);c(dd,cd);c(Rb,Oa);c(md,vb);c(Sb,nc);c(Uc,Yc);c(Tb,Ja);c(Vb,Ta);c(Vc,Hc);c(zd,yd);c(Ld,Sd);c(ze,Fe);c(Wb,Pa);c(Xb,Sa);c(Cd,Fd(this));pd(this.model);td(this.model,a[Q]);this.model.set(jb,Lc())};pc.prototype.get=function(a){return this.model.get(a)};pc.prototype.set=function(a,b){this.model.set(a,b)};
|
||||
pc.prototype.send=function(a){if(!(1>arguments.length)){if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else b=arguments[0]&&arguments[0][Va],c=arguments;b&&(c=za(me[b]||[],c),c[Va]=b,this.model.set(c,void 0,!0),this.filters.D(this.model),this.model.data.m={})}};pc.prototype.ma=function(a,b){var c=this;u(a,c,b)||(v(a,function(){u(a,c,b)}),y(String(c.get(V)),a,void 0,b,!0))};
|
||||
var td=function(a,b){var c=P(a,U);a.data.set(la,"_ga"==c?"_gid":c+"_gid");if("cookie"==P(a,ac)){hc=!1;c=Ca(P(a,U));c=Xd(a,c);if(!c){c=P(a,W);var d=P(a,$b)||xa();c=Xc("__utma",d,c);void 0!=c?(J(10),c=c.O[1]+"."+c.O[2]):c=void 0}c&&(hc=!0);if(d=c&&!a.get(Hd))if(d=c.split("."),2!=d.length)d=!1;else if(d=Number(d[1])){var e=R(a,Zb);d=d+e<(new Date).getTime()/1E3}else d=!1;d&&(c=void 0);c&&(a.data.set(xd,c),a.data.set(Q,c),(c=uc(a))&&a.data.set(I,c));if(a.get(je)&&(c=a.get(ce),d=a.get(ie),!c||d&&"aw.ds"!=
|
||||
d)){c={};if(M){d=[];e=M.cookie.split(";");for(var g=/^\s*_gac_(UA-\d+-\d+)=\s*(.+?)\s*$/,ca=0;ca<e.length;ca++){var l=e[ca].match(g);l&&d.push({ja:l[1],value:l[2]})}e={};if(d&&d.length)for(g=0;g<d.length;g++)(ca=d[g].value.split("."),"1"!=ca[0]||3!=ca.length)?c&&(c.na=!0):ca[1]&&(e[d[g].ja]?c&&(c.pa=!0):e[d[g].ja]=[],e[d[g].ja].push({timestamp:ca[1],qa:ca[2]}));d=e}else d={};d=d[P(a,Na)];le(c);d&&0!=d.length&&(c=d[0],a.data.set(fe,c.timestamp),a.data.set(ce,c.qa))}}if(a.get(Hd)){c=be("_ga",!!a.get(cc));
|
||||
g=be("_gl",!!a.get(cc));d=De.get(a.get(cc));e=d._ga;g&&0<g.indexOf("_ga*")&&!e&&J(30);if(b||!a.get(Je))g=!1;else if(g=a.get(F),void 0===g||g("analytics_storage"))g=!1;else{J(84);a.data.set(Le,1);if(g=d._up)(g=Jc.exec(M.referrer))?(g=g[1],ca=M.location.hostname,g=ca===g||0<=ca.indexOf("."+g)||0<=g.indexOf("."+ca)?!0:!1):g=!1;g=g?!0:!1}ca=d.gclid;l=d._gac;if(c||e||ca||l)if(c&&e&&J(36),a.get(bc)||ye(a.get(Kd))||g){e&&(J(38),a.data.set(Q,e),d._gid&&(J(51),a.data.set(I,d._gid)));ca?(J(82),a.data.set(ce,
|
||||
ca),d.gclsrc&&a.data.set(ie,d.gclsrc)):l&&(e=l.split("."))&&2===e.length&&(J(37),a.data.set(ce,e[0]),a.data.set(fe,e[1]));if(d=d._fplc)J(83),a.data.set(Ae,d);if(c)b:if(d=c.indexOf("."),-1==d)J(22);else{e=c.substring(0,d);g=c.substring(d+1);d=g.indexOf(".");c=g.substring(0,d);g=g.substring(d+1);if("1"==e){if(d=g,ke(d,c)){J(23);break b}}else if("2"==e){d=g.indexOf("-");e="";0<d?(e=g.substring(0,d),d=g.substring(d+1)):d=g.substring(1);if(ke(e+d,c)){J(53);break b}e&&(J(2),a.data.set(I,e))}else{J(22);
|
||||
break b}J(11);a.data.set(Q,d);if(c=be("_gac",!!a.get(cc)))c=c.split("."),"1"!=c[0]||4!=c.length?J(72):ke(c[3],c[1])?J(71):(a.data.set(ce,c[3]),a.data.set(fe,c[2]),J(70))}}else J(21)}b&&(J(9),a.data.set(Q,K(b)));a.get(Q)||(b=(b=O.gaGlobal)&&b.from_cookie&&"cookie"!==P(a,ac)?void 0:(b=b&&b.vid)&&-1!==b.search(jd)?b:void 0,b?(J(17),a.data.set(Q,b)):(J(8),a.data.set(Q,ra())));a.get(I)||(J(3),a.data.set(I,ra()));mc(a);b=O.gaGlobal=O.gaGlobal||{};c=P(a,Q);a=c===P(a,xd);if(void 0==b.vid||a&&!b.from_cookie)b.vid=
|
||||
c,b.from_cookie=a},pd=function(a){var b=O.navigator,c=O.screen,d=M.location;a.set(lb,ya(!!a.get(ec),!!a.get(Kd)));if(d){var e=d.pathname||"";"/"!=e.charAt(0)&&(J(31),e="/"+e);a.set(kb,d.protocol+"//"+d.hostname+e+d.search)}c&&a.set(qb,c.width+"x"+c.height);c&&a.set(pb,c.colorDepth+"-bit");c=M.documentElement;var g=(e=M.body)&&e.clientWidth&&e.clientHeight,ca=[];c&&c.clientWidth&&c.clientHeight&&("CSS1Compat"===M.compatMode||!g)?ca=[c.clientWidth,c.clientHeight]:g&&(ca=[e.clientWidth,e.clientHeight]);
|
||||
c=0>=ca[0]||0>=ca[1]?"":ca.join("x");a.set(rb,c);a.set(tb,fc());a.set(ob,M.characterSet||M.charset);a.set(sb,b&&"function"===typeof b.javaEnabled&&b.javaEnabled()||!1);a.set(nb,(b&&(b.language||b.browserLanguage)||"").toLowerCase());a.data.set(ce,be("gclid",!0));a.data.set(ie,be("gclsrc",!0));a.data.set(fe,Math.round((new Date).getTime()/1E3));if(d&&a.get(cc)&&(b=M.location.hash)){b=b.split(/[?&#]+/);d=[];for(c=0;c<b.length;++c)(D(b[c],"utm_id")||D(b[c],"utm_campaign")||D(b[c],"utm_source")||D(b[c],
|
||||
"utm_medium")||D(b[c],"utm_term")||D(b[c],"utm_content")||D(b[c],"gclid")||D(b[c],"dclid")||D(b[c],"gclsrc"))&&d.push(b[c]);0<d.length&&(b="#"+d.join("&"),a.set(kb,a.get(kb)+b))}},me={pageview:[mb],event:[ub,xb,yb,zb],social:[Bb,Cb,Db],timing:[Mb,Nb,Pb,Ob]};var rc=function(a){if("prerender"==M.visibilityState)return!1;a();return!0},z=function(a){if(!rc(a)){J(16);var b=!1,c=function(){if(!b&&rc(a)){b=!0;var d=c,e=M;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};L(M,"visibilitychange",c)}};var te=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,sc=function(a){if(ea(a[0]))this.u=a[0];else{var b=te.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.K=b[2]||"",this.methodName=b[3],this.a=[].slice.call(a,1),this.K||(this.A="create"==this.methodName,this.i="require"==this.methodName,this.g="provide"==this.methodName,this.ba="remove"==this.methodName),this.i&&(3<=this.a.length?(this.X=this.a[1],this.W=this.a[2]):this.a[1]&&(qa(this.a[1])?this.X=this.a[1]:this.W=this.a[1])));b=a[1];a=a[2];if(!this.methodName)throw"abort";
|
||||
if(this.i&&(!qa(b)||""==b))throw"abort";if(this.g&&(!qa(b)||""==b||!ea(a)))throw"abort";if(ud(this.c)||ud(this.K))throw"abort";if(this.g&&"t0"!=this.c)throw"abort";}};function ud(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Yd,Zd,$d,A;Yd=new ee;$d=new ee;A=new ee;Zd={ec:45,ecommerce:46,linkid:47};
|
||||
var u=function(a,b,c){b==N||b.get(V);var d=Yd.get(a);if(!ea(d))return!1;b.plugins_=b.plugins_||new ee;if(b.plugins_.get(a))return!0;b.plugins_.set(a,new d(b,c||{}));return!0},y=function(a,b,c,d,e){if(!ea(Yd.get(b))&&!$d.get(b)){Zd.hasOwnProperty(b)&&J(Zd[b]);a=N.j(a);if(p.test(b)){J(52);if(!a)return!0;c=Ke(a.model,b,d,e)}!c&&Zd.hasOwnProperty(b)?(J(39),c=b+".js"):J(43);if(c){if(a){var g=a.get(oe);qa(g)||(g=void 0)}c&&0<=c.indexOf("/")||(c=(g||bd(!1))+"/plugins/ua/"+c);d=ae(c);g=d.protocol;c=M.location.protocol;
|
||||
("https:"==g||g==c||("http:"!=g?0:"http:"==c))&&B(d)&&(Id(d.url,void 0,e),$d.set(b,!0))}}},v=function(a,b){var c=A.get(a)||[];c.push(b);A.set(a,c)},C=function(a,b){Yd.set(a,b);b=A.get(a)||[];for(var c=0;c<b.length;c++)b[c]();A.set(a,[])},B=function(a){var b=ae(M.location.href);if(D(a.url,Ge(1))||D(a.url,Ge(2)))return!0;if(a.query||0<=a.url.indexOf("?")||0<=a.path.indexOf("://"))return!1;if(a.host==b.host&&a.port==b.port)return!0;b="http:"==a.protocol?80:443;return"www.google-analytics.com"==a.host&&
|
||||
(a.port||b)==b&&D(a.path,"/plugins/")?!0:!1},ae=function(a){function b(l){var k=l.hostname||"",w=0<=k.indexOf("]");k=k.split(w?"]":":")[0].toLowerCase();w&&(k+="]");w=(l.protocol||"").toLowerCase();w=1*l.port||("http:"==w?80:"https:"==w?443:"");l=l.pathname||"";D(l,"/")||(l="/"+l);return[k,""+w,l]}var c=M.createElement("a");c.href=M.location.href;var d=(c.protocol||"").toLowerCase(),e=b(c),g=c.search||"",ca=d+"//"+e[0]+(e[1]?":"+e[1]:"");D(a,"//")?a=d+a:D(a,"/")?a=ca+a:!a||D(a,"?")?a=ca+e[2]+(a||
|
||||
g):0>a.split("/")[0].indexOf(":")&&(a=ca+e[2].substring(0,e[2].lastIndexOf("/"))+"/"+a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:c.search||"",url:a||""}};var Z={ga:function(){Z.f=[]}};Z.ga();Z.D=function(a){var b=Z.J.apply(Z,arguments);b=Z.f.concat(b);for(Z.f=[];0<b.length&&!Z.v(b[0])&&!(b.shift(),0<Z.f.length););Z.f=Z.f.concat(b)};Z.J=function(a){for(var b=[],c=0;c<arguments.length;c++)try{var d=new sc(arguments[c]);d.g?C(d.a[0],d.a[1]):(d.i&&(d.ha=y(d.c,d.a[0],d.X,d.W)),b.push(d))}catch(e){}return b};
|
||||
Z.v=function(a){try{if(a.u)a.u.call(O,N.j("t0"));else{var b=a.c==gb?N:N.j(a.c);if(a.A){if("t0"==a.c&&(b=N.create.apply(N,a.a),null===b))return!0}else if(a.ba)N.remove(a.c);else if(b)if(a.i){if(a.ha&&(a.ha=y(a.c,a.a[0],a.X,a.W)),!u(a.a[0],b,a.W))return!0}else if(a.K){var c=a.methodName,d=a.a,e=b.plugins_.get(a.K);e[c].apply(e,d)}else b[a.methodName].apply(b,a.a)}}catch(g){}};var N=function(a){J(1);Z.D.apply(Z,[arguments])};N.h={};N.P=[];N.L=0;N.ya=0;N.answer=42;var we=[Na,W,V];N.create=function(a){var b=za(we,[].slice.call(arguments));b[V]||(b[V]="t0");var c=""+b[V];if(N.h[c])return N.h[c];if(da(b))return null;b=new pc(b);N.h[c]=b;N.P.push(b);c=qc().tracker_created;if(ea(c))try{c(b)}catch(d){}return b};N.remove=function(a){for(var b=0;b<N.P.length;b++)if(N.P[b].get(V)==a){N.P.splice(b,1);N.h[a]=null;break}};N.j=function(a){return N.h[a]};N.getAll=function(){return N.P.slice(0)};
|
||||
N.N=function(){"ga"!=gb&&J(49);var a=O[gb];if(!a||42!=a.answer){N.L=a&&a.l;N.ya=1*new Date;N.loaded=!0;var b=O[gb]=N;X("create",b,b.create);X("remove",b,b.remove);X("getByName",b,b.j,5);X("getAll",b,b.getAll,6);b=pc.prototype;X("get",b,b.get,7);X("set",b,b.set,4);X("send",b,b.send);X("requireSync",b,b.ma);b=Ya.prototype;X("get",b,b.get);X("set",b,b.set);if("https:"!=M.location.protocol&&!Ba){a:{b=M.getElementsByTagName("script");for(var c=0;c<b.length&&100>c;c++){var d=b[c].src;if(d&&0==d.indexOf(bd(!0)+
|
||||
"/analytics")){b=!0;break a}}b=!1}b&&(Ba=!0)}(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);X("passthrough",b,b.$,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};var Oe=N.N,Pe=O[gb];Pe&&Pe.r?Oe():z(Oe);z(function(){Z.D(["provide","render",ua])});})(window);
|
||||
Binary file not shown.
@@ -1,99 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<<<<<<< HEAD
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="vinlottis-icons" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="arrow-left" horiz-adv-x="1233" d="M1193.594 394.643h-1062.068v121.96h1062.068v-121.96zM527.897-37.299l86.389 86.389-437.024 406.533 437.024 406.533-86.389 86.389-528.493-492.922z" />
|
||||
<glyph unicode="" glyph-name="arrow-long-right" horiz-adv-x="1720" d="M27.014 498.697h1578.188v-105.8h-1578.188v105.8zM1256.94 873.404l-70.533-74.942 374.709-352.668-374.709-352.668 70.533-74.942 458.468 427.61z" />
|
||||
<glyph unicode="" glyph-name="arrow-long-left" horiz-adv-x="1720" d="M1692.974 414.939h-1578.163v105.799h1578.163v-105.799zM463.067 40.235l70.531 74.941-374.703 352.662 374.703 348.253-70.531 79.349-458.46-427.603z" />
|
||||
<glyph unicode="" glyph-name="arrow-right" horiz-adv-x="1233" d="M39.406 501.357h1062.069v-121.96h-1062.069v121.96zM705.103 933.299l-86.389-86.389 437.024-406.533-437.024-406.533 86.389-86.389 528.493 492.922z" />
|
||||
<glyph unicode="" glyph-name="ballon" horiz-adv-x="1013" d="M270.94 546.983c-17.834 67.772 1.784 139.111 51.722 189.049l30.319-30.319c-39.237-39.237-53.504-94.525-39.237-148.029l-42.803-10.701zM511.711 52.959v42.803c99.875 0 117.71 55.288 135.545 103.443 3.566 10.701 7.135 19.618 10.701 26.753 19.618 44.586 48.154 82.041 82.041 115.927l28.536-30.319c-30.319-28.536-55.288-64.206-73.122-103.443-3.566-5.35-5.35-14.268-8.919-23.186-16.051-48.154-44.586-131.979-174.782-131.979zM511.711 52.959c-108.793 0-151.596 96.308-176.565 148.029-3.566 5.35-5.35 12.485-7.135 16.051-17.834 35.671-41.019 65.989-67.772 92.742l28.536 30.319c32.103-30.319 58.854-65.989 78.474-105.227 1.784-5.35 5.35-10.701 8.919-17.834 23.186-49.938 55.288-123.060 137.328-123.060v-41.019h-1.784zM144.312 579.087c-3.566 98.091 32.103 190.832 99.875 263.956s158.731 112.359 256.821 115.927c99.875 3.566 192.616-32.103 265.74-99.875 71.338-67.772 114.142-158.731 115.927-256.821 0-7.135 0-16.051 0-23.186l-42.803 1.784c0 7.135 0 14.268 0 21.403-3.566 87.391-39.237 167.647-103.443 228.287-64.206 58.854-146.246 90.958-233.636 87.391s-167.647-39.237-228.287-103.443c-58.854-64.206-90.958-146.246-87.391-233.636l-42.803-1.784zM260.24 311.564c-73.122 69.556-115.927 167.647-115.927 267.522h42.803c0-89.176 37.453-176.565 101.66-237.204l-28.536-30.319zM766.75 311.564l-28.536 30.319c64.206 60.639 101.66 148.029 101.66 237.204h42.803c0-99.875-41.019-197.967-115.927-267.522zM590.183-64.751h-42.803c0 8.919-14.268 30.319-32.103 49.938-17.834-19.618-32.103-41.019-32.103-49.938h-42.803c0 35.671 46.369 82.041 60.639 96.308l14.268 14.268 14.268-14.268c14.268-14.268 60.639-60.639 60.639-96.308z" />
|
||||
<glyph unicode="" glyph-name="bars" horiz-adv-x="1022" d="M50.027-49.221h-44.28v450.175h142.063v-42.434h-97.784zM813.849-49.221h-44.28v736.146h142.063v-42.434h-97.784zM431.937-49.221h-44.28v594.083h142.063v-44.28h-97.784zM505.737 686.925l-284.126 188.187-177.117-177.117-31.364 31.364 202.947 202.947 289.662-191.877 321.026 219.553 23.984-35.054zM859.973 803.158h-42.434v127.303h-127.303v42.434h169.739zM959.602 688.77h44.28v-739.836h-44.28v739.836zM577.692 546.706h44.28v-595.928h-44.28v595.928zM197.626 402.798h44.28v-452.019h-44.28v452.019z" />
|
||||
<glyph unicode="" glyph-name="bottle" horiz-adv-x="571" d="M449-58.977h-307.909v494.087h41.532v-452.555h226.277v452.555h40.1zM218.425 516.742l-28.643 30.075c22.914 21.482 35.804 50.125 35.804 81.632v8.593h41.532v-8.593c0-42.964-17.185-83.063-48.693-111.706zM374.529 516.742c-31.507 30.075-48.693 68.742-48.693 111.706v8.593h41.532v-8.593c0-31.507 12.889-60.149 35.804-81.632l-28.643-30.075zM450.432 435.111h-41.532c0 31.507-12.889 60.149-35.804 81.632l28.643 30.075c31.507-28.643 48.693-70.175 48.693-111.706zM182.622 435.111h-41.532c0 42.964 17.185 83.063 48.693 111.706l28.643-30.075c-22.914-21.482-35.804-50.125-35.804-81.632zM225.586 823.22h41.532v-187.61h-41.532v187.61zM325.836 823.22h41.532v-187.61h-41.532v187.61zM221.29 894.827h141.782v-41.532h-141.782v41.532zM218.425 965.001h144.646v-41.532h-144.646v41.532z" />
|
||||
<glyph unicode="" glyph-name="cake-chart" horiz-adv-x="1021" d="M458.341-44.346c-22.295 0-46.448 1.857-68.742 5.574-120.764 18.579-224.807 81.748-297.266 180.217-70.601 96.611-100.327 217.376-81.748 336.282 29.727 195.081 180.217 347.429 375.297 379.014l7.432-42.732c-174.644-29.727-312.129-167.212-338.139-341.855-33.443-222.948 118.906-432.892 343.713-468.193 222.948-33.443 432.892 118.906 468.193 343.713l42.732-7.432c-37.159-224.807-230.38-384.586-451.472-384.586zM1010.14 507.453h-453.33v453.33h22.295c237.811 0 431.035-193.222 431.035-431.035v-22.295zM601.399 552.043h364.15c-11.148 195.081-169.070 353.003-364.15 364.15v-364.15zM915.386 386.689h-479.34v477.482h44.589v-432.892h434.751z" />
|
||||
<glyph unicode="" glyph-name="stopwatch" horiz-adv-x="933" d="M618.798 188.608l-165.156 198.187v351.686h44.689v-334.199l155.441-184.586zM471.129-38.725c-242.877 0-439.121 196.245-439.121 439.121s196.245 439.121 439.121 439.121 439.121-196.245 439.121-439.121-196.245-439.121-439.121-439.121zM471.129 792.885c-215.674 0-392.489-176.814-392.489-392.489s176.814-392.489 392.489-392.489 392.489 176.814 392.489 392.489-174.872 392.489-392.489 392.489zM447.812 971.642h46.632v-170.985h-46.632v170.985zM317.631 985.243h308.939v-46.632h-308.939v46.632z" />
|
||||
<glyph unicode="" glyph-name="cloud" horiz-adv-x="1655" d="M373.421 86.077h1049.578v-66.816h-1049.578v66.816zM256.492 22.044c-119.713 11.136-217.154 100.225-242.21 217.154-30.624 147.554 61.249 295.106 206.017 331.299 0 125.282 83.521 236.642 208.803 264.482 144.769 36.193 295.106-52.896 331.299-200.45l-64.033-16.704c-27.84 111.361-139.202 178.178-250.562 153.121-103.009-25.056-169.825-119.713-155.905-222.723l2.783-30.624-44.545-8.352c-116.929-25.056-192.098-139.202-167.041-258.913 19.489-89.089 91.872-155.905 183.745-167.041l-8.352-61.249zM1420.215 19.26l-2.783 66.816c72.385 8.352 130.848 64.033 147.554 133.634 19.489 94.658-41.761 186.53-133.634 206.017l-41.761 8.352 2.783 30.624c11.136 83.521-41.761 158.689-122.497 178.178h-5.568c-86.304 19.489-175.393-33.408-197.666-122.497l-64.033 16.704c30.624 122.497 155.905 200.45 278.402 169.825h5.568c103.009-25.056 172.61-116.929 172.61-219.937 119.713-33.408 194.881-155.905 169.825-278.402-22.272-103.009-105.792-178.178-208.803-189.313zM690.8 642.881c-22.272 94.658 25.056 194.881 111.361 236.642 103.009 50.113 228.29 5.568 278.402-97.441l-58.465-27.84c-33.408 69.6-119.713 100.225-189.313 66.816-61.249-27.84-91.872-97.441-77.952-161.473l-64.033-16.704z" />
|
||||
<glyph unicode="" glyph-name="dart" horiz-adv-x="1013" d="M459.718 120.99v0c-143.177 0-259.84 116.662-258.073 259.84 0 143.177 116.662 258.073 258.073 258.073v0c44.19 0 88.381-12.374 127.268-33.583l-21.213-35.354c-31.816 17.675-68.937 28.282-106.058 28.282v0c-120.199 0-217.417-97.219-217.417-217.417s97.219-217.417 217.417-217.417v0c120.199 0 217.417 97.219 217.417 217.417 0 37.121-10.606 76.007-28.282 107.825l35.354 21.213c22.98-38.888 35.354-84.845 35.354-129.036-1.767-143.177-118.431-259.84-259.84-259.84zM459.718-50.468c-134.34 0-266.91 61.866-349.988 178.529-141.408 194.437-97.219 464.884 95.451 604.525 153.782 109.592 358.826 109.592 509.074-3.534l-22.98-33.583c-137.874 102.52-321.706 102.52-459.58 3.534-174.995-125.5-213.881-369.432-88.381-544.425s369.432-213.881 544.425-88.381c84.845 60.098 139.641 150.248 157.318 254.536 15.908 102.52-7.071 205.044-68.937 289.889l33.583 24.747c67.17-93.684 93.684-208.578 76.007-321.706s-79.543-213.881-173.227-281.051c-77.776-56.563-166.156-83.078-252.769-83.078zM493.941 444.747l315.015 314.925 29.993-30.002-315.015-314.925-29.993 30.002zM892.784 713.143c-12.374 0-24.747 0-37.121 1.767s-26.514 1.767-40.655 3.534l-17.675 1.767v17.675c-1.767 14.141-3.534 26.514-3.534 38.888-1.767 24.747-1.767 49.494 0 76.007 0 21.213 10.606 40.655 26.514 56.563 12.374 14.141 31.816 21.213 53.029 19.444 28.282-1.767 53.029-21.213 61.866-47.724v-3.534c1.767-7.071 1.767-12.374 1.767-19.444 7.071 0 14.141-1.767 21.213-3.534 19.444-5.302 35.354-19.444 42.423-35.354 3.534-7.071 7.071-17.675 7.071-26.514 0-19.444-7.071-38.888-19.444-54.796-15.908-15.908-37.121-24.747-60.098-24.747-10.606 0-22.98 0-35.354 0zM836.22 759.101c7.071 0 14.141-1.767 22.98-1.767 22.98-1.767 45.957-1.767 68.937 0h3.534c10.606 0 22.98 5.302 30.049 12.374 5.302 5.302 8.839 14.141 8.839 22.98 0 3.534-1.767 7.071-3.534 10.606-3.534 7.071-8.839 12.374-15.908 14.141-8.839 1.767-17.675 1.767-28.282 0l-26.514-3.534 1.767 26.514c0 10.606 0 19.444-1.767 30.049-3.534 10.606-14.141 17.675-24.747 17.675-8.839 0-15.908-3.534-22.98-8.839-7.071-7.071-12.374-17.675-12.374-28.282-1.767-22.98-1.767-45.957 0-68.937-1.767-8.839-1.767-15.908 0-22.98zM459.718 287.146c-51.262 0-91.917 40.655-91.917 91.917s40.655 91.917 91.917 91.917 91.917-40.655 91.917-91.917-40.655-91.917-91.917-91.917zM459.718 430.325c-28.282 0-51.262-22.98-51.262-51.262s22.98-51.262 51.262-51.262 51.262 22.98 51.262 51.262-22.98 51.262-51.262 51.262z" />
|
||||
<glyph unicode="" glyph-name="eye-1" horiz-adv-x="1739" d="M1534.010 360.702h-91.094l-11.387 22.774c-68.321 117.664-167.006 216.348-284.669 284.669-371.967 216.348-850.211 87.298-1066.56-284.669l-79.707 45.547c239.122 417.515 774.299 557.952 1188.019 318.829 125.254-72.116 227.735-174.597 303.647-296.056h41.752v-91.094zM1499.849 451.796h204.962v-91.094h-204.962v91.094zM786.278 79.828c-148.028 0-269.487 121.46-269.487 269.487s121.46 269.487 269.487 269.487 269.487-121.46 269.487-269.487-121.46-269.487-269.487-269.487zM786.278 527.707c-98.685 0-178.392-79.707-178.392-178.392s79.707-178.392 178.392-178.392 178.392 79.707 178.392 178.392-79.707 178.392-178.392 178.392z" />
|
||||
<glyph unicode="" glyph-name="eye-2" horiz-adv-x="1198" d="M1068.604 184.358h-62.959l-7.87 15.74c-47.219 81.322-115.425 149.528-196.747 196.747-257.084 149.528-587.619 60.335-737.147-196.747l-55.089 31.48c165.269 285.94 535.152 385.624 821.092 220.357 86.569-49.842 157.398-120.671 209.864-204.617h28.856c0 0 0-62.959 0-62.959zM1044.994 247.317h141.658v-62.959h-141.658v62.959zM549.19-9.765c-102.309 0-186.254 83.946-186.254 186.254s83.946 186.254 186.254 186.254 186.254-83.946 186.254-186.254-83.946-186.254-186.254-186.254zM549.19 299.784c-68.206 0-123.296-55.089-123.296-123.296s55.089-123.296 123.296-123.296 123.296 55.089 123.296 123.296c0 68.206-55.089 123.296-123.296 123.296zM528.204 926.752h62.959v-173.137h-62.959v173.137zM836.173 652.924l102.334 129.768 49.435-38.985-102.334-129.768-49.435 38.985zM137.111 744.075l50.071 38.165 98.595-129.351-50.071-38.165-98.595 129.351z" />
|
||||
<glyph unicode="" glyph-name="eye-3" horiz-adv-x="1337" d="M697.122 515.16v0c-262.241 0-505.296 140.716-636.418 364.58l67.16 38.378c118.327-201.48 335.8-329.402 569.258-329.402v0c233.46 0 450.93 124.725 569.258 329.402l67.16-38.378c-131.122-223.867-374.175-364.58-636.418-364.58zM633.162 176.165h76.753v-211.072h-76.753v211.072zM149.619 188.967l124.696 158.24 60.283-47.503-124.696-158.24-60.283 47.503zM1006.499 295.077l61.042 46.529 120.197-157.694-61.042-46.529-120.197 157.694z" />
|
||||
<glyph unicode="" glyph-name="eye-4" horiz-adv-x="1379" d="M693.748-70.634c-65.084 0-117.964 52.88-117.964 117.964s52.88 117.964 117.964 117.964 117.964-52.88 117.964-117.964-52.88-117.964-117.964-117.964zM693.748 71.735c-12.203 0-24.407-12.203-24.407-24.407s12.203-24.407 24.407-24.407c12.203 0 24.407 12.203 24.407 24.407s-12.203 24.407-24.407 24.407zM644.936 897.483h97.625v-349.823h-97.625v349.823zM1086.422 401.88l206.769 261.739 76.607-60.518-206.769-261.739-76.607 60.518zM22.445 606.631l77.612 59.22 199.866-261.94-77.612-59.22-199.866 261.94zM87.658 120.548l-85.422 48.813c69.151 122.032 170.845 219.657 292.875 292.875 183.047 105.761 398.636 134.235 602.023 81.354 203.386-56.948 378.298-187.115 484.058-370.162l-85.422-48.813c-93.558 162.709-244.063 276.605-423.043 325.418s-370.162 24.407-528.804-69.151c-105.761-69.151-195.25-154.573-256.267-260.334z" />
|
||||
<glyph unicode="" glyph-name="eye-5" horiz-adv-x="1020" d="M512.41-57.338c-223.412 0-404.27 180.859-404.27 404.27s180.859 406.931 406.931 406.931c223.412 0 406.931-180.859 406.931-406.931s-186.178-404.27-409.59-404.27zM512.41 690.031c-188.836 0-343.099-154.262-343.099-343.099s154.262-343.099 343.099-343.099 343.099 154.262 343.099 343.099c0 191.498-154.262 343.099-343.099 343.099zM515.070 264.481c-45.215 0-82.452 37.236-82.452 82.452s37.236 82.452 82.452 82.452 82.452-37.236 82.452-82.452-37.236-82.452-82.452-82.452zM515.070 368.21c-10.639 0-21.278-7.979-21.278-21.278s7.979-21.278 21.278-21.278 18.617 10.639 18.617 21.278-7.979 21.278-18.617 21.278zM480.494 966.637h63.834v-255.331h-63.834v255.331zM799.409 606.416l150.084 189.873 50.077-39.584-150.084-189.873-50.077 39.584zM29.206 757.88l50.721 38.753 145.322-190.205-50.721-38.753-145.322 190.205z" />
|
||||
<glyph unicode="" glyph-name="eye-6" horiz-adv-x="1020" d="M509.725 234.041c-46.895 0-87.929 38.103-87.929 87.929s38.103 87.929 87.929 87.929 87.929-38.103 87.929-87.929-38.103-87.929-87.929-87.929zM509.725 339.555c-8.793 0-17.585-8.793-17.585-17.585s8.793-17.585 17.585-17.585c8.793 0 17.585 8.793 17.585 17.585s-5.862 17.585-17.585 17.585zM474.553 946.261h70.343v-257.924h-70.343v257.924zM797.941 584.125l152.62 193.193 55.198-43.605-152.62-193.193-55.198 43.605zM15.553 732.705l55.909 42.691 147.64-193.35-55.909-42.691-147.64 193.35zM512.656-59.055c-202.236 0-398.61 105.514-509.986 293.096l61.55 35.172c143.617-246.2 460.16-331.197 706.359-187.58 79.135 43.964 143.617 111.376 187.58 187.58l61.55-35.172c-52.757-87.929-126.032-161.202-213.959-213.959-93.791-55.688-193.443-79.135-293.096-79.135zM958.161 374.727c-70.343 120.169-181.72 205.167-313.612 240.339s-272.578 17.585-392.747-52.757c-79.135-43.964-143.617-111.376-190.512-190.512l-61.55 35.172c52.757 87.929 126.032 164.133 213.959 213.959 278.44 161.202 638.947 64.481 800.15-213.959l-55.688-32.241z" />
|
||||
<glyph unicode="" glyph-name="eye-7" horiz-adv-x="1492" d="M699.992 382.813h100.935v-370.096h-100.935v370.096zM39.886 317.081l218.641 277.46 79.275-62.469-218.641-277.46-79.275 62.469zM1168.077 535.027l80.254 61.217 211.71-277.543-80.254-61.217-211.71 277.543zM750.46 365.99c-4.205 0-4.205 0 0 0-302.805 0-580.376 159.814-731.779 420.562l88.318 50.467c130.374-227.104 378.507-370.096 643.461-370.096v0c264.954 0 508.881 142.992 643.461 370.096l88.318-50.467c-155.609-260.75-433.179-420.562-731.779-420.562z" />
|
||||
<glyph unicode="" glyph-name="eye-8" d="M500.604 248.557c-45.588 0-85.476 37.040-85.476 85.476s37.040 85.476 85.476 85.476c48.436 0 85.476-37.040 85.476-85.476s-37.040-85.476-85.476-85.476zM534.794 940.907h-68.38v-250.726h68.38v250.726zM929.448 777.228l-148.362-187.803 53.66-42.39 148.362 187.803-53.66 42.39zM74.194 774.747l-54.349-41.502 143.521-187.955 54.347 41.502-143.52 187.955zM503.453-36.36c-196.592 0-387.487 102.571-495.756 284.916l59.832 34.19c139.608-239.332 447.321-321.957 686.651-182.348 76.929 42.737 139.608 108.267 182.348 182.348l59.832-34.19c-51.284-85.476-122.514-156.704-207.991-207.991-91.173-54.135-188.045-76.929-284.916-76.929zM936.528 385.318c-68.38 116.818-176.649 199.443-304.862 233.632s-264.973 17.094-381.789-51.284c-76.929-42.737-139.608-108.267-185.196-185.196l-59.832 34.19c51.284 85.476 122.514 159.555 207.991 207.991 270.673 156.704 621.121 62.682 777.825-207.991l-54.135-31.341z" />
|
||||
<glyph unicode="" glyph-name="face-1" horiz-adv-x="1103" d="M847.49 685.688c-23.283 0-40.746 17.463-40.746 40.746s17.463 40.746 40.746 40.746c23.283 0 40.746-17.463 40.746-40.746 1.94-23.283-17.463-40.746-40.746-40.746zM847.49 732.253c-1.94 0-5.822-1.94-5.822-5.822 0-5.822 9.702-5.822 9.702 0 1.94 3.879 0 5.822-3.879 5.822zM244.056 584.793c-36.867 0-73.73 9.702-106.717 29.105s-58.209 44.625-77.612 77.612l40.746 23.283c15.524-25.224 34.924-46.568 60.149-60.149 79.552-46.568 182.388-19.403 228.956 60.149l40.746-23.283c-40.746-67.911-112.536-106.717-186.267-106.717zM824.206 953.448h46.568v-89.254h-46.568v89.254zM937.966 833.318l51.661 65.511 36.566-28.834-51.661-65.511-36.566 28.834zM669.986 872.923l36.975 28.309 50.721-66.246-36.975-28.309-50.721 66.246zM546.743 251.059c-85.371 0-155.223 69.851-155.223 155.223h46.568c0-60.149 48.508-110.597 110.597-110.597 60.149 0 110.597 48.508 110.597 110.597h46.568c-3.879-85.371-73.73-155.223-159.105-155.223zM523.461 80.315h46.568v-102.835h-46.568v102.835zM288.562 88.444l61.264 77.709 36.57-28.828-61.264-77.709-36.57 28.828zM710.636 136.099l37.021 28.252 58.859-77.128-37.021-28.252-58.859 77.128zM849.43 582.85c-19.403 0-36.867 1.94-56.27 7.762-54.327 15.524-100.895 50.447-128.061 98.955l40.746 23.283c21.343-38.806 58.209-65.971 100.895-77.612s87.314-5.822 126.12 15.524c25.224 15.524 46.568 36.867 62.089 62.089l40.746-23.283c-19.403-32.984-46.568-60.149-79.552-79.552-34.924-17.463-69.851-27.165-106.717-27.165zM993.011 740.015c-46.568 79.552-147.464 108.657-227.015 62.089-25.224-15.524-46.568-36.867-62.089-62.089l-40.746 23.283c19.403 32.984 46.568 60.149 79.552 79.552 102.835 58.209 232.836 23.283 291.045-79.552l-40.746-23.283z" />
|
||||
<glyph unicode="" glyph-name="face-2" horiz-adv-x="1013" d="M636.961 62.376h-261.201v40.593h218.843v47.652h42.358zM462.24 11.194h67.066v-42.358h-67.066v42.358zM769.326 701.259c-21.179 0-37.061 15.885-37.061 37.061s15.885 37.061 37.061 37.061c21.179 0 37.061-15.885 37.061-37.061 1.765-21.179-15.885-37.061-37.061-37.061zM769.326 743.617c-1.765 0-5.294-1.765-5.294-5.294 0-5.294 8.824-5.294 8.824 0 1.765 3.529 0 5.294-3.529 5.294zM89.849 750.676l-37.061 21.179c52.946 93.539 171.194 125.307 264.731 72.361 30.002-17.649 54.711-42.358 72.361-72.361l-37.061-21.179c-14.12 22.944-33.532 42.358-56.475 56.475-72.361 40.593-165.897 15.885-206.491-56.475zM495.772 330.636c-33.532 0-67.066 8.824-97.068 26.473s-52.946 40.593-70.595 70.595l37.061 21.179c14.12-22.944 31.768-42.358 54.711-54.711 35.297-21.179 75.89-26.473 114.716-15.885s72.361 35.297 91.774 70.595l37.061-21.179c-26.473-44.122-67.066-77.655-118.248-90.007-15.885-5.294-33.532-7.059-49.417-7.059zM748.15 944.813h42.358v-81.184h-42.358v81.184zM851.709 835.654l46.992 59.588 33.26-26.227-46.992-59.588-33.26 26.227zM607.928 871.613l33.632 25.749 46.134-60.255-33.632-25.749-46.134 60.255zM638.726 750.676l-37.061 21.179c17.649 30.002 40.593 52.946 70.595 70.595 44.122 26.473 97.068 33.532 146.485 19.415s91.774-45.888 118.248-90.007l-37.061-21.179c-21.179 35.297-52.946 60.005-91.774 70.595s-79.419 5.294-114.716-15.885c-22.944-12.353-42.358-31.768-54.711-54.711zM220.451 701.259c-21.179 0-37.061 15.885-37.061 37.061s15.885 37.061 37.061 37.061 37.061-15.885 37.061-37.061c1.765-21.179-15.885-37.061-37.061-37.061zM220.451 743.617c-1.765 0-5.294-1.765-5.294-5.294 0-5.294 8.824-5.294 8.824 0 1.765 3.529 0 5.294-3.529 5.294z" />
|
||||
<glyph unicode="" glyph-name="face-3" horiz-adv-x="1013" d="M226.738 602.24c-79.377 0-142.517 64.943-142.517 142.517s64.943 142.517 142.517 142.517 144.32-63.141 144.32-142.517-64.943-142.517-144.32-142.517zM226.738 845.782c-55.924 0-101.024-45.101-101.024-101.024s46.904-101.024 101.024-101.024 101.024 45.101 101.024 101.024-45.101 101.024-101.024 101.024zM787.782 602.24c-79.377 0-142.517 64.943-142.517 142.517s64.943 142.517 142.517 142.517 142.517-64.943 142.517-142.517-63.141-142.517-142.517-142.517zM787.782 845.782c-55.924 0-101.024-45.101-101.024-101.024s45.101-101.024 101.024-101.024 101.024 45.101 101.024 101.024-45.101 101.024-101.024 101.024zM486.516 62.846h43.295v-82.986h-43.295v82.986zM508.163 328.035c-19.845 0-39.687 7.217-54.121 21.647l-95.613 95.613 30.667 30.667 95.613-95.613c12.628-12.628 36.081-12.628 48.708 0l95.613 95.613 30.667-30.667-95.613-95.613c-16.237-14.43-36.081-21.647-55.924-21.647zM789.586 706.873c-21.647 0-37.884 16.237-37.884 37.884s16.237 37.884 37.884 37.884c21.647 0 37.884-16.237 37.884-37.884s-18.039-37.884-37.884-37.884zM789.586 750.168c-1.803 0-5.412-1.803-5.412-5.412 0-5.412 9.020-5.412 9.020 0 0 3.608-1.803 5.412-3.608 5.412zM508.163 52.022c-32.473 0-66.749 9.020-97.416 25.256-30.667 18.039-55.924 43.295-73.963 73.963l37.884 21.647c14.43-23.453 34.275-43.295 57.729-57.729 73.963-43.295 169.575-16.237 211.069 57.729l37.884-21.647c-37.884-63.141-104.633-99.218-173.184-99.218zM766.135 955.826h43.295v-82.986h-43.295v82.986zM871.904 844.14l48.034 60.908 33.997-26.809-48.034-60.908-33.997 26.809zM622.752 880.963l34.375 26.321 47.156-61.591-34.375-26.321-47.156 61.591zM226.738 706.873c-21.647 0-37.884 16.237-37.884 37.884s16.237 37.884 37.884 37.884 37.884-16.237 37.884-37.884c1.803-21.647-16.237-37.884-37.884-37.884zM226.738 750.168c-1.803 0-5.412-1.803-5.412-5.412 0-5.412 9.020-5.412 9.020 0 1.803 3.608 0 5.412-3.608 5.412z" />
|
||||
<glyph unicode="" glyph-name="heart-sparks" horiz-adv-x="771" d="M408.161-31.548l-25.575 34.709c38.362 29.231 76.726 60.287 104.129 85.861 63.94 51.152 122.4 109.611 175.376 171.724 38.362 47.499 54.806 89.515 51.152 135.189-1.827 43.845-20.095 84.036-51.152 113.264-14.614 12.788-32.885 23.748-52.979 29.231-36.538 12.788-78.554 9.134-113.264-9.134-14.614-9.134-27.402-18.268-40.192-31.055l-29.231 31.055c14.614 14.614 31.055 27.402 49.323 38.362 45.67 23.748 98.65 27.402 147.974 10.961 25.575-7.307 47.499-20.095 67.594-38.362 40.192-36.538 63.94-89.515 65.767-142.496 3.654-56.633-16.441-109.611-62.113-166.244-54.806-63.94-115.093-124.225-180.858-177.205-27.402-21.921-65.767-54.806-105.957-85.861zM371.624 947.643h43.845v-96.822h-43.845v96.822zM545.217 797.863l56.64 71.658 34.395-27.187-56.64-71.658-34.395 27.187zM151.153 842.614l34.866 26.587 55.391-72.634-34.866-26.587-55.391 72.634zM322.296 12.294c-10.961 9.134-25.575 21.921-38.362 32.885l-14.614 12.788c-63.94 52.979-126.053 113.264-179.030 177.205-45.67 56.633-65.767 107.786-62.113 166.244 1.827 54.806 25.575 105.957 65.767 142.496 20.095 18.268 42.016 31.055 67.594 38.362 47.499 16.441 100.475 12.788 146.149-10.961 40.192-25.575 73.074-56.633 100.475-93.168l-32.885-27.402c-23.748 32.885-52.979 60.287-87.69 82.208-32.885 18.268-74.901 20.095-111.439 7.307-20.095-7.307-36.538-16.441-52.979-29.231-31.055-29.231-49.323-69.42-51.152-113.264-1.827-47.499 12.788-89.515 51.152-137.014 52.979-62.113 111.439-118.746 173.551-171.724l14.614-12.788c12.788-10.961 27.402-23.748 36.538-31.055l-25.575-32.885zM389.891-44.336l-27.402 34.709 18.268 14.614c32.885 25.575 67.594 52.979 105.957 85.861 63.94 51.152 122.4 109.611 175.376 171.724 38.362 47.499 54.806 89.515 51.152 135.189l43.845 1.827c3.654-56.633-16.441-109.611-62.113-166.244-54.806-63.94-115.093-124.225-180.858-177.205-38.362-32.885-73.074-62.113-105.957-85.861l-18.268-14.614z" />
|
||||
<glyph unicode="" glyph-name="heart" horiz-adv-x="764" d="M402.133 123.917l-23.669 32.772c38.235 29.131 76.469 60.083 103.778 85.572 63.724 50.98 121.985 109.241 174.785 171.145 38.235 47.337 54.621 89.215 50.98 134.731-1.82 43.696-20.028 83.75-50.98 112.883-14.566 12.746-32.772 23.669-52.8 29.131-36.414 12.746-78.289 9.103-112.883-9.103-14.566-9.103-27.309-18.207-40.055-30.952l-29.131 30.952c14.566 14.566 30.952 27.309 49.159 38.235 45.516 23.669 98.316 27.309 147.476 10.925 25.489-7.281 47.337-20.028 67.364-38.235 40.055-36.414 63.724-89.215 65.544-142.013 3.641-56.441-16.386-109.241-61.903-165.682-54.621-63.724-114.704-123.805-180.247-176.607-27.309-21.848-67.364-52.8-107.421-83.75zM318.383 165.794c-10.925 9.103-25.489 21.848-38.235 32.772l-14.566 12.746c-63.724 52.8-125.627 112.883-178.427 176.607-45.516 56.441-65.544 107.421-61.903 165.682 1.82 54.621 25.489 105.599 65.544 142.013 20.028 18.207 41.875 30.952 67.364 38.235 47.337 16.386 100.138 12.746 145.654-10.925 40.055-25.489 72.827-56.441 100.138-92.855l-32.772-27.309c-23.669 32.772-52.8 60.083-87.392 81.93-32.772 18.207-74.649 20.028-111.063 7.281-20.028-7.281-36.414-16.386-52.8-29.131-30.952-29.131-49.159-69.187-50.98-112.883-1.82-47.337 12.746-89.215 50.98-136.551 52.8-61.903 111.063-118.344 172.965-171.145l14.566-12.746c12.746-10.925 27.309-23.669 36.414-30.952l-25.489-32.772zM385.748 109.353l-27.309 34.594 18.207 14.566c32.772 25.489 67.364 52.8 105.599 85.572 63.724 50.98 121.985 109.241 174.785 171.145 38.235 47.337 54.621 89.215 50.98 134.731l43.696 1.82c3.641-56.441-16.386-109.241-61.903-165.682-54.621-63.724-114.704-123.805-180.247-176.607-38.235-32.772-72.827-61.903-105.599-85.572l-18.207-14.566z" />
|
||||
<glyph unicode="" glyph-name="medal" horiz-adv-x="907" d="M710.019-62.954l-42.286 5.287c3.523 37 12.334 74 26.429 109.238l5.287 15.857 15.857-1.762c37-5.287 74-15.857 107.476-31.713l-17.619-38.762c-26.429 10.57-52.856 19.381-79.285 26.429-5.287-28.19-12.334-56.381-15.857-84.572zM507.929 150.347l36.619 21.148 103.092-178.52-36.619-21.148-103.092 178.52zM694.354 242.892l36.627 21.126 96.827-167.878-36.627-21.126-96.827 167.878zM181.446-62.954c-3.523 28.19-8.81 56.381-17.619 82.809-28.19-5.287-54.619-14.096-81.049-26.429l-17.619 38.762c35.239 15.857 70.475 26.429 107.476 31.713l15.857 1.762 5.287-15.857c14.096-35.239 22.906-72.238 26.429-109.238l-38.762-3.523zM244.322-5.182l103.135 178.478 36.611-21.156-103.135-178.478-36.611 21.156zM64.86 96.444l96.809 167.898 36.632-21.121-96.809-167.898-36.632 21.121zM424.588 744.001h42.286v-95.142h-42.286v95.142zM546.724 614.377l56.795 71.893 33.182-26.213-56.795-71.893-33.182 26.213zM255.658 660.427l33.618 25.65 54.507-71.436-33.618-25.65-54.507 71.436zM445.73 194.285c-202.62 0-366.477 163.857-366.477 366.477s163.857 368.24 366.477 368.24 366.477-163.857 366.477-366.477-163.857-368.24-366.477-368.24zM445.73 886.716c-179.714 0-324.191-146.237-324.191-324.191 0-179.714 146.237-324.191 324.191-324.191 179.714 0 324.191 146.237 324.191 324.191 1.762 177.953-144.477 324.191-324.191 324.191z" />
|
||||
<glyph unicode="" glyph-name="megaphone" horiz-adv-x="1473" d="M301.758 20.312v57.992c38.66 0 77.321 14.499 103.902 43.493 26.58 26.58 43.493 65.24 43.493 103.902h57.992c-2.415-113.567-94.236-205.387-205.387-205.387zM67.374 223.282v439.771l195.721-77.321-21.747-53.159-118.4 45.911v-270.628l118.4 48.327 21.747-53.159zM937.251-15.934l-620.994 299.625v309.288l620.994 299.625v-908.538zM371.832 319.934l507.427-246.465v727.313l-507.427-244.048v-236.8zM1319.030 467.332h147.395v-57.992h-147.395v57.992zM1200.19 158.853l35.956 45.501 109.96-86.892-35.956-45.501-109.96 86.892zM1201.794 716.695l109.592 83.423 35.124-46.143-109.592-83.423-35.124 46.143z" />
|
||||
<glyph unicode="" glyph-name="phone" horiz-adv-x="871" d="M691.62 770.026l-71.318-71.317 32.296-32.294 71.317 71.317-32.294 32.294zM801.738 542.789l-94.503-11.057 5.308-45.361 94.503 11.057-5.308 45.361zM455.681 850.946l-12.29-92.436 45.274-6.019 12.29 92.436-45.274 6.019zM696.313 6.505c-1.902 0-5.709 0-7.611 0-390.114 9.516-669.856 287.353-681.274 675.565 0 1.902 0 49.478 17.127 79.926 17.127 32.351 68.508 72.315 78.024 79.926 26.642 24.739 66.606 24.739 93.248-1.902l114.179-114.179c26.642-26.642 26.642-68.508 0-93.248l-55.187-57.090c-9.516-9.516-9.516-22.836 0-32.351l293.062-293.062c9.516-9.516 22.836-9.516 32.351 0l55.187 55.187c26.642 26.642 68.508 26.642 93.248 0l114.179-114.179c24.739-24.739 26.642-66.606 1.902-93.248-7.611-9.516-49.478-62.799-79.926-79.926-30.448-11.418-53.284-11.418-68.508-11.418zM148.249 815.279c-5.709 0-11.418-1.902-15.224-5.709l-1.902-1.902c-15.224-11.418-55.187-45.672-68.508-66.606-9.516-15.224-11.418-45.672-9.516-57.090 9.516-363.472 272.128-624.184 639.407-633.698 11.418 0 28.544 0 53.284 11.418 17.127 9.516 49.478 45.672 64.701 66.606l1.902 1.902c9.516 9.516 9.516 22.836 0 32.351l-114.179 114.179c-7.611 7.611-22.836 7.611-32.351 0l-55.187-55.187c-26.642-26.642-68.508-26.642-93.248 0l-294.965 291.16c-26.642 26.642-26.642 68.508 0 93.248l55.187 55.187c9.516 9.516 9.516 22.836 0 32.351l-114.179 114.179c-3.807 5.709-9.516 7.611-15.224 7.611z" />
|
||||
<glyph unicode="" glyph-name="plus" d="M530.601 917.643h-102.298v-943.933h102.298v943.933zM953.743 494.499h-943.933v-102.298h943.933v102.298z" />
|
||||
<glyph unicode="" glyph-name="spark" horiz-adv-x="1883" d="M861.919 796.748h181.462v-368.595h-181.462v368.595zM1518.035 238.759l214.328 271.506 142.43-112.434-214.328-271.506-142.43 112.434zM35.557 398.718l144.244 110.094 209.865-274.966-144.244-110.094-209.865 274.966z" />
|
||||
<glyph unicode="" glyph-name="tag" horiz-adv-x="844" d="M490.344-9.609l-447.874 447.874v356.353h356.353l440.085-440.085-29.209-29.209-428.4 426.454h-295.987v-295.987l405.033-405.033 258.989 260.936 31.156-31.156-290.145-290.145zM274.196 479.156c-44.787 0-81.786 36.998-81.786 81.786s36.998 81.786 81.786 81.786c44.787 0 81.786-36.998 81.786-81.786s-36.998-81.786-81.786-81.786zM274.196 607.675c-25.313 0-48.682-21.42-48.682-48.682 0-25.313 21.42-48.682 48.682-48.682s48.682 21.42 48.682 48.682c0 27.263-23.367 48.682-48.682 48.682z" />
|
||||
<glyph unicode="" glyph-name="talk" horiz-adv-x="987" d="M343.903-83.22v214.003h45.452v-115.524l238.623 208.322h7.575c172.339 0 303.014 140.143 303.014 327.633 0 179.914-145.825 325.739-325.739 327.633h-227.26c-179.914 0-325.739-145.825-327.633-325.739 0-181.808 147.719-327.633 327.633-327.633v0-45.452c-206.427 0-373.084 166.656-373.084 371.19 1.894 206.427 168.551 373.084 373.084 373.084v0h227.26c204.534 0 371.19-166.656 371.19-371.19 0-208.322-145.825-367.404-340.89-371.19l-299.226-265.137z" />
|
||||
<glyph unicode="" glyph-name="cross" horiz-adv-x="1017" d="M579.952 307.286l76.888 76.997 343.281-342.795-76.888-76.997-343.281 342.795zM-0.049 887.658l76.943 76.943 343.038-343.038-76.943-76.943-343.038 343.038zM0.721 38.043l923.318 923.318 76.943-76.943-923.318-923.318-76.943 76.943z" />
|
||||
=======
|
||||
<metadata>
|
||||
<json>
|
||||
<![CDATA[
|
||||
{
|
||||
"fontFamily": "icomoon",
|
||||
"majorVersion": 1,
|
||||
"minorVersion": 0,
|
||||
"version": "Version 1.0",
|
||||
"fontId": "icomoon",
|
||||
"psName": "icomoon",
|
||||
"subFamily": "Regular",
|
||||
"fullName": "icomoon",
|
||||
"description": "Font generated by IcoMoon."
|
||||
}
|
||||
]]>
|
||||
</json>
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="icomoon" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="arrow-left" data-tags="arrow-left" horiz-adv-x="1233" d="M1193.594 394.643h-1062.068v121.96h1062.068v-121.96zM527.897-37.299l86.389 86.389-437.024 406.533 437.024 406.533-86.389 86.389-528.493-492.922z" />
|
||||
<glyph unicode="" glyph-name="arrow-long-right" data-tags="Arrow-long-Digital-black" horiz-adv-x="1720" d="M27.014 498.697h1578.188v-105.8h-1578.188v105.8zM1256.94 873.404l-70.533-74.942 374.709-352.668-374.709-352.668 70.533-74.942 458.468 427.61z" />
|
||||
<glyph unicode="" glyph-name="arrow-long-left" data-tags="arrow-long-left" horiz-adv-x="1720" d="M1692.974 414.939h-1578.163v105.799h1578.163v-105.799zM463.067 40.235l70.531 74.941-374.703 352.662 374.703 348.253-70.531 79.349-458.46-427.603z" />
|
||||
<glyph unicode="" glyph-name="arrow-right" data-tags="Arrow-short-Digital-black" horiz-adv-x="1233" d="M39.406 501.357h1062.069v-121.96h-1062.069v121.96zM705.103 933.299l-86.389-86.389 437.024-406.533-437.024-406.533 86.389-86.389 528.493 492.922z" />
|
||||
<glyph unicode="" glyph-name="ballon" data-tags="ballon" horiz-adv-x="1013" d="M270.94 546.983c-17.834 67.772 1.784 139.111 51.722 189.049l30.319-30.319c-39.237-39.237-53.504-94.525-39.237-148.029l-42.803-10.701zM511.711 52.959v42.803c99.875 0 117.71 55.288 135.545 103.443 3.566 10.701 7.135 19.618 10.701 26.753 19.618 44.586 48.154 82.041 82.041 115.927l28.536-30.319c-30.319-28.536-55.288-64.206-73.122-103.443-3.566-5.35-5.35-14.268-8.919-23.186-16.051-48.154-44.586-131.979-174.782-131.979zM511.711 52.959c-108.793 0-151.596 96.308-176.565 148.029-3.566 5.35-5.35 12.485-7.135 16.051-17.834 35.671-41.019 65.989-67.772 92.742l28.536 30.319c32.103-30.319 58.854-65.989 78.474-105.227 1.784-5.35 5.35-10.701 8.919-17.834 23.186-49.938 55.288-123.060 137.328-123.060v-41.019h-1.784zM144.312 579.087c-3.566 98.091 32.103 190.832 99.875 263.956s158.731 112.359 256.821 115.927c99.875 3.566 192.616-32.103 265.74-99.875 71.338-67.772 114.142-158.731 115.927-256.821 0-7.135 0-16.051 0-23.186l-42.803 1.784c0 7.135 0 14.268 0 21.403-3.566 87.391-39.237 167.647-103.443 228.287-64.206 58.854-146.246 90.958-233.636 87.391s-167.647-39.237-228.287-103.443c-58.854-64.206-90.958-146.246-87.391-233.636l-42.803-1.784zM260.24 311.564c-73.122 69.556-115.927 167.647-115.927 267.522h42.803c0-89.176 37.453-176.565 101.66-237.204l-28.536-30.319zM766.75 311.564l-28.536 30.319c64.206 60.639 101.66 148.029 101.66 237.204h42.803c0-99.875-41.019-197.967-115.927-267.522zM590.183-64.751h-42.803c0 8.919-14.268 30.319-32.103 49.938-17.834-19.618-32.103-41.019-32.103-49.938h-42.803c0 35.671 46.369 82.041 60.639 96.308l14.268 14.268 14.268-14.268c14.268-14.268 60.639-60.639 60.639-96.308z" />
|
||||
<glyph unicode="" glyph-name="bars" data-tags="bars" horiz-adv-x="1022" d="M50.027-49.221h-44.28v450.175h142.063v-42.434h-97.784zM813.849-49.221h-44.28v736.146h142.063v-42.434h-97.784zM431.937-49.221h-44.28v594.083h142.063v-44.28h-97.784zM505.737 686.925l-284.126 188.187-177.117-177.117-31.364 31.364 202.947 202.947 289.662-191.877 321.026 219.553 23.984-35.054zM859.973 803.158h-42.434v127.303h-127.303v42.434h169.739zM959.602 688.77h44.28v-739.836h-44.28v739.836zM577.692 546.706h44.28v-595.928h-44.28v595.928zM197.626 402.798h44.28v-452.019h-44.28v452.019z" />
|
||||
<glyph unicode="" glyph-name="bottle" data-tags="bottle" horiz-adv-x="571" d="M449-58.977h-307.909v494.087h41.532v-452.555h226.277v452.555h40.1zM218.425 516.742l-28.643 30.075c22.914 21.482 35.804 50.125 35.804 81.632v8.593h41.532v-8.593c0-42.964-17.185-83.063-48.693-111.706zM374.529 516.742c-31.507 30.075-48.693 68.742-48.693 111.706v8.593h41.532v-8.593c0-31.507 12.889-60.149 35.804-81.632l-28.643-30.075zM450.432 435.111h-41.532c0 31.507-12.889 60.149-35.804 81.632l28.643 30.075c31.507-28.643 48.693-70.175 48.693-111.706zM182.622 435.111h-41.532c0 42.964 17.185 83.063 48.693 111.706l28.643-30.075c-22.914-21.482-35.804-50.125-35.804-81.632zM225.586 823.22h41.532v-187.61h-41.532v187.61zM325.836 823.22h41.532v-187.61h-41.532v187.61zM221.29 894.827h141.782v-41.532h-141.782v41.532zM218.425 965.001h144.646v-41.532h-144.646v41.532z" />
|
||||
<glyph unicode="" glyph-name="cake-chart" data-tags="cake-chart" horiz-adv-x="1021" d="M458.341-44.346c-22.295 0-46.448 1.857-68.742 5.574-120.764 18.579-224.807 81.748-297.266 180.217-70.601 96.611-100.327 217.376-81.748 336.282 29.727 195.081 180.217 347.429 375.297 379.014l7.432-42.732c-174.644-29.727-312.129-167.212-338.139-341.855-33.443-222.948 118.906-432.892 343.713-468.193 222.948-33.443 432.892 118.906 468.193 343.713l42.732-7.432c-37.159-224.807-230.38-384.586-451.472-384.586zM1010.14 507.453h-453.33v453.33h22.295c237.811 0 431.035-193.222 431.035-431.035v-22.295zM601.399 552.043h364.15c-11.148 195.081-169.070 353.003-364.15 364.15v-364.15zM915.386 386.689h-479.34v477.482h44.589v-432.892h434.751z" />
|
||||
<glyph unicode="" glyph-name="stopwatch" data-tags="stopwatch" horiz-adv-x="933" d="M618.798 188.608l-165.156 198.187v351.686h44.689v-334.199l155.441-184.586zM471.129-38.725c-242.877 0-439.121 196.245-439.121 439.121s196.245 439.121 439.121 439.121 439.121-196.245 439.121-439.121-196.245-439.121-439.121-439.121zM471.129 792.885c-215.674 0-392.489-176.814-392.489-392.489s176.814-392.489 392.489-392.489 392.489 176.814 392.489 392.489-174.872 392.489-392.489 392.489zM447.812 971.642h46.632v-170.985h-46.632v170.985zM317.631 985.243h308.939v-46.632h-308.939v46.632z" />
|
||||
<glyph unicode="" glyph-name="cloud" data-tags="cloud" horiz-adv-x="1655" d="M373.421 86.077h1049.578v-66.816h-1049.578v66.816zM256.492 22.044c-119.713 11.136-217.154 100.225-242.21 217.154-30.624 147.554 61.249 295.106 206.017 331.299 0 125.282 83.521 236.642 208.803 264.482 144.769 36.193 295.106-52.896 331.299-200.45l-64.033-16.704c-27.84 111.361-139.202 178.178-250.562 153.121-103.009-25.056-169.825-119.713-155.905-222.723l2.783-30.624-44.545-8.352c-116.929-25.056-192.098-139.202-167.041-258.913 19.489-89.089 91.872-155.905 183.745-167.041l-8.352-61.249zM1420.215 19.26l-2.783 66.816c72.385 8.352 130.848 64.033 147.554 133.634 19.489 94.658-41.761 186.53-133.634 206.017l-41.761 8.352 2.783 30.624c11.136 83.521-41.761 158.689-122.497 178.178h-5.568c-86.304 19.489-175.393-33.408-197.666-122.497l-64.033 16.704c30.624 122.497 155.905 200.45 278.402 169.825h5.568c103.009-25.056 172.61-116.929 172.61-219.937 119.713-33.408 194.881-155.905 169.825-278.402-22.272-103.009-105.792-178.178-208.803-189.313zM690.8 642.881c-22.272 94.658 25.056 194.881 111.361 236.642 103.009 50.113 228.29 5.568 278.402-97.441l-58.465-27.84c-33.408 69.6-119.713 100.225-189.313 66.816-61.249-27.84-91.872-97.441-77.952-161.473l-64.033-16.704z" />
|
||||
<glyph unicode="" glyph-name="dart" data-tags="dart" horiz-adv-x="1013" d="M459.718 120.99v0c-143.177 0-259.84 116.662-258.073 259.84 0 143.177 116.662 258.073 258.073 258.073v0c44.19 0 88.381-12.374 127.268-33.583l-21.213-35.354c-31.816 17.675-68.937 28.282-106.058 28.282v0c-120.199 0-217.417-97.219-217.417-217.417s97.219-217.417 217.417-217.417v0c120.199 0 217.417 97.219 217.417 217.417 0 37.121-10.606 76.007-28.282 107.825l35.354 21.213c22.98-38.888 35.354-84.845 35.354-129.036-1.767-143.177-118.431-259.84-259.84-259.84zM459.718-50.468c-134.34 0-266.91 61.866-349.988 178.529-141.408 194.437-97.219 464.884 95.451 604.525 153.782 109.592 358.826 109.592 509.074-3.534l-22.98-33.583c-137.874 102.52-321.706 102.52-459.58 3.534-174.995-125.5-213.881-369.432-88.381-544.425s369.432-213.881 544.425-88.381c84.845 60.098 139.641 150.248 157.318 254.536 15.908 102.52-7.071 205.044-68.937 289.889l33.583 24.747c67.17-93.684 93.684-208.578 76.007-321.706s-79.543-213.881-173.227-281.051c-77.776-56.563-166.156-83.078-252.769-83.078zM493.941 444.747l315.015 314.925 29.993-30.002-315.015-314.925-29.993 30.002zM892.784 713.143c-12.374 0-24.747 0-37.121 1.767s-26.514 1.767-40.655 3.534l-17.675 1.767v17.675c-1.767 14.141-3.534 26.514-3.534 38.888-1.767 24.747-1.767 49.494 0 76.007 0 21.213 10.606 40.655 26.514 56.563 12.374 14.141 31.816 21.213 53.029 19.444 28.282-1.767 53.029-21.213 61.866-47.724v-3.534c1.767-7.071 1.767-12.374 1.767-19.444 7.071 0 14.141-1.767 21.213-3.534 19.444-5.302 35.354-19.444 42.423-35.354 3.534-7.071 7.071-17.675 7.071-26.514 0-19.444-7.071-38.888-19.444-54.796-15.908-15.908-37.121-24.747-60.098-24.747-10.606 0-22.98 0-35.354 0zM836.22 759.101c7.071 0 14.141-1.767 22.98-1.767 22.98-1.767 45.957-1.767 68.937 0h3.534c10.606 0 22.98 5.302 30.049 12.374 5.302 5.302 8.839 14.141 8.839 22.98 0 3.534-1.767 7.071-3.534 10.606-3.534 7.071-8.839 12.374-15.908 14.141-8.839 1.767-17.675 1.767-28.282 0l-26.514-3.534 1.767 26.514c0 10.606 0 19.444-1.767 30.049-3.534 10.606-14.141 17.675-24.747 17.675-8.839 0-15.908-3.534-22.98-8.839-7.071-7.071-12.374-17.675-12.374-28.282-1.767-22.98-1.767-45.957 0-68.937-1.767-8.839-1.767-15.908 0-22.98zM459.718 287.146c-51.262 0-91.917 40.655-91.917 91.917s40.655 91.917 91.917 91.917 91.917-40.655 91.917-91.917-40.655-91.917-91.917-91.917zM459.718 430.325c-28.282 0-51.262-22.98-51.262-51.262s22.98-51.262 51.262-51.262 51.262 22.98 51.262 51.262-22.98 51.262-51.262 51.262z" />
|
||||
<glyph unicode="" glyph-name="eye-1" data-tags="eye-1" horiz-adv-x="1739" d="M1534.010 360.702h-91.094l-11.387 22.774c-68.321 117.664-167.006 216.348-284.669 284.669-371.967 216.348-850.211 87.298-1066.56-284.669l-79.707 45.547c239.122 417.515 774.299 557.952 1188.019 318.829 125.254-72.116 227.735-174.597 303.647-296.056h41.752v-91.094zM1499.849 451.796h204.962v-91.094h-204.962v91.094zM786.278 79.828c-148.028 0-269.487 121.46-269.487 269.487s121.46 269.487 269.487 269.487 269.487-121.46 269.487-269.487-121.46-269.487-269.487-269.487zM786.278 527.707c-98.685 0-178.392-79.707-178.392-178.392s79.707-178.392 178.392-178.392 178.392 79.707 178.392 178.392-79.707 178.392-178.392 178.392z" />
|
||||
<glyph unicode="" glyph-name="eye-2" data-tags="eye-2" horiz-adv-x="1198" d="M1068.604 184.358h-62.959l-7.87 15.74c-47.219 81.322-115.425 149.528-196.747 196.747-257.084 149.528-587.619 60.335-737.147-196.747l-55.089 31.48c165.269 285.94 535.152 385.624 821.092 220.357 86.569-49.842 157.398-120.671 209.864-204.617h28.856c0 0 0-62.959 0-62.959zM1044.994 247.317h141.658v-62.959h-141.658v62.959zM549.19-9.765c-102.309 0-186.254 83.946-186.254 186.254s83.946 186.254 186.254 186.254 186.254-83.946 186.254-186.254-83.946-186.254-186.254-186.254zM549.19 299.784c-68.206 0-123.296-55.089-123.296-123.296s55.089-123.296 123.296-123.296 123.296 55.089 123.296 123.296c0 68.206-55.089 123.296-123.296 123.296zM528.204 926.752h62.959v-173.137h-62.959v173.137zM836.173 652.924l102.334 129.768 49.435-38.985-102.334-129.768-49.435 38.985zM137.111 744.075l50.071 38.165 98.595-129.351-50.071-38.165-98.595 129.351z" />
|
||||
<glyph unicode="" glyph-name="eye-3" data-tags="eye-3" horiz-adv-x="1337" d="M697.122 515.16v0c-262.241 0-505.296 140.716-636.418 364.58l67.16 38.378c118.327-201.48 335.8-329.402 569.258-329.402v0c233.46 0 450.93 124.725 569.258 329.402l67.16-38.378c-131.122-223.867-374.175-364.58-636.418-364.58zM633.162 176.165h76.753v-211.072h-76.753v211.072zM149.619 188.967l124.696 158.24 60.283-47.503-124.696-158.24-60.283 47.503zM1006.499 295.077l61.042 46.529 120.197-157.694-61.042-46.529-120.197 157.694z" />
|
||||
<glyph unicode="" glyph-name="eye-4" data-tags="eye-4" horiz-adv-x="1379" d="M693.748-70.634c-65.084 0-117.964 52.88-117.964 117.964s52.88 117.964 117.964 117.964 117.964-52.88 117.964-117.964-52.88-117.964-117.964-117.964zM693.748 71.735c-12.203 0-24.407-12.203-24.407-24.407s12.203-24.407 24.407-24.407c12.203 0 24.407 12.203 24.407 24.407s-12.203 24.407-24.407 24.407zM644.936 897.483h97.625v-349.823h-97.625v349.823zM1086.422 401.88l206.769 261.739 76.607-60.518-206.769-261.739-76.607 60.518zM22.445 606.631l77.612 59.22 199.866-261.94-77.612-59.22-199.866 261.94zM87.658 120.548l-85.422 48.813c69.151 122.032 170.845 219.657 292.875 292.875 183.047 105.761 398.636 134.235 602.023 81.354 203.386-56.948 378.298-187.115 484.058-370.162l-85.422-48.813c-93.558 162.709-244.063 276.605-423.043 325.418s-370.162 24.407-528.804-69.151c-105.761-69.151-195.25-154.573-256.267-260.334z" />
|
||||
<glyph unicode="" glyph-name="eye-5" data-tags="eye-5" horiz-adv-x="1020" d="M512.41-57.338c-223.412 0-404.27 180.859-404.27 404.27s180.859 406.931 406.931 406.931c223.412 0 406.931-180.859 406.931-406.931s-186.178-404.27-409.59-404.27zM512.41 690.031c-188.836 0-343.099-154.262-343.099-343.099s154.262-343.099 343.099-343.099 343.099 154.262 343.099 343.099c0 191.498-154.262 343.099-343.099 343.099zM515.070 264.481c-45.215 0-82.452 37.236-82.452 82.452s37.236 82.452 82.452 82.452 82.452-37.236 82.452-82.452-37.236-82.452-82.452-82.452zM515.070 368.21c-10.639 0-21.278-7.979-21.278-21.278s7.979-21.278 21.278-21.278 18.617 10.639 18.617 21.278-7.979 21.278-18.617 21.278zM480.494 966.637h63.834v-255.331h-63.834v255.331zM799.409 606.416l150.084 189.873 50.077-39.584-150.084-189.873-50.077 39.584zM29.206 757.88l50.721 38.753 145.322-190.205-50.721-38.753-145.322 190.205z" />
|
||||
<glyph unicode="" glyph-name="eye-6" data-tags="eye-6" horiz-adv-x="1020" d="M509.725 234.041c-46.895 0-87.929 38.103-87.929 87.929s38.103 87.929 87.929 87.929 87.929-38.103 87.929-87.929-38.103-87.929-87.929-87.929zM509.725 339.555c-8.793 0-17.585-8.793-17.585-17.585s8.793-17.585 17.585-17.585c8.793 0 17.585 8.793 17.585 17.585s-5.862 17.585-17.585 17.585zM474.553 946.261h70.343v-257.924h-70.343v257.924zM797.941 584.125l152.62 193.193 55.198-43.605-152.62-193.193-55.198 43.605zM15.553 732.705l55.909 42.691 147.64-193.35-55.909-42.691-147.64 193.35zM512.656-59.055c-202.236 0-398.61 105.514-509.986 293.096l61.55 35.172c143.617-246.2 460.16-331.197 706.359-187.58 79.135 43.964 143.617 111.376 187.58 187.58l61.55-35.172c-52.757-87.929-126.032-161.202-213.959-213.959-93.791-55.688-193.443-79.135-293.096-79.135zM958.161 374.727c-70.343 120.169-181.72 205.167-313.612 240.339s-272.578 17.585-392.747-52.757c-79.135-43.964-143.617-111.376-190.512-190.512l-61.55 35.172c52.757 87.929 126.032 164.133 213.959 213.959 278.44 161.202 638.947 64.481 800.15-213.959l-55.688-32.241z" />
|
||||
<glyph unicode="" glyph-name="eye-7" data-tags="eye-7" horiz-adv-x="1492" d="M699.992 382.813h100.935v-370.096h-100.935v370.096zM39.886 317.081l218.641 277.46 79.275-62.469-218.641-277.46-79.275 62.469zM1168.077 535.027l80.254 61.217 211.71-277.543-80.254-61.217-211.71 277.543zM750.46 365.99c-4.205 0-4.205 0 0 0-302.805 0-580.376 159.814-731.779 420.562l88.318 50.467c130.374-227.104 378.507-370.096 643.461-370.096v0c264.954 0 508.881 142.992 643.461 370.096l88.318-50.467c-155.609-260.75-433.179-420.562-731.779-420.562z" />
|
||||
<glyph unicode="" glyph-name="eye-8" data-tags="eye-8" d="M500.604 248.557c-45.588 0-85.476 37.040-85.476 85.476s37.040 85.476 85.476 85.476c48.436 0 85.476-37.040 85.476-85.476s-37.040-85.476-85.476-85.476zM534.794 940.907h-68.38v-250.726h68.38v250.726zM929.448 777.228l-148.362-187.803 53.66-42.39 148.362 187.803-53.66 42.39zM74.194 774.747l-54.349-41.502 143.521-187.955 54.347 41.502-143.52 187.955zM503.453-36.36c-196.592 0-387.487 102.571-495.756 284.916l59.832 34.19c139.608-239.332 447.321-321.957 686.651-182.348 76.929 42.737 139.608 108.267 182.348 182.348l59.832-34.19c-51.284-85.476-122.514-156.704-207.991-207.991-91.173-54.135-188.045-76.929-284.916-76.929zM936.528 385.318c-68.38 116.818-176.649 199.443-304.862 233.632s-264.973 17.094-381.789-51.284c-76.929-42.737-139.608-108.267-185.196-185.196l-59.832 34.19c51.284 85.476 122.514 159.555 207.991 207.991 270.673 156.704 621.121 62.682 777.825-207.991l-54.135-31.341z" />
|
||||
<glyph unicode="" glyph-name="face-1" data-tags="face-1" horiz-adv-x="1103" d="M847.49 685.688c-23.283 0-40.746 17.463-40.746 40.746s17.463 40.746 40.746 40.746c23.283 0 40.746-17.463 40.746-40.746 1.94-23.283-17.463-40.746-40.746-40.746zM847.49 732.253c-1.94 0-5.822-1.94-5.822-5.822 0-5.822 9.702-5.822 9.702 0 1.94 3.879 0 5.822-3.879 5.822zM244.056 584.793c-36.867 0-73.73 9.702-106.717 29.105s-58.209 44.625-77.612 77.612l40.746 23.283c15.524-25.224 34.924-46.568 60.149-60.149 79.552-46.568 182.388-19.403 228.956 60.149l40.746-23.283c-40.746-67.911-112.536-106.717-186.267-106.717zM824.206 953.448h46.568v-89.254h-46.568v89.254zM937.966 833.318l51.661 65.511 36.566-28.834-51.661-65.511-36.566 28.834zM669.986 872.923l36.975 28.309 50.721-66.246-36.975-28.309-50.721 66.246zM546.743 251.059c-85.371 0-155.223 69.851-155.223 155.223h46.568c0-60.149 48.508-110.597 110.597-110.597 60.149 0 110.597 48.508 110.597 110.597h46.568c-3.879-85.371-73.73-155.223-159.105-155.223zM523.461 80.315h46.568v-102.835h-46.568v102.835zM288.562 88.444l61.264 77.709 36.57-28.828-61.264-77.709-36.57 28.828zM710.636 136.099l37.021 28.252 58.859-77.128-37.021-28.252-58.859 77.128zM849.43 582.85c-19.403 0-36.867 1.94-56.27 7.762-54.327 15.524-100.895 50.447-128.061 98.955l40.746 23.283c21.343-38.806 58.209-65.971 100.895-77.612s87.314-5.822 126.12 15.524c25.224 15.524 46.568 36.867 62.089 62.089l40.746-23.283c-19.403-32.984-46.568-60.149-79.552-79.552-34.924-17.463-69.851-27.165-106.717-27.165zM993.011 740.015c-46.568 79.552-147.464 108.657-227.015 62.089-25.224-15.524-46.568-36.867-62.089-62.089l-40.746 23.283c19.403 32.984 46.568 60.149 79.552 79.552 102.835 58.209 232.836 23.283 291.045-79.552l-40.746-23.283z" />
|
||||
<glyph unicode="" glyph-name="face-2" data-tags="face-2" horiz-adv-x="1013" d="M636.961 62.376h-261.201v40.593h218.843v47.652h42.358zM462.24 11.194h67.066v-42.358h-67.066v42.358zM769.326 701.259c-21.179 0-37.061 15.885-37.061 37.061s15.885 37.061 37.061 37.061c21.179 0 37.061-15.885 37.061-37.061 1.765-21.179-15.885-37.061-37.061-37.061zM769.326 743.617c-1.765 0-5.294-1.765-5.294-5.294 0-5.294 8.824-5.294 8.824 0 1.765 3.529 0 5.294-3.529 5.294zM89.849 750.676l-37.061 21.179c52.946 93.539 171.194 125.307 264.731 72.361 30.002-17.649 54.711-42.358 72.361-72.361l-37.061-21.179c-14.12 22.944-33.532 42.358-56.475 56.475-72.361 40.593-165.897 15.885-206.491-56.475zM495.772 330.636c-33.532 0-67.066 8.824-97.068 26.473s-52.946 40.593-70.595 70.595l37.061 21.179c14.12-22.944 31.768-42.358 54.711-54.711 35.297-21.179 75.89-26.473 114.716-15.885s72.361 35.297 91.774 70.595l37.061-21.179c-26.473-44.122-67.066-77.655-118.248-90.007-15.885-5.294-33.532-7.059-49.417-7.059zM748.15 944.813h42.358v-81.184h-42.358v81.184zM851.709 835.654l46.992 59.588 33.26-26.227-46.992-59.588-33.26 26.227zM607.928 871.613l33.632 25.749 46.134-60.255-33.632-25.749-46.134 60.255zM638.726 750.676l-37.061 21.179c17.649 30.002 40.593 52.946 70.595 70.595 44.122 26.473 97.068 33.532 146.485 19.415s91.774-45.888 118.248-90.007l-37.061-21.179c-21.179 35.297-52.946 60.005-91.774 70.595s-79.419 5.294-114.716-15.885c-22.944-12.353-42.358-31.768-54.711-54.711zM220.451 701.259c-21.179 0-37.061 15.885-37.061 37.061s15.885 37.061 37.061 37.061 37.061-15.885 37.061-37.061c1.765-21.179-15.885-37.061-37.061-37.061zM220.451 743.617c-1.765 0-5.294-1.765-5.294-5.294 0-5.294 8.824-5.294 8.824 0 1.765 3.529 0 5.294-3.529 5.294z" />
|
||||
<glyph unicode="" glyph-name="face-3" data-tags="face-3" horiz-adv-x="1013" d="M226.738 602.24c-79.377 0-142.517 64.943-142.517 142.517s64.943 142.517 142.517 142.517 144.32-63.141 144.32-142.517-64.943-142.517-144.32-142.517zM226.738 845.782c-55.924 0-101.024-45.101-101.024-101.024s46.904-101.024 101.024-101.024 101.024 45.101 101.024 101.024-45.101 101.024-101.024 101.024zM787.782 602.24c-79.377 0-142.517 64.943-142.517 142.517s64.943 142.517 142.517 142.517 142.517-64.943 142.517-142.517-63.141-142.517-142.517-142.517zM787.782 845.782c-55.924 0-101.024-45.101-101.024-101.024s45.101-101.024 101.024-101.024 101.024 45.101 101.024 101.024-45.101 101.024-101.024 101.024zM486.516 62.846h43.295v-82.986h-43.295v82.986zM508.163 328.035c-19.845 0-39.687 7.217-54.121 21.647l-95.613 95.613 30.667 30.667 95.613-95.613c12.628-12.628 36.081-12.628 48.708 0l95.613 95.613 30.667-30.667-95.613-95.613c-16.237-14.43-36.081-21.647-55.924-21.647zM789.586 706.873c-21.647 0-37.884 16.237-37.884 37.884s16.237 37.884 37.884 37.884c21.647 0 37.884-16.237 37.884-37.884s-18.039-37.884-37.884-37.884zM789.586 750.168c-1.803 0-5.412-1.803-5.412-5.412 0-5.412 9.020-5.412 9.020 0 0 3.608-1.803 5.412-3.608 5.412zM508.163 52.022c-32.473 0-66.749 9.020-97.416 25.256-30.667 18.039-55.924 43.295-73.963 73.963l37.884 21.647c14.43-23.453 34.275-43.295 57.729-57.729 73.963-43.295 169.575-16.237 211.069 57.729l37.884-21.647c-37.884-63.141-104.633-99.218-173.184-99.218zM766.135 955.826h43.295v-82.986h-43.295v82.986zM871.904 844.14l48.034 60.908 33.997-26.809-48.034-60.908-33.997 26.809zM622.752 880.963l34.375 26.321 47.156-61.591-34.375-26.321-47.156 61.591zM226.738 706.873c-21.647 0-37.884 16.237-37.884 37.884s16.237 37.884 37.884 37.884 37.884-16.237 37.884-37.884c1.803-21.647-16.237-37.884-37.884-37.884zM226.738 750.168c-1.803 0-5.412-1.803-5.412-5.412 0-5.412 9.020-5.412 9.020 0 1.803 3.608 0 5.412-3.608 5.412z" />
|
||||
<glyph unicode="" glyph-name="heart-sparks" data-tags="heart-sparks" d="M528.030-50.799l-26.401 35.829c39.6 30.175 79.203 62.233 107.49 88.633 66.004 52.803 126.351 113.149 181.037 177.267 39.6 49.032 56.575 92.405 52.803 139.553-1.886 45.26-20.744 86.749-52.803 116.92-15.086 13.201-33.947 24.515-54.689 30.175-37.717 13.201-81.090 9.429-116.92-9.429-15.086-9.429-28.287-18.858-41.489-32.057l-30.175 32.057c15.086 15.086 32.057 28.287 50.915 39.6 47.144 24.515 101.834 28.287 152.751 11.315 26.401-7.543 49.032-20.744 69.776-39.6 41.489-37.717 66.004-92.405 67.89-147.096 3.772-58.461-16.972-113.149-64.118-171.61-56.575-66.004-118.808-128.235-186.696-182.925-28.287-22.629-67.89-56.575-109.377-88.633zM490.314 960.001h45.26v-99.947h-45.26v99.947zM669.51 805.386l58.468 73.971 35.505-28.065-58.468-73.971-35.505 28.065zM262.726 851.581l35.991 27.445 57.179-74.979-35.991-27.445-57.179 74.979zM439.394-5.541c-11.315 9.429-26.401 22.629-39.6 33.947l-15.086 13.201c-66.004 54.689-130.122 116.92-184.809 182.925-47.144 58.461-67.89 111.265-64.118 171.61 1.886 56.575 26.401 109.377 67.89 147.096 20.744 18.858 43.372 32.057 69.776 39.6 49.032 16.972 103.718 13.201 150.867-11.315 41.489-26.401 75.433-58.461 103.718-96.175l-33.947-28.287c-24.515 33.947-54.689 62.233-90.521 84.862-33.947 18.858-77.319 20.744-115.036 7.543-20.744-7.543-37.717-16.972-54.689-30.175-32.057-30.175-50.915-71.661-52.803-116.92-1.886-49.032 13.201-92.405 52.803-141.437 54.689-64.118 115.036-122.579 179.153-177.267l15.086-13.201c13.201-11.315 28.287-24.515 37.717-32.057l-26.401-33.947zM509.171-63.999l-28.287 35.829 18.858 15.086c33.947 26.401 69.776 54.689 109.377 88.633 66.004 52.803 126.351 113.149 181.037 177.267 39.6 49.032 56.575 92.405 52.803 139.553l45.26 1.886c3.772-58.461-16.972-113.149-64.118-171.61-56.575-66.004-118.808-128.235-186.696-182.925-39.6-33.947-75.433-64.118-109.377-88.633l-18.858-15.086z" />
|
||||
<glyph unicode="" glyph-name="heart" data-tags="heart" d="M531.224 21.793l-33.325 46.142c53.833 41.015 107.665 84.595 146.115 120.482 89.721 71.778 171.75 153.807 246.091 240.966 53.833 66.649 76.904 125.611 71.778 189.696-2.562 61.522-28.199 117.917-71.778 158.935-20.508 17.946-46.142 33.325-74.34 41.015-51.27 17.946-110.228 12.817-158.935-12.817-20.508-12.817-38.45-25.635-56.396-43.579l-41.015 43.579c20.508 20.508 43.579 38.45 69.214 53.833 64.085 33.325 138.425 38.45 207.641 15.382 35.888-10.251 66.649-28.199 94.846-53.833 56.396-51.27 89.721-125.611 92.283-199.949 5.126-79.467-23.071-153.807-87.157-233.274-76.904-89.721-161.499-174.313-253.781-248.656-38.45-30.761-94.846-74.34-151.245-117.917zM413.308 80.754c-15.382 12.817-35.888 30.761-53.833 46.142l-20.508 17.946c-89.721 74.34-176.878 158.935-251.218 248.656-64.085 79.467-92.283 151.245-87.157 233.274 2.562 76.904 35.888 148.679 92.283 199.949 28.199 25.635 58.958 43.579 94.846 53.833 66.649 23.071 140.99 17.946 205.075-15.382 56.396-35.888 102.538-79.467 140.99-130.736l-46.142-38.45c-33.325 46.142-74.34 84.595-123.045 115.354-46.142 25.635-105.103 28.199-156.372 10.251-28.199-10.251-51.27-23.071-74.34-41.015-43.579-41.015-69.214-97.413-71.778-158.935-2.562-66.649 17.946-125.611 71.778-192.259 74.34-87.157 156.372-166.624 243.528-240.966l20.508-17.946c17.946-15.382 38.45-33.325 51.27-43.579l-35.888-46.142zM508.155 1.288l-38.45 48.707 25.635 20.508c46.142 35.888 94.846 74.34 148.679 120.482 89.721 71.778 171.75 153.807 246.091 240.966 53.833 66.649 76.904 125.611 71.778 189.696l61.522 2.562c5.126-79.467-23.071-153.807-87.157-233.274-76.904-89.721-161.499-174.313-253.781-248.656-53.833-46.142-102.538-87.157-148.679-120.482l-25.635-20.508z" />
|
||||
<glyph unicode="" glyph-name="medal" data-tags="medal" d="M784.202-63.998l-43.652 5.458c3.637 38.195 12.732 76.39 27.283 112.767l5.458 16.369 16.369-1.819c38.195-5.458 76.39-16.369 110.948-32.737l-18.188-40.014c-27.283 10.912-54.563 20.007-81.846 27.283-5.458-29.101-12.732-58.202-16.369-87.304zM575.585 156.192l37.802 21.831 106.422-184.288-37.802-21.831-106.422 184.288zM768.032 251.726l37.81 21.808 99.955-173.301-37.81-21.808-99.955 173.301zM238.555-63.998c-3.637 29.101-9.095 58.202-18.188 85.484-29.101-5.458-56.383-14.551-83.667-27.283l-18.188 40.014c36.377 16.369 72.752 27.283 110.948 32.737l16.369 1.819 5.458-16.369c14.551-36.377 23.646-74.571 27.283-112.767l-40.014-3.637zM303.462-4.361l106.467 184.243 37.794-21.839-106.467-184.243-37.794 21.839zM118.204 100.548l99.937 173.322 37.815-21.803-99.937-173.322-37.815 21.803zM489.551 769.024h43.652v-98.215h-43.652v98.215zM615.632 635.211l58.63 74.215 34.254-27.060-58.63-74.215-34.254 27.060zM315.164 682.75l34.704 26.479 56.268-73.744-34.704-26.479-56.268 73.744zM511.376 201.549c-209.165 0-378.316 169.15-378.316 378.316s169.15 380.135 378.316 380.135 378.316-169.15 378.316-378.316-169.15-380.135-378.316-380.135zM511.376 916.349c-185.519 0-334.663-150.961-334.663-334.663 0-185.519 150.961-334.663 334.663-334.663 185.519 0 334.663 150.961 334.663 334.663 1.819 183.702-149.145 334.663-334.663 334.663z" />
|
||||
<glyph unicode="" glyph-name="megaphone" data-tags="megaphone" horiz-adv-x="1473" d="M301.758 20.312v57.992c38.66 0 77.321 14.499 103.902 43.493 26.58 26.58 43.493 65.24 43.493 103.902h57.992c-2.415-113.567-94.236-205.387-205.387-205.387zM67.374 223.282v439.771l195.721-77.321-21.747-53.159-118.4 45.911v-270.628l118.4 48.327 21.747-53.159zM937.251-15.934l-620.994 299.625v309.288l620.994 299.625v-908.538zM371.832 319.934l507.427-246.465v727.313l-507.427-244.048v-236.8zM1319.030 467.332h147.395v-57.992h-147.395v57.992zM1200.19 158.853l35.956 45.501 109.96-86.892-35.956-45.501-109.96 86.892zM1201.794 716.695l109.592 83.423 35.124-46.143-109.592-83.423-35.124 46.143z" />
|
||||
<glyph unicode="" glyph-name="phone" data-tags="phone" horiz-adv-x="871" d="M691.62 770.026l-71.318-71.317 32.296-32.294 71.317 71.317-32.294 32.294zM801.738 542.789l-94.503-11.057 5.308-45.361 94.503 11.057-5.308 45.361zM455.681 850.946l-12.29-92.436 45.274-6.019 12.29 92.436-45.274 6.019zM696.313 6.505c-1.902 0-5.709 0-7.611 0-390.114 9.516-669.856 287.353-681.274 675.565 0 1.902 0 49.478 17.127 79.926 17.127 32.351 68.508 72.315 78.024 79.926 26.642 24.739 66.606 24.739 93.248-1.902l114.179-114.179c26.642-26.642 26.642-68.508 0-93.248l-55.187-57.090c-9.516-9.516-9.516-22.836 0-32.351l293.062-293.062c9.516-9.516 22.836-9.516 32.351 0l55.187 55.187c26.642 26.642 68.508 26.642 93.248 0l114.179-114.179c24.739-24.739 26.642-66.606 1.902-93.248-7.611-9.516-49.478-62.799-79.926-79.926-30.448-11.418-53.284-11.418-68.508-11.418zM148.249 815.279c-5.709 0-11.418-1.902-15.224-5.709l-1.902-1.902c-15.224-11.418-55.187-45.672-68.508-66.606-9.516-15.224-11.418-45.672-9.516-57.090 9.516-363.472 272.128-624.184 639.407-633.698 11.418 0 28.544 0 53.284 11.418 17.127 9.516 49.478 45.672 64.701 66.606l1.902 1.902c9.516 9.516 9.516 22.836 0 32.351l-114.179 114.179c-7.611 7.611-22.836 7.611-32.351 0l-55.187-55.187c-26.642-26.642-68.508-26.642-93.248 0l-294.965 291.16c-26.642 26.642-26.642 68.508 0 93.248l55.187 55.187c9.516 9.516 9.516 22.836 0 32.351l-114.179 114.179c-3.807 5.709-9.516 7.611-15.224 7.611z" />
|
||||
<glyph unicode="" glyph-name="plus" data-tags="plus" d="M530.601 917.643h-102.298v-943.933h102.298v943.933zM953.743 494.499h-943.933v-102.298h943.933v102.298z" />
|
||||
<glyph unicode="" glyph-name="spark" data-tags="spark" horiz-adv-x="1883" d="M861.919 796.748h181.462v-368.595h-181.462v368.595zM1518.035 238.759l214.328 271.506 142.43-112.434-214.328-271.506-142.43 112.434zM35.557 398.718l144.244 110.094 209.865-274.966-144.244-110.094-209.865 274.966z" />
|
||||
<glyph unicode="" glyph-name="tag" data-tags="tag" horiz-adv-x="844" d="M490.344-9.609l-447.874 447.874v356.353h356.353l440.085-440.085-29.209-29.209-428.4 426.454h-295.987v-295.987l405.033-405.033 258.989 260.936 31.156-31.156-290.145-290.145zM274.196 479.156c-44.787 0-81.786 36.998-81.786 81.786s36.998 81.786 81.786 81.786c44.787 0 81.786-36.998 81.786-81.786s-36.998-81.786-81.786-81.786zM274.196 607.675c-25.313 0-48.682-21.42-48.682-48.682 0-25.313 21.42-48.682 48.682-48.682s48.682 21.42 48.682 48.682c0 27.263-23.367 48.682-48.682 48.682z" />
|
||||
<glyph unicode="" glyph-name="talk" data-tags="talk" horiz-adv-x="987" d="M343.903-83.22v214.003h45.452v-115.524l238.623 208.322h7.575c172.339 0 303.014 140.143 303.014 327.633 0 179.914-145.825 325.739-325.739 327.633h-227.26c-179.914 0-325.739-145.825-327.633-325.739 0-181.808 147.719-327.633 327.633-327.633v0-45.452c-206.427 0-373.084 166.656-373.084 371.19 1.894 206.427 168.551 373.084 373.084 373.084v0h227.26c204.534 0 371.19-166.656 371.19-371.19 0-208.322-145.825-367.404-340.89-371.19l-299.226-265.137z" />
|
||||
<glyph unicode="" glyph-name="cross" data-tags="cross" horiz-adv-x="1017" d="M579.952 307.286l76.888 76.997 343.281-342.795-76.888-76.997-343.281 342.795zM-0.049 887.658l76.943 76.943 343.038-343.038-76.943-76.943-343.038 343.038zM0.721 38.043l923.318 923.318 76.943-76.943-923.318-923.318-76.943 76.943z" />
|
||||
>>>>>>> 627f96b4205143f053a77c3948f846e8864282b6
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 59 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -6,9 +6,7 @@ const PreLotteryWine = new Schema({
|
||||
vivinoLink: String,
|
||||
rating: Number,
|
||||
id: String,
|
||||
image: String,
|
||||
price: String,
|
||||
country: String
|
||||
image: String
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("PreLotteryWine", PreLotteryWine);
|
||||
@@ -8,11 +8,7 @@ const VirtualWinner = new Schema({
|
||||
green: Number,
|
||||
blue: Number,
|
||||
red: Number,
|
||||
yellow: Number,
|
||||
id: String,
|
||||
timestamp_drawn: Number,
|
||||
timestamp_sent: Number,
|
||||
timestamp_limit: Number
|
||||
yellow: Number
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("VirtualWinner", VirtualWinner);
|
||||
100
server.js
100
server.js
@@ -4,41 +4,61 @@ const server = require("http").Server(app);
|
||||
const io = require("socket.io")(server);
|
||||
const path = require("path");
|
||||
const session = require("express-session");
|
||||
const User = require(path.join(__dirname + "/api/schemas/User"));
|
||||
const User = require(path.join(__dirname + "/schemas/User"));
|
||||
|
||||
const apiRouter = require(path.join(__dirname + "/api/router.js"));
|
||||
const updateApi = require(path.join(__dirname + "/api/update"));
|
||||
const retrieveApi = require(path.join(__dirname + "/api/retrieve"));
|
||||
const subscriptionApi = require(path.join(__dirname + "/api/subscriptions"));
|
||||
const loginApi = require(path.join(__dirname + "/api/login"));
|
||||
const wineinfoApi = require(path.join(__dirname + "/api/wineinfo"));
|
||||
const virtualApi = require(path.join(__dirname + "/api/virtualLottery"));
|
||||
|
||||
//This is required for the chat to work
|
||||
const chat = require(path.join(__dirname + "/api/chat"))(io);
|
||||
const chatHistory = require(path.join(__dirname + "/api/chatHistory"));
|
||||
|
||||
const bodyParser = require("body-parser");
|
||||
|
||||
const mongoose = require("mongoose");
|
||||
const MongoStore = require("connect-mongo")(session);
|
||||
const cors = require("cors");
|
||||
|
||||
// mongoose / database
|
||||
console.log("Trying to connect with mongodb..");
|
||||
const referrerPolicy = require("referrer-policy");
|
||||
const helmet = require("helmet");
|
||||
const featurePolicy = require("feature-policy");
|
||||
|
||||
const compression = require("compression");
|
||||
app.use(compression());
|
||||
|
||||
app.use(
|
||||
featurePolicy({
|
||||
features: {
|
||||
fullscreen: ["*"],
|
||||
//vibrate: ["'none'"],
|
||||
payment: ["'none'"],
|
||||
microphone: ["'none'"],
|
||||
camera: ["'self'"],
|
||||
speaker: ["*"],
|
||||
syncXhr: ["'self'"]
|
||||
//notifications: ["'self'"]
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(helmet());
|
||||
app.use(helmet.frameguard({ action: "sameorigin" }));
|
||||
app.use(referrerPolicy({ policy: "origin" }));
|
||||
|
||||
app.use(cors());
|
||||
mongoose.promise = global.Promise;
|
||||
mongoose.connect("mongodb://localhost/vinlottis", {
|
||||
useCreateIndex: true,
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
serverSelectionTimeoutMS: 10000 // initial connection timeout
|
||||
}).then(_ => console.log("Mongodb connection established!"))
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
console.error("ERROR! Mongodb required to run.");
|
||||
process.exit(1);
|
||||
})
|
||||
mongoose.set("debug", false);
|
||||
mongoose.connect("mongodb://localhost/vinlottis");
|
||||
mongoose.set("debug", true);
|
||||
|
||||
// middleware
|
||||
const setupCORS = require(path.join(__dirname, "/api/middleware/setupCORS"));
|
||||
const setupHeaders = require(path.join(__dirname, "/api/middleware/setupHeaders"));
|
||||
app.use(setupCORS)
|
||||
app.use(setupHeaders)
|
||||
|
||||
// parse application/json
|
||||
app.use(express.json());
|
||||
app.use(
|
||||
bodyParser.urlencoded({
|
||||
extended: true
|
||||
})
|
||||
);
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use(
|
||||
session({
|
||||
@@ -52,34 +72,34 @@ app.use(
|
||||
})
|
||||
);
|
||||
|
||||
app.set('socketio', io); // set io instance to key "socketio"
|
||||
|
||||
const passport = require("passport");
|
||||
const LocalStrategy = require("passport-local");
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// use static authenticate method of model in LocalStrategy
|
||||
passport.use(new LocalStrategy(User.authenticate()));
|
||||
|
||||
// use static serialize and deserialize of model for passport session support
|
||||
passport.serializeUser(User.serializeUser());
|
||||
passport.deserializeUser(User.deserializeUser());
|
||||
|
||||
// files
|
||||
app.use("/public", express.static(path.join(__dirname, "public")));
|
||||
app.use("/service-worker.js", express.static(path.join(__dirname, "public/sw/serviceWorker.js")));
|
||||
|
||||
// api endpoints
|
||||
app.use("/api/", apiRouter);
|
||||
|
||||
// redirects
|
||||
app.get("/dagens", (req, res) => res.redirect("/#/dagens"));
|
||||
app.get("/winner/:id", (req, res) => res.redirect("/#/winner/" + req.params.id));
|
||||
|
||||
// push-notifications
|
||||
app.use("/dist", express.static(path.join(__dirname, "public/dist")));
|
||||
app.use("/", loginApi);
|
||||
app.use("/api/", updateApi);
|
||||
app.use("/api/", retrieveApi);
|
||||
app.use("/api/", wineinfoApi);
|
||||
app.use("/api/", chatHistory);
|
||||
app.use("/api/virtual/", virtualApi(io));
|
||||
app.use("/subscription", subscriptionApi);
|
||||
|
||||
// No other route defined, return index file
|
||||
app.use("/", (req, res) => res.sendFile(path.join(__dirname + "/public/dist/index.html")));
|
||||
app.get("/dagens", function(req, res) {
|
||||
res.redirect("/#/dagens");
|
||||
});
|
||||
|
||||
app.use("/service-worker.js", function(req, res) {
|
||||
res.sendFile(path.join(__dirname, "public/sw/serviceWorker.js"));
|
||||
});
|
||||
|
||||
server.listen(30030);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<banner :routes="routes"/>
|
||||
<banner />
|
||||
<router-view />
|
||||
<Footer />
|
||||
<UpdateToast
|
||||
v-if="showToast"
|
||||
:text="toastText"
|
||||
@@ -15,48 +14,17 @@
|
||||
<script>
|
||||
import ServiceWorkerMixin from "@/mixins/ServiceWorkerMixin";
|
||||
import banner from "@/ui/Banner";
|
||||
import Footer from "@/ui/Footer";
|
||||
import UpdateToast from "@/ui/UpdateToast";
|
||||
|
||||
export default {
|
||||
name: "vinlottis",
|
||||
components: { banner, UpdateToast, Footer },
|
||||
components: { banner, UpdateToast },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
showToast: false,
|
||||
toastText: null,
|
||||
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"
|
||||
}
|
||||
]
|
||||
refreshToast: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -77,31 +45,37 @@ export default {
|
||||
methods: {
|
||||
closeToast: function() {
|
||||
this.showToast = false;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "styles/global.scss";
|
||||
@import "styles/positioning.scss";
|
||||
@import "styles/vinlottis-icons";
|
||||
@import "./styles/global.scss";
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 600;
|
||||
src: url("/../public/assets/fonts/bold.woff"),
|
||||
url("/../public/assets/fonts/bold.woff") format("woff"), local("Arial");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 300;
|
||||
src: url("/../public/assets/fonts/regular.eot"),
|
||||
url("/../public/assets/fonts/regular.woff") format("woff"), local("Arial");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: $primary;
|
||||
background-color: #dbeede;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.app-container {
|
||||
background-color: white;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: 80px auto 100px;
|
||||
|
||||
.main-container{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
261
src/api.js
Normal file
261
src/api.js
Normal file
@@ -0,0 +1,261 @@
|
||||
const BASE_URL = __APIURL__ || window.location.origin;
|
||||
|
||||
const statistics = () => {
|
||||
const url = new URL('/api/purchase/statistics', BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const colorStatistics = () => {
|
||||
const url = new URL("/api/purchase/statistics/color", BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const highscoreStatistics = () => {
|
||||
const url = new URL("/api/highscore/statistics", BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const overallWineStatistics = () => {
|
||||
const url = new URL("/api/wines/statistics/overall", BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
|
||||
const chartWinsByColor = () => {
|
||||
const url = new URL("/api/purchase/statistics/color", BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const chartPurchaseByColor = () => {
|
||||
const url = new URL("/api/purchase/statistics", BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
|
||||
const prelottery = () => {
|
||||
const url = new URL("/api/wines/prelottery", BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const log = (sendObject) => {
|
||||
const url = new URL("/api/log", BASE_URL)
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const addAttendee = sendObject => {
|
||||
const url = new URL("/api/virtual/attendee", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const getVirtualWinner = () => {
|
||||
const url = new URL("/api/virtual/winner", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const attendeesSecure = () => {
|
||||
const url = new URL("/api/virtual/attendees/secure", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const winnersSecure = () => {
|
||||
const url = new URL("/api/virtual/winners/secure", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const winners = () => {
|
||||
const url = new URL("/api/virtual/winners", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const deleteWinners = () => {
|
||||
const url = new URL("/api/virtual/winners", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
}
|
||||
|
||||
const deleteAttendees = () => {
|
||||
const url = new URL("/api/virtual/attendees", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const attendees = () => {
|
||||
const url = new URL("/api/virtual/attendees", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
}
|
||||
|
||||
const logWines = (wines) => {
|
||||
const url = new URL("/api/log/wines", BASE_URL)
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(wines)
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const wineSchema = () => {
|
||||
const url = new URL("/api/log/schema", BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
const barcodeToVinmonopolet = (id) => {
|
||||
const url = new URL("/api/wineinfo/" + id, BASE_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.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 url = new URL("/login", BASE_URL)
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password })
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json()
|
||||
} else {
|
||||
return handleErrors(resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const register = (username, password) => {
|
||||
const url = new URL("/register", BASE_URL)
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password })
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json()
|
||||
} else {
|
||||
return handleErrors(resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getChatHistory = (skip=null, take=null) => {
|
||||
const url = new URL("/api/chat/history", BASE_URL);
|
||||
if (!isNaN(skip))
|
||||
url.searchParams.append("skip", skip);
|
||||
if (!isNaN(take))
|
||||
url.searchParams.append("take", take);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
|
||||
}
|
||||
|
||||
export {
|
||||
statistics,
|
||||
colorStatistics,
|
||||
highscoreStatistics,
|
||||
overallWineStatistics,
|
||||
chartWinsByColor,
|
||||
chartPurchaseByColor,
|
||||
prelottery,
|
||||
log,
|
||||
logWines,
|
||||
wineSchema,
|
||||
barcodeToVinmonopolet,
|
||||
login,
|
||||
register,
|
||||
addAttendee,
|
||||
getVirtualWinner,
|
||||
attendeesSecure,
|
||||
attendees,
|
||||
winners,
|
||||
winnersSecure,
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
getChatHistory
|
||||
};
|
||||
173
src/components/AllWinesPage.vue
Normal file
173
src/components/AllWinesPage.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="outer">
|
||||
<div class="container">
|
||||
<h1 class="title">Alle viner</h1>
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" :fullscreen="true" :inlineSlot="true">
|
||||
<div class="winners-container">
|
||||
<div class="color-wins" :class="{ 'big': fullscreen }"
|
||||
v-if="wine.blue || wine.red || wine.green || wine.yellow">
|
||||
<span class="label">Vinnende lodd:</span>
|
||||
<span class="color-win blue">{{wine.blue == undefined ? 0 : wine.blue}}</span>
|
||||
<span class="color-win red">{{wine.red == undefined ? 0 : wine.red}}</span>
|
||||
<span class="color-win green">{{wine.green == undefined ? 0 : wine.green}}</span>
|
||||
<span class="color-win yellow">{{wine.yellow == undefined ? 0 : wine.yellow}}</span>
|
||||
</div>
|
||||
<div class="name-wins" v-if="wine.winners">
|
||||
<span class="label">Vunnet av:</span>
|
||||
<span class="names" v-for="winner in wine.winners">- {{ winner }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Wine>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { page, event } from "vue-analytics";
|
||||
import Banner from "@/ui/Banner";
|
||||
import Wine from "@/ui/Wine";
|
||||
import { overallWineStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Banner,
|
||||
Wine
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wines: []
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
const wines = await overallWineStatistics();
|
||||
this.wines = wines.sort((a, b) =>
|
||||
a.rating > b.rating ? -1 : 1
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/styles/media-queries";
|
||||
|
||||
h1 {
|
||||
font-family: knowit, Arial;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@include desktop {
|
||||
margin: 0 2rem;
|
||||
|
||||
> div {
|
||||
max-width: max-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.winners-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 2rem;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: row;
|
||||
width: max-content;
|
||||
margin: 0.75rem;
|
||||
|
||||
&:not(&:first-child) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name-wins {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
|
||||
.names {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.color-wins {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
|
||||
margin-left: -3px; // offset span.color-win margin
|
||||
|
||||
@include mobile {
|
||||
width: 25vw;
|
||||
margin-left: -2px; // offset span.color-win margin
|
||||
}
|
||||
@include desktop {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.color-wins.big {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
span.color-win {
|
||||
border: 2px solid transparent;
|
||||
color: #333;
|
||||
display: block;
|
||||
padding: 25px;
|
||||
font-size: 1.3rem;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
/* max-height: calc(3rem + 18px); */
|
||||
/* max-width: calc(3rem + 18px); */
|
||||
width: 1rem;
|
||||
margin: 3px;
|
||||
touch-action: manipulation;
|
||||
height: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 900;
|
||||
|
||||
@include mobile {
|
||||
margin: 2px;
|
||||
padding: 10px;
|
||||
font-size: 1rem;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
grow: 0.5;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #c8f9df;
|
||||
}
|
||||
&.blue {
|
||||
background: #d4f2fe;
|
||||
}
|
||||
&.red {
|
||||
background: #fbd7de;
|
||||
}
|
||||
&.yellow {
|
||||
background: #fff6d6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,14 +5,15 @@
|
||||
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
||||
</p>
|
||||
|
||||
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" />
|
||||
<RaffleGenerator @numberOfBallots="val => this.numberOfBallots = val" />
|
||||
|
||||
<Vipps class="vipps" :amount="numberOfRaffles" />
|
||||
<Vipps class="vipps" :amount="numberOfBallots" />
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { page, event } from "vue-analytics";
|
||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||
import Vipps from "@/ui/Vipps";
|
||||
import Countdown from "@/ui/Countdown";
|
||||
@@ -26,7 +27,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hardStart: false,
|
||||
numberOfRaffles: null
|
||||
numberOfBallots: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -43,7 +44,7 @@ export default {
|
||||
this.hardStart = true;
|
||||
},
|
||||
track() {
|
||||
window.ga('send', 'pageview', '/lottery/generate');
|
||||
this.$ga.page("/lottery/generate");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,9 @@ import VirtualLotteryPage from "@/components/VirtualLotteryPage";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tabs
|
||||
Tabs,
|
||||
GeneratePage,
|
||||
VirtualLotteryPage
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -78,14 +78,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="submitLottery">Send inn lotteri</button>
|
||||
</div>
|
||||
|
||||
<h3>Vinnere</h3>
|
||||
<a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a>
|
||||
<div class="winner-container" v-if="winners.length > 0">
|
||||
<wine v-for="winner in winners" :key="winner" :wine="winner.wine">
|
||||
<wine v-for="winner in winners" :key="winner" :wine="winner.wine" :inlineSlot="true">
|
||||
<div class="winner-element">
|
||||
<div class="color-selector">
|
||||
<div class="label-div">
|
||||
@@ -112,30 +107,16 @@
|
||||
@click="winner.color = 'yellow'"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="label-div">
|
||||
<label for="winner-name">Navn vinner</label>
|
||||
<input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="label-div">
|
||||
<label for="potential-winner-name">Virtuelle vinnere</label>
|
||||
<select
|
||||
id="potential-winner-name"
|
||||
type="text"
|
||||
placeholder="Navn"
|
||||
v-model="winner.potentialWinner"
|
||||
@change="potentialChange($event, winner)"
|
||||
>
|
||||
<option
|
||||
v-for="fetchedWinner in fetchedWinners"
|
||||
:value="stringify(fetchedWinner)"
|
||||
>{{fetchedWinner.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</wine>
|
||||
|
||||
<div class="button-container column">
|
||||
<button class="vin-button" @click="submitLotteryWinners">Send inn vinnere</button>
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="sendInfo">Send inn vinnere</button>
|
||||
<button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,17 +126,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventBus from "@/mixins/EventBus";
|
||||
import { dateString } from '@/utils'
|
||||
import {
|
||||
prelottery,
|
||||
sendLotteryWinners,
|
||||
sendLottery,
|
||||
logWines,
|
||||
wineSchema,
|
||||
winnersSecure,
|
||||
attendees
|
||||
} from "@/api";
|
||||
import { prelottery, log, logWines, wineSchema } from "@/api";
|
||||
import TextToast from "@/ui/TextToast";
|
||||
import Wine from "@/ui/Wine";
|
||||
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
|
||||
@@ -166,7 +137,6 @@ export default {
|
||||
return {
|
||||
payed: undefined,
|
||||
winners: [],
|
||||
fetchedWinners: [],
|
||||
wines: [],
|
||||
pushMessage: "",
|
||||
pushLink: "/",
|
||||
@@ -190,67 +160,8 @@ export default {
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.setWinnerdataToStorage();
|
||||
eventBus.$off("tab-change", () => {
|
||||
this.fetchColorsAndWinners();
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.fetchColorsAndWinners();
|
||||
|
||||
eventBus.$on("tab-change", () => {
|
||||
this.fetchColorsAndWinners();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
stringify(json) {
|
||||
return JSON.stringify(json);
|
||||
},
|
||||
potentialChange(event, winner) {
|
||||
let data = JSON.parse(event.target.value);
|
||||
winner.name = data.name;
|
||||
winner.color = data.color;
|
||||
},
|
||||
async fetchColorsAndWinners() {
|
||||
let winners = await winnersSecure();
|
||||
let _attendees = await attendees();
|
||||
let colors = {
|
||||
red: 0,
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0
|
||||
};
|
||||
this.payed = 0;
|
||||
for (let i = 0; i < _attendees.length; i++) {
|
||||
let attendee = _attendees[i];
|
||||
colors.red += attendee.red;
|
||||
colors.blue += attendee.blue;
|
||||
colors.green += attendee.green;
|
||||
colors.yellow += attendee.yellow;
|
||||
this.payed +=
|
||||
(attendee.red + attendee.blue + attendee.green + attendee.yellow) *
|
||||
10;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.lotteryColors.length; i++) {
|
||||
let currentColor = this.lotteryColors[i];
|
||||
switch (currentColor.css) {
|
||||
case "red":
|
||||
currentColor.value = colors.red;
|
||||
break;
|
||||
case "blue":
|
||||
currentColor.value = colors.blue;
|
||||
break;
|
||||
a;
|
||||
case "green":
|
||||
currentColor.value = colors.green;
|
||||
break;
|
||||
case "yellow":
|
||||
currentColor.value = colors.yellow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.fetchedWinners = winners;
|
||||
},
|
||||
amIBeingEdited(wine) {
|
||||
return this.editWine.id == wine.id && this.editWine.name == wine.name;
|
||||
},
|
||||
@@ -262,10 +173,9 @@ export default {
|
||||
this.winners.push({
|
||||
name: "",
|
||||
color: "",
|
||||
potentialWinner: "",
|
||||
wine: {
|
||||
name: wine.name,
|
||||
vivinoLink: wine.vivinoLink,
|
||||
link: wine.link,
|
||||
rating: wine.rating,
|
||||
image: wine.image,
|
||||
id: wine.id
|
||||
@@ -312,7 +222,7 @@ export default {
|
||||
},
|
||||
sendWines: async function() {
|
||||
let response = await logWines(this.wines);
|
||||
if (response.success == true) {
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
@@ -325,12 +235,12 @@ export default {
|
||||
color: "",
|
||||
wine: {
|
||||
name: "",
|
||||
vivinoLink: "",
|
||||
link: "",
|
||||
rating: ""
|
||||
}
|
||||
});
|
||||
},
|
||||
submitLottery: async function(event) {
|
||||
sendInfo: async function(event) {
|
||||
const colors = {
|
||||
red: this.lotteryColors.filter(c => c.css == "red")[0].value,
|
||||
green: this.lotteryColors.filter(c => c.css == "green")[0].value,
|
||||
@@ -339,63 +249,48 @@ export default {
|
||||
};
|
||||
|
||||
let sendObject = {
|
||||
lottery: {
|
||||
date: dateString(new Date()),
|
||||
purchase: {
|
||||
date: new Date(),
|
||||
...colors
|
||||
}
|
||||
},
|
||||
winners: this.winners
|
||||
};
|
||||
|
||||
if (sendObject.lottery.red == undefined) {
|
||||
if (sendObject.purchase.red == undefined) {
|
||||
alert("Rød må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.green == undefined) {
|
||||
if (sendObject.purchase.green == undefined) {
|
||||
alert("Grønn må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.yellow == undefined) {
|
||||
if (sendObject.purchase.yellow == undefined) {
|
||||
alert("Gul må defineres");
|
||||
return;
|
||||
}
|
||||
if (sendObject.lottery.blue == undefined) {
|
||||
if (sendObject.purchase.blue == undefined) {
|
||||
alert("Blå må defineres");
|
||||
return;
|
||||
}
|
||||
|
||||
sendObject.lottery.bought =
|
||||
sendObject.purchase.bought =
|
||||
parseInt(colors.blue) +
|
||||
parseInt(colors.red) +
|
||||
parseInt(colors.green) +
|
||||
parseInt(colors.yellow);
|
||||
const stolen = sendObject.lottery.bought - parseInt(this.payed) / 10;
|
||||
const stolen = sendObject.purchase.bought - parseInt(this.payed) / 10;
|
||||
if (isNaN(stolen) || stolen == undefined) {
|
||||
alert("Betalt må registreres");
|
||||
return;
|
||||
}
|
||||
sendObject.lottery.stolen = stolen;
|
||||
sendObject.purchase.stolen = stolen;
|
||||
|
||||
let response = await sendLottery(sendObject);
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(response.message || "Noe gikk galt under innsending");
|
||||
}
|
||||
},
|
||||
submitLotteryWinners: async function(event) {
|
||||
let sendObject = {
|
||||
lottery: {
|
||||
date: dateString(new Date()),
|
||||
winners: this.winners
|
||||
}
|
||||
}
|
||||
|
||||
if (sendObject.lottery.winners.length == 0) {
|
||||
if (sendObject.winners.length == 0) {
|
||||
alert("Det må være med vinnere");
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < sendObject.lottery.winners.length; i++) {
|
||||
let currentWinner = sendObject.lottery.winners[i];
|
||||
for (let i = 0; i < sendObject.winners.length; i++) {
|
||||
let currentWinner = sendObject.winners[i];
|
||||
|
||||
if (currentWinner.name == undefined || currentWinner.name == "") {
|
||||
alert("Navn må defineres");
|
||||
@@ -407,7 +302,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
let response = await sendLotteryWinners(sendObject);
|
||||
let response = await log(sendObject);
|
||||
if (response == true) {
|
||||
alert("Sendt!");
|
||||
window.location.reload();
|
||||
@@ -446,6 +341,7 @@ export default {
|
||||
}
|
||||
},
|
||||
setWinnerdataToStorage() {
|
||||
console.log("saving localstorage");
|
||||
localStorage.setItem("winners", JSON.stringify(this.winners));
|
||||
localStorage.setItem(
|
||||
"colorValues",
|
||||
@@ -466,13 +362,7 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
select {
|
||||
margin: 0 0 auto;
|
||||
height: 2rem;
|
||||
min-width: 0;
|
||||
width: 98%;
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@@ -486,24 +376,12 @@ h2 {
|
||||
font-family: knowit, Arial;
|
||||
}
|
||||
|
||||
.wine-link {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid $link-color;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 90%;
|
||||
margin: 2rem auto;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
padding: 0 1.5rem 3rem;
|
||||
|
||||
@@ -513,7 +391,8 @@ hr {
|
||||
}
|
||||
}
|
||||
.winner-container {
|
||||
width: 100%;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
@@ -526,13 +405,7 @@ hr {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .wine {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
flex-direction: column;
|
||||
}
|
||||
.edit {
|
||||
width: 100%;
|
||||
@@ -546,10 +419,10 @@ hr {
|
||||
}
|
||||
.winner-element {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
|
||||
> div {
|
||||
margin-bottom: 1rem;
|
||||
@include desktop {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
@@ -643,7 +516,7 @@ hr {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
margin: 3rem auto 1rem;
|
||||
margin: 3rem auto 0;
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
@@ -659,9 +532,9 @@ hr {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1 class="title">Dagens viner</h1>
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||
<div class="outer">
|
||||
<div class="container">
|
||||
<h1 class="title">Dagens viner</h1>
|
||||
<div class="wines-container">
|
||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" :fullscreen="true" :inlineSlot="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { prelottery } from "@/api";
|
||||
import { page, event } from "vue-analytics";
|
||||
import Banner from "@/ui/Banner";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
@@ -23,14 +25,14 @@ export default {
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
prelottery().then(wines => this.wines = wines);
|
||||
const _wines = await fetch("/api/wines/prelottery");
|
||||
this.wines = await _wines.json();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries";
|
||||
@import "@/styles/variables";
|
||||
@import "./src/styles/media-queries";
|
||||
|
||||
.wine-image {
|
||||
height: 250px;
|
||||
@@ -44,7 +46,7 @@ h1 {
|
||||
.wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
justify-content: space-between;
|
||||
margin: 0 2rem;
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
@@ -108,7 +110,7 @@ a:visited {
|
||||
font-family: Arial;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $link-color;
|
||||
border-bottom: 1px solid #ff5fff;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
321
src/components/VinlottisPage.vue
Normal file
321
src/components/VinlottisPage.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div class="outer">
|
||||
<div class="container">
|
||||
<div class="header-top">
|
||||
<h1 class="title" @click="startCountdown">Vinlotteri</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="generate-link">
|
||||
Vil du til lotteriet?
|
||||
<span class="subtext generator-link">Trykk her</span>
|
||||
</router-link>
|
||||
<div class="chart-container">
|
||||
<PurchaseGraph class="purchase" />
|
||||
<WinGraph class="win" />
|
||||
</div>
|
||||
<router-link to="dagens" class="generate-link" v-if="todayExists">
|
||||
Lurer du på dagens fangst?
|
||||
<span class="subtext generator-link">Se her</span>
|
||||
</router-link>
|
||||
<div class="bottom-container">
|
||||
<div class="left-bottom">
|
||||
<TotalBought />
|
||||
<hr class="bought-and-highscore-separator" />
|
||||
<div class="highscore-and-wines">
|
||||
<Highscore class="highscore-container" />
|
||||
<Wines class="wines-container" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="vipps-outer-container">
|
||||
<Vipps class="vipps-inner-container" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { page, event } from "vue-analytics";
|
||||
import PurchaseGraph from "@/ui/PurchaseGraph";
|
||||
import TotalBought from "@/ui/TotalBought";
|
||||
import Highscore from "@/ui/Highscore";
|
||||
import WinGraph from "@/ui/WinGraph";
|
||||
import Banner from "@/ui/Banner";
|
||||
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,
|
||||
Banner,
|
||||
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
|
||||
);
|
||||
},
|
||||
todayExists: () => {
|
||||
return prelottery()
|
||||
.then(wines => wines.length > 0)
|
||||
.catch(() => false)
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.$ga.page("/");
|
||||
},
|
||||
startCountdown() {
|
||||
this.hardStart = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.notification-request-button {
|
||||
cursor: pointer;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 1350px;
|
||||
margin: auto;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
|
||||
@include mobile {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 3.8rem;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
cursor: pointer;
|
||||
margin: auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.left-bottom {
|
||||
width: 75%;
|
||||
|
||||
@include mobile {
|
||||
width: calc(100% - 20px);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bought-and-highscore-separator {
|
||||
border: none;
|
||||
|
||||
@include desktop {
|
||||
border-bottom: 1px solid rgb(237, 237, 237);
|
||||
}
|
||||
}
|
||||
|
||||
.highscore-and-wines {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 1.5rem;
|
||||
justify-content: space-between;
|
||||
|
||||
@include mobile {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.highscore-container {
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wines-container {
|
||||
width: 65%;
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vipps-outer-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@include desktop {
|
||||
margin-left: 20px;
|
||||
border-left: 1px solid rgb(237, 237, 237);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.outer {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
h1 {
|
||||
width: 100vw;
|
||||
text-align: center;
|
||||
font-family: "knowit";
|
||||
}
|
||||
|
||||
.generate-link {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
text-align: center;
|
||||
margin-bottom: 0px;
|
||||
|
||||
@include mobile {
|
||||
width: 60vw;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.vipps-image {
|
||||
width: 250px;
|
||||
margin: auto;
|
||||
display: block;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.generator-link {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ff5fff;
|
||||
}
|
||||
|
||||
.win,
|
||||
.purchase {
|
||||
width: 48%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.vipps-inner-container {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
margin-left: 30px;
|
||||
|
||||
@include mobile {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 99vw;
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
.wine-and-highscore-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 20vw;
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.purchase,
|
||||
.win {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.generate-link {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding: 0;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.outer {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bottom-container,
|
||||
.highscore-and-wines {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wines-container,
|
||||
.vipps-outer-container,
|
||||
.vipps-container {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
434
src/components/VirtualLotteryPage.vue
Normal file
434
src/components/VirtualLotteryPage.vue
Normal file
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="title">Virtuelt lotteri</h1>
|
||||
<h2
|
||||
v-if="
|
||||
attendees.length <= 0 &&
|
||||
winners.length <= 0 &&
|
||||
attendeesFetched &&
|
||||
winnersFetched
|
||||
"
|
||||
>
|
||||
Her var det lite.. Sikker på at det er en virtuell trekning nå?
|
||||
</h2>
|
||||
<div class="title-info">
|
||||
<h2>Send vipps med melding "Vinlotteri" for å bli registrert til virtuelt lotteri</h2>
|
||||
<p>Send gjerne melding om fargeønsker også</p>
|
||||
</div>
|
||||
|
||||
<router-link to="/dagens" class="generate-link" v-if="todayExists">
|
||||
Lurer du på dagens fangst?
|
||||
<span class="subtext generator-link">Se her</span>
|
||||
</router-link>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Live oversikt av lodd kjøp i dag</h2>
|
||||
<div class="colors">
|
||||
<div
|
||||
v-for="color in Object.keys(ticketsBought)"
|
||||
:class="color + ' colors-box'"
|
||||
:key="color"
|
||||
>
|
||||
<div class="colors-overlay">
|
||||
<p>{{ ticketsBought[color] }} kjøpt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WinnerDraw
|
||||
:currentWinnerDrawn="currentWinnerDrawn"
|
||||
:currentWinner="currentWinner"
|
||||
:attendees="attendees"
|
||||
/>
|
||||
|
||||
<Winners :winners="winners" />
|
||||
<hr />
|
||||
<div class="middle-elements">
|
||||
<Attendees :attendees="attendees" class="outer-attendees" />
|
||||
<Chat
|
||||
class="outer-chat"
|
||||
:chatHistory="chatHistory"
|
||||
:usernameAllowed="usernameAllowed"
|
||||
v-on:message="sendMessage"
|
||||
v-on:username="setUsername"
|
||||
/>
|
||||
</div>
|
||||
<Vipps class="vipps" :amount="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { page, event } from "vue-analytics";
|
||||
import { attendees, winners, getChatHistory, prelottery } from "@/api";
|
||||
import Chat from "@/ui/Chat";
|
||||
import Vipps from "@/ui/Vipps";
|
||||
import Attendees from "@/ui/Attendees";
|
||||
import Winners from "@/ui/Winners";
|
||||
import WinnerDraw from "@/ui/WinnerDraw";
|
||||
import io from "socket.io-client";
|
||||
|
||||
export default {
|
||||
components: { Chat, Attendees, Winners, WinnerDraw, Vipps },
|
||||
data() {
|
||||
return {
|
||||
attendees: [],
|
||||
winners: [],
|
||||
currentWinnerDrawn: false,
|
||||
currentWinner: {},
|
||||
socket: null,
|
||||
attendeesFetched: false,
|
||||
winnersFetched: false,
|
||||
chatHistory: [],
|
||||
usernameAccepted: false,
|
||||
username: null,
|
||||
wasDisconnected: false,
|
||||
emitUsernameOnConnect: false,
|
||||
ticketsBought: {}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
getChatHistory()
|
||||
.then(messages => this.chatHistory = messages)
|
||||
},
|
||||
mounted() {
|
||||
this.track();
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
this.socket = io(`${window.location.hostname}:${window.location.port}`);
|
||||
this.socket.on("color_winner", msg => {});
|
||||
|
||||
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("winner", async msg => {
|
||||
this.currentWinnerDrawn = true;
|
||||
this.currentWinner = { name: msg.name, color: msg.color };
|
||||
|
||||
setTimeout(() => {
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
this.currentWinner = null;
|
||||
this.currentWinnerDrawn = false;
|
||||
}, 12000);
|
||||
});
|
||||
this.socket.on("refresh_data", async msg => {
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
});
|
||||
this.socket.on("new_attendee", async msg => {
|
||||
this.getAttendees();
|
||||
});
|
||||
this.socket.on("accept_username", accepted => {
|
||||
this.usernameAccepted = accepted;
|
||||
if (!accepted) {
|
||||
this.username = null;
|
||||
} else {
|
||||
window.localStorage.setItem("username", this.username);
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
},
|
||||
computed: {
|
||||
todayExists: () => {
|
||||
return prelottery()
|
||||
.then(wines => wines.length > 0)
|
||||
.catch(() => false)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUsername: function(username) {
|
||||
this.username = username;
|
||||
if (!this.socket || !this.socket.emit) {
|
||||
this.emitUsernameOnConnect = true;
|
||||
return;
|
||||
}
|
||||
this.socket.emit("username", { username });
|
||||
},
|
||||
sendMessage: function(msg) {
|
||||
this.socket.emit("chat", { message: msg });
|
||||
},
|
||||
getWinners: async function() {
|
||||
let response = await winners();
|
||||
if (response) {
|
||||
this.winners = response;
|
||||
}
|
||||
this.winnersFetched = true;
|
||||
},
|
||||
getAttendees: async function() {
|
||||
let response = await attendees();
|
||||
if (response) {
|
||||
this.attendees = response;
|
||||
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;
|
||||
},
|
||||
track() {
|
||||
this.$ga.page("/lottery/game");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- TODO move link styling to global with more generic name -->
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
.generate-link {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
text-align: center;
|
||||
margin-bottom: 0px;
|
||||
|
||||
@include mobile {
|
||||
width: 60vw;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.vipps-image {
|
||||
width: 250px;
|
||||
margin: auto;
|
||||
display: block;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.generator-link {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ff5fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
.color-selector {
|
||||
margin-bottom: 0.65rem;
|
||||
margin-right: 1rem;
|
||||
|
||||
@include desktop {
|
||||
min-width: 175px;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
max-width: 25vw;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 2px solid unset;
|
||||
|
||||
&.green {
|
||||
border-color: $green;
|
||||
}
|
||||
&.blue {
|
||||
border-color: $dark-blue;
|
||||
}
|
||||
&.red {
|
||||
border-color: $red;
|
||||
}
|
||||
&.yellow {
|
||||
border-color: $dark-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: 2px solid transparent;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
|
||||
// disable-dbl-tap-zoom
|
||||
touch-action: manipulation;
|
||||
|
||||
@include mobile {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #c8f9df;
|
||||
}
|
||||
&.blue {
|
||||
background: #d4f2fe;
|
||||
}
|
||||
&.red {
|
||||
background: #fbd7de;
|
||||
}
|
||||
&.yellow {
|
||||
background: #fff6d6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
}
|
||||
|
||||
.label-div {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-box {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 20px;
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
@include mobile {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 0.25rem;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
width: 70%;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font-size: 1.5rem;
|
||||
height: unset;
|
||||
max-height: unset;
|
||||
|
||||
@include mobile {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.green,
|
||||
.green .colors-overlay > input {
|
||||
background-color: $light-green;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.blue,
|
||||
.blue .colors-overlay > input {
|
||||
background-color: $light-blue;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.yellow,
|
||||
.yellow .colors-overlay > input {
|
||||
background-color: $light-yellow;
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
.red,
|
||||
.red .colors-overlay > input {
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
hr {
|
||||
width: 80%;
|
||||
}
|
||||
h1,
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
.current-draw {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.title-info {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.outer-chat {
|
||||
margin: 0 60px 0 10px;
|
||||
@include mobile {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.outer-attendees {
|
||||
margin: 0 10px 0 45px;
|
||||
@include mobile {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.center-new-winner {
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.middle-elements {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
|
||||
@include mobile {
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.vipps {
|
||||
margin-top: 70px;
|
||||
display: flex;
|
||||
padding-bottom: 50px;
|
||||
justify-content: center;
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@
|
||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">
|
||||
<div :class="winner.color + '-ballot'" class="ballot-element">
|
||||
<span>{{ winner.name }}</span>
|
||||
<span>{{ winner.phoneNumber }}</span>
|
||||
<span>Rød: {{ winner.red }}</span>
|
||||
@@ -47,11 +47,11 @@
|
||||
<span class="name">{{ attendee.name }}</span>
|
||||
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
||||
</div>
|
||||
<div class="raffles-container">
|
||||
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||
<div class="ballots-container">
|
||||
<div class="red-ballot ballot-element small">{{ attendee.red }}</div>
|
||||
<div class="blue-ballot ballot-element small">{{ attendee.blue }}</div>
|
||||
<div class="green-ballot ballot-element small">{{ attendee.green }}</div>
|
||||
<div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,8 +106,6 @@
|
||||
</div>
|
||||
<br />
|
||||
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
|
||||
|
||||
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -120,17 +118,13 @@ import {
|
||||
attendees,
|
||||
winnersSecure,
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
finishedDraw,
|
||||
prelottery
|
||||
deleteAttendees
|
||||
} from "@/api";
|
||||
import TextToast from "@/ui/TextToast";
|
||||
import RaffleGenerator from "@/ui/RaffleGenerator";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RaffleGenerator,
|
||||
TextToast
|
||||
RaffleGenerator
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -140,7 +134,7 @@ export default {
|
||||
blue: 0,
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
raffles: 0,
|
||||
ballots: 0,
|
||||
randomColors: false,
|
||||
attendees: [],
|
||||
winners: [],
|
||||
@@ -149,9 +143,7 @@ export default {
|
||||
drawTime: 20,
|
||||
currentWinners: 1,
|
||||
numberOfWinners: 4,
|
||||
socket: null,
|
||||
toastText: undefined,
|
||||
showToast: false
|
||||
socket: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -173,23 +165,12 @@ export default {
|
||||
this.socket.on("new_attendee", async msg => {
|
||||
this.getAttendees();
|
||||
});
|
||||
|
||||
window.finishedDraw = finishedDraw;
|
||||
},
|
||||
methods: {
|
||||
setWithRandomColors(colors) {
|
||||
Object.keys(colors).forEach(color => (this[color] = colors[color]));
|
||||
},
|
||||
sendAttendee: async function() {
|
||||
if (this.red == 0 && this.blue == 0 && this.green == 0 && this.yellow == 0) {
|
||||
alert('Ingen farger valgt!')
|
||||
return;
|
||||
}
|
||||
if (this.name == 0 && this.phoneNumber) {
|
||||
alert('Ingen navn eller tlf satt!')
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await addAttendee({
|
||||
name: this.name,
|
||||
phoneNumber: this.phoneNumber,
|
||||
@@ -197,17 +178,12 @@ export default {
|
||||
blue: this.blue,
|
||||
green: this.green,
|
||||
yellow: this.yellow,
|
||||
raffles: this.raffles
|
||||
ballots: this.ballots
|
||||
});
|
||||
|
||||
if (response == true) {
|
||||
this.toastText = `Sendt inn deltaker: ${this.name}`;
|
||||
this.showToast = true;
|
||||
|
||||
alert("Sendt inn deltaker!");
|
||||
this.name = null;
|
||||
this.phoneNumber = null;
|
||||
this.yellow = 0;
|
||||
this.green = 0;
|
||||
this.red = 0;
|
||||
this.blue = 0;
|
||||
|
||||
@@ -225,29 +201,19 @@ export default {
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
drawWinner: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
|
||||
this.drawingWinner = true;
|
||||
let response = await getVirtualWinner();
|
||||
|
||||
if (response.success) {
|
||||
console.log("Winner:", response.winner);
|
||||
if (this.currentWinners < this.numberOfWinners) {
|
||||
this.countdown();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
let finished = await finishedDraw();
|
||||
if(finished) {
|
||||
alert("SMS'er er sendt ut!");
|
||||
} else {
|
||||
alert("Noe gikk galt under SMS utsendelser.. Sjekk logg og database for id'er.");
|
||||
}
|
||||
}
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
this.drawingWinner = true;
|
||||
let response = await getVirtualWinner();
|
||||
if (response) {
|
||||
if (this.currentWinners < this.numberOfWinners) {
|
||||
this.countdown();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
alert("Noe gikk galt under trekningen..! " + response["message"]);
|
||||
}
|
||||
this.getWinners();
|
||||
this.getAttendees();
|
||||
} else {
|
||||
this.drawingWinner = false;
|
||||
alert("Noe gikk galt under trekningen..!");
|
||||
}
|
||||
},
|
||||
countdown: function() {
|
||||
@@ -270,23 +236,19 @@ export default {
|
||||
}, 1000);
|
||||
},
|
||||
deleteAllWinners: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil slette vinnere?")) {
|
||||
let response = await deleteWinners();
|
||||
if (response) {
|
||||
this.getWinners();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
let response = await deleteWinners();
|
||||
if (response) {
|
||||
this.getWinners();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
},
|
||||
deleteAllAttendees: async function() {
|
||||
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
|
||||
let response = await deleteAttendees();
|
||||
if (response) {
|
||||
this.getAttendees();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
let response = await deleteAttendees();
|
||||
if (response) {
|
||||
this.getAttendees();
|
||||
} else {
|
||||
alert("Klarte ikke hente ut vinnere");
|
||||
}
|
||||
},
|
||||
getWinners: async function() {
|
||||
@@ -355,13 +317,13 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
.ballot-element {
|
||||
width: 140px;
|
||||
height: 150px;
|
||||
margin: 20px 0;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
@@ -379,19 +341,19 @@ hr {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.green-raffle {
|
||||
&.green-ballot {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-raffle {
|
||||
&.blue-ballot {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-raffle {
|
||||
&.yellow-ballot {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-raffle {
|
||||
&.red-ballot {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
@@ -423,7 +385,7 @@ button {
|
||||
margin: 0 auto;
|
||||
|
||||
& .name-and-phone,
|
||||
& .raffles-container {
|
||||
& .ballots-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -432,7 +394,7 @@ button {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& .raffles-container {
|
||||
& .ballots-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,8 @@ var serviceWorkerRegistrationMixin = {
|
||||
localStorage.removeItem("push");
|
||||
}
|
||||
}
|
||||
if (window.location.href.includes('localhost')) {
|
||||
console.info("Service worker manually disabled while on localhost.")
|
||||
} else {
|
||||
this.registerPushListener();
|
||||
this.registerServiceWorker();
|
||||
}
|
||||
this.registerPushListener();
|
||||
this.registerServiceWorker();
|
||||
},
|
||||
methods: {
|
||||
registerPushListener: function() {
|
||||
@@ -96,4 +92,4 @@ var serviceWorkerRegistrationMixin = {
|
||||
}
|
||||
};
|
||||
|
||||
export default serviceWorkerRegistrationMixin;
|
||||
module.exports = serviceWorkerRegistrationMixin;
|
||||
49
src/routes/vinlottisRouter.js
Normal file
49
src/routes/vinlottisRouter.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import VinlottisPage from "@/components/VinlottisPage";
|
||||
import GeneratePage from "@/components/GeneratePage";
|
||||
import TodaysPage from "@/components/TodaysPage";
|
||||
import AllWinesPage from "@/components/AllWinesPage";
|
||||
|
||||
import LoginPage from "@/components/LoginPage";
|
||||
import CreatePage from "@/components/CreatePage";
|
||||
|
||||
import AdminPage from "@/components/AdminPage";
|
||||
|
||||
import VirtualLotteryPage from "@/components/VirtualLotteryPage";
|
||||
import LotteryPage from "@/components/LotteryPage";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "*",
|
||||
component: VinlottisPage
|
||||
},
|
||||
{
|
||||
path: "/lottery",
|
||||
component: LotteryPage
|
||||
},
|
||||
{
|
||||
path: "/dagens",
|
||||
component: TodaysPage
|
||||
},
|
||||
{
|
||||
path: "/viner",
|
||||
component: AllWinesPage
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
component: LoginPage
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
component: CreatePage
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
component: AdminPage
|
||||
},
|
||||
{
|
||||
path: "/lottery/:tab",
|
||||
component: LotteryPage
|
||||
}
|
||||
];
|
||||
|
||||
export { routes };
|
||||
@@ -4,13 +4,13 @@
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 600;
|
||||
src: url("/public/assets/fonts/bold.woff");
|
||||
src: url("/../../public/assets/fonts/bold.woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 300;
|
||||
src: url("/public/assets/fonts/regular.woff");
|
||||
src: url("/../../public/assets/fonts/regular.eot");
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -18,10 +18,6 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
@@ -78,16 +74,6 @@ a {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin-right: unset;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
&:not(.row) {
|
||||
flex-direction: column;
|
||||
@@ -112,16 +98,16 @@ textarea {
|
||||
|
||||
.vin-button {
|
||||
font-family: Arial;
|
||||
$color: #b7debd;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background: $primary;
|
||||
background: $color;
|
||||
color: #333;
|
||||
padding: 10px 30px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
width: fit-content;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.3rem;
|
||||
height: 4rem;
|
||||
max-height: 4rem;
|
||||
cursor: pointer;
|
||||
@@ -132,15 +118,6 @@ textarea {
|
||||
// disable-dbl-tap-zoom
|
||||
touch-action: manipulation;
|
||||
|
||||
&.auto-height {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: $red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -156,175 +133,41 @@ textarea {
|
||||
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
&:hover {
|
||||
transform: scale(1.02) translateZ(0);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:disabled{
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: min-content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vin-link {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $link-color;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
text-decoration: none;
|
||||
color: $matte-text-color;
|
||||
|
||||
&:focus, &:hover {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.margin-top {
|
||||
&-md {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.margin-left {
|
||||
&-md {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.margin-right {
|
||||
&-md {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.margin-bottom {
|
||||
&-md {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
&-sm {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.width {
|
||||
&-100 {
|
||||
width: 100%;
|
||||
}
|
||||
&-75 {
|
||||
width: 75%;
|
||||
}
|
||||
&-50 {
|
||||
width: 50%;
|
||||
}
|
||||
&-25 {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
.ballot-element {
|
||||
margin: 20px 0;
|
||||
-webkit-mask-image: url(/public/assets/images/lodd.svg);
|
||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url(/public/assets/images/lodd.svg);
|
||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
color: #333333;
|
||||
|
||||
&.green-raffle {
|
||||
&.green-ballot {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-raffle {
|
||||
&.blue-ballot {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-raffle {
|
||||
&.yellow-ballot {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-raffle {
|
||||
&.red-ballot {
|
||||
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,5 +68,4 @@ form {
|
||||
width: calc(100% - 5rem);
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
18
src/styles/media-queries.scss
Normal file
18
src/styles/media-queries.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
$phone-xs-width: 480px;
|
||||
$tablet-p-width: 768px;
|
||||
$tablet-l-width: 1024px;
|
||||
$desktop-width: 1200px;
|
||||
$desktop-l-width: 1600px;
|
||||
$mobile-width: 768px;
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: #{$mobile-width + 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mobile {
|
||||
@media (max-width: #{$mobile-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user