Merged master into feature branch.

This commit is contained in:
2021-01-22 12:18:03 +01:00
108 changed files with 5021 additions and 5098 deletions

15
.babelrc Normal file
View File

@@ -0,0 +1,15 @@
{
presets: [
[
"@babel/preset-env",
{
modules: false,
targets: {
browsers: ["IE 11", "> 5%"]
},
useBuiltIns: "usage",
corejs: "3"
}
]
]
}

View File

@@ -1,22 +1,46 @@
const path = require("path");
const { addMessage } = require(path.join(__dirname + "/redis.js"));
const validateUsername = (username) => {
let error = undefined;
const illegalChars = /\W/;
const minLength = 3;
const maxLength = 15;
if (typeof username !== 'string') {
error = 'Ugyldig brukernavn.';
} else if (username.length === 0) {
error = 'Vennligst oppgi brukernavn.';
} else if (username.length < minLength || username.length > maxLength) {
error = `Brukernavn må være mellom ${minLength}-${maxLength} karaktere.`
} else if (illegalChars.test(username)) {
error = 'Brukernavn kan bare inneholde tall og bokstaver.'
}
return error;
}
const io = (io) => {
io.on("connection", socket => {
let username = null;
socket.on("username", msg => {
if (msg.username == null) {
const usernameValidationError = validateUsername(msg.username);
if (usernameValidationError) {
username = null;
socket.emit("accept_username", false);
return;
}
if (msg.username.length > 3 && msg.username.length < 30) {
socket.emit("accept_username", {
reason: usernameValidationError,
success: false,
username: undefined
});
} else {
username = msg.username;
socket.emit("accept_username", true);
return;
socket.emit("accept_username", {
reason: undefined,
success: true,
username: msg.username
});
}
socket.emit("accept_username", false);
});
socket.on("chat", msg => {

View File

@@ -1,33 +1,29 @@
const express = require("express");
const path = require("path");
const router = express.Router();
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
router.use((req, res, next) => {
next();
});
const getAllHistory = (req, res) => {
let { page, limit } = req.query;
page = !isNaN(page) ? Number(page) : undefined;
limit = !isNaN(limit) ? Number(limit) : undefined;
router.route("/chat/history").get(async (req, res) => {
let { skip, take } = req.query;
skip = !isNaN(skip) ? Number(skip) : undefined;
take = !isNaN(take) ? Number(take) : undefined;
return history(page, limit)
.then(messages => res.json(messages))
.catch(error => res.status(500).json({
message: error.message,
success: false
}));
};
try {
const messages = await history(skip, take);
res.json(messages)
} catch(error) {
res.status(500).send(error);
}
});
const deleteHistory = (req, res) => {
return clearHistory()
.then(message => res.json(message))
.catch(error => res.status(500).json({
message: error.message,
success: false
}));
};
router.route("/chat/history").delete(async (req, res) => {
try {
const messages = await clearHistory();
res.json(messages)
} catch(error) {
res.status(500).send(error);
}
});
module.exports = router;
module.exports = {
getAllHistory,
deleteHistory
};

View File

@@ -1,59 +0,0 @@
const passport = require("passport");
const path = require("path");
const User = require(path.join(__dirname + "/../schemas/User"));
const router = require("express").Router();
router.get("/", function(req, res) {
res.sendFile(path.join(__dirname + "/../public/index.html"));
});
router.get("/register", function(req, res) {
res.sendFile(path.join(__dirname + "/../public/index.html"));
});
// router.post("/register", function(req, res, next) {
// User.register(
// new User({ username: req.body.username }),
// req.body.password,
// function(err) {
// if (err) {
// if (err.name == "UserExistsError")
// res.status(409).send({ success: false, message: err.message })
// else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
// res.status(400).send({ success: false, message: err.message })
// return next(err);
// }
// return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
// }
// );
// });
router.get("/login", function(req, res) {
res.sendFile(path.join(__dirname + "/../public/index.html"));
});
router.post("/login", function(req, res, next) {
passport.authenticate("local", function(err, user, info) {
if (err) {
if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
return res.status(400).send({ message: err.message, success: false })
return next(err);
}
if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false })
req.logIn(user, (err) => {
if (err) { return next(err) }
return res.status(200).send({ message: "Velkommen " + user.username, success: true })
})
})(req, res, next);
});
router.get("/logout", function(req, res) {
req.logout();
res.redirect("/");
});
module.exports = router;

View File

@@ -1,7 +1,7 @@
const path = require('path');
const Highscore = require(path.join(__dirname + '/../schemas/Highscore'));
const Wine = require(path.join(__dirname + '/../schemas/Wine'));
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();

View File

@@ -29,7 +29,7 @@ async function sendWineSelectMessage(winnerObject) {
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 }.\nKan hentes hos ${ config.name } på kontoret. Ha en ellers fin helg!`)
`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) {
@@ -40,7 +40,7 @@ async function sendLastWinnerMessage(winnerObject, wineObject) {
return sendMessageToUser(
winnerObject.phoneNumber,
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, vinen kan hentes hos ${ config.name } på kontoret. Ha en ellers fin helg!`
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, du vil bli kontaktet av ${ config.name } ang henting. Ha en ellers fin helg!`
);
}

View File

@@ -1,5 +1,10 @@
const mustBeAuthenticated = (req, res, next) => {
console.log(req.isAuthenticated());
if (process.env.NODE_ENV == "development") {
console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
req.isAuthenticated = () => true;
return next();
}
if (!req.isAuthenticated()) {
return res.status(401).send({
success: false,

View File

@@ -0,0 +1,6 @@
const openCORS = (req, res, next) => {
res.set("Access-Control-Allow-Origin", "*")
return next();
};
module.exports = openCORS;

View File

@@ -0,0 +1,37 @@
const camelToKebabCase = str => str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
const mapFeaturePolicyToString = (features) => {
return Object.entries(features).map(([key, value]) => {
key = camelToKebabCase(key)
value = value == "*" ? value : `'${ value }'`
return `${key} ${value}`
}).join("; ")
}
const setupHeaders = (req, res, next) => {
res.set("Access-Control-Allow-Headers", "Content-Type")
// Security
res.set("X-Content-Type-Options", "nosniff");
res.set("X-XSS-Protection", "1; mode=block");
res.set("X-Frame-Options", "SAMEORIGIN");
res.set("X-DNS-Prefetch-Control", "off");
res.set("X-Download-Options", "noopen");
res.set("Strict-Transport-Security", "max-age=15552000; includeSubDomains")
// Feature policy
const features = {
fullscreen: "*",
payment: "none",
microphone: "none",
camera: "self",
speaker: "*",
syncXhr: "self"
}
const featureString = mapFeaturePolicyToString(features);
res.set("Feature-Policy", featureString)
return next();
}
module.exports = setupHeaders;

View File

@@ -1,5 +1,5 @@
const path = require("path");
const Highscore = require(path.join(__dirname + "/../schemas/Highscore"));
const Highscore = require(path.join(__dirname, "/schemas/Highscore"));
async function findSavePerson(foundWinner, wonWine, date) {
let person = await Highscore.findOne({

View File

@@ -1,29 +1,40 @@
const { promisify } = require("util"); // from node
let client;
let llenAsync;
let lrangeAsync;
try {
const redis = require("redis");
console.log("trying to create redis");
console.log("Trying to connect with redis..");
client = redis.createClient();
client.zcount = promisify(client.zcount).bind(client);
client.zadd = promisify(client.zadd).bind(client);
client.zrevrange = promisify(client.zrevrange).bind(client);
client.del = promisify(client.del).bind(client);
client.on("connect", () => console.log("Redis connection established!"));
client.on("error", function(err) {
client.quit();
console.error("Missing redis-configurations..");
console.error("Unable to connect to redis, setting up redis-mock.");
client = {
rpush: function() {
console.log("redis-dummy lpush", arguments);
if (typeof arguments[arguments.length - 1] == "function") {
arguments[arguments.length - 1](null);
}
zcount: function() {
console.log("redis-dummy zcount", arguments);
return Promise.resolve()
},
lrange: function() {
console.log("redis-dummy lrange", arguments);
if (typeof arguments[arguments.length - 1] == "function") {
arguments[arguments.length - 1](null);
}
zadd: function() {
console.log("redis-dummy zadd", arguments);
return Promise.resolve();
},
zrevrange: function() {
console.log("redis-dummy zrevrange", arguments);
return Promise.resolve(null);
},
del: function() {
console.log("redis-dummy del", arguments);
if (typeof arguments[arguments.length - 1] == "function") {
arguments[arguments.length - 1](null);
}
return Promise.resolve();
}
};
});
@@ -31,36 +42,46 @@ try {
const addMessage = message => {
const json = JSON.stringify(message);
client.rpush("messages", json);
return message;
return client.zadd("messages", message.timestamp, json)
.then(position => {
return {
success: true
}
})
};
const history = (skip = 0, take = 20) => {
skip = (1 + skip) * -1; // negate to get FIFO
return new Promise((resolve, reject) =>
client.lrange("messages", skip * take, skip, (err, data) => {
if (err) {
console.log(err);
reject(err);
}
const history = (page=1, limit=10) => {
const start = (page - 1) * limit;
const stop = (limit * page) - 1;
data = data.map(data => JSON.parse(data));
resolve(data);
const getTotalCount = client.zcount("messages", '-inf', '+inf');
const getMessages = client.zrevrange("messages", start, stop);
return Promise.all([getTotalCount, getMessages])
.then(([totalCount, messages]) => {
if (messages) {
return {
messages: messages.map(entry => JSON.parse(entry)).reverse(),
count: messages.length,
total: totalCount
}
} else {
return {
messages: [],
count: 0,
total: 0
}
}
})
);
};
const clearHistory = () => {
return new Promise((resolve, reject) =>
client.del("messages", (err, success) => {
if (err) {
console.log(err);
reject(err);
return client.del("messages")
.then(success => {
return {
success: success == 1 ? true : false
}
resolve(success == 1 ? true : false);
})
);
};
module.exports = {

View File

@@ -1,10 +1,10 @@
const express = require("express");
const path = require("path");
const RequestedWine = require(path.join(
__dirname + "/../schemas/RequestedWine"
__dirname, "/schemas/RequestedWine"
));
const Wine = require(path.join(
__dirname + "/../schemas/Wine"
__dirname, "/schemas/Wine"
));
const deleteRequestedWineById = async (req, res) => {

View File

@@ -1,10 +1,10 @@
const path = require("path");
const Purchase = require(path.join(__dirname + "/../schemas/Purchase"));
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
const Highscore = require(path.join(__dirname + "/../schemas/Highscore"));
const 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) => {

View File

@@ -1,22 +1,21 @@
const express = require("express");
const path = require("path");
// Middleware
const mustBeAuthenticated = require(__dirname + "/../middleware/mustBeAuthenticated");
const setAdminHeaderIfAuthenticated = require(__dirname + "/../middleware/setAdminHeaderIfAuthenticated");
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 loginApi = require(path.join(__dirname + "/login"));
const wineinfo = require(path.join(__dirname + "/wineinfo"));
const virtualApi = require(path.join(__dirname + "/virtualLottery"));
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"
__dirname, "/virtualRegistration"
));
const lottery = require(path.join(__dirname + "/lottery"));
const lottery = require(path.join(__dirname, "/lottery"));
const chatHistoryApi = require(path.join(__dirname, "/chatHistory"));
const router = express.Router();
@@ -61,10 +60,11 @@ router.post('/winner/notify/:id', virtualRegistrationApi.sendNotificationToWinne
router.get('/winner/:id', virtualRegistrationApi.getWinesToWinnerById);
router.post('/winner/:id', virtualRegistrationApi.registerWinnerSelection);
// router.use("/api/", updateApi);
// router.use("/api/", retrieveApi);
// router.use("/api/", wineinfoApi);
// router.use("/api/lottery", lottery);
// router.use("/virtual-registration/", virtualRegistrationApi);
router.get('/chat/history', chatHistoryApi.getAllHistory)
router.delete('/chat/history', mustBeAuthenticated, chatHistoryApi.deleteHistory)
router.post('/login', userApi.login);
router.post('/register', mustBeAuthenticated, userApi.register);
router.get('/logout', userApi.logout);
module.exports = router;

View File

@@ -5,11 +5,11 @@ const webpush = require("web-push"); //requiring the web-push module
const schedule = require("node-schedule");
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"
));

View File

@@ -1,14 +1,14 @@
const express = require("express");
const path = require("path");
const sub = require(path.join(__dirname + "/../api/subscriptions"));
const sub = require(path.join(__dirname, "/subscriptions"));
const _wineFunctions = require(path.join(__dirname + "/../api/wine"));
const _personFunctions = require(path.join(__dirname + "/../api/person"));
const Subscription = require(path.join(__dirname + "/../schemas/Subscription"));
const Lottery = require(path.join(__dirname + "/../schemas/Purchase"));
const _wineFunctions = require(path.join(__dirname, "/wine"));
const _personFunctions = require(path.join(__dirname, "/person"));
const Subscription = require(path.join(__dirname, "/schemas/Subscription"));
const Lottery = require(path.join(__dirname, "/schemas/Purchase"));
const PreLotteryWine = require(path.join(
__dirname + "/../schemas/PreLotteryWine"
__dirname, "/schemas/PreLotteryWine"
));
const submitWines = async (req, res) => {

51
api/user.js Normal file
View File

@@ -0,0 +1,51 @@
const passport = require("passport");
const path = require("path");
const User = require(path.join(__dirname, "/schemas/User"));
const router = require("express").Router();
const register = (req, res, next) => {
User.register(
new User({ username: req.body.username }),
req.body.password,
function(err) {
if (err) {
if (err.name == "UserExistsError")
res.status(409).send({ success: false, message: err.message })
else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
res.status(400).send({ success: false, message: err.message })
return next(err);
}
return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
}
);
};
const login = (req, res, next) => {
passport.authenticate("local", function(err, user, info) {
if (err) {
if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
return res.status(400).send({ message: err.message, success: false })
return next(err);
}
if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false })
req.logIn(user, (err) => {
if (err) { return next(err) }
return res.status(200).send({ message: "Velkommen " + user.username, success: true })
})
})(req, res, next);
};
const logout = (req, res) => {
req.logout();
res.redirect("/");
};
module.exports = {
register,
login,
logout
};

View File

@@ -1,13 +1,13 @@
const path = require("path");
const crypto = require("crypto");
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
const Message = require(path.join(__dirname + "/message"));
const { findAndNotifyNextWinner } = require(path.join(__dirname + "/virtualRegistration"));
const config = require(path.join(__dirname, "/../config/defaults/lottery"));
const Message = require(path.join(__dirname, "/message"));
const { findAndNotifyNextWinner } = require(path.join(__dirname, "/virtualRegistration"));
const Attendee = require(path.join(__dirname + "/../schemas/Attendee"));
const VirtualWinner = require(path.join(__dirname + "/../schemas/VirtualWinner"));
const PreLotteryWine = require(path.join(__dirname + "/../schemas/PreLotteryWine"));
const Attendee = require(path.join(__dirname, "/schemas/Attendee"));
const VirtualWinner = require(path.join(__dirname, "/schemas/VirtualWinner"));
const PreLotteryWine = require(path.join(__dirname, "/schemas/PreLotteryWine"));
const winners = async (req, res) => {
@@ -166,8 +166,16 @@ 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 });
io.emit("winner", {
color: colorToChooseFrom,
name: winner.name,
winner_count: winners.length + 1
});
let newWinnerElement = new VirtualWinner({
name: winner.name,

View File

@@ -1,13 +1,13 @@
const path = require("path");
const _wineFunctions = require(path.join(__dirname + "/../api/wine"));
const _personFunctions = require(path.join(__dirname + "/../api/person"));
const Message = require(path.join(__dirname + "/../api/message"));
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"
__dirname, "/schemas/VirtualWinner"
));
const PreLotteryWine = require(path.join(
__dirname + "/../schemas/PreLotteryWine"
__dirname, "/schemas/PreLotteryWine"
));

View File

@@ -1,5 +1,5 @@
const path = require("path");
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
const Wine = require(path.join(__dirname, "/schemas/Wine"));
async function findSaveWine(prelotteryWine) {
let wonWine = await Wine.findOne({ name: prelotteryWine.name });

View File

@@ -2,7 +2,7 @@ try {
module.exports = require("../env/lottery.config");
} catch (e) {
console.error(
"You haven't defined lottery-configs, you sure you want to continue without them?"
"⚠️ You haven't defined lottery-configs, you sure you want to continue without them?\n"
);
module.exports = {
name: "NAME MISSING",
@@ -11,7 +11,6 @@ try {
message: "INSERT MESSAGE",
date: 5,
hours: 15,
apiUrl: "http://localhost:30030",
gatewayToken: "asd"
};
}

View File

@@ -2,7 +2,7 @@ try {
module.exports = require("../env/push.config");
} catch (e) {
console.error(
"You haven't defined push-parameters, you sure you want to continue without them?"
"⚠️ You haven't defined push-parameters, you sure you want to continue without them?\n"
);
module.exports = { publicKey: false, privateKey: false, mailto: false };
}

View File

@@ -5,7 +5,8 @@ module.exports = {
message: "VINLOTTERI",
date: 5,
hours: 15,
apiUrl: undefined,
gatewayToken: undefined,
vinmonopoletToken: undefined
vinmonopoletToken: undefined,
googleanalytics_trackingId: undefined,
googleanalytics_cookieLifetime: 60 * 60 * 24 * 14
};

View File

@@ -2,14 +2,14 @@
const webpack = require("webpack");
const helpers = require("./helpers");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const ServiceWorkerConfig = {
resolve: {
extensions: [".js", ".vue"]
},
entry: {
serviceWorker: [helpers.root("src/service-worker", "service-worker")]
serviceWorker: [helpers.root("frontend/service-worker", "service-worker")]
},
optimization: {
minimizer: []
@@ -19,7 +19,7 @@ const ServiceWorkerConfig = {
{
test: /\.js$/,
loader: "babel-loader",
include: [helpers.root("src/service-worker", "service-worker")]
include: [helpers.root("frontend/service-worker", "service-worker")]
}
]
},
@@ -31,11 +31,10 @@ const ServiceWorkerConfig = {
//filename: "js/[name].bundle.js"
},
optimization: {
minimize: true,
minimizer: [
new UglifyJSPlugin({
cache: true,
parallel: false,
sourceMap: false
new TerserPlugin({
test: /\.js(\?.*)?$/i,
})
]
},

View File

@@ -1,28 +0,0 @@
"use strict";
const HtmlWebpackPlugin = require("html-webpack-plugin");
const helpers = require("./helpers");
const VinlottisConfig = {
entry: {
vinlottis: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
},
optimization: {
minimizer: [
new HtmlWebpackPlugin({
chunks: ["vinlottis"],
filename: "../index.html",
template: "./src/templates/Create.html",
inject: true,
minify: {
removeComments: true,
collapseWhitespace: false,
preserveLineBreaks: true,
removeAttributeQuotes: true
}
})
]
}
};
module.exports = VinlottisConfig;

View File

@@ -11,10 +11,16 @@ const webpackConfig = function(isDev) {
resolve: {
extensions: [".js", ".vue"],
alias: {
vue$: isDev ? "vue/dist/vue.min.js" : "vue/dist/vue.min.js",
"@": helpers.root("src")
vue$: "vue/dist/vue.min.js",
"@": helpers.root("frontend")
}
},
entry: {
vinlottis: helpers.root("frontend", "vinlottis-init")
},
externals: {
moment: 'moment' // comes with chart.js
},
module: {
rules: [
{
@@ -33,35 +39,31 @@ const webpackConfig = function(isDev) {
},
{
test: /\.js$/,
loader: "babel-loader",
include: [helpers.root("src")]
use: [ "babel-loader" ],
include: [helpers.root("frontend")]
},
{
test: /\.css$/,
use: [
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
MiniCSSExtractPlugin.loader,
{ loader: "css-loader", options: { sourceMap: isDev } }
]
},
{
test: /\.scss$/,
use: [
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
{ loader: "css-loader", options: { sourceMap: isDev } },
{ loader: "sass-loader", options: { sourceMap: isDev } }
]
},
{
test: /\.sass$/,
use: [
isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
MiniCSSExtractPlugin.loader,
{ loader: "css-loader", options: { sourceMap: isDev } },
{ loader: "sass-loader", options: { sourceMap: isDev } }
]
},
{
test: /\.woff(2)?(\?[a-z0-9]+)?$/,
loader: "url-loader?limit=10000&mimetype=application/font-woff"
loader: "url-loader",
options: {
limit: 10000,
mimetype: "application/font-woff"
}
},
{
test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/,
@@ -72,14 +74,16 @@ const webpackConfig = function(isDev) {
plugins: [
new VueLoaderPlugin(),
new webpack.DefinePlugin({
__ENV__: JSON.stringify(process.env.NODE_ENV),
__NAME__: JSON.stringify(env.name),
__PHONE__: JSON.stringify(env.phone),
__PRICE__: env.price,
__MESSAGE__: JSON.stringify(env.message),
__DATE__: env.date,
__HOURS__: env.hours,
__APIURL__: JSON.stringify(env.apiUrl),
__PUSHENABLED__: JSON.stringify(require("./defaults/push") != false)
__PUSHENABLED__: JSON.stringify(require("./defaults/push") != false),
__GA_TRACKINGID__: JSON.stringify(env.googleanalytics_trackingId),
__GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime
})
]
};

View File

@@ -1,52 +1,63 @@
"use strict";
const webpack = require("webpack");
const merge = require("webpack-merge");
const { merge } = require("webpack-merge");
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
const HtmlPlugin = require("html-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const helpers = require("./helpers");
const commonConfig = require("./webpack.config.common");
const environment = require("./env/dev.env");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
let webpackConfig = merge(commonConfig(true), {
mode: "development",
devtool: "cheap-module-eval-source-map",
devtool: "eval-cheap-module-source-map",
output: {
path: helpers.root("dist"),
publicPath: "/",
filename: "js/[name].bundle.js",
chunkFilename: "js/[id].chunk.js"
filename: "js/[name].bundle.js"
},
optimization: {
runtimeChunk: "single",
concatenateModules: true,
splitChunks: {
chunks: "all"
chunks: "initial"
}
},
plugins: [
new webpack.EnvironmentPlugin(environment),
new webpack.HotModuleReplacementPlugin(),
new FriendlyErrorsPlugin()
new FriendlyErrorsPlugin(),
new MiniCSSExtractPlugin({
filename: "css/[name].css"
})
],
devServer: {
compress: true,
historyApiFallback: true,
host: "0.0.0.0",
hot: true,
overlay: true,
stats: {
normal: true
}
},
proxy: {
"/api": {
target: "http://localhost:30030",
changeOrigin: true
},
"/socket.io": {
target: "ws://localhost:30030",
changeOrigin: false,
ws: true
}
},
writeToDisk: false
}
});
webpackConfig = merge(webpackConfig, {
entry: {
main: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
},
plugins: [
new HtmlPlugin({
template: "src/templates/Create.html",
chunksSortMode: "dependency"
new HtmlWebpackPlugin({
template: "frontend/templates/Index.html"
})
]
});

View File

@@ -3,12 +3,15 @@
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const path = require("path");
const webpack = require("webpack");
const merge = require("webpack-merge");
const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const helpers = require("./helpers");
const commonConfig = require("./webpack.config.common");
const isProd = process.env.NODE_ENV === "production";
const environment = isProd
? require("./env/prod.env")
@@ -16,11 +19,11 @@ const environment = isProd
const webpackConfig = merge(commonConfig(false), {
mode: "production",
stats: { children: false },
output: {
path: helpers.root("public/dist"),
publicPath: "/dist/",
filename: "js/[name].bundle.[hash:7].js"
//filename: "js/[name].bundle.js"
publicPath: "/public/dist/",
filename: "js/[name].bundle.[fullhash:7].js"
},
optimization: {
splitChunks: {
@@ -33,37 +36,47 @@ const webpackConfig = merge(commonConfig(false), {
}
}
},
minimize: true,
minimizer: [
new HtmlWebpackPlugin({
chunks: ["vinlottis"],
filename: "index.html",
template: "./frontend/templates/Index.html",
inject: true,
minify: {
removeComments: true,
collapseWhitespace: false,
preserveLineBreaks: true,
removeAttributeQuotes: true
}
}),
new OptimizeCSSAssetsPlugin({
cssProcessorPluginOptions: {
preset: ["default", { discardComments: { removeAll: true } }]
}
}),
new UglifyJSPlugin({
cache: true,
parallel: false,
sourceMap: !isProd
new TerserPlugin({
test: /\.js(\?.*)?$/i,
})
]
},
plugins: [
new CleanWebpackPlugin(),
new CleanWebpackPlugin(), // clean output folder
new webpack.EnvironmentPlugin(environment),
new MiniCSSExtractPlugin({
filename: "css/[name].[hash:7].css"
//filename: "css/[name].css"
filename: "css/[name].[fullhash:7].css"
})
]
});
if (!isProd) {
webpackConfig.devtool = "source-map";
}
if (process.env.npm_config_report) {
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
if (process.env.BUILD_REPORT) {
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
module.exports = webpackConfig;

View File

@@ -28,17 +28,21 @@ export default {
toastText: null,
refreshToast: false,
routes: [
{
name: "Virtuelt lotteri",
route: "/lottery"
},
{
name: "Dagens viner",
route: "/dagens/"
},
{
name: "Historie",
route: "/history/"
name: "Highscore",
route: "/highscore"
},
{
name: "Lotteriet",
route: "/lottery/game/"
name: "Historie",
route: "/history/"
},
{
name: "Foreslå vin",
@@ -49,8 +53,8 @@ export default {
route: "/requested-wines"
},
{
name: "Highscore",
route: "/highscore"
name: "Login",
route: "/login"
}
]
};
@@ -83,22 +87,6 @@ export default {
@import "styles/positioning.scss";
@import "styles/vinlottis-icons";
@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;
}

369
frontend/api.js Normal file
View File

@@ -0,0 +1,369 @@
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);
}
})
}
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
};

View File

@@ -39,8 +39,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "../styles/media-queries.scss";
@import "./src/styles/variables.scss";
@import "@/styles/media-queries.scss";
@import "@/styles/variables.scss";
.container {
width: 90vw;

View File

@@ -32,7 +32,6 @@
</template>
<script>
import { page, event } from "vue-analytics";
import Banner from "@/ui/Banner";
import Wine from "@/ui/Wine";
import { overallWineStatistics } from "@/api";
@@ -62,8 +61,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/media-queries";
@import "./src/styles/variables";
@import "@/styles/media-queries";
@import "@/styles/variables";
.container {
width: 90vw;

View File

@@ -13,7 +13,6 @@
</template>
<script>
import { page, event } from "vue-analytics";
import RaffleGenerator from "@/ui/RaffleGenerator";
import Vipps from "@/ui/Vipps";
import Countdown from "@/ui/Countdown";
@@ -44,7 +43,7 @@ export default {
this.hardStart = true;
},
track() {
this.$ga.page("/lottery/generate");
window.ga('send', 'pageview', '/lottery/generate');
}
}
};

View File

@@ -93,8 +93,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/media-queries.scss";
@import "./src/styles/variables.scss";
@import "@/styles/media-queries.scss";
@import "@/styles/variables.scss";
$elementSpacing: 3.5rem;
.el-spacing {

View File

@@ -125,8 +125,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/variables";
@import "./src/styles/media-queries";
@import "@/styles/variables";
@import "@/styles/media-queries";
$elementSpacing: 3rem;

View File

@@ -513,8 +513,7 @@ hr {
}
}
.winner-container {
width: max-content;
max-width: 100%;
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@@ -527,7 +526,13 @@ hr {
margin-top: 2rem;
display: flex;
justify-content: center;
flex-direction: column;
flex-direction: row;
flex-wrap: wrap;
> .wine {
margin-right: 1rem;
margin-bottom: 1rem;
}
}
.edit {
width: 100%;
@@ -654,9 +659,9 @@ hr {
width: 150px;
height: 150px;
margin: 20px;
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
-webkit-mask-image: url(/public/assets/images/lodd.svg);
background-repeat: no-repeat;
mask-image: url(/../../public/assets/images/lodd.svg);
mask-image: url(/public/assets/images/lodd.svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;

View File

@@ -97,9 +97,9 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/media-queries";
@import "./src/styles/global";
@import "./src/styles/variables";
@import "@/styles/media-queries";
@import "@/styles/global";
@import "@/styles/variables";
h1{

View File

@@ -8,7 +8,6 @@
</template>
<script>
import { page, event } from "vue-analytics";
import { prelottery } from "@/api";
import Banner from "@/ui/Banner";
import Wine from "@/ui/Wine";
@@ -30,8 +29,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/media-queries";
@import "./src/styles/variables";
@import "@/styles/media-queries";
@import "@/styles/variables";
.wine-image {
height: 250px;

View File

@@ -2,7 +2,7 @@
<main class="main-container">
<section class="top-container">
<div class="want-to-win">
<h1>
Vil du også vinne?
@@ -17,12 +17,12 @@
/>
</div>
<router-link to="/lottery/game" class="participate-button">
<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="/lottery/generate" class="see-details-link">
<router-link to="/generate" class="see-details-link">
Se vipps detaljer og QR-kode
</router-link>
@@ -40,33 +40,32 @@
</div>
</section>
<section class="content-container">
<div class="scroll-info">
<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 { page, event } from "vue-analytics";
import PurchaseGraph from "@/ui/PurchaseGraph";
import TotalBought from "@/ui/TotalBought";
import Highscore from "@/ui/Highscore";
@@ -121,7 +120,7 @@ export default {
this.hardStart = way;
},
track() {
this.$ga.page("/");
window.ga('send', 'pageview', '/');
},
startCountdown() {
this.hardStart = true;
@@ -161,7 +160,7 @@ export default {
font-size: 2em;
font-weight: 400;
}
@include tablet {
h1{
font-size: 3em;
@@ -187,7 +186,7 @@ export default {
align-items: center;
text-decoration: none;
color: black;
i {
color: $link-color;
margin-left: 5px;
@@ -207,7 +206,7 @@ export default {
.see-details-link {
grid-row: 6 / 8;
grid-column: 2 / -1;
@include tablet {
grid-row: 6 / 8;
grid-column: 2 / 10;
@@ -325,7 +324,7 @@ h1 {
display: grid;
grid-template-columns: repeat(12, 1fr);
row-gap: 5em;
.scroll-info {
display: flex;
align-items: center;

View File

@@ -0,0 +1,388 @@
<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 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 {
display: flex;
flex-wrap: wrap;
margin-bottom: 4rem;
h2 {
width: 100%;
grid-column: 1 / 5;
}
.wine {
margin-right: 1rem;
margin-bottom: 1rem;
}
}
</style>

View File

@@ -359,9 +359,9 @@ hr {
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;

View File

@@ -70,7 +70,7 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/global";
@import "@/styles/global";
.container {
display: flex;
justify-content: center;

View File

@@ -9,8 +9,12 @@ var serviceWorkerRegistrationMixin = {
localStorage.removeItem("push");
}
}
this.registerPushListener();
this.registerServiceWorker();
if (window.location.href.includes('localhost')) {
console.info("Service worker manually disabled while on localhost.")
} else {
this.registerPushListener();
this.registerServiceWorker();
}
},
methods: {
registerPushListener: function() {
@@ -92,4 +96,4 @@ var serviceWorkerRegistrationMixin = {
}
};
module.exports = serviceWorkerRegistrationMixin;
export default serviceWorkerRegistrationMixin;

124
frontend/router.js Normal file
View File

@@ -0,0 +1,124 @@
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 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
}
];
export { routes };

View File

@@ -1,7 +1,7 @@
@import "./media-queries.scss";
@import "./variables.scss";
.top-banner{
.top-banner {
position: sticky;
top: 0;
z-index: 1;
@@ -24,11 +24,11 @@
}
}
.company-logo{
.company-logo {
grid-area: logo;
}
.menu-toggle-container{
.menu-toggle-container {
grid-area: menu;
color: #1e1e1e;
border-radius: 50% 50%;
@@ -40,11 +40,12 @@
flex-direction: column;
justify-content: center;
align-items: center;
&:hover{
&:hover {
cursor: pointer;
}
span{
span {
display: block;
position: relative;
border-radius: 3px;
@@ -53,46 +54,45 @@
background: #111;
z-index: 1;
transform-origin: 4px 0px;
transition:
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{
span:first-child {
transform-origin: 0% 0%;
margin-bottom: 4px;
}
span:nth-last-child(2){
span:nth-last-child(2) {
transform-origin: 0% 100%;
margin-bottom: 4px;
}
&.open{
&.open {
span{
opacity: 1;
transform: rotate(-45deg) translate(2px, -2px);
background: #232323;
}
span:nth-last-child(2){
span:nth-last-child(2) {
opacity: 0;
transform: rotate(0deg) scale(0.2, 0.2);
}
span:nth-last-child(3){
span:nth-last-child(3) {
transform: rotate(45deg) translate(3.5px, -2px);
}
}
&.open{
&.open {
background: #fff;
}
}
.menu{
.menu {
position: fixed;
top: 0;
background-color: $primary;
@@ -108,15 +108,33 @@
justify-content: center;
row-gap: 3em;
&.collapsed{
&.collapsed {
max-height: 0%;
}
a{
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{
.single-route {
font-size: 3em;
outline: 0;
text-decoration: none;
@@ -124,16 +142,17 @@
border-bottom: 4px solid transparent;
display: block;
&.open{
&.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{
&:hover {
cursor: pointer;
border-color: $link-color;
}
}
}

View File

@@ -4,13 +4,13 @@
@font-face {
font-family: "knowit";
font-weight: 600;
src: url("/../../public/assets/fonts/bold.woff");
src: url("/public/assets/fonts/bold.woff");
}
@font-face {
font-family: "knowit";
font-weight: 300;
src: url("/../../public/assets/fonts/regular.eot");
src: url("/public/assets/fonts/regular.woff");
}
body {
@@ -112,10 +112,9 @@ textarea {
.vin-button {
font-family: Arial;
$color: #b7debd;
position: relative;
display: inline-block;
background: $color;
background: $primary;
color: #333;
padding: 10px 30px;
margin: 0;
@@ -188,7 +187,7 @@ textarea {
.vin-link {
font-weight: bold;
border-bottom: 1px solid $link-color;
font-size: 1rem;
font-size: inherit;
cursor: pointer;
text-decoration: none;
@@ -272,9 +271,9 @@ textarea {
.raffle-element {
margin: 20px 0;
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
-webkit-mask-image: url(/public/assets/images/lodd.svg);
background-repeat: no-repeat;
mask-image: url(/../../public/assets/images/lodd.svg);
mask-image: url(/public/assets/images/lodd.svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
color: #333333;

View File

@@ -68,4 +68,5 @@ form {
width: calc(100% - 5rem);
background-color: $light-red;
color: $red;
font-size: 1.5rem;
}

View File

@@ -25,4 +25,16 @@ $desktop-max: 2004px;
@media (min-width: #{$desktop-max + 1px}){
@content;
}
}
.desktop-only {
@include mobile {
display: none;
}
}
.mobile-only {
@include tablet {
display: none;
}
}

View File

@@ -55,6 +55,7 @@
<div id="app"></div>
<noscript>Du trenger vin, jeg trenger javascript!</noscript>
<script src="/public/analytics.js" async></script>
</body>
</html>

View File

@@ -1,6 +1,5 @@
<template>
<div class="attendees" v-if="attendees.length > 0">
<h2>Deltakere ({{ attendees.length }})</h2>
<div class="attendees-container" ref="attendees">
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
<span class="attendee-name">{{ attendee.name }}</span>
@@ -42,10 +41,16 @@ export default {
@import "../styles/global.scss";
@import "../styles/variables.scss";
@import "../styles/media-queries.scss";
.attendee-name {
width: 60%;
}
hr {
border: 2px solid black;
width: 100%;
}
.raffle-element {
font-size: 0.75rem;
width: 45px;
@@ -56,20 +61,24 @@ export default {
font-weight: bold;
font-size: 0.75rem;
text-align: center;
&:not(:last-of-type) {
margin-right: 1rem;
}
}
.attendees {
display: flex;
flex-direction: column;
align-items: center;
width: 65%;
height: 100%;
height: auto;
}
.attendees-container {
width: 100%;
height: 100%;
overflow-y: scroll;
max-height: 550px;
}
.attendee {
@@ -78,5 +87,9 @@ export default {
align-items: center;
width: 100%;
margin: 0 auto;
&:not(:last-of-type) {
border-bottom: 2px solid #d7d8d7;
}
}
</style>

View File

@@ -13,10 +13,11 @@
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'" >
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link" >
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{route.name}}</a>
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{ route.name }}</a>
<i class="icon icon--arrow-right"></i>
</router-link>
</nav>
<div class="clock">
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
<span v-if="days > 0">{{ pad(days) }}:</span>

347
frontend/ui/Chat.vue Normal file
View File

@@ -0,0 +1,347 @@
<template>
<div class="chat-container">
<span class="logged-in-username" v-if="username">Logget inn som: <span class="username">{{ username }}</span> <button @click="removeUsername">Logg ut</button></span>
<div class="history" ref="history" v-if="chatHistory.length > 0">
<div class="opaque-skirt"></div>
<div v-if="hasMorePages" class="fetch-older-history">
<button @click="loadMoreHistory">Hent eldre meldinger</button>
</div>
<div class="history-message"
v-for="(history, index) in chatHistory"
:key="`${history.username}-${history.timestamp}-${index}`"
>
<div>
<span class="username">{{ history.username }}</span>
<span class="timestamp">{{ getTime(history.timestamp) }}</span>
</div>
<span class="message">{{ history.message }}</span>
</div>
</div>
<div v-if="username" class="user-actions">
<input @keyup.enter="sendMessage" type="text" v-model="message" placeholder="Melding.." />
<button @click="sendMessage">Send</button>
</div>
<div v-else class="username-dialog">
<input
type="text"
@keyup.enter="setUsername"
v-model="temporaryUsername"
maxlength="30"
placeholder="Ditt navn.."
/>
<div class="validation-error" v-if="validationError">
{{ validationError }}
</div>
<button @click="setUsername">Lagre brukernavn</button>
</div>
</div>
</template>
<script>
import { getChatHistory } from "@/api";
import io from "socket.io-client";
export default {
data() {
return {
socket: null,
chatHistory: [],
hasMorePages: true,
message: "",
page: 1,
pageSize: 100,
temporaryUsername: null,
username: null,
validationError: undefined
};
},
created() {
getChatHistory(1, this.pageSize)
.then(resp => {
this.chatHistory = resp.messages;
this.hasMorePages = resp.total != resp.messages.length;
});
const username = window.localStorage.getItem('username');
if (username) {
this.username = username;
this.emitUsernameOnConnect = true;
}
},
watch: {
chatHistory: {
handler: function(newVal, oldVal) {
if (oldVal.length == 0) {
this.scrollToBottomOfHistory();
}
else if (newVal && newVal.length == oldVal.length) {
if (this.isScrollPositionAtBottom()) {
this.scrollToBottomOfHistory();
}
} else {
const prevOldestMessage = oldVal[0];
this.scrollToMessageElement(prevOldestMessage);
}
},
deep: true
}
},
beforeDestroy() {
this.socket.disconnect();
this.socket = null;
},
mounted() {
this.socket = io(window.location.origin);
this.socket.on("chat", msg => {
this.chatHistory.push(msg);
});
this.socket.on("disconnect", msg => {
this.wasDisconnected = true;
});
this.socket.on("connect", msg => {
if (
this.emitUsernameOnConnect ||
(this.wasDisconnected && this.username != null)
) {
this.setUsername(this.username);
}
});
this.socket.on("accept_username", msg => {
const { reason, success, username } = msg;
this.usernameAccepted = success;
if (success !== true) {
this.username = null;
this.validationError = reason;
} else {
this.usernameAllowed = true;
this.username = username;
this.validationError = null;
window.localStorage.setItem("username", username);
}
});
},
methods: {
loadMoreHistory() {
let { page, pageSize } = this;
page = page + 1;
getChatHistory(page, pageSize)
.then(resp => {
this.chatHistory = resp.messages.concat(this.chatHistory);
this.page = page;
this.hasMorePages = resp.total != this.chatHistory.length;
});
},
pad(num) {
if (num > 9) return num;
return `0${num}`;
},
getTime(timestamp) {
let date = new Date(timestamp);
const timeString = `${this.pad(date.getHours())}:${this.pad(
date.getMinutes()
)}:${this.pad(date.getSeconds())}`;
if (date.getDate() == new Date().getDate()) {
return timeString;
}
return `${date.toLocaleDateString()} ${timeString}`;
},
sendMessage() {
const message = { message: this.message };
this.socket.emit("chat", message);
this.message = '';
this.scrollToBottomOfHistory();
},
setUsername(username=undefined) {
if (this.temporaryUsername) {
username = this.temporaryUsername;
}
const message = { username: username };
this.socket.emit("username", message);
},
removeUsername() {
this.username = null;
this.temporaryUsername = null;
window.localStorage.removeItem("username");
},
isScrollPositionAtBottom() {
const { history } = this.$refs;
if (history) {
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
}
return false
},
scrollToBottomOfHistory() {
setTimeout(() => {
const { history } = this.$refs;
history.scrollTop = history.scrollHeight;
}, 1);
},
scrollToMessageElement(message) {
const elemTimestamp = this.getTime(message.timestamp);
const self = this;
const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText;
const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp;
setTimeout(() => {
const { history } = self.$refs;
const childrenElements = Array.from(history.getElementsByClassName('history-message'));
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
history.scrollTop = elemInNewList.offsetTop - 70
}, 1);
}
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/media-queries.scss";
@import "@/styles/variables.scss";
.chat-container {
position: relative;
transform: translate3d(0,0,0);
}
input {
width: 100%;
height: 3.25rem;
}
.logged-in-username {
position: absolute;
top: 0.75rem;
left: 1rem;
color: $matte-text-color;
width: calc(100% - 2rem);
button {
width: unset;
padding: 5px 10px;
position: absolute;
right: 0rem;
}
.username {
border-bottom: 2px solid $link-color;
}
}
.user-actions {
display: flex;
}
.history {
height: 75%;
overflow-y: scroll;
position: relative;
max-height: 550px;
margin-top: 2rem;
&-message {
display: flex;
flex-direction: column;
margin: 0.35rem 0;
position: relative;
.username {
font-weight: bold;
font-size: 1.05rem;
margin-right: 0.3rem;
}
.timestamp {
font-size: 0.9rem;
top: 2px;
position: absolute;
}
}
&-message:nth-of-type(2) {
margin-top: 1rem;
}
& .opaque-skirt {
width: calc(100% - 2rem);
position: fixed;
height: 2rem;
z-index: 1;
background: linear-gradient(
to bottom,
white,
rgba(255, 255, 255, 0)
);
}
& .fetch-older-history {
display: flex;
justify-content: center;
margin: 1rem 0;
}
@include mobile {
height: 300px;
}
}
.username-dialog {
display: flex;
flex-direction: row;
justify-content: center;
position: relative;
.validation-error {
position: absolute;
background-color: $light-red;
color: $red;
top: -3.5rem;
left: 0.5rem;
padding: 0.75rem;
border-radius: 4px;
&::before {
content: '';
position: absolute;
top: 2.1rem;
left: 2rem;
width: 1rem;
height: 1rem;
transform: rotate(45deg);
background-color: $light-red;
}
}
}
button {
position: relative;
display: inline-block;
background: #b7debd;
color: #333;
padding: 10px 30px;
border: 0;
width: fit-content;
font-size: 1rem;
/* height: 1.5rem; */
/* max-height: 1.5rem; */
margin: 0 2px;
cursor: pointer;
font-weight: 500;
transition: transform 0.5s ease;
-webkit-font-smoothing: antialiased;
touch-action: manipulation;
@include mobile {
padding: 10px 10px;
}
}
</style>

97
frontend/ui/Footer.vue Normal file
View File

@@ -0,0 +1,97 @@
<template>
<footer>
<ul>
<li>
<a href="https://github.com/KevinMidboe/vinlottis" class="github">
<span>Utforsk koden 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>

View File

@@ -112,13 +112,11 @@ export default {
this.emitColors()
if (window.location.hostname == "localhost") {
return;
}
this.$ga.event({
window.ga('send', {
hitType: "event",
eventCategory: "Raffles",
eventAction: "Generate",
eventValue: JSON.stringify(this.colors)
eventLabel: JSON.stringify(this.colors)
});
return;
}
@@ -292,9 +290,9 @@ label .text {
width: 150px;
height: 150px;
margin: 20px;
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
-webkit-mask-image: url(/public/assets/images/lodd.svg);
background-repeat: no-repeat;
mask-image: url(/../../public/assets/images/lodd.svg);
mask-image: url(/public/assets/images/lodd.svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;

View File

@@ -77,7 +77,7 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/variables";
@import "@/styles/variables";
.requested-count {
display: flex;

View File

@@ -94,8 +94,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/variables";
@import "./src/styles/global";
@import "@/styles/variables";
@import "@/styles/global";
video {
width: 100%;

View File

@@ -170,7 +170,7 @@ export default {
flex-direction: column;
position: relative;
@include raffle;
.win-percentage {
margin-left: 30px;
font-size: 50px;

81
frontend/ui/VippsPill.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<div aria-label="button" role="button" @click="openVipps" tabindex="0">
<img src="public/assets/images/vipps-pay_with_vipps_pill.png" />
</div>
</template>
<script>
export default {
props: {
amount: {
type: Number,
default: 1
}
},
data() {
return {
phone: __PHONE__,
name: __NAME__,
price: __PRICE__,
message: __MESSAGE__
};
},
computed: {
isMobile: function() {
return this.isMobileFunction();
},
priceToPay: function() {
return this.amount * (this.price * 100);
},
vippsUrlBasedOnUserAgent: function() {
if (navigator.userAgent.includes("iPhone")) {
return (
"https://qr.vipps.no/28/2/01/031/47" +
this.phone.replace(/ /g, "") +
"?v=1&m=" +
this.message +
"&a=" +
this.priceToPay
);
}
return (
"https://qr.vipps.no/28/2/01/031/47" +
this.phone.replace(/ /g, "") +
"?v=1&m=" +
this.message
);
}
},
methods: {
openVipps() {
if (!this.isMobileFunction()) {
return;
}
window.location.assign(this.vippsUrlBasedOnUserAgent);
},
isMobileFunction() {
if (
navigator.userAgent.match(/Android/i) ||
navigator.userAgent.match(/webOS/i) ||
navigator.userAgent.match(/iPhone/i) ||
navigator.userAgent.match(/iPad/i) ||
navigator.userAgent.match(/iPod/i) ||
navigator.userAgent.match(/BlackBerry/i) ||
navigator.userAgent.match(/Windows Phone/i)
) {
return true;
} else {
return false;
}
}
}
};
</script>
<style lang="scss" scoped>
img {
cursor: pointer;
width: 100%;
}
</style>

View File

@@ -62,8 +62,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/media-queries";
@import "./src/styles/variables";
@import "@/styles/media-queries";
@import "@/styles/variables";
.wine {
padding: 1rem;

View File

@@ -24,7 +24,6 @@
</template>
<script>
import { event } from "vue-analytics";
import Wine from "@/ui/Wine";
import { overallWineStatistics } from "@/api";
@@ -124,8 +123,8 @@ export default {
</script>
<style lang="scss" scoped>
@import "./src/styles/variables.scss";
@import "./src/styles/global.scss";
@import "@/styles/variables.scss";
@import "@/styles/global.scss";
@import "../styles/media-queries.scss";
.wines-main-container {

View File

@@ -1,36 +1,18 @@
<template>
<div class="current-drawn-container">
<div class="current-draw" v-if="drawing">
<h2>TREKKER</h2>
<div
:class="currentColor + '-raffle'"
class="raffle-element center-new-winner"
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
>
<span v-if="currentName && colorDone">{{ currentName }}</span>
</div>
<br />
<br />
<br />
<br />
<br />
</div>
<div class="current-draw" v-if="drawingDone">
<h2>VINNER</h2>
<div
:class="currentColor + '-raffle'"
class="raffle-element center-new-winner"
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
>
<span v-if="currentName && colorDone">{{ currentName }}</span>
</div>
<br />
<br />
<br />
<br />
<br />
<div class="current-drawn-container" v-if="drawing">
<h2 v-if="winnersNameDrawn !== true">TREKKER {{ ordinalNumber() }} VINNER</h2>
<h2 v-else>VINNER</h2>
<div
:class="currentColor + '-raffle'"
class="raffle-element"
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
>
<span v-if="currentName && colorDone">{{ currentName }}</span>
</div>
<br />
<br />
</div>
</template>
@@ -61,14 +43,13 @@ export default {
nameTimeout: null,
colorDone: false,
drawing: false,
drawingDone: false,
winnersNameDrawn: false,
winnerQueue: []
};
},
watch: {
currentWinner: function(currentWinner) {
if (currentWinner == null) {
this.drawingDone = false;
return;
}
if (this.drawing) {
@@ -76,6 +57,7 @@ export default {
return;
}
this.drawing = true;
this.winnersNameDrawn = false;
this.currentName = null;
this.currentColor = null;
this.nameRounds = 0;
@@ -99,8 +81,7 @@ export default {
this.drawColor(this.currentWinnerLocal.color);
return;
}
this.drawing = false;
this.drawingDone = true;
this.winnersNameDrawn = true;
this.startConfetti(this.currentName);
return;
}
@@ -114,7 +95,7 @@ export default {
}, 50);
},
drawColor: function(winnerColor) {
this.drawingDone = false;
this.winnersNameDrawn = false;
if (this.colorRounds == 100) {
this.currentColor = winnerColor;
this.colorDone = true;
@@ -129,7 +110,7 @@ export default {
clearTimeout(this.colorTimeout);
this.colorTimeout = setTimeout(() => {
this.drawColor(winnerColor);
}, 50);
}, 70);
},
getRotation: function() {
if (this.colorDone) {
@@ -151,8 +132,8 @@ export default {
return "yellow";
}
},
startConfetti(currentName){
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
startConfetti(currentName) {
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
var duration = 7 * 1000;
var animationEnd = Date.now() + duration;
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20};
@@ -161,22 +142,25 @@ export default {
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
const self = this;
var interval = setInterval(function() {
var timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
self.drawing = false;
console.time("drawing finished")
return clearInterval(interval);
}
if(currentName == "Amund Brandsrud"){
}
if (currentName == "Amund Brandsrud") {
runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135});
runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45});
runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45});
runCannon(uberDefaults, {y: 1 }, {angle: 90});
runCannon(uberDefaults, {x: 0 }, {angle: 45});
runCannon(uberDefaults, {x: 1 }, {angle: 135});
}else{
runCannon(uberDefaults, {x: 1 }, {angle: 135});
} else {
runCannon(defaults, {x: 0 }, {angle: 45});
runCannon(defaults, {x: 1 }, {angle: 135});
runCannon(defaults, {y: 1 }, {angle: 90});
}
}, 250);
@@ -184,6 +168,23 @@ export default {
confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle))
}
},
ordinalNumber(number=this.currentWinnerLocal.winnerCount) {
const dictonary = {
1: "første",
2: "andre",
3: "tredje",
4: "fjerde",
5: "femte",
6: "sjette",
7: "syvende",
8: "åttende",
9: "niende",
10: "tiende",
11: "ellevte",
12: "tolvte"
};
return number in dictonary ? dictonary[number] : number;
}
}
};
@@ -196,22 +197,27 @@ export default {
h2 {
text-align: center;
text-transform: uppercase;
}
.current-drawn-container {
display: flex;
justify-content: center;
align-items: center;
grid-column: 1 / 5;
display: grid;
place-items: center;
position: relative;
}
.raffle-element {
width: 140px;
height: 140px;
font-size: 1.2rem;
width: 280px;
height: 300px;
font-size: 2rem;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.75rem;
text-align: center;
-webkit-mask-size: cover;
-moz-mask-size: cover;
mask-size: cover;
}
</style>

View File

@@ -1,14 +1,22 @@
<template>
<div>
<h2 v-if="winners.length > 0"> {{ title ? title : 'Vinnere' }}</h2>
<div class="winners" v-if="winners.length > 0">
<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>
<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>
@@ -17,6 +25,9 @@ export default {
winners: {
type: Array
},
drawing: {
type: Boolean,
},
title: {
type: String,
required: false
@@ -30,11 +41,28 @@ export default {
@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;
}
.winners {
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;
@@ -52,4 +80,21 @@ h2 {
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>

View File

@@ -1,4 +1,4 @@
const dateString = (date) => {
if (typeof(date) == "string") {
date = new Date(date);
@@ -20,7 +20,7 @@ function daysAgo(date) {
return Math.round(Math.abs((new Date() - new Date(date)) / day));
}
module.exports = {
export {
dateString,
humanReadableDate,
daysAgo

View File

@@ -0,0 +1,54 @@
import Vue from "vue";
import VueRouter from "vue-router";
import { routes } from "@/router.js";
import Vinlottis from "@/Vinlottis";
import * as Sentry from "@sentry/browser";
import { Vue as VueIntegration } from "@sentry/integrations";
Vue.use(VueRouter);
const ENV = window.location.href.includes("localhost") ? "development" : "production";
if (ENV !== "development") {
Sentry.init({
dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
integrations: [
new VueIntegration({ Vue })
],
beforeSend: event => {
console.error(event);
return event;
}
})
}
// Add global GA variables
window.ga = window.ga || function(){
window.ga.q = window.ga.q || [];
window.ga.q.push(arguments);
};
ga.l = 1 * new Date();
// Initiate
ga('create', __GA_TRACKINGID__, {
'allowAnchor': false,
'cookieExpires': __GA_COOKIELIFETIME__, // Time in seconds
'cookieFlags': 'SameSite=Strict; Secure'
});
ga('set', 'anonymizeIp', true); // Enable IP Anonymization/IP masking
ga('send', 'pageview');
if (ENV == 'development')
window[`ga-disable-${__GA_TRACKINGID__}`] = true;
const router = new VueRouter({
routes: routes
});
new Vue({
el: "#app",
router,
components: { Vinlottis },
template: "<Vinlottis/>",
render: h => h(Vinlottis)
});

View File

@@ -4,80 +4,64 @@
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "cross-env NODE_ENV=production webpack --progress",
"build-report": "cross-env NODE_ENV=production BUILD_REPORT=true webpack --progress",
"dev": "yarn webpack serve --mode development --env development",
"start": "node server.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server",
"build": "cross-env NODE_ENV=production webpack --hide-modules"
"start-noauth": "cross-env NODE_ENV=development node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@babel/polyfill": "~7.2",
"@sentry/browser": "^5.27.4",
"@sentry/integrations": "^5.27.4",
"@sentry/tracing": "^5.27.4",
"@zxing/library": "^0.15.2",
"body-parser": "^1.19.0",
"@sentry/browser": "^5.28.0",
"@sentry/integrations": "^5.28.0",
"@zxing/library": "^0.18.3",
"canvas-confetti": "^1.2.0",
"cross-env": "^7.0.3",
"chart.js": "^2.9.3",
"clean-webpack-plugin": "^3.0.0",
"compression": "^1.7.4",
"connect-mongo": "^3.2.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-session": "^1.17.0",
"extract-text-webpack-plugin": "^3.0.2",
"feature-policy": "^0.4.0",
"helmet": "^3.21.2",
"moment": "^2.24.0",
"mongoose": "^5.8.7",
"mongoose": "^5.11.4",
"node-fetch": "^2.6.0",
"node-sass": "^4.13.0",
"node-sass": "^5.0.0",
"node-schedule": "^1.3.2",
"passport": "^0.4.1",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^6.0.1",
"qrcode": "^1.4.4",
"referrer-policy": "^1.2.0",
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"socket.io": "^3.0.3",
"socket.io-client": "^3.0.3",
"vue": "~2.6",
"vue-analytics": "^5.22.1",
"vue-router": "~3.0",
"vuex": "^3.1.1",
"vue-router": "~3.4.9",
"vuex": "^3.6.0",
"web-push": "^3.4.3"
},
"devDependencies": {
"@babel/core": "~7.2",
"@babel/plugin-proposal-class-properties": "~7.3",
"@babel/plugin-proposal-decorators": "~7.3",
"@babel/plugin-proposal-json-strings": "~7.2",
"@babel/plugin-syntax-dynamic-import": "~7.2",
"@babel/plugin-syntax-import-meta": "~7.2",
"@babel/preset-env": "~7.3",
"babel-loader": "~8.0",
"compression-webpack-plugin": "^3.1.0",
"cross-env": "^6.0.3",
"css-loader": "^3.2.0",
"file-loader": "^4.2.0",
"@babel/core": "~7.12",
"@babel/preset-env": "~7.12",
"babel-loader": "~8.2.2",
"clean-webpack-plugin": "^3.0.0",
"core-js": "3.8.1",
"css-loader": "^5.0.1",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "~1.7",
"google-maps-api-loader": "^1.1.1",
"html-webpack-plugin": "~3.2",
"mini-css-extract-plugin": "~0.5",
"optimize-css-assets-webpack-plugin": "~3.2",
"pm2": "^4.2.3",
"html-webpack-plugin": "5.0.0-alpha.15",
"mini-css-extract-plugin": "~1.3.2",
"optimize-css-assets-webpack-plugin": "~5.0.4",
"redis": "^3.0.2",
"sass-loader": "~7.1",
"uglifyjs-webpack-plugin": "~1.2",
"url-loader": "^2.2.0",
"vue-loader": "~15.6",
"sass-loader": "~10.1.0",
"url-loader": "^4.1.1",
"vue-loader": "~15.9.5",
"vue-style-loader": "~4.1",
"vue-template-compiler": "~2.6",
"webpack": "~4.41.5",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "~3.2",
"webpack-dev-server": "~3.1",
"webpack-hot-middleware": "~2.24",
"webpack-merge": "~4.2"
"vue-template-compiler": "^2.6.12",
"webpack": "~5.10.0",
"webpack-bundle-analyzer": "^4.2.0",
"webpack-cli": "~4.2.0",
"webpack-dev-server": "~3.11",
"webpack-merge": "~5.4"
}
}

View File

@@ -1,12 +0,0 @@
{
"apps": [
{
"name": "vinlottis",
"script": "./server.js",
"watch": true,
"instances": "max",
"exec_mode": "cluster",
"ignore_watch": ["./node_modules", "./public/assets/"]
}
]
}

88
public/analytics.js Normal file
View File

@@ -0,0 +1,88 @@
// 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("&gtm")==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("&gtm")&&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.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -4,59 +4,41 @@ 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 + "/schemas/User"));
const User = require(path.join(__dirname + "/api/schemas/User"));
const apiRouter = require(path.join(__dirname + "/api/router.js"));
const loginApi = require(path.join(__dirname + "/api/login"));
const subscriptionApi = require(path.join(__dirname + "/api/subscriptions"));
//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");
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 / database
console.log("Trying to connect with mongodb..");
mongoose.promise = global.Promise;
mongoose.connect("mongodb://localhost/vinlottis");
mongoose.set("debug", true);
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);
app.use(
bodyParser.urlencoded({
extended: true
})
);
app.use(bodyParser.json());
// 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(
session({
@@ -70,36 +52,34 @@ app.use(
})
);
app.set('socketio', io);
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("/dist", express.static(path.join(__dirname, "public/dist")));
app.use("/", loginApi);
app.use("/api/", chatHistory);
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("/subscription", subscriptionApi);
app.get("/dagens", function(req, res) {
res.redirect("/#/dagens");
});
app.get("/winner/:id", function(req, res) {
res.redirect("/#/winner/" + req.params.id);
});
app.use("/service-worker.js", function(req, res) {
res.sendFile(path.join(__dirname, "public/sw/serviceWorker.js"));
});
// No other route defined, return index file
app.use("/", (req, res) => res.sendFile(path.join(__dirname + "/public/dist/index.html")));
server.listen(30030);

Some files were not shown because too many files have changed in this diff Show More