Merged master into feature branch.
This commit is contained in:
		
							
								
								
									
										15
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  presets: [
 | 
			
		||||
   [
 | 
			
		||||
     "@babel/preset-env",
 | 
			
		||||
      {
 | 
			
		||||
        modules: false,
 | 
			
		||||
        targets: {
 | 
			
		||||
          browsers: ["IE 11", "> 5%"]
 | 
			
		||||
        },
 | 
			
		||||
        useBuiltIns: "usage",
 | 
			
		||||
        corejs: "3"
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								api/chat.js
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								api/chat.js
									
									
									
									
									
								
							@@ -1,22 +1,46 @@
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const { addMessage } = require(path.join(__dirname + "/redis.js"));
 | 
			
		||||
 | 
			
		||||
const validateUsername = (username) => {
 | 
			
		||||
  let error = undefined;
 | 
			
		||||
  const illegalChars = /\W/;
 | 
			
		||||
  const minLength = 3;
 | 
			
		||||
  const maxLength = 15;
 | 
			
		||||
 | 
			
		||||
  if (typeof username !== 'string') {
 | 
			
		||||
    error = 'Ugyldig brukernavn.';
 | 
			
		||||
  } else if (username.length === 0) {
 | 
			
		||||
    error = 'Vennligst oppgi brukernavn.';
 | 
			
		||||
  } else if (username.length < minLength || username.length > maxLength) {
 | 
			
		||||
    error = `Brukernavn må være mellom ${minLength}-${maxLength} karaktere.`
 | 
			
		||||
  } else if (illegalChars.test(username)) {
 | 
			
		||||
    error = 'Brukernavn kan bare inneholde tall og bokstaver.'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return error;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const io = (io) => {
 | 
			
		||||
  io.on("connection", socket => {
 | 
			
		||||
    let username = null;
 | 
			
		||||
 | 
			
		||||
    socket.on("username", msg => {
 | 
			
		||||
      if (msg.username == null) {
 | 
			
		||||
      const usernameValidationError = validateUsername(msg.username);
 | 
			
		||||
      if (usernameValidationError) {
 | 
			
		||||
        username = null;
 | 
			
		||||
        socket.emit("accept_username", false);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (msg.username.length > 3 && msg.username.length < 30) {
 | 
			
		||||
        socket.emit("accept_username", {
 | 
			
		||||
          reason: usernameValidationError,
 | 
			
		||||
          success: false,
 | 
			
		||||
          username: undefined
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        username = msg.username;
 | 
			
		||||
        socket.emit("accept_username", true);
 | 
			
		||||
        return;
 | 
			
		||||
        socket.emit("accept_username", {
 | 
			
		||||
          reason: undefined,
 | 
			
		||||
          success: true,
 | 
			
		||||
          username: msg.username
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      socket.emit("accept_username", false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on("chat", msg => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,29 @@
 | 
			
		||||
const express = require("express");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
 | 
			
		||||
const { history, clearHistory } = require(path.join(__dirname + "/../api/redis"));
 | 
			
		||||
 | 
			
		||||
router.use((req, res, next) => {
 | 
			
		||||
  next();
 | 
			
		||||
});
 | 
			
		||||
const getAllHistory = (req, res) => {
 | 
			
		||||
  let { page, limit } = req.query;
 | 
			
		||||
  page = !isNaN(page) ? Number(page) : undefined;
 | 
			
		||||
  limit = !isNaN(limit) ? Number(limit) : undefined;
 | 
			
		||||
 | 
			
		||||
router.route("/chat/history").get(async (req, res) => {
 | 
			
		||||
  let { skip, take } = req.query;
 | 
			
		||||
  skip = !isNaN(skip) ? Number(skip) : undefined;
 | 
			
		||||
  take = !isNaN(take) ? Number(take) : undefined;
 | 
			
		||||
  return history(page, limit)
 | 
			
		||||
    .then(messages => res.json(messages))
 | 
			
		||||
    .catch(error =>  res.status(500).json({
 | 
			
		||||
      message: error.message,
 | 
			
		||||
      success: false
 | 
			
		||||
    }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const messages = await history(skip, take);
 | 
			
		||||
    res.json(messages)
 | 
			
		||||
  } catch(error) {
 | 
			
		||||
    res.status(500).send(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
const deleteHistory = (req, res) => {
 | 
			
		||||
  return clearHistory()
 | 
			
		||||
    .then(message => res.json(message))
 | 
			
		||||
    .catch(error => res.status(500).json({
 | 
			
		||||
      message: error.message,
 | 
			
		||||
      success: false
 | 
			
		||||
    }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
router.route("/chat/history").delete(async (req, res) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const messages = await clearHistory();
 | 
			
		||||
    res.json(messages)
 | 
			
		||||
  } catch(error) {
 | 
			
		||||
    res.status(500).send(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
module.exports = {
 | 
			
		||||
  getAllHistory,
 | 
			
		||||
  deleteHistory
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								api/login.js
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								api/login.js
									
									
									
									
									
								
							@@ -1,59 +0,0 @@
 | 
			
		||||
const passport = require("passport");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const User = require(path.join(__dirname + "/../schemas/User"));
 | 
			
		||||
const router = require("express").Router();
 | 
			
		||||
 | 
			
		||||
router.get("/", function(req, res) {
 | 
			
		||||
  res.sendFile(path.join(__dirname + "/../public/index.html"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get("/register", function(req, res) {
 | 
			
		||||
  res.sendFile(path.join(__dirname + "/../public/index.html"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// router.post("/register", function(req, res, next) {
 | 
			
		||||
//   User.register(
 | 
			
		||||
//     new User({ username: req.body.username }),
 | 
			
		||||
//     req.body.password,
 | 
			
		||||
//     function(err) {
 | 
			
		||||
//       if (err) {
 | 
			
		||||
//         if (err.name == "UserExistsError")
 | 
			
		||||
//           res.status(409).send({ success: false, message: err.message })
 | 
			
		||||
//         else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
 | 
			
		||||
//           res.status(400).send({ success: false, message: err.message })
 | 
			
		||||
//         return next(err);
 | 
			
		||||
//       }
 | 
			
		||||
 | 
			
		||||
//       return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
 | 
			
		||||
//      }
 | 
			
		||||
//   );
 | 
			
		||||
// });
 | 
			
		||||
 | 
			
		||||
router.get("/login", function(req, res) {
 | 
			
		||||
  res.sendFile(path.join(__dirname + "/../public/index.html"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.post("/login", function(req, res, next) {
 | 
			
		||||
  passport.authenticate("local", function(err, user, info) { 
 | 
			
		||||
    if (err) {
 | 
			
		||||
      if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
 | 
			
		||||
        return res.status(400).send({ message: err.message, success: false })
 | 
			
		||||
      return next(err);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false })
 | 
			
		||||
 | 
			
		||||
    req.logIn(user, (err) => {
 | 
			
		||||
      if (err) { return next(err) }
 | 
			
		||||
 | 
			
		||||
      return res.status(200).send({ message: "Velkommen " + user.username, success: true })
 | 
			
		||||
    })
 | 
			
		||||
  })(req, res, next);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get("/logout", function(req, res) {
 | 
			
		||||
  req.logout();
 | 
			
		||||
  res.redirect("/");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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!`
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,10 @@
 | 
			
		||||
const mustBeAuthenticated = (req, res, next) => {
 | 
			
		||||
  console.log(req.isAuthenticated());
 | 
			
		||||
  if (process.env.NODE_ENV == "development") {
 | 
			
		||||
    console.info(`Restricted endpoint ${req.originalUrl}, allowing with environment development.`)
 | 
			
		||||
    req.isAuthenticated = () => true;
 | 
			
		||||
    return next();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!req.isAuthenticated()) {
 | 
			
		||||
    return res.status(401).send({
 | 
			
		||||
      success: false,
 | 
			
		||||
							
								
								
									
										6
									
								
								api/middleware/setupCORS.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								api/middleware/setupCORS.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
const openCORS = (req, res, next) => {
 | 
			
		||||
  res.set("Access-Control-Allow-Origin", "*")
 | 
			
		||||
  return next();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = openCORS;
 | 
			
		||||
							
								
								
									
										37
									
								
								api/middleware/setupHeaders.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								api/middleware/setupHeaders.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
const camelToKebabCase = str => str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
 | 
			
		||||
 | 
			
		||||
const mapFeaturePolicyToString = (features) => {
 | 
			
		||||
  return Object.entries(features).map(([key, value]) => {
 | 
			
		||||
    key = camelToKebabCase(key)
 | 
			
		||||
    value = value == "*" ? value : `'${ value }'`
 | 
			
		||||
    return `${key} ${value}`
 | 
			
		||||
  }).join("; ")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const setupHeaders = (req, res, next) => {
 | 
			
		||||
  res.set("Access-Control-Allow-Headers", "Content-Type")
 | 
			
		||||
 | 
			
		||||
  // Security
 | 
			
		||||
  res.set("X-Content-Type-Options", "nosniff");
 | 
			
		||||
  res.set("X-XSS-Protection", "1; mode=block");
 | 
			
		||||
  res.set("X-Frame-Options", "SAMEORIGIN");
 | 
			
		||||
  res.set("X-DNS-Prefetch-Control", "off");
 | 
			
		||||
  res.set("X-Download-Options", "noopen");
 | 
			
		||||
  res.set("Strict-Transport-Security", "max-age=15552000; includeSubDomains")
 | 
			
		||||
 | 
			
		||||
  // Feature policy
 | 
			
		||||
  const features = {
 | 
			
		||||
    fullscreen: "*",
 | 
			
		||||
    payment: "none",
 | 
			
		||||
    microphone: "none",
 | 
			
		||||
    camera: "self",
 | 
			
		||||
    speaker: "*",
 | 
			
		||||
    syncXhr: "self"
 | 
			
		||||
  }
 | 
			
		||||
  const featureString = mapFeaturePolicyToString(features);
 | 
			
		||||
  res.set("Feature-Policy", featureString)
 | 
			
		||||
 | 
			
		||||
  return next();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = setupHeaders;
 | 
			
		||||
@@ -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({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										93
									
								
								api/redis.js
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								api/redis.js
									
									
									
									
									
								
							@@ -1,29 +1,40 @@
 | 
			
		||||
const { promisify } = require("util"); // from node
 | 
			
		||||
 | 
			
		||||
let client;
 | 
			
		||||
let llenAsync;
 | 
			
		||||
let lrangeAsync;
 | 
			
		||||
try {
 | 
			
		||||
  const redis = require("redis");
 | 
			
		||||
  console.log("trying to create 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 = {
 | 
			
		||||
 
 | 
			
		||||
@@ -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) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -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) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
));
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										51
									
								
								api/user.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
const passport = require("passport");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const User = require(path.join(__dirname, "/schemas/User"));
 | 
			
		||||
const router = require("express").Router();
 | 
			
		||||
 | 
			
		||||
const register = (req, res, next) => {
 | 
			
		||||
  User.register(
 | 
			
		||||
    new User({ username: req.body.username }),
 | 
			
		||||
    req.body.password,
 | 
			
		||||
    function(err) {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        if (err.name == "UserExistsError")
 | 
			
		||||
          res.status(409).send({ success: false, message: err.message })
 | 
			
		||||
        else if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
 | 
			
		||||
          res.status(400).send({ success: false, message: err.message })
 | 
			
		||||
        return next(err);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return res.status(200).send({ message: "Bruker registrert. Velkommen " + req.body.username, success: true })
 | 
			
		||||
     }
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const login = (req, res, next) => {
 | 
			
		||||
  passport.authenticate("local", function(err, user, info) {
 | 
			
		||||
    if (err) {
 | 
			
		||||
      if (err.name == "MissingUsernameError" || err.name == "MissingPasswordError")
 | 
			
		||||
        return res.status(400).send({ message: err.message, success: false })
 | 
			
		||||
      return next(err);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!user) return res.status(404).send({ message: "Incorrect username or password", success: false })
 | 
			
		||||
 | 
			
		||||
    req.logIn(user, (err) => {
 | 
			
		||||
      if (err) { return next(err) }
 | 
			
		||||
 | 
			
		||||
      return res.status(200).send({ message: "Velkommen " + user.username, success: true })
 | 
			
		||||
    })
 | 
			
		||||
  })(req, res, next);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const logout = (req, res) => {
 | 
			
		||||
  req.logout();
 | 
			
		||||
  res.redirect("/");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  register,
 | 
			
		||||
  login,
 | 
			
		||||
  logout
 | 
			
		||||
};
 | 
			
		||||
@@ -1,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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 });
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								config/env/lottery.config.example.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								config/env/lottery.config.example.js
									
									
									
									
										vendored
									
									
								
							@@ -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
 | 
			
		||||
};
 | 
			
		||||
@@ -2,14 +2,14 @@
 | 
			
		||||
 | 
			
		||||
const webpack = require("webpack");
 | 
			
		||||
const helpers = require("./helpers");
 | 
			
		||||
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
 | 
			
		||||
const TerserPlugin = require("terser-webpack-plugin");
 | 
			
		||||
 | 
			
		||||
const ServiceWorkerConfig = {
 | 
			
		||||
  resolve: {
 | 
			
		||||
    extensions: [".js", ".vue"]
 | 
			
		||||
  },
 | 
			
		||||
  entry: {
 | 
			
		||||
    serviceWorker: [helpers.root("src/service-worker", "service-worker")]
 | 
			
		||||
    serviceWorker: [helpers.root("frontend/service-worker", "service-worker")]
 | 
			
		||||
  },
 | 
			
		||||
  optimization: {
 | 
			
		||||
    minimizer: []
 | 
			
		||||
@@ -19,7 +19,7 @@ const ServiceWorkerConfig = {
 | 
			
		||||
      {
 | 
			
		||||
        test: /\.js$/,
 | 
			
		||||
        loader: "babel-loader",
 | 
			
		||||
        include: [helpers.root("src/service-worker", "service-worker")]
 | 
			
		||||
        include: [helpers.root("frontend/service-worker", "service-worker")]
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
@@ -31,11 +31,10 @@ const ServiceWorkerConfig = {
 | 
			
		||||
    //filename: "js/[name].bundle.js"
 | 
			
		||||
  },
 | 
			
		||||
  optimization: {
 | 
			
		||||
    minimize: true,
 | 
			
		||||
    minimizer: [
 | 
			
		||||
      new UglifyJSPlugin({
 | 
			
		||||
        cache: true,
 | 
			
		||||
        parallel: false,
 | 
			
		||||
        sourceMap: false
 | 
			
		||||
      new TerserPlugin({
 | 
			
		||||
        test: /\.js(\?.*)?$/i,
 | 
			
		||||
      })
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
 | 
			
		||||
const helpers = require("./helpers");
 | 
			
		||||
 | 
			
		||||
const VinlottisConfig = {
 | 
			
		||||
  entry: {
 | 
			
		||||
    vinlottis: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
 | 
			
		||||
  },
 | 
			
		||||
  optimization: {
 | 
			
		||||
    minimizer: [
 | 
			
		||||
      new HtmlWebpackPlugin({
 | 
			
		||||
        chunks: ["vinlottis"],
 | 
			
		||||
        filename: "../index.html",
 | 
			
		||||
        template: "./src/templates/Create.html",
 | 
			
		||||
        inject: true,
 | 
			
		||||
        minify: {
 | 
			
		||||
          removeComments: true,
 | 
			
		||||
          collapseWhitespace: false,
 | 
			
		||||
          preserveLineBreaks: true,
 | 
			
		||||
          removeAttributeQuotes: true
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = VinlottisConfig;
 | 
			
		||||
@@ -11,10 +11,16 @@ const webpackConfig = function(isDev) {
 | 
			
		||||
    resolve: {
 | 
			
		||||
      extensions: [".js", ".vue"],
 | 
			
		||||
      alias: {
 | 
			
		||||
        vue$: isDev ? "vue/dist/vue.min.js" : "vue/dist/vue.min.js",
 | 
			
		||||
        "@": helpers.root("src")
 | 
			
		||||
        vue$: "vue/dist/vue.min.js",
 | 
			
		||||
        "@": helpers.root("frontend")
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    entry: {
 | 
			
		||||
      vinlottis: helpers.root("frontend", "vinlottis-init")
 | 
			
		||||
    },
 | 
			
		||||
    externals: {
 | 
			
		||||
        moment: 'moment' // comes with chart.js
 | 
			
		||||
    },
 | 
			
		||||
    module: {
 | 
			
		||||
      rules: [
 | 
			
		||||
        {
 | 
			
		||||
@@ -33,35 +39,31 @@ const webpackConfig = function(isDev) {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          test: /\.js$/,
 | 
			
		||||
          loader: "babel-loader",
 | 
			
		||||
          include: [helpers.root("src")]
 | 
			
		||||
          use: [ "babel-loader" ],
 | 
			
		||||
          include: [helpers.root("frontend")]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          test: /\.css$/,
 | 
			
		||||
          use: [
 | 
			
		||||
            isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
 | 
			
		||||
            MiniCSSExtractPlugin.loader,
 | 
			
		||||
            { loader: "css-loader", options: { sourceMap: isDev } }
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          test: /\.scss$/,
 | 
			
		||||
          use: [
 | 
			
		||||
            isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
 | 
			
		||||
            { loader: "css-loader", options: { sourceMap: isDev } },
 | 
			
		||||
            { loader: "sass-loader", options: { sourceMap: isDev } }
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          test: /\.sass$/,
 | 
			
		||||
          use: [
 | 
			
		||||
            isDev ? "vue-style-loader" : MiniCSSExtractPlugin.loader,
 | 
			
		||||
            MiniCSSExtractPlugin.loader,
 | 
			
		||||
            { loader: "css-loader", options: { sourceMap: isDev } },
 | 
			
		||||
            { loader: "sass-loader", options: { sourceMap: isDev } }
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          test: /\.woff(2)?(\?[a-z0-9]+)?$/,
 | 
			
		||||
          loader: "url-loader?limit=10000&mimetype=application/font-woff"
 | 
			
		||||
          loader: "url-loader",
 | 
			
		||||
          options: {
 | 
			
		||||
            limit: 10000,
 | 
			
		||||
            mimetype: "application/font-woff"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          test: /\.(ttf|eot|svg)(\?[a-z0-9]+)?$/,
 | 
			
		||||
@@ -72,14 +74,16 @@ const webpackConfig = function(isDev) {
 | 
			
		||||
    plugins: [
 | 
			
		||||
      new VueLoaderPlugin(),
 | 
			
		||||
      new webpack.DefinePlugin({
 | 
			
		||||
        __ENV__: JSON.stringify(process.env.NODE_ENV),
 | 
			
		||||
        __NAME__: JSON.stringify(env.name),
 | 
			
		||||
        __PHONE__: JSON.stringify(env.phone),
 | 
			
		||||
        __PRICE__: env.price,
 | 
			
		||||
        __MESSAGE__: JSON.stringify(env.message),
 | 
			
		||||
        __DATE__: env.date,
 | 
			
		||||
        __HOURS__: env.hours,
 | 
			
		||||
        __APIURL__: JSON.stringify(env.apiUrl),
 | 
			
		||||
        __PUSHENABLED__: JSON.stringify(require("./defaults/push") != false)
 | 
			
		||||
        __PUSHENABLED__: JSON.stringify(require("./defaults/push") != false),
 | 
			
		||||
        __GA_TRACKINGID__: JSON.stringify(env.googleanalytics_trackingId),
 | 
			
		||||
        __GA_COOKIELIFETIME__: env.googleanalytics_cookieLifetime
 | 
			
		||||
      })
 | 
			
		||||
    ]
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +1,63 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const webpack = require("webpack");
 | 
			
		||||
const merge = require("webpack-merge");
 | 
			
		||||
const { merge } = require("webpack-merge");
 | 
			
		||||
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
 | 
			
		||||
const HtmlPlugin = require("html-webpack-plugin");
 | 
			
		||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
 | 
			
		||||
const helpers = require("./helpers");
 | 
			
		||||
const commonConfig = require("./webpack.config.common");
 | 
			
		||||
const environment = require("./env/dev.env");
 | 
			
		||||
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
 | 
			
		||||
 | 
			
		||||
let webpackConfig = merge(commonConfig(true), {
 | 
			
		||||
  mode: "development",
 | 
			
		||||
  devtool: "cheap-module-eval-source-map",
 | 
			
		||||
  devtool: "eval-cheap-module-source-map",
 | 
			
		||||
  output: {
 | 
			
		||||
    path: helpers.root("dist"),
 | 
			
		||||
    publicPath: "/",
 | 
			
		||||
    filename: "js/[name].bundle.js",
 | 
			
		||||
    chunkFilename: "js/[id].chunk.js"
 | 
			
		||||
    filename: "js/[name].bundle.js"
 | 
			
		||||
  },
 | 
			
		||||
  optimization: {
 | 
			
		||||
    runtimeChunk: "single",
 | 
			
		||||
    concatenateModules: true,
 | 
			
		||||
    splitChunks: {
 | 
			
		||||
      chunks: "all"
 | 
			
		||||
      chunks: "initial"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    new webpack.EnvironmentPlugin(environment),
 | 
			
		||||
    new webpack.HotModuleReplacementPlugin(),
 | 
			
		||||
    new FriendlyErrorsPlugin()
 | 
			
		||||
    new FriendlyErrorsPlugin(),
 | 
			
		||||
    new MiniCSSExtractPlugin({
 | 
			
		||||
      filename: "css/[name].css"
 | 
			
		||||
    })
 | 
			
		||||
  ],
 | 
			
		||||
  devServer: {
 | 
			
		||||
    compress: true,
 | 
			
		||||
    historyApiFallback: true,
 | 
			
		||||
    host: "0.0.0.0",
 | 
			
		||||
    hot: true,
 | 
			
		||||
    overlay: true,
 | 
			
		||||
    stats: {
 | 
			
		||||
      normal: true
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    proxy: {
 | 
			
		||||
      "/api": {
 | 
			
		||||
        target: "http://localhost:30030",
 | 
			
		||||
        changeOrigin: true
 | 
			
		||||
      },
 | 
			
		||||
      "/socket.io": {
 | 
			
		||||
        target: "ws://localhost:30030",
 | 
			
		||||
        changeOrigin: false,
 | 
			
		||||
        ws: true
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    writeToDisk: false
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
webpackConfig = merge(webpackConfig, {
 | 
			
		||||
  entry: {
 | 
			
		||||
    main: ["@babel/polyfill", helpers.root("src", "vinlottis-init")]
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    new HtmlPlugin({
 | 
			
		||||
      template: "src/templates/Create.html",
 | 
			
		||||
      chunksSortMode: "dependency"
 | 
			
		||||
    new HtmlWebpackPlugin({
 | 
			
		||||
      template: "frontend/templates/Index.html"
 | 
			
		||||
    })
 | 
			
		||||
  ]
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,15 @@
 | 
			
		||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
const webpack = require("webpack");
 | 
			
		||||
const merge = require("webpack-merge");
 | 
			
		||||
const { merge } = require("webpack-merge");
 | 
			
		||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
 | 
			
		||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
 | 
			
		||||
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
 | 
			
		||||
const UglifyJSPlugin = require("uglifyjs-webpack-plugin");
 | 
			
		||||
const TerserPlugin = require("terser-webpack-plugin");
 | 
			
		||||
 | 
			
		||||
const helpers = require("./helpers");
 | 
			
		||||
const commonConfig = require("./webpack.config.common");
 | 
			
		||||
 | 
			
		||||
const isProd = process.env.NODE_ENV === "production";
 | 
			
		||||
const environment = isProd
 | 
			
		||||
  ? require("./env/prod.env")
 | 
			
		||||
@@ -16,11 +19,11 @@ const environment = isProd
 | 
			
		||||
 | 
			
		||||
const webpackConfig = merge(commonConfig(false), {
 | 
			
		||||
  mode: "production",
 | 
			
		||||
  stats: { children: false },
 | 
			
		||||
  output: {
 | 
			
		||||
    path: helpers.root("public/dist"),
 | 
			
		||||
    publicPath: "/dist/",
 | 
			
		||||
    filename: "js/[name].bundle.[hash:7].js"
 | 
			
		||||
    //filename: "js/[name].bundle.js"
 | 
			
		||||
    publicPath: "/public/dist/",
 | 
			
		||||
    filename: "js/[name].bundle.[fullhash:7].js"
 | 
			
		||||
  },
 | 
			
		||||
  optimization: {
 | 
			
		||||
    splitChunks: {
 | 
			
		||||
@@ -33,37 +36,47 @@ const webpackConfig = merge(commonConfig(false), {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    minimize: true,
 | 
			
		||||
    minimizer: [
 | 
			
		||||
      new HtmlWebpackPlugin({
 | 
			
		||||
        chunks: ["vinlottis"],
 | 
			
		||||
        filename: "index.html",
 | 
			
		||||
        template: "./frontend/templates/Index.html",
 | 
			
		||||
        inject: true,
 | 
			
		||||
        minify: {
 | 
			
		||||
          removeComments: true,
 | 
			
		||||
          collapseWhitespace: false,
 | 
			
		||||
          preserveLineBreaks: true,
 | 
			
		||||
          removeAttributeQuotes: true
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      new OptimizeCSSAssetsPlugin({
 | 
			
		||||
        cssProcessorPluginOptions: {
 | 
			
		||||
          preset: ["default", { discardComments: { removeAll: true } }]
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      new UglifyJSPlugin({
 | 
			
		||||
        cache: true,
 | 
			
		||||
        parallel: false,
 | 
			
		||||
        sourceMap: !isProd
 | 
			
		||||
      new TerserPlugin({
 | 
			
		||||
        test: /\.js(\?.*)?$/i,
 | 
			
		||||
      })
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    new CleanWebpackPlugin(),
 | 
			
		||||
    new CleanWebpackPlugin(), // clean output folder
 | 
			
		||||
    new webpack.EnvironmentPlugin(environment),
 | 
			
		||||
    new MiniCSSExtractPlugin({
 | 
			
		||||
      filename: "css/[name].[hash:7].css"
 | 
			
		||||
      //filename: "css/[name].css"
 | 
			
		||||
      filename: "css/[name].[fullhash:7].css"
 | 
			
		||||
    })
 | 
			
		||||
  ]
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (!isProd) {
 | 
			
		||||
  webpackConfig.devtool = "source-map";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  if (process.env.npm_config_report) {
 | 
			
		||||
    const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
 | 
			
		||||
      .BundleAnalyzerPlugin;
 | 
			
		||||
    webpackConfig.plugins.push(new BundleAnalyzerPlugin());
 | 
			
		||||
  }
 | 
			
		||||
if (process.env.BUILD_REPORT) {
 | 
			
		||||
  const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
 | 
			
		||||
    .BundleAnalyzerPlugin;
 | 
			
		||||
  webpackConfig.plugins.push(new BundleAnalyzerPlugin());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = webpackConfig;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										369
									
								
								frontend/api.js
									
									
									
									
									
										Normal 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
 | 
			
		||||
};
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -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 {
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
@@ -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{
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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;
 | 
			
		||||
							
								
								
									
										388
									
								
								frontend/components/VirtualLotteryPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								frontend/components/VirtualLotteryPage.vue
									
									
									
									
									
										Normal 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 på utviklingen</span> og <span class="vin-link">chat om trekningen</span>
 | 
			
		||||
            <i class="icon icon--arrow-left" @click="scrollToContent"></i></p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </header>
 | 
			
		||||
 | 
			
		||||
    <div class="container" ref="content">
 | 
			
		||||
      <WinnerDraw
 | 
			
		||||
        :currentWinnerDrawn="currentWinnerDrawn"
 | 
			
		||||
        :currentWinner="currentWinner"
 | 
			
		||||
        :attendees="attendees"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div class="todays-raffles">
 | 
			
		||||
        <h2>Liste av lodd kjøpt i dag</h2>
 | 
			
		||||
 | 
			
		||||
        <div class="raffle-container">
 | 
			
		||||
          <div v-for="color in Object.keys(ticketsBought)" :class="color + '-raffle raffle-element'" :key="color">
 | 
			
		||||
            <span>{{ ticketsBought[color] }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <Winners :winners="winners" class="winners" :drawing="currentWinner" />
 | 
			
		||||
 | 
			
		||||
      <div class="container-attendees">
 | 
			
		||||
        <h2>Deltakere ({{ attendees.length }})</h2>
 | 
			
		||||
        <Attendees :attendees="attendees" class="attendees" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="container-chat">
 | 
			
		||||
        <h2>Chat</h2>
 | 
			
		||||
        <Chat class="chat" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="container wines-container">
 | 
			
		||||
      <h2>Dagens fangst ({{ wines.length }})</h2>
 | 
			
		||||
      <Wine :wine="wine" v-for="wine in wines" :key="wine" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { attendees, winners, prelottery } from "@/api";
 | 
			
		||||
import Chat from "@/ui/Chat";
 | 
			
		||||
import Vipps from "@/ui/Vipps";
 | 
			
		||||
import VippsPill from "@/ui/VippsPill";
 | 
			
		||||
import Attendees from "@/ui/Attendees";
 | 
			
		||||
import Wine from "@/ui/Wine";
 | 
			
		||||
import Winners from "@/ui/Winners";
 | 
			
		||||
import WinnerDraw from "@/ui/WinnerDraw";
 | 
			
		||||
import io from "socket.io-client";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: { Chat, Attendees, Winners, WinnerDraw, Vipps, VippsPill, Wine },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      attendees: [],
 | 
			
		||||
      winners: [],
 | 
			
		||||
      wines: [],
 | 
			
		||||
      currentWinnerDrawn: false,
 | 
			
		||||
      currentWinner: null,
 | 
			
		||||
      socket: null,
 | 
			
		||||
      attendeesFetched: false,
 | 
			
		||||
      wasDisconnected: false,
 | 
			
		||||
      ticketsBought: {
 | 
			
		||||
        "red": 0,
 | 
			
		||||
        "blue": 0,
 | 
			
		||||
        "green": 0,
 | 
			
		||||
        "yellow": 0
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.track();
 | 
			
		||||
    this.getAttendees();
 | 
			
		||||
    this.getTodaysWines();
 | 
			
		||||
    this.getWinners();
 | 
			
		||||
    this.socket = io(window.location.origin);
 | 
			
		||||
    this.socket.on("color_winner", msg => {});
 | 
			
		||||
 | 
			
		||||
    this.socket.on("disconnect", msg => {
 | 
			
		||||
      this.wasDisconnected = true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.socket.on("winner", async msg => {
 | 
			
		||||
      this.currentWinnerDrawn = true;
 | 
			
		||||
      this.currentWinner = {
 | 
			
		||||
        name: msg.name,
 | 
			
		||||
        color: msg.color,
 | 
			
		||||
        winnerCount: msg.winner_count
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.getWinners();
 | 
			
		||||
        this.getAttendees();
 | 
			
		||||
        this.currentWinner = null;
 | 
			
		||||
        this.currentWinnerDrawn = false;
 | 
			
		||||
      }, 19250);
 | 
			
		||||
    });
 | 
			
		||||
    this.socket.on("refresh_data", async msg => {
 | 
			
		||||
      this.getAttendees();
 | 
			
		||||
      this.getWinners();
 | 
			
		||||
    });
 | 
			
		||||
    this.socket.on("new_attendee", async msg => {
 | 
			
		||||
      this.getAttendees();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    this.socket.disconnect();
 | 
			
		||||
    this.socket = null;
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getWinners: async function() {
 | 
			
		||||
      let response = await winners();
 | 
			
		||||
      if (response) {
 | 
			
		||||
        this.winners = response;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    getTodaysWines() {
 | 
			
		||||
      prelottery()
 | 
			
		||||
        .then(wines => {
 | 
			
		||||
          this.wines = wines;
 | 
			
		||||
          this.todayExists = wines.length > 0;
 | 
			
		||||
        })
 | 
			
		||||
        .catch(_ => this.todayExists = false)
 | 
			
		||||
    },
 | 
			
		||||
    getAttendees: async function() {
 | 
			
		||||
      let response = await attendees();
 | 
			
		||||
      if (response) {
 | 
			
		||||
        this.attendees = response;
 | 
			
		||||
        if (this.attendees == undefined || this.attendees.length == 0) {
 | 
			
		||||
          this.attendeesFetched = true;
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        const addValueOfListObjectByKey = (list, key) =>
 | 
			
		||||
          list.map(object => object[key]).reduce((a, b) => a + b);
 | 
			
		||||
 | 
			
		||||
        this.ticketsBought = {
 | 
			
		||||
          red: addValueOfListObjectByKey(response, "red"),
 | 
			
		||||
          blue: addValueOfListObjectByKey(response, "blue"),
 | 
			
		||||
          green: addValueOfListObjectByKey(response, "green"),
 | 
			
		||||
          yellow: addValueOfListObjectByKey(response, "yellow")
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      this.attendeesFetched = true;
 | 
			
		||||
    },
 | 
			
		||||
    scrollToContent() {
 | 
			
		||||
      console.log(window.scrollY)
 | 
			
		||||
      const intersectingHeaderHeight = this.$refs.header.getBoundingClientRect().bottom - 50;
 | 
			
		||||
      const { scrollY } = window;
 | 
			
		||||
      let scrollHeight = intersectingHeaderHeight;
 | 
			
		||||
      if (scrollY > 0) {
 | 
			
		||||
        scrollHeight = intersectingHeaderHeight + scrollY;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      window.scrollTo({
 | 
			
		||||
        top: scrollHeight,
 | 
			
		||||
        behavior: "smooth"
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    track() {
 | 
			
		||||
      window.ga('send', 'pageview', '/lottery/game');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 | 
			
		||||
@import "../styles/variables.scss";
 | 
			
		||||
@import "../styles/media-queries.scss";
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
  width: 80vw;
 | 
			
		||||
  padding: 0 10vw;
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    width: 90vw;
 | 
			
		||||
    padding: 0 5vw;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: repeat(4, 1fr);
 | 
			
		||||
 | 
			
		||||
  > div, > section {
 | 
			
		||||
    @include mobile {
 | 
			
		||||
      grid-column: span 5;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h2 {
 | 
			
		||||
  font-size: 1.1rem;
 | 
			
		||||
  margin-bottom: 1.75rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header {
 | 
			
		||||
  h1 {
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    font-size: 3rem;
 | 
			
		||||
    margin: 4rem 0 2rem;
 | 
			
		||||
 | 
			
		||||
    @include mobile {
 | 
			
		||||
      margin-top: 1rem;
 | 
			
		||||
      font-size: 2.75rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  background-color: $primary;
 | 
			
		||||
  padding-bottom: 3rem;
 | 
			
		||||
  margin-bottom: 3rem;
 | 
			
		||||
 | 
			
		||||
  .instructions {
 | 
			
		||||
    grid-column: 1 / 4;
 | 
			
		||||
 | 
			
		||||
    @include mobile {
 | 
			
		||||
      grid-column: span 5;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .vipps-qr {
 | 
			
		||||
    grid-column: 4;
 | 
			
		||||
    margin-left: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .vipps-pill {
 | 
			
		||||
    margin: 0 auto 2rem;
 | 
			
		||||
    max-width: 80vw;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .call-to-action {
 | 
			
		||||
    grid-column: span 5;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ol {
 | 
			
		||||
    font-size: 1.4rem;
 | 
			
		||||
    line-height: 3rem;
 | 
			
		||||
    color: $matte-text-color;
 | 
			
		||||
 | 
			
		||||
    @include mobile {
 | 
			
		||||
      line-height: 2rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  p {
 | 
			
		||||
    font-size: 1.4rem;
 | 
			
		||||
    line-height: 2rem;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    .vin-link {
 | 
			
		||||
      cursor: default;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .icon {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      bottom: 3px;
 | 
			
		||||
      color: $link-color;
 | 
			
		||||
      margin-left: 0.5rem;
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      transform: rotate(-90deg);
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .vin-link {
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
    border-width: 2px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.todays-raffles {
 | 
			
		||||
  grid-column: 1;
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    order: 2;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.raffle-container {
 | 
			
		||||
  width: 165px;
 | 
			
		||||
  height: 175px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .raffle-element {
 | 
			
		||||
    font-size: 1.6rem;
 | 
			
		||||
    color: $matte-text-color;
 | 
			
		||||
    height: 75px;
 | 
			
		||||
    width: 75px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.winners {
 | 
			
		||||
  grid-column: 2 / 5;
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    order: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container-attendees {
 | 
			
		||||
  grid-column: 1 / 3;
 | 
			
		||||
  margin-right: 1rem;
 | 
			
		||||
  margin-top: 2rem;
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
    order: 4;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  > div {
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
 | 
			
		||||
    -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
 | 
			
		||||
    -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
 | 
			
		||||
    box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container-chat {
 | 
			
		||||
  grid-column: 3 / 5;
 | 
			
		||||
  margin-left: 1rem;
 | 
			
		||||
  margin-top: 2rem;
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    order: 3;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  > div {
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
 | 
			
		||||
    -webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
 | 
			
		||||
    -moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
 | 
			
		||||
    box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.wines-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  margin-bottom: 4rem;
 | 
			
		||||
 | 
			
		||||
  h2 {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    grid-column: 1 / 5;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .wine {
 | 
			
		||||
    margin-right: 1rem;
 | 
			
		||||
    margin-bottom: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -70,7 +70,7 @@ export default {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/styles/global";
 | 
			
		||||
@import "@/styles/global";
 | 
			
		||||
.container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										124
									
								
								frontend/router.js
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -68,4 +68,5 @@ form {
 | 
			
		||||
  width: calc(100% - 5rem);
 | 
			
		||||
  background-color: $light-red;
 | 
			
		||||
  color: $red;
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										347
									
								
								frontend/ui/Chat.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,347 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="chat-container">
 | 
			
		||||
    <span class="logged-in-username" v-if="username">Logget inn som: <span class="username">{{ username }}</span> <button @click="removeUsername">Logg ut</button></span>
 | 
			
		||||
 | 
			
		||||
    <div class="history" ref="history" v-if="chatHistory.length > 0">
 | 
			
		||||
      <div class="opaque-skirt"></div>
 | 
			
		||||
      <div v-if="hasMorePages" class="fetch-older-history">
 | 
			
		||||
        <button @click="loadMoreHistory">Hent eldre meldinger</button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="history-message"
 | 
			
		||||
        v-for="(history, index) in chatHistory"
 | 
			
		||||
        :key="`${history.username}-${history.timestamp}-${index}`"
 | 
			
		||||
      >
 | 
			
		||||
        <div>
 | 
			
		||||
          <span class="username">{{ history.username }}</span>
 | 
			
		||||
          <span class="timestamp">{{ getTime(history.timestamp) }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="message">{{ history.message }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div v-if="username" class="user-actions">
 | 
			
		||||
      <input @keyup.enter="sendMessage" type="text" v-model="message" placeholder="Melding.." />
 | 
			
		||||
      <button @click="sendMessage">Send</button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div v-else class="username-dialog">
 | 
			
		||||
      <input
 | 
			
		||||
        type="text"
 | 
			
		||||
        @keyup.enter="setUsername"
 | 
			
		||||
        v-model="temporaryUsername"
 | 
			
		||||
        maxlength="30"
 | 
			
		||||
        placeholder="Ditt navn.."
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div class="validation-error" v-if="validationError">
 | 
			
		||||
        {{ validationError }}
 | 
			
		||||
      </div>
 | 
			
		||||
      <button @click="setUsername">Lagre brukernavn</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { getChatHistory } from "@/api";
 | 
			
		||||
import io from "socket.io-client";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      socket: null,
 | 
			
		||||
      chatHistory: [],
 | 
			
		||||
      hasMorePages: true,
 | 
			
		||||
      message: "",
 | 
			
		||||
      page: 1,
 | 
			
		||||
      pageSize: 100,
 | 
			
		||||
      temporaryUsername: null,
 | 
			
		||||
      username: null,
 | 
			
		||||
      validationError: undefined
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    getChatHistory(1, this.pageSize)
 | 
			
		||||
      .then(resp => {
 | 
			
		||||
        this.chatHistory = resp.messages;
 | 
			
		||||
        this.hasMorePages = resp.total != resp.messages.length;
 | 
			
		||||
      });
 | 
			
		||||
    const username = window.localStorage.getItem('username');
 | 
			
		||||
    if (username) {
 | 
			
		||||
      this.username = username;
 | 
			
		||||
      this.emitUsernameOnConnect = true;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    chatHistory: {
 | 
			
		||||
      handler: function(newVal, oldVal) {
 | 
			
		||||
        if (oldVal.length == 0) {
 | 
			
		||||
          this.scrollToBottomOfHistory();
 | 
			
		||||
        }
 | 
			
		||||
        else if (newVal && newVal.length == oldVal.length) {
 | 
			
		||||
          if (this.isScrollPositionAtBottom()) {
 | 
			
		||||
            this.scrollToBottomOfHistory();
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          const prevOldestMessage = oldVal[0];
 | 
			
		||||
          this.scrollToMessageElement(prevOldestMessage);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      deep: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    this.socket.disconnect();
 | 
			
		||||
    this.socket = null;
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.socket = io(window.location.origin);
 | 
			
		||||
    this.socket.on("chat", msg => {
 | 
			
		||||
      this.chatHistory.push(msg);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.socket.on("disconnect", msg => {
 | 
			
		||||
      this.wasDisconnected = true;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.socket.on("connect", msg => {
 | 
			
		||||
      if (
 | 
			
		||||
        this.emitUsernameOnConnect ||
 | 
			
		||||
        (this.wasDisconnected && this.username != null)
 | 
			
		||||
      ) {
 | 
			
		||||
        this.setUsername(this.username);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.socket.on("accept_username", msg => {
 | 
			
		||||
      const { reason, success, username } = msg;
 | 
			
		||||
      this.usernameAccepted = success;
 | 
			
		||||
 | 
			
		||||
      if (success !== true) {
 | 
			
		||||
        this.username = null;
 | 
			
		||||
        this.validationError = reason;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.usernameAllowed = true;
 | 
			
		||||
        this.username = username;
 | 
			
		||||
        this.validationError = null;
 | 
			
		||||
        window.localStorage.setItem("username", username);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    loadMoreHistory() {
 | 
			
		||||
      let { page, pageSize } = this;
 | 
			
		||||
      page = page + 1;
 | 
			
		||||
 | 
			
		||||
      getChatHistory(page, pageSize)
 | 
			
		||||
        .then(resp => {
 | 
			
		||||
          this.chatHistory = resp.messages.concat(this.chatHistory);
 | 
			
		||||
          this.page = page;
 | 
			
		||||
          this.hasMorePages = resp.total != this.chatHistory.length;
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    pad(num) {
 | 
			
		||||
      if (num > 9) return num;
 | 
			
		||||
      return `0${num}`;
 | 
			
		||||
    },
 | 
			
		||||
    getTime(timestamp) {
 | 
			
		||||
      let date = new Date(timestamp);
 | 
			
		||||
      const timeString = `${this.pad(date.getHours())}:${this.pad(
 | 
			
		||||
        date.getMinutes()
 | 
			
		||||
      )}:${this.pad(date.getSeconds())}`;
 | 
			
		||||
 | 
			
		||||
      if (date.getDate() == new Date().getDate()) {
 | 
			
		||||
        return timeString;
 | 
			
		||||
      }
 | 
			
		||||
      return `${date.toLocaleDateString()} ${timeString}`;
 | 
			
		||||
    },
 | 
			
		||||
    sendMessage() {
 | 
			
		||||
      const message = { message: this.message };
 | 
			
		||||
      this.socket.emit("chat", message);
 | 
			
		||||
      this.message = '';
 | 
			
		||||
      this.scrollToBottomOfHistory();
 | 
			
		||||
    },
 | 
			
		||||
    setUsername(username=undefined) {
 | 
			
		||||
      if (this.temporaryUsername) {
 | 
			
		||||
        username = this.temporaryUsername;
 | 
			
		||||
      }
 | 
			
		||||
      const message = { username: username };
 | 
			
		||||
      this.socket.emit("username", message);
 | 
			
		||||
    },
 | 
			
		||||
    removeUsername() {
 | 
			
		||||
      this.username = null;
 | 
			
		||||
      this.temporaryUsername = null;
 | 
			
		||||
      window.localStorage.removeItem("username");
 | 
			
		||||
    },
 | 
			
		||||
    isScrollPositionAtBottom() {
 | 
			
		||||
      const { history } = this.$refs;
 | 
			
		||||
      if (history) {
 | 
			
		||||
        return history.offsetHeight + history.scrollTop >= history.scrollHeight;
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
    },
 | 
			
		||||
    scrollToBottomOfHistory() {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const { history } = this.$refs;
 | 
			
		||||
        history.scrollTop = history.scrollHeight;
 | 
			
		||||
      }, 1);
 | 
			
		||||
    },
 | 
			
		||||
    scrollToMessageElement(message) {
 | 
			
		||||
      const elemTimestamp = this.getTime(message.timestamp);
 | 
			
		||||
      const self = this;
 | 
			
		||||
      const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText;
 | 
			
		||||
      const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp;
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const { history } = self.$refs;
 | 
			
		||||
        const childrenElements = Array.from(history.getElementsByClassName('history-message'));
 | 
			
		||||
 | 
			
		||||
        const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
 | 
			
		||||
        history.scrollTop = elemInNewList.offsetTop - 70
 | 
			
		||||
      }, 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "@/styles/media-queries.scss";
 | 
			
		||||
@import "@/styles/variables.scss";
 | 
			
		||||
 | 
			
		||||
.chat-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  transform: translate3d(0,0,0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 3.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.logged-in-username {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0.75rem;
 | 
			
		||||
  left: 1rem;
 | 
			
		||||
  color: $matte-text-color;
 | 
			
		||||
  width: calc(100% - 2rem);
 | 
			
		||||
 | 
			
		||||
  button {
 | 
			
		||||
    width: unset;
 | 
			
		||||
    padding: 5px 10px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .username {
 | 
			
		||||
    border-bottom: 2px solid $link-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.user-actions {
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.history {
 | 
			
		||||
  height: 75%;
 | 
			
		||||
  overflow-y: scroll;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  max-height: 550px;
 | 
			
		||||
  margin-top: 2rem;
 | 
			
		||||
 | 
			
		||||
  &-message {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    margin: 0.35rem 0;
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    .username {
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
      font-size: 1.05rem;
 | 
			
		||||
      margin-right: 0.3rem;
 | 
			
		||||
    }
 | 
			
		||||
    .timestamp {
 | 
			
		||||
      font-size: 0.9rem;
 | 
			
		||||
      top: 2px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &-message:nth-of-type(2) {
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .opaque-skirt {
 | 
			
		||||
    width: calc(100% - 2rem);
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    height: 2rem;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    background: linear-gradient(
 | 
			
		||||
      to bottom,
 | 
			
		||||
      white,
 | 
			
		||||
      rgba(255, 255, 255, 0)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .fetch-older-history {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    margin: 1rem 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    height: 300px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.username-dialog {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  .validation-error {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    background-color: $light-red;
 | 
			
		||||
    color: $red;
 | 
			
		||||
    top: -3.5rem;
 | 
			
		||||
    left: 0.5rem;
 | 
			
		||||
    padding: 0.75rem;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
    &::before {
 | 
			
		||||
      content: '';
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 2.1rem;
 | 
			
		||||
      left: 2rem;
 | 
			
		||||
      width: 1rem;
 | 
			
		||||
      height: 1rem;
 | 
			
		||||
      transform: rotate(45deg);
 | 
			
		||||
      background-color: $light-red;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  background: #b7debd;
 | 
			
		||||
  color: #333;
 | 
			
		||||
  padding: 10px 30px;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  width: fit-content;
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  /* height: 1.5rem; */
 | 
			
		||||
  /* max-height: 1.5rem; */
 | 
			
		||||
  margin: 0 2px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  transition: transform 0.5s ease;
 | 
			
		||||
  -webkit-font-smoothing: antialiased;
 | 
			
		||||
  touch-action: manipulation;
 | 
			
		||||
 | 
			
		||||
  @include mobile {
 | 
			
		||||
    padding: 10px 10px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										97
									
								
								frontend/ui/Footer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								frontend/ui/Footer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <footer>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>
 | 
			
		||||
        <a href="https://github.com/KevinMidboe/vinlottis" class="github">
 | 
			
		||||
          <span>Utforsk koden på 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>
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
@@ -77,7 +77,7 @@ export default {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@import "./src/styles/variables";
 | 
			
		||||
@import "@/styles/variables";
 | 
			
		||||
 | 
			
		||||
.requested-count {
 | 
			
		||||
  display: flex;
 | 
			
		||||
@@ -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%;
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										81
									
								
								frontend/ui/VippsPill.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div aria-label="button" role="button" @click="openVipps" tabindex="0">
 | 
			
		||||
    <img src="public/assets/images/vipps-pay_with_vipps_pill.png" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    amount: {
 | 
			
		||||
      type: Number,
 | 
			
		||||
      default: 1
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      phone: __PHONE__,
 | 
			
		||||
      name: __NAME__,
 | 
			
		||||
      price: __PRICE__,
 | 
			
		||||
      message: __MESSAGE__
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    isMobile: function() {
 | 
			
		||||
      return this.isMobileFunction();
 | 
			
		||||
    },
 | 
			
		||||
    priceToPay: function() {
 | 
			
		||||
      return this.amount * (this.price * 100);
 | 
			
		||||
    },
 | 
			
		||||
    vippsUrlBasedOnUserAgent: function() {
 | 
			
		||||
      if (navigator.userAgent.includes("iPhone")) {
 | 
			
		||||
        return (
 | 
			
		||||
          "https://qr.vipps.no/28/2/01/031/47" +
 | 
			
		||||
          this.phone.replace(/ /g, "") +
 | 
			
		||||
          "?v=1&m=" +
 | 
			
		||||
          this.message +
 | 
			
		||||
          "&a=" +
 | 
			
		||||
          this.priceToPay
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        "https://qr.vipps.no/28/2/01/031/47" +
 | 
			
		||||
        this.phone.replace(/ /g, "") +
 | 
			
		||||
        "?v=1&m=" +
 | 
			
		||||
        this.message
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    openVipps() {
 | 
			
		||||
      if (!this.isMobileFunction()) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      window.location.assign(this.vippsUrlBasedOnUserAgent);
 | 
			
		||||
    },
 | 
			
		||||
    isMobileFunction() {
 | 
			
		||||
      if (
 | 
			
		||||
        navigator.userAgent.match(/Android/i) ||
 | 
			
		||||
        navigator.userAgent.match(/webOS/i) ||
 | 
			
		||||
        navigator.userAgent.match(/iPhone/i) ||
 | 
			
		||||
        navigator.userAgent.match(/iPad/i) ||
 | 
			
		||||
        navigator.userAgent.match(/iPod/i) ||
 | 
			
		||||
        navigator.userAgent.match(/BlackBerry/i) ||
 | 
			
		||||
        navigator.userAgent.match(/Windows Phone/i)
 | 
			
		||||
      ) {
 | 
			
		||||
        return true;
 | 
			
		||||
      } else {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
img {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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 {
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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
 | 
			
		||||
							
								
								
									
										54
									
								
								frontend/vinlottis-init.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								frontend/vinlottis-init.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import VueRouter from "vue-router";
 | 
			
		||||
import { routes } from "@/router.js";
 | 
			
		||||
import Vinlottis from "@/Vinlottis";
 | 
			
		||||
 | 
			
		||||
import * as Sentry from "@sentry/browser";
 | 
			
		||||
import { Vue as VueIntegration } from "@sentry/integrations";
 | 
			
		||||
 | 
			
		||||
Vue.use(VueRouter);
 | 
			
		||||
 | 
			
		||||
const ENV = window.location.href.includes("localhost") ? "development" : "production";
 | 
			
		||||
if (ENV !== "development") {
 | 
			
		||||
  Sentry.init({
 | 
			
		||||
    dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
 | 
			
		||||
    integrations: [
 | 
			
		||||
      new VueIntegration({ Vue })
 | 
			
		||||
    ],
 | 
			
		||||
    beforeSend: event => {
 | 
			
		||||
      console.error(event);
 | 
			
		||||
      return event;
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Add global GA variables
 | 
			
		||||
window.ga = window.ga || function(){
 | 
			
		||||
  window.ga.q = window.ga.q || [];
 | 
			
		||||
  window.ga.q.push(arguments);
 | 
			
		||||
};
 | 
			
		||||
ga.l = 1 * new Date();
 | 
			
		||||
 | 
			
		||||
// Initiate
 | 
			
		||||
ga('create', __GA_TRACKINGID__, {
 | 
			
		||||
  'allowAnchor': false,
 | 
			
		||||
  'cookieExpires': __GA_COOKIELIFETIME__, // Time in seconds
 | 
			
		||||
  'cookieFlags': 'SameSite=Strict; Secure'
 | 
			
		||||
});
 | 
			
		||||
ga('set', 'anonymizeIp', true); // Enable IP Anonymization/IP masking
 | 
			
		||||
ga('send', 'pageview');
 | 
			
		||||
 | 
			
		||||
if (ENV == 'development')
 | 
			
		||||
  window[`ga-disable-${__GA_TRACKINGID__}`] = true;
 | 
			
		||||
 | 
			
		||||
const router = new VueRouter({
 | 
			
		||||
  routes: routes
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
new Vue({
 | 
			
		||||
  el: "#app",
 | 
			
		||||
  router,
 | 
			
		||||
  components: { Vinlottis },
 | 
			
		||||
  template: "<Vinlottis/>",
 | 
			
		||||
  render: h => h(Vinlottis)
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										84
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								package.json
									
									
									
									
									
								
							@@ -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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								pm2.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								pm2.json
									
									
									
									
									
								
							@@ -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
									
								
							
							
						
						
									
										88
									
								
								public/analytics.js
									
									
									
									
									
										Normal 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(">m")==b&&(g.G=!0);1===e?(g.ia=!!a.get("anonymizeIp"),g.sync=d,b=String(a.get("name")),"t0"!=b&&(g.target=b),G(String(a.get("trackingId")))||(g.clientId=String(a.get(Q)),g.ka=Number(a.get(n)),
 | 
			
		||||
c=c.palindrome?r:q,c=(c=M.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,g.la=c,g.qa=E(P(a,kb),"gclid"))):2===e&&(g.context="c",ca={allow_google_signals:a.get(Ud),allow_ad_personalization_signals:a.get(xe)});He(g,ca);return t(g)};var H={},Jd=function(a,b){b||(b=(b=P(a,V))&&"t0"!=b?Wd.test(b)?"_gat_"+Cc(P(a,Na)):"_gat_"+Cc(b):"_gat");this.Y=b},Rd=function(a,b){var c=b.get(Wb);b.set(Wb,function(e){Pd(a,e,ed);Pd(a,e,ia);var g=c(e);Qd(a,e);return g});var d=b.get(Xb);b.set(Xb,function(e){var g=d(e);if(se(e)){J(80);var ca={U:re(e,1),google:re(e,2),count:0};pe("https://stats.g.doubleclick.net/j/collect",ca.U,ca);e.set(ed,"",!0)}return g})},Pd=function(a,b,c){!1===b.get(Ud)||b.get(c)||("1"==Ca(a.Y)[0]?b.set(c,"",!0):b.set(c,""+hd(),
 | 
			
		||||
!0))},Qd=function(a,b){se(b)&&zc(a.Y,"1",P(b,Yb),P(b,W),P(b,Na),6E4,P(b,Be))},se=function(a){return!!a.get(ed)&&!1!==a.get(Ud)},Ne=function(a){return!H[P(a,Na)]&&void 0===a.get(">m")&&void 0===a.get(fa)&&void 0===a.get(gd)&&void 0===a.get(oe)},re=function(a,b){var c=new ee,d=function(g){$a(g).F&&c.set($a(g).F,a.get(g))};d(hb);d(ib);d(Na);d(Q);d(ed);1==b&&(d(Ad),d(ia),d(I));!1===a.get(xe)&&c.set("npa","1");c.set($a(ld).F,Td(a));var e="";c.map(function(g,ca){e+=K(g)+"=";e+=K(""+ca)+"&"});e+="z="+
 | 
			
		||||
hd();1==b?e="t=dc&aip=1&_r=3&"+e:2==b&&(e="t=sr&aip=1&_r=4&slf_rd=1&"+e);return e},Me=function(a){if(Ne(a))return H[P(a,Na)]=!0,function(b){if(b&&!H[b]){var c=Ke(a,b);Id(c);H[b]=!0}}},Wd=/^gtm\d+$/;var fd=function(a,b){a=a.model;if(!a.get("dcLoaded")){var c=new $c(Dd(a));c.set(29);a.set(Gd,c.C);b=b||{};var d;b[U]&&(d=Cc(b[U]));b=new Jd(a,d);Rd(b,a);a.set("dcLoaded",!0)}};var Sd=function(a){if(!a.get("dcLoaded")&&"cookie"==a.get(ac)){var b=new Jd(a);Pd(b,a,ed);Pd(b,a,ia);Qd(b,a);b=se(a);var c=Ne(a);b&&a.set(Md,1,!0);c&&a.set(Od,1,!0);if(b||c)a.set(ad,"d",!0),J(79),a.set(qe,{U:re(a,1),google:re(a,2),V:Me(a),count:0},!0)}};var Lc=function(){var a=O.gaGlobal=O.gaGlobal||{};return a.hid=a.hid||hd()};var wb=/^(UA|YT|MO|GP)-(\d+)-(\d+)$/,pc=function(a){function b(e,g){d.model.data.set(e,g)}function c(e,g){b(e,g);d.filters.add(e)}var d=this;this.model=new Ya;this.filters=new Ha;b(V,a[V]);b(Na,sa(a[Na]));b(U,a[U]);b(W,a[W]||xa());b(Yb,a[Yb]);b(Zb,a[Zb]);b(Hd,a[Hd]);b(Be,a[Be]);b($b,a[$b]);b(Wc,a[Wc]);b(bc,a[bc]);b(cc,a[cc]);b(Ka,a[Ka]);b(dc,a[dc]);b(ec,a[ec]);b(ac,a[ac]);b(Ad,a[Ad]);b(n,a[n]);b(Kd,a[Kd]);b(je,a[je]);b(Ze,a[Ze]);b(oe,a[oe]);b(Je,a[Je]);b(F,a[F]);b(hb,1);b(ib,"j87");c(Qb,Ma);c(oa,
 | 
			
		||||
ua);c(dd,cd);c(Rb,Oa);c(md,vb);c(Sb,nc);c(Uc,Yc);c(Tb,Ja);c(Vb,Ta);c(Vc,Hc);c(zd,yd);c(Ld,Sd);c(ze,Fe);c(Wb,Pa);c(Xb,Sa);c(Cd,Fd(this));pd(this.model);td(this.model,a[Q]);this.model.set(jb,Lc())};pc.prototype.get=function(a){return this.model.get(a)};pc.prototype.set=function(a,b){this.model.set(a,b)};
 | 
			
		||||
pc.prototype.send=function(a){if(!(1>arguments.length)){if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else b=arguments[0]&&arguments[0][Va],c=arguments;b&&(c=za(me[b]||[],c),c[Va]=b,this.model.set(c,void 0,!0),this.filters.D(this.model),this.model.data.m={})}};pc.prototype.ma=function(a,b){var c=this;u(a,c,b)||(v(a,function(){u(a,c,b)}),y(String(c.get(V)),a,void 0,b,!0))};
 | 
			
		||||
var td=function(a,b){var c=P(a,U);a.data.set(la,"_ga"==c?"_gid":c+"_gid");if("cookie"==P(a,ac)){hc=!1;c=Ca(P(a,U));c=Xd(a,c);if(!c){c=P(a,W);var d=P(a,$b)||xa();c=Xc("__utma",d,c);void 0!=c?(J(10),c=c.O[1]+"."+c.O[2]):c=void 0}c&&(hc=!0);if(d=c&&!a.get(Hd))if(d=c.split("."),2!=d.length)d=!1;else if(d=Number(d[1])){var e=R(a,Zb);d=d+e<(new Date).getTime()/1E3}else d=!1;d&&(c=void 0);c&&(a.data.set(xd,c),a.data.set(Q,c),(c=uc(a))&&a.data.set(I,c));if(a.get(je)&&(c=a.get(ce),d=a.get(ie),!c||d&&"aw.ds"!=
 | 
			
		||||
d)){c={};if(M){d=[];e=M.cookie.split(";");for(var g=/^\s*_gac_(UA-\d+-\d+)=\s*(.+?)\s*$/,ca=0;ca<e.length;ca++){var l=e[ca].match(g);l&&d.push({ja:l[1],value:l[2]})}e={};if(d&&d.length)for(g=0;g<d.length;g++)(ca=d[g].value.split("."),"1"!=ca[0]||3!=ca.length)?c&&(c.na=!0):ca[1]&&(e[d[g].ja]?c&&(c.pa=!0):e[d[g].ja]=[],e[d[g].ja].push({timestamp:ca[1],qa:ca[2]}));d=e}else d={};d=d[P(a,Na)];le(c);d&&0!=d.length&&(c=d[0],a.data.set(fe,c.timestamp),a.data.set(ce,c.qa))}}if(a.get(Hd)){c=be("_ga",!!a.get(cc));
 | 
			
		||||
g=be("_gl",!!a.get(cc));d=De.get(a.get(cc));e=d._ga;g&&0<g.indexOf("_ga*")&&!e&&J(30);if(b||!a.get(Je))g=!1;else if(g=a.get(F),void 0===g||g("analytics_storage"))g=!1;else{J(84);a.data.set(Le,1);if(g=d._up)(g=Jc.exec(M.referrer))?(g=g[1],ca=M.location.hostname,g=ca===g||0<=ca.indexOf("."+g)||0<=g.indexOf("."+ca)?!0:!1):g=!1;g=g?!0:!1}ca=d.gclid;l=d._gac;if(c||e||ca||l)if(c&&e&&J(36),a.get(bc)||ye(a.get(Kd))||g){e&&(J(38),a.data.set(Q,e),d._gid&&(J(51),a.data.set(I,d._gid)));ca?(J(82),a.data.set(ce,
 | 
			
		||||
ca),d.gclsrc&&a.data.set(ie,d.gclsrc)):l&&(e=l.split("."))&&2===e.length&&(J(37),a.data.set(ce,e[0]),a.data.set(fe,e[1]));if(d=d._fplc)J(83),a.data.set(Ae,d);if(c)b:if(d=c.indexOf("."),-1==d)J(22);else{e=c.substring(0,d);g=c.substring(d+1);d=g.indexOf(".");c=g.substring(0,d);g=g.substring(d+1);if("1"==e){if(d=g,ke(d,c)){J(23);break b}}else if("2"==e){d=g.indexOf("-");e="";0<d?(e=g.substring(0,d),d=g.substring(d+1)):d=g.substring(1);if(ke(e+d,c)){J(53);break b}e&&(J(2),a.data.set(I,e))}else{J(22);
 | 
			
		||||
break b}J(11);a.data.set(Q,d);if(c=be("_gac",!!a.get(cc)))c=c.split("."),"1"!=c[0]||4!=c.length?J(72):ke(c[3],c[1])?J(71):(a.data.set(ce,c[3]),a.data.set(fe,c[2]),J(70))}}else J(21)}b&&(J(9),a.data.set(Q,K(b)));a.get(Q)||(b=(b=O.gaGlobal)&&b.from_cookie&&"cookie"!==P(a,ac)?void 0:(b=b&&b.vid)&&-1!==b.search(jd)?b:void 0,b?(J(17),a.data.set(Q,b)):(J(8),a.data.set(Q,ra())));a.get(I)||(J(3),a.data.set(I,ra()));mc(a);b=O.gaGlobal=O.gaGlobal||{};c=P(a,Q);a=c===P(a,xd);if(void 0==b.vid||a&&!b.from_cookie)b.vid=
 | 
			
		||||
c,b.from_cookie=a},pd=function(a){var b=O.navigator,c=O.screen,d=M.location;a.set(lb,ya(!!a.get(ec),!!a.get(Kd)));if(d){var e=d.pathname||"";"/"!=e.charAt(0)&&(J(31),e="/"+e);a.set(kb,d.protocol+"//"+d.hostname+e+d.search)}c&&a.set(qb,c.width+"x"+c.height);c&&a.set(pb,c.colorDepth+"-bit");c=M.documentElement;var g=(e=M.body)&&e.clientWidth&&e.clientHeight,ca=[];c&&c.clientWidth&&c.clientHeight&&("CSS1Compat"===M.compatMode||!g)?ca=[c.clientWidth,c.clientHeight]:g&&(ca=[e.clientWidth,e.clientHeight]);
 | 
			
		||||
c=0>=ca[0]||0>=ca[1]?"":ca.join("x");a.set(rb,c);a.set(tb,fc());a.set(ob,M.characterSet||M.charset);a.set(sb,b&&"function"===typeof b.javaEnabled&&b.javaEnabled()||!1);a.set(nb,(b&&(b.language||b.browserLanguage)||"").toLowerCase());a.data.set(ce,be("gclid",!0));a.data.set(ie,be("gclsrc",!0));a.data.set(fe,Math.round((new Date).getTime()/1E3));if(d&&a.get(cc)&&(b=M.location.hash)){b=b.split(/[?&#]+/);d=[];for(c=0;c<b.length;++c)(D(b[c],"utm_id")||D(b[c],"utm_campaign")||D(b[c],"utm_source")||D(b[c],
 | 
			
		||||
"utm_medium")||D(b[c],"utm_term")||D(b[c],"utm_content")||D(b[c],"gclid")||D(b[c],"dclid")||D(b[c],"gclsrc"))&&d.push(b[c]);0<d.length&&(b="#"+d.join("&"),a.set(kb,a.get(kb)+b))}},me={pageview:[mb],event:[ub,xb,yb,zb],social:[Bb,Cb,Db],timing:[Mb,Nb,Pb,Ob]};var rc=function(a){if("prerender"==M.visibilityState)return!1;a();return!0},z=function(a){if(!rc(a)){J(16);var b=!1,c=function(){if(!b&&rc(a)){b=!0;var d=c,e=M;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};L(M,"visibilitychange",c)}};var te=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,sc=function(a){if(ea(a[0]))this.u=a[0];else{var b=te.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.K=b[2]||"",this.methodName=b[3],this.a=[].slice.call(a,1),this.K||(this.A="create"==this.methodName,this.i="require"==this.methodName,this.g="provide"==this.methodName,this.ba="remove"==this.methodName),this.i&&(3<=this.a.length?(this.X=this.a[1],this.W=this.a[2]):this.a[1]&&(qa(this.a[1])?this.X=this.a[1]:this.W=this.a[1])));b=a[1];a=a[2];if(!this.methodName)throw"abort";
 | 
			
		||||
if(this.i&&(!qa(b)||""==b))throw"abort";if(this.g&&(!qa(b)||""==b||!ea(a)))throw"abort";if(ud(this.c)||ud(this.K))throw"abort";if(this.g&&"t0"!=this.c)throw"abort";}};function ud(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Yd,Zd,$d,A;Yd=new ee;$d=new ee;A=new ee;Zd={ec:45,ecommerce:46,linkid:47};
 | 
			
		||||
var u=function(a,b,c){b==N||b.get(V);var d=Yd.get(a);if(!ea(d))return!1;b.plugins_=b.plugins_||new ee;if(b.plugins_.get(a))return!0;b.plugins_.set(a,new d(b,c||{}));return!0},y=function(a,b,c,d,e){if(!ea(Yd.get(b))&&!$d.get(b)){Zd.hasOwnProperty(b)&&J(Zd[b]);a=N.j(a);if(p.test(b)){J(52);if(!a)return!0;c=Ke(a.model,b,d,e)}!c&&Zd.hasOwnProperty(b)?(J(39),c=b+".js"):J(43);if(c){if(a){var g=a.get(oe);qa(g)||(g=void 0)}c&&0<=c.indexOf("/")||(c=(g||bd(!1))+"/plugins/ua/"+c);d=ae(c);g=d.protocol;c=M.location.protocol;
 | 
			
		||||
("https:"==g||g==c||("http:"!=g?0:"http:"==c))&&B(d)&&(Id(d.url,void 0,e),$d.set(b,!0))}}},v=function(a,b){var c=A.get(a)||[];c.push(b);A.set(a,c)},C=function(a,b){Yd.set(a,b);b=A.get(a)||[];for(var c=0;c<b.length;c++)b[c]();A.set(a,[])},B=function(a){var b=ae(M.location.href);if(D(a.url,Ge(1))||D(a.url,Ge(2)))return!0;if(a.query||0<=a.url.indexOf("?")||0<=a.path.indexOf("://"))return!1;if(a.host==b.host&&a.port==b.port)return!0;b="http:"==a.protocol?80:443;return"www.google-analytics.com"==a.host&&
 | 
			
		||||
(a.port||b)==b&&D(a.path,"/plugins/")?!0:!1},ae=function(a){function b(l){var k=l.hostname||"",w=0<=k.indexOf("]");k=k.split(w?"]":":")[0].toLowerCase();w&&(k+="]");w=(l.protocol||"").toLowerCase();w=1*l.port||("http:"==w?80:"https:"==w?443:"");l=l.pathname||"";D(l,"/")||(l="/"+l);return[k,""+w,l]}var c=M.createElement("a");c.href=M.location.href;var d=(c.protocol||"").toLowerCase(),e=b(c),g=c.search||"",ca=d+"//"+e[0]+(e[1]?":"+e[1]:"");D(a,"//")?a=d+a:D(a,"/")?a=ca+a:!a||D(a,"?")?a=ca+e[2]+(a||
 | 
			
		||||
g):0>a.split("/")[0].indexOf(":")&&(a=ca+e[2].substring(0,e[2].lastIndexOf("/"))+"/"+a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:c.search||"",url:a||""}};var Z={ga:function(){Z.f=[]}};Z.ga();Z.D=function(a){var b=Z.J.apply(Z,arguments);b=Z.f.concat(b);for(Z.f=[];0<b.length&&!Z.v(b[0])&&!(b.shift(),0<Z.f.length););Z.f=Z.f.concat(b)};Z.J=function(a){for(var b=[],c=0;c<arguments.length;c++)try{var d=new sc(arguments[c]);d.g?C(d.a[0],d.a[1]):(d.i&&(d.ha=y(d.c,d.a[0],d.X,d.W)),b.push(d))}catch(e){}return b};
 | 
			
		||||
Z.v=function(a){try{if(a.u)a.u.call(O,N.j("t0"));else{var b=a.c==gb?N:N.j(a.c);if(a.A){if("t0"==a.c&&(b=N.create.apply(N,a.a),null===b))return!0}else if(a.ba)N.remove(a.c);else if(b)if(a.i){if(a.ha&&(a.ha=y(a.c,a.a[0],a.X,a.W)),!u(a.a[0],b,a.W))return!0}else if(a.K){var c=a.methodName,d=a.a,e=b.plugins_.get(a.K);e[c].apply(e,d)}else b[a.methodName].apply(b,a.a)}}catch(g){}};var N=function(a){J(1);Z.D.apply(Z,[arguments])};N.h={};N.P=[];N.L=0;N.ya=0;N.answer=42;var we=[Na,W,V];N.create=function(a){var b=za(we,[].slice.call(arguments));b[V]||(b[V]="t0");var c=""+b[V];if(N.h[c])return N.h[c];if(da(b))return null;b=new pc(b);N.h[c]=b;N.P.push(b);c=qc().tracker_created;if(ea(c))try{c(b)}catch(d){}return b};N.remove=function(a){for(var b=0;b<N.P.length;b++)if(N.P[b].get(V)==a){N.P.splice(b,1);N.h[a]=null;break}};N.j=function(a){return N.h[a]};N.getAll=function(){return N.P.slice(0)};
 | 
			
		||||
N.N=function(){"ga"!=gb&&J(49);var a=O[gb];if(!a||42!=a.answer){N.L=a&&a.l;N.ya=1*new Date;N.loaded=!0;var b=O[gb]=N;X("create",b,b.create);X("remove",b,b.remove);X("getByName",b,b.j,5);X("getAll",b,b.getAll,6);b=pc.prototype;X("get",b,b.get,7);X("set",b,b.set,4);X("send",b,b.send);X("requireSync",b,b.ma);b=Ya.prototype;X("get",b,b.get);X("set",b,b.set);if("https:"!=M.location.protocol&&!Ba){a:{b=M.getElementsByTagName("script");for(var c=0;c<b.length&&100>c;c++){var d=b[c].src;if(d&&0==d.indexOf(bd(!0)+
 | 
			
		||||
"/analytics")){b=!0;break a}}b=!1}b&&(Ba=!0)}(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);X("passthrough",b,b.$,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};var Oe=N.N,Pe=O[gb];Pe&&Pe.r?Oe():z(Oe);z(function(){Z.D(["provide","render",ua])});})(window);
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/assets/images/logo-github.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/assets/images/logo-github.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/assets/images/vipps-pay_with_vipps_pill.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/assets/images/vipps-pay_with_vipps_pill.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.6 KiB  | 
							
								
								
									
										94
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								server.js
									
									
									
									
									
								
							@@ -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
		Reference in New Issue
	
	Block a user