Merge branch 'master' of github.com:KevinMidboe/vinlottis into feat/better-today-wines-layout
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;
|
||||
@@ -86,27 +85,14 @@ h1 {
|
||||
}
|
||||
|
||||
#wines-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
|
||||
@include desktop {
|
||||
margin: 0 2rem;
|
||||
}
|
||||
|
||||
> div {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.winners-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
|
||||
> div:not(:last-of-type) {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -122,10 +122,14 @@ h1 {
|
||||
|
||||
.filter input {
|
||||
font-size: 1rem;
|
||||
width: 30%;
|
||||
width: 100%;
|
||||
border-color: black;
|
||||
border-width: 1.5px;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
@include desktop {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.highscore-header {
|
||||
@@ -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{
|
||||
207
frontend/components/Salgsbetingelser.vue
Normal file
207
frontend/components/Salgsbetingelser.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Slagsbetingelser</h1>
|
||||
<section class="chapter cf" id="chapter-1">
|
||||
<h2 class="h2-title">Innledning</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dette kjøpet er regulert av de nedenstående standard salgsbetingelser for forbrukerkjøp av varer over Internett. Forbrukerkjøp over internett reguleres hovedsakelig av avtaleloven, forbrukerkjøpsloven, markedsføringsloven, angrerettloven og ehandelsloven, og disse lovene gir forbrukeren ufravikelige rettigheter. Lovene er tilgjengelig på
|
||||
<a target="_blank" class="vin-link" href="http://www.lovdata.no/" rel="noopener">www.lovdata.no.</a>
|
||||
Vilkårene i denne avtalen skal ikke forstås som noen begrensning i de lovbestemte rettighetene, men oppstiller partenes viktigste rettigheter og plikter for handelen.
|
||||
</p>
|
||||
<p>
|
||||
Salgsbetingelsene er utarbeidet og anbefalt av Forbrukertilsynet.
|
||||
<a class="vin-link" href="https://forbrukertilsynet.no/lov-og-rett/veiledninger-og-retningslinjer/veiledning-standard-salgsbetingelser-forbrukerkjop-varer-internett">For en bedre forståelse av disse salgsbetingelsene, se Forbrukertilsynets veileder her. </a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-2">
|
||||
<h2 class="h2-title">1. Avtalen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Avtalen består av disse salgsbetingelsene, opplysninger gitt i bestillingsløsningen og eventuelt særskilt avtalte vilkår. Ved eventuell motstrid mellom opplysningene, går det som særskilt er avtalt mellom partene foran, så fremt det ikke strider mot ufravikelig lovgivning.</p>
|
||||
<p>Avtalen vil i tillegg bli utfylt av relevante lovbestemmelser som regulerer kjøp av varer mellom næringsdrivende og forbrukere.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-3">
|
||||
<h2 class="h2-title">2. Partene</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Selger er KEVIN MIDBØE, Schleppegrells gate 18, questions@vinlottis.no/kevin.midboe@gmail.com, 926432478, og betegnes i det følgende som selger/selgeren.</p>
|
||||
<p>Kjøper er den forbrukeren som foretar bestillingen, og betegnes i det følgende som kjøper/kjøperen.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-4">
|
||||
<h2 class="h2-title">3. Pris</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Den oppgitte prisen for varen og tjenester er den totale prisen kjøper skal betale. Denne prisen inkluderer alle avgifter og tilleggskostnader. Ytterligere kostnader som selger før kjøpet ikke har informert om, skal kjøper ikke bære.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-5">
|
||||
<h2 class="h2-title">4. Avtaleinngåelse</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Avtalen er bindende for begge parter når kjøperen har sendt sin bestilling til selgeren.</p>
|
||||
<p>Avtalen er likevel ikke bindende hvis det har forekommet skrive- eller tastefeil i tilbudet fra selgeren i bestillingsløsningen i nettbutikken eller i kjøperens bestilling, og den annen part innså eller burde ha innsett at det forelå en slik feil.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-6">
|
||||
<h2 class="h2-title">5. Betalingen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Selgeren kan kreve betaling for varen fra det tidspunkt den blir sendt fra selgeren til kjøperen.</p>
|
||||
<p>Dersom kjøperen bruker kredittkort eller debetkort ved betaling, kan selgeren reservere kjøpesummen på kortet ved bestilling. Kortet blir belastet samme dag som varen sendes.</p>
|
||||
<p>Ved betaling med faktura, blir fakturaen til kjøperen utstedt ved forsendelse av varen. Betalingsfristen fremgår av fakturaen og er på minimum 14 dager fra mottak.</p>
|
||||
<p>Kjøpere under 18 år kan ikke betale med etterfølgende faktura.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-7">
|
||||
<h2 class="h2-title">6. Levering</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Levering er skjedd når kjøperen, eller hans representant, har overtatt tingen.</p>
|
||||
<p>Hvis ikke leveringstidspunkt fremgår av bestillingsløsningen, skal selgeren levere varen til kjøper uten unødig opphold og senest 30 dager etter bestillingen fra kunden. Varen skal leveres hos kjøperen med mindre annet er særskilt avtalt mellom partene.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-8">
|
||||
<h2 class="h2-title">7. Risikoen for varen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Risikoen for varen går over på kjøper når han, eller hans representant, har fått varene levert i tråd med punkt 6.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-9">
|
||||
<h2 class="h2-title">8. Angrerett</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Med mindre avtalen er unntatt fra angrerett, kan kjøperen angre kjøpet av varen i henhold til angrerettloven.</p>
|
||||
<p>Kjøperen må gi selger melding om bruk av angreretten innen 14 dager fra fristen begynner å løpe. I fristen inkluderes alle kalenderdager. Dersom fristen ender på en lørdag, helligdag eller høytidsdag forlenges fristen til nærmeste virkedag.</p>
|
||||
<p>Angrefristen anses overholdt dersom melding er sendt før utløpet av fristen. Kjøper har bevisbyrden for at angreretten er blitt gjort gjeldende, og meldingen bør derfor skje skriftlig (angrerettskjema, e-post eller brev).</p>
|
||||
<p>Angrefristen begynner å løpe:</p>
|
||||
<ul>
|
||||
<li>Ved kjøp av enkeltstående varer vil angrefristen løpe fra dagen etter varen(e) er mottatt.</li>
|
||||
<li>Selges et abonnement, eller innebærer avtalen regelmessig levering av identiske varer, løper fristen fra dagen etter første forsendelse er mottatt.</li>
|
||||
<li>Består kjøpet av flere leveranser, vil angrefristen løpe fra dagen etter siste leveranse er mottatt.</li>
|
||||
</ul>
|
||||
<p>Angrefristen utvides til 12 måneder etter utløpet av den opprinnelige fristen dersom selger ikke før avtaleinngåelsen opplyser om at det foreligger angrerett og standardisert angreskjema. Tilsvarende gjelder ved manglende opplysning om vilkår, tidsfrister og fremgangsmåte for å benytte angreretten. Sørger den næringsdrivende for å gi opplysningene i løpet av disse 12 månedene, utløper angrefristen likevel 14 dager etter den dagen kjøperen mottok opplysningene.</p>
|
||||
<p>Ved bruk av angreretten må varen leveres tilbake til selgeren uten unødig opphold og senest 14 dager fra melding om bruk av angreretten er gitt. Kjøper dekker de direkte kostnadene ved å returnere varen, med mindre annet er avtalt eller selger har unnlatt å opplyse om at kjøper skal dekke returkostnadene. Selgeren kan ikke fastsette gebyr for kjøperens bruk av angreretten.</p>
|
||||
<p>Kjøper kan prøve eller teste varen på en forsvarlig måte for å fastslå varens art, egenskaper og funksjon, uten at angreretten faller bort. Dersom prøving eller test av varen går utover hva som er forsvarlig og nødvendig, kan kjøperen bli ansvarlig for eventuell redusert verdi på varen.</p>
|
||||
<p>Selgeren er forpliktet til å tilbakebetale kjøpesummen til kjøperen uten unødig opphold, og senest 14 dager fra selgeren fikk melding om kjøperens beslutning om å benytte angreretten. Selger har rett til å holde tilbake betalingen til han har mottatt varene fra kjøperen, eller til kjøper har lagt frem dokumentasjon for at varene er sendt tilbake.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-10">
|
||||
<h2 class="h2-title">9. Forsinkelse og manglende levering - kjøpernes rettigheter og frist for å melde krav</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dersom selgeren ikke leverer varen eller leverer den for sent i henhold til avtalen mellom partene, og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold til reglene i forbrukerkjøpslovens kapittel 5 etter omstendighetene
|
||||
<em>holde kjøpesummen tilbake</em>
|
||||
, kreve
|
||||
<em>oppfyl</em>
|
||||
<em>lelse</em>
|
||||
,
|
||||
<em>heve </em>
|
||||
avtalen og/eller kreve
|
||||
<em>erstatning </em>
|
||||
fra selgeren.
|
||||
</p>
|
||||
<p>Ved krav om misligholdsbeføyelser bør meldingen av bevishensyn være skriftlig (for eksempel e-post).</p>
|
||||
<h3>Oppfyllelse</h3>
|
||||
<p>Kjøper kan fastholde kjøpet og kreve oppfyllelse fra selger. Kjøper kan imidlertid ikke kreve oppfyllelse dersom det foreligger en hindring som selgeren ikke kan overvinne, eller dersom oppfyllelse vil medføre en så stor ulempe eller kostnad for selger at det står i vesentlig misforhold til kjøperens interesse i at selgeren oppfyller. Skulle vanskene falle bort innen rimelig tid, kan kjøper likevel kreve oppfyllelse.</p>
|
||||
<p>Kjøperen taper sin rett til å kreve oppfyllelse om han eller hun venter urimelig lenge med å fremme kravet.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Dersom selgeren ikke leverer varen på leveringstidspunktet, skal kjøperen oppfordre selger til å levere innen en rimelig tilleggsfrist for oppfyllelse. Dersom selger ikke leverer varen innen tilleggsfristen, kan kjøperen heve kjøpet.</p>
|
||||
<p>Kjøper kan imidlertid heve kjøpet umiddelbart hvis selger nekter å levere varen. Tilsvarende gjelder dersom levering til avtalt tid var avgjørende for inngåelsen av avtalen, eller dersom kjøperen har underrettet selger om at leveringstidspunktet er avgjørende.</p>
|
||||
<p>Leveres tingen etter tilleggsfristen forbrukeren har satt eller etter leveringstidspunktet som var avgjørende for inngåelsen av avtalen, må krav om heving gjøres gjeldende innen rimelig tid etter at kjøperen fikk vite om leveringen.</p>
|
||||
<h3>Erstatning</h3>
|
||||
<p>Kjøperen kan kreve erstatning for lidt tap som følge av forsinkelsen. Dette gjelder imidlertid ikke dersom selgeren godtgjør at forsinkelsen skyldes hindring utenfor selgers kontroll som ikke med rimelighet kunne blitt tatt i betraktning på avtaletiden, unngått, eller overvunnet følgene av.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-11">
|
||||
<h2 class="h2-title">10. Mangel ved varen - kjøperens rettigheter og reklamasjonsfrist</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Hvis det foreligger en mangel ved varen må kjøper innen rimelig tid etter at den ble oppdaget eller burde ha blitt oppdaget, gi selger melding om at han eller hun vil påberope seg mangelen. Kjøper har alltid reklamert tidsnok dersom det skjer innen 2 mnd. fra mangelen ble oppdaget eller burde blitt oppdaget. Reklamasjon kan skje senest to år etter at kjøper overtok varen. Dersom varen eller deler av den er ment å vare vesentlig lenger enn to år, er reklamasjonsfristen fem år.</p>
|
||||
<p>
|
||||
Dersom varen har en mangel og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold til reglene i forbrukerkjøpsloven kapittel 6 etter omstendighetene
|
||||
<em>holde kjøpesummen tilbake</em>
|
||||
, velge mellom
|
||||
<em>retting </em>
|
||||
og
|
||||
<em>omlevering</em>
|
||||
, kreve
|
||||
<em>prisavslag</em>
|
||||
, kreve avtalen hevet og/eller kreve
|
||||
<em>erstatning </em>
|
||||
fra selgeren.
|
||||
</p>
|
||||
<p>Reklamasjon til selgeren bør skje skriftlig.</p>
|
||||
<h3>Retting eller omlevering</h3>
|
||||
<p>Kjøperen kan velge mellom å kreve mangelen rettet eller levering av tilsvarende ting. Selger kan likevel motsette seg kjøperens krav dersom gjennomføringen av kravet er umulig eller volder selgeren urimelige kostnader. Retting eller omlevering skal foretas innen rimelig tid. Selger har i utgangspunktet ikke rett til å foreta mer enn to avhjelpsforsøk for samme mangel.</p>
|
||||
<h3>Prisavslag</h3>
|
||||
<p>Kjøper kan kreve et passende prisavslag dersom varen ikke blir rettet eller omlevert. Dette innebærer at forholdet mellom nedsatt og avtalt pris svarer til forholdet mellom tingens verdi i mangelfull og kontraktsmessig stand. Dersom særlige grunner taler for det, kan prisavslaget i stedet settes lik mangelens betydning for kjøperen.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Dersom varen ikke er rettet eller omlevert, kan kjøperen også heve kjøpet når mangelen ikke er uvesentlig.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-12">
|
||||
<h2 class="h2-title">11. Selgerens rettigheter ved kjøperens mislighold</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dersom kjøperen ikke betaler eller oppfyller de øvrige pliktene etter avtalen eller loven, og dette ikke skyldes selgeren eller forhold på selgerens side, kan selgeren i henhold til reglene i forbrukerkjøpsloven kapittel 9 etter omstendighetene
|
||||
<em>holde</em>
|
||||
<em>varen tilbake</em>
|
||||
, kreve
|
||||
<em>oppfyllelse </em>
|
||||
av avtalen, kreve avtalen
|
||||
<em>hevet </em>
|
||||
samt kreve
|
||||
<em>erstatning </em>
|
||||
fra kjøperen. Selgeren vil også etter omstendighetene kunne kreve
|
||||
<em>renter ved forsinket betaling, inkassogebyr</em>
|
||||
og et rimelig
|
||||
<em>gebyr ved uavhentede varer</em>
|
||||
.
|
||||
</p>
|
||||
<h3>Oppfyllelse</h3>
|
||||
<p>Selger kan fastholde kjøpet og kreve at kjøperen betaler kjøpesummen. Er varen ikke levert, taper selgeren sin rett dersom han venter urimelig lenge med å fremme kravet.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Selger kan heve avtalen dersom det foreligger vesentlig betalingsmislighold eller annet vesentlig mislighold fra kjøperens side. Selger kan likevel ikke heve dersom hele kjøpesummen er betalt. Fastsetter selger en rimelig tilleggsfrist for oppfyllelse og kjøperen ikke betaler innen denne fristen, kan selger heve kjøpet.</p>
|
||||
<h3>Renter ved forsinket betaling/inkassogebyr</h3>
|
||||
<p>Dersom kjøperen ikke betaler kjøpesummen i henhold til avtalen, kan selger kreve renter av kjøpesummen etter forsinkelsesrenteloven. Ved manglende betaling kan kravet, etter forutgående varsel, bli sendt til Kjøper kan da bli holdt ansvarlig for gebyr etter inkassoloven.</p>
|
||||
<h3>Gebyr ved uavhentede ikke-forskuddsbetalte varer</h3>
|
||||
<p>Dersom kjøperen unnlater å hente ubetalte varer, kan selger belaste kjøper med et gebyr. Gebyret skal maksimalt dekke selgerens faktiske utlegg for å levere varen til kjøperen. Et slikt gebyr kan ikke belastes kjøpere under 18 år.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-13">
|
||||
<h2 class="h2-title">12. Garanti</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Garanti som gis av selgeren eller produsenten, gir kjøperen rettigheter i tillegg til de kjøperen allerede har etter ufravikelig lovgivning. En garanti innebærer dermed ingen begrensninger i kjøperens rett til reklamasjon og krav ved forsinkelse eller mangler etter punkt 9 og 10.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-14">
|
||||
<h2 class="h2-title">13. Personopplysninger</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Behandlingsansvarlig for innsamlede personopplysninger er selger. Med mindre kjøperen samtykker til noe annet, kan selgeren, i tråd med personopplysningsloven, kun innhente og lagre de personopplysninger som er nødvendig for at selgeren skal kunne gjennomføre forpliktelsene etter avtalen. Kjøperens personopplysninger vil kun bli utlevert til andre hvis det er nødvendig for at selger skal få gjennomført avtalen med kjøperen, eller i lovbestemte tilfelle.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-15">
|
||||
<h2 class="h2-title">14. Konfliktløsning</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Klager rettes til selger innen rimelig tid, jf. punkt 9 og 10. Partene skal forsøke å løse eventuelle tvister i minnelighet. Dersom dette ikke lykkes, kan kjøperen ta kontakt med Forbrukerrådet for mekling. Forbrukerrådet er tilgjengelig på telefon 23 400 500 eller
|
||||
<a target="_blank" class="vin-link" href="http://www.forbrukerradet.no/" rel="noopener">www.forbrukerradet.no.</a>
|
||||
</p>
|
||||
<p>
|
||||
Europa-Kommisjonens klageportal kan også brukes hvis du ønsker å inngi en klage. Det er særlig relevant, hvis du er forbruker bosatt i et annet EU-land. Klagen inngis her:
|
||||
<a class="vin-link" href="http://ec.europa.eu/odr">http://ec.europa.eu/odr</a>
|
||||
.
|
||||
</p>
|
||||
<p> </p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.container {
|
||||
margin: 3rem;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -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,10 @@
|
||||
@import "./media-queries.scss";
|
||||
@import "./variables.scss";
|
||||
|
||||
.top-banner{
|
||||
.top-banner {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 1fr 0.5fr;
|
||||
grid-template-areas: "menu logo clock";
|
||||
@@ -9,16 +12,23 @@
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
background-color: $primary;
|
||||
-webkit-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
|
||||
-moz-box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
|
||||
box-shadow: 0px 0px 22px -8px rgba(0, 0, 0, 0.65);
|
||||
|
||||
// ios homescreen app whitespace above header fix.
|
||||
&::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
position: absolute;
|
||||
top: -3rem;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.company-logo{
|
||||
.company-logo {
|
||||
grid-area: logo;
|
||||
}
|
||||
|
||||
.menu-toggle-container{
|
||||
.menu-toggle-container {
|
||||
grid-area: menu;
|
||||
color: #1e1e1e;
|
||||
border-radius: 50% 50%;
|
||||
@@ -30,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;
|
||||
@@ -43,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;
|
||||
@@ -98,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;
|
||||
@@ -114,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)
|
||||
});
|
||||
81
package.json
81
package.json
@@ -4,77 +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",
|
||||
"@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