Vinlottispage UI redesign #56
129
api/lottery.js
@@ -1,63 +1,66 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
mongoose.connect('mongodb://localhost:27017/vinlottis', {
|
|
||||||
useNewUrlParser: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const Highscore = require(path.join(__dirname + '/../schemas/Highscore'));
|
const Highscore = require(path.join(__dirname + '/../schemas/Highscore'));
|
||||||
const Wine = require(path.join(__dirname + '/../schemas/Wine'));
|
const Wine = require(path.join(__dirname + '/../schemas/Wine'));
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
const epochToDateString = date => new Date(parseInt(date)).toDateString();
|
const epochToDateString = date => new Date(parseInt(date)).toDateString();
|
||||||
|
|
||||||
const getHighscoreByDates = highscore => {
|
const sortNewestFirst = (lotteries) => {
|
||||||
let groupedLotteries = {}
|
return lotteries.sort((a, b) => parseInt(a.date) < parseInt(b.date) ? 1 : -1)
|
||||||
|
}
|
||||||
|
|
||||||
highscore.forEach(user => {
|
const groupHighscoreByDate = async (highscore=undefined) => {
|
||||||
user.wins.map(win => {
|
if (highscore == undefined)
|
||||||
|
highscore = await Highscore.find();
|
||||||
|
|
||||||
|
const highscoreByDate = [];
|
||||||
|
|
||||||
|
highscore.forEach(person => {
|
||||||
|
person.wins.map(win => {
|
||||||
const epochDate = new Date(win.date).setHours(0,0,0,0);
|
const epochDate = new Date(win.date).setHours(0,0,0,0);
|
||||||
const obj = {
|
const winnerObject = {
|
||||||
name: user.name,
|
name: person.name,
|
||||||
color: win.color,
|
color: win.color,
|
||||||
wine: win.wine,
|
wine: win.wine,
|
||||||
date: epochDate
|
date: epochDate
|
||||||
}
|
}
|
||||||
|
|
||||||
groupedLotteries[epochDate] ?
|
const existingDateIndex = highscoreByDate.findIndex(el => el.date == epochDate)
|
||||||
groupedLotteries[epochDate].push(obj) : groupedLotteries[epochDate] = [obj];
|
if (existingDateIndex > -1)
|
||||||
|
highscoreByDate[existingDateIndex].winners.push(winnerObject);
|
||||||
|
else
|
||||||
|
highscoreByDate.push({
|
||||||
|
date: epochDate,
|
||||||
|
winners: [winnerObject]
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return groupedLotteries
|
return sortNewestFirst(highscoreByDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedHighscoreToSortedList = groupedLotteries => {
|
const resolveWineReferences = (highscoreObject, key) => {
|
||||||
return Object.keys(groupedLotteries).map(key => {
|
const listWithWines = highscoreObject[key]
|
||||||
const winners = groupedLotteries[key];
|
|
||||||
return {
|
|
||||||
date: parseInt(key),
|
|
||||||
dateString: epochToDateString(key),
|
|
||||||
winners
|
|
||||||
}
|
|
||||||
}).sort((a,b) => parseInt(a.date) > parseInt(b.date) ? 1 : -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveWineReferences = listWithWines => {
|
|
||||||
return Promise.all(listWithWines.map(element =>
|
return Promise.all(listWithWines.map(element =>
|
||||||
Wine.findById(element.wine)
|
Wine.findById(element.wine)
|
||||||
.then(wine => {
|
.then(wine => {
|
||||||
element.wine = wine
|
element.wine = wine
|
||||||
return element
|
return element
|
||||||
})
|
}))
|
||||||
))
|
)
|
||||||
|
.then(resolvedListWithWines => {
|
||||||
|
highscoreObject[key] = resolvedListWithWines;
|
||||||
|
return highscoreObject
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
// end utils
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
const all = (req, res) => {
|
const all = (req, res) => {
|
||||||
return Highscore.find()
|
return Highscore.find()
|
||||||
.then(highscore => getHighscoreByDates(highscore))
|
.then(highscore => groupHighscoreByDate(highscore))
|
||||||
.then(groupedLotteries => groupedHighscoreToSortedList(groupedLotteries))
|
|
||||||
.then(lotteries => res.send({
|
.then(lotteries => res.send({
|
||||||
message: "Lotteries by date!",
|
message: "Lotteries by date!",
|
||||||
lotteries
|
lotteries
|
||||||
@@ -65,56 +68,60 @@ const all = (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const latest = (req, res) => {
|
const latest = (req, res) => {
|
||||||
return Highscore.find()
|
return groupHighscoreByDate()
|
||||||
.then(highscore => getHighscoreByDates(highscore))
|
.then(lotteries => lotteries.shift()) // first element in list
|
||||||
.then(groupedLotteries => groupedHighscoreToSortedList(groupedLotteries))
|
.then(latestLottery => resolveWineReferences(latestLottery, "winners"))
|
||||||
.then(lotteries => res.send({
|
.then(lottery => res.send({
|
||||||
message: "Latest lottery!",
|
message: "Latest lottery!",
|
||||||
lottery: lotteries.slice(-1).pop()
|
winners: lottery.winners
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const byEpochDate = (req, res) => {
|
const byEpochDate = (req, res) => {
|
||||||
const { date } = req.params;
|
let { date } = req.params;
|
||||||
|
date = new Date(new Date(parseInt(date)).setHours(0,0,0,0)).getTime()
|
||||||
const dateString = epochToDateString(date);
|
const dateString = epochToDateString(date);
|
||||||
|
|
||||||
return Highscore.find()
|
return groupHighscoreByDate()
|
||||||
.then(highscore => getHighscoreByDates(highscore))
|
.then(lotteries => {
|
||||||
.then(async (lotteries) => {
|
const lottery = lotteries.filter(lottery => lottery.date == date)
|
||||||
const lottery = lotteries[date];
|
if (lottery.length > 0) {
|
||||||
|
return lottery[0]
|
||||||
if (lottery != null) {
|
|
||||||
return res.send({
|
|
||||||
message: `Lottery for date: ${dateString}`,
|
|
||||||
lottery: await resolveWineReferences(lottery)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
return res.status(404).send({
|
return res.status(404).send({
|
||||||
message: `No lottery found for date: ${dateString}`
|
message: `No lottery found for date: ${ dateString }`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(lottery => resolveWineReferences(lottery, "winners"))
|
||||||
|
.then(lottery => res.send({
|
||||||
|
message: `Lottery for date: ${ dateString}`,
|
||||||
|
date,
|
||||||
|
winners: lottery.winners
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const byName = (req, res) => {
|
const byName = (req, res) => {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
|
const regexName = new RegExp(name, "i"); // lowercase regex of the name
|
||||||
|
|
||||||
return Highscore.find({ name })
|
return Highscore.find({ name })
|
||||||
.then(async (highscore) => {
|
.then(highscore => {
|
||||||
highscore = highscore[0]
|
if (highscore.length > 0) {
|
||||||
if (highscore) {
|
return highscore[0]
|
||||||
const highscoreWithResolvedWines = await resolveWineReferences(highscore.wins)
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
message: `Lottery winnings by name: ${name}`,
|
|
||||||
highscore: highscoreWithResolvedWines
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
return res.status(404).send({
|
return res.status(404).send({
|
||||||
message: `Name: ${ name } not found in leaderboards.`
|
message: `Name: ${ name } not found in leaderboards.`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.then(highscore => resolveWineReferences(highscore, "wins"))
|
||||||
|
.then(highscore => res.send({
|
||||||
|
message: `Lottery winnings for name: ${ name }.`,
|
||||||
|
name: highscore.name,
|
||||||
|
highscore: sortNewestFirst(highscore.wins)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ const https = require("https");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
|
const config = require(path.join(__dirname + "/../config/defaults/lottery"));
|
||||||
|
|
||||||
|
const dateString = (date) => {
|
||||||
|
if (typeof(date) == "string") {
|
||||||
|
date = new Date(date);
|
||||||
|
}
|
||||||
|
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
||||||
|
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
||||||
|
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
||||||
|
|
||||||
|
return `${da}-${mo}-${ye}`
|
||||||
|
}
|
||||||
|
|
||||||
async function sendWineSelectMessage(winnerObject) {
|
async function sendWineSelectMessage(winnerObject) {
|
||||||
winnerObject.timestamp_sent = new Date().getTime();
|
winnerObject.timestamp_sent = new Date().getTime();
|
||||||
winnerObject.timestamp_limit = new Date().getTime() * 600000;
|
winnerObject.timestamp_limit = new Date().getTime() * 600000;
|
||||||
@@ -15,6 +26,12 @@ 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!`)
|
||||||
|
}
|
||||||
|
|
||||||
async function sendLastWinnerMessage(winnerObject, wineObject) {
|
async function sendLastWinnerMessage(winnerObject, wineObject) {
|
||||||
console.log(`User ${winnerObject.id} is only one left, chosing wine for him/her.`);
|
console.log(`User ${winnerObject.id} is only one left, chosing wine for him/her.`);
|
||||||
winnerObject.timestamp_sent = new Date().getTime();
|
winnerObject.timestamp_sent = new Date().getTime();
|
||||||
@@ -23,7 +40,7 @@ async function sendLastWinnerMessage(winnerObject, wineObject) {
|
|||||||
|
|
||||||
return sendMessageToUser(
|
return sendMessageToUser(
|
||||||
winnerObject.phoneNumber,
|
winnerObject.phoneNumber,
|
||||||
`Gratulerer som heldig vinner av vinlotteriet ${winnerObject.name}! Du har vunnet vinen ${wineObject.name}, og vil få nærmere info om hvordan/hvor du kan hente vinen snarest. Ha en ellers fin helg!`
|
`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!`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +99,11 @@ async function gatewayRequest(body) {
|
|||||||
res.setEncoding('utf8');
|
res.setEncoding('utf8');
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
res.on("data", (d) => resolve(JSON.parse(d)));
|
res.on("data", (data) => {
|
||||||
|
console.log("Response from message gateway:", data)
|
||||||
|
|
||||||
|
resolve(JSON.parse(data))
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
res.on("data", (data) => {
|
res.on("data", (data) => {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
@@ -103,6 +124,7 @@ async function gatewayRequest(body) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendWineSelectMessage,
|
sendWineSelectMessage,
|
||||||
|
sendWineConfirmation,
|
||||||
sendLastWinnerMessage,
|
sendLastWinnerMessage,
|
||||||
sendWineSelectMessageTooLate,
|
sendWineSelectMessageTooLate,
|
||||||
sendInitialMessageToWinners
|
sendInitialMessageToWinners
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const mongoose = require("mongoose");
|
|
||||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
|
||||||
useNewUrlParser: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const Purchase = require(path.join(__dirname + "/../schemas/Purchase"));
|
const Purchase = require(path.join(__dirname + "/../schemas/Purchase"));
|
||||||
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
|
const Wine = require(path.join(__dirname + "/../schemas/Wine"));
|
||||||
@@ -143,7 +139,9 @@ const allWinesSummary = async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(Object.values(wines));
|
wines = Object.values(wines).reverse()
|
||||||
|
|
||||||
|
return res.json(wines);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ const express = require("express");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const webpush = require("web-push"); //requiring the web-push module
|
const webpush = require("web-push"); //requiring the web-push module
|
||||||
const mongoose = require("mongoose");
|
|
||||||
const schedule = require("node-schedule");
|
const schedule = require("node-schedule");
|
||||||
|
|
||||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
|
||||||
useNewUrlParser: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const mustBeAuthenticated = require(path.join(
|
const mustBeAuthenticated = require(path.join(
|
||||||
__dirname + "/../middleware/mustBeAuthenticated"
|
__dirname + "/../middleware/mustBeAuthenticated"
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const mongoose = require("mongoose");
|
|
||||||
mongoose.connect("mongodb://localhost:27017/vinlottis", {
|
|
||||||
useNewUrlParser: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const sub = require(path.join(__dirname + "/../api/subscriptions"));
|
const sub = require(path.join(__dirname + "/../api/subscriptions"));
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const attendees = async (req, res) => {
|
|||||||
attendee = attendees[i];
|
attendee = attendees[i];
|
||||||
attendeesRedacted.push({
|
attendeesRedacted.push({
|
||||||
name: attendee.name,
|
name: attendee.name,
|
||||||
ballots: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
raffles: attendee.red + attendee.blue + attendee.yellow + attendee.green,
|
||||||
red: attendee.red,
|
red: attendee.red,
|
||||||
blue: attendee.blue,
|
blue: attendee.blue,
|
||||||
green: attendee.green,
|
green: attendee.green,
|
||||||
@@ -99,27 +99,27 @@ const drawWinner = async (req, res) => {
|
|||||||
message: "No attendees left that have not won."
|
message: "No attendees left that have not won."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let ballotColors = [];
|
let raffleColors = [];
|
||||||
for (let i = 0; i < allContestants.length; i++) {
|
for (let i = 0; i < allContestants.length; i++) {
|
||||||
let currentContestant = allContestants[i];
|
let currentContestant = allContestants[i];
|
||||||
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
for (let blue = 0; blue < currentContestant.blue; blue++) {
|
||||||
ballotColors.push("blue");
|
raffleColors.push("blue");
|
||||||
}
|
}
|
||||||
for (let red = 0; red < currentContestant.red; red++) {
|
for (let red = 0; red < currentContestant.red; red++) {
|
||||||
ballotColors.push("red");
|
raffleColors.push("red");
|
||||||
}
|
}
|
||||||
for (let green = 0; green < currentContestant.green; green++) {
|
for (let green = 0; green < currentContestant.green; green++) {
|
||||||
ballotColors.push("green");
|
raffleColors.push("green");
|
||||||
}
|
}
|
||||||
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
for (let yellow = 0; yellow < currentContestant.yellow; yellow++) {
|
||||||
ballotColors.push("yellow");
|
raffleColors.push("yellow");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ballotColors = shuffle(ballotColors);
|
raffleColors = shuffle(raffleColors);
|
||||||
|
|
||||||
let colorToChooseFrom =
|
let colorToChooseFrom =
|
||||||
ballotColors[Math.floor(Math.random() * ballotColors.length)];
|
raffleColors[Math.floor(Math.random() * raffleColors.length)];
|
||||||
let findObject = { winner: false };
|
let findObject = { winner: false };
|
||||||
|
|
||||||
findObject[colorToChooseFrom] = { $gt: 0 };
|
findObject[colorToChooseFrom] = { $gt: 0 };
|
||||||
@@ -187,7 +187,10 @@ const drawWinner = async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await newWinnerElement.save();
|
await newWinnerElement.save();
|
||||||
return res.json(winner);
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
winner
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const finish = async (req, res) => {
|
const finish = async (req, res) => {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const registerWinnerSelection = async (req, res) => {
|
|||||||
let wonWine = await _wineFunctions.findSaveWine(prelotteryWine);
|
let wonWine = await _wineFunctions.findSaveWine(prelotteryWine);
|
||||||
await prelotteryWine.delete();
|
await prelotteryWine.delete();
|
||||||
await _personFunctions.findSavePerson(foundWinner, wonWine, date);
|
await _personFunctions.findSavePerson(foundWinner, wonWine, date);
|
||||||
|
await Message.sendWineConfirmation(foundWinner, wonWine, date);
|
||||||
|
|
||||||
await foundWinner.delete();
|
await foundWinner.delete();
|
||||||
console.info("Saved winners choice.");
|
console.info("Saved winners choice.");
|
||||||
@@ -95,15 +96,19 @@ const registerWinnerSelection = async (req, res) => {
|
|||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
const chooseLastWineForUser = (winner, prelotteryWine) => {
|
const chooseLastWineForUser = (winner, preLotteryWine) => {
|
||||||
let date = new Date();
|
let date = new Date();
|
||||||
date.setHours(5, 0, 0, 0);
|
date.setHours(5, 0, 0, 0);
|
||||||
|
|
||||||
return _wineFunctions.findSaveWine(preLotteryWine)
|
return _wineFunctions.findSaveWine(preLotteryWine)
|
||||||
.then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date))
|
.then(wonWine => _personFunctions.findSavePerson(winner, wonWine, date))
|
||||||
.then(() => prelotteryWine.delete())
|
.then(() => preLotteryWine.delete())
|
||||||
.then(() => Message.sendLastWinnerMessage(winner, preLotteryWine))
|
.then(() => Message.sendLastWinnerMessage(winner, preLotteryWine))
|
||||||
.then(() => winner.delete());
|
.then(() => winner.delete())
|
||||||
|
.catch(err => {
|
||||||
|
console.log("Error thrown from chooseLastWineForUser: " + err);
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const findAndNotifyNextWinner = async () => {
|
const findAndNotifyNextWinner = async () => {
|
||||||
@@ -113,10 +118,13 @@ const findAndNotifyNextWinner = async () => {
|
|||||||
let winesLeft = await PreLotteryWine.find();
|
let winesLeft = await PreLotteryWine.find();
|
||||||
|
|
||||||
if (winnersLeft.length > 1) {
|
if (winnersLeft.length > 1) {
|
||||||
|
console.log("multiple winners left, choose next in line")
|
||||||
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
nextWinner = winnersLeft[0]; // multiple winners left, choose next in line
|
||||||
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
} else if (winnersLeft.length == 1 && winesLeft.length > 1) {
|
||||||
|
console.log("one winner left, but multiple wines")
|
||||||
nextWinner = winnersLeft[0] // one winner left, but multiple wines
|
nextWinner = winnersLeft[0] // one winner left, but multiple wines
|
||||||
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
} else if (winnersLeft.length == 1 && winesLeft.length == 1) {
|
||||||
|
console.log("one winner and one wine left, choose for user")
|
||||||
nextWinner = winnersLeft[0] // one winner and one wine left, choose for user
|
nextWinner = winnersLeft[0] // one winner and one wine left, choose for user
|
||||||
wine = winesLeft[0]
|
wine = winesLeft[0]
|
||||||
return chooseLastWineForUser(nextWinner, wine);
|
return chooseLastWineForUser(nextWinner, wine);
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #dbeede;
|
background-color: $primary;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
29
src/api.js
@@ -333,6 +333,31 @@ const historyAll = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const historyByDate = (date) => {
|
||||||
|
const url = new URL(`/api/lottery/by-date/${ date }`, BASE_URL);
|
||||||
|
|
||||||
|
return fetch(url.href).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
return resp.json();
|
||||||
|
} else {
|
||||||
|
return handleErrors(resp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWinnerByName = (name) => {
|
||||||
|
const encodedName = encodeURIComponent(name)
|
||||||
|
const url = new URL(`/api/lottery/by-name/${name}`, BASE_URL);
|
||||||
|
|
||||||
|
return fetch(url.href).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
return resp.json();
|
||||||
|
} else {
|
||||||
|
return handleErrors(resp);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
statistics,
|
statistics,
|
||||||
colorStatistics,
|
colorStatistics,
|
||||||
@@ -364,5 +389,7 @@ export {
|
|||||||
finishedDraw,
|
finishedDraw,
|
||||||
getAmIWinner,
|
getAmIWinner,
|
||||||
postWineChosen,
|
postWineChosen,
|
||||||
historyAll
|
historyAll,
|
||||||
|
historyByDate,
|
||||||
|
getWinnerByName
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="outer">
|
<div class="container">
|
||||||
<div class="container">
|
<h1 class="">Alle viner</h1>
|
||||||
<h1 class="title">Alle viner</h1>
|
|
||||||
<div class="wines-container">
|
<div id="wines-container">
|
||||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" :fullscreen="true" :inlineSlot="true">
|
<Wine :wine="wine" v-for="(wine, _, index) in wines" :key="wine._id">
|
||||||
<div class="winners-container">
|
<div class="winners-container">
|
||||||
<div class="color-wins" :class="{ 'big': fullscreen }"
|
|
||||||
v-if="wine.blue || wine.red || wine.green || wine.yellow">
|
<span class="label">Vinnende lodd:</span>
|
||||||
<span class="label">Vinnende lodd:</span>
|
<div class="flex row">
|
||||||
<span class="color-win blue">{{wine.blue == undefined ? 0 : wine.blue}}</span>
|
<span class="raffle-element blue-raffle">{{ wine.blue == null ? 0 : wine.blue }}</span>
|
||||||
<span class="color-win red">{{wine.red == undefined ? 0 : wine.red}}</span>
|
<span class="raffle-element red-raffle">{{ wine.red == null ? 0 : wine.red }}</span>
|
||||||
<span class="color-win green">{{wine.green == undefined ? 0 : wine.green}}</span>
|
<span class="raffle-element green-raffle">{{ wine.green == null ? 0 : wine.green }}</span>
|
||||||
<span class="color-win yellow">{{wine.yellow == undefined ? 0 : wine.yellow}}</span>
|
<span class="raffle-element yellow-raffle">{{ wine.yellow == null ? 0 : wine.yellow }}</span>
|
||||||
</div>
|
|
||||||
<div class="name-wins" v-if="wine.winners">
|
|
||||||
<span class="label">Vunnet av:</span>
|
|
||||||
<span class="names" v-for="winner in wine.winners">- {{ winner }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Wine>
|
|
||||||
</div>
|
<div class="name-wins">
|
||||||
|
<span class="label">Vunnet av:</span>
|
||||||
|
<ul class="names">
|
||||||
|
<li v-for="(winner, index) in wine.winners">
|
||||||
|
<router-link class="vin-link" :to="`/highscore/` + winner">{{ winner }}</router-link>
|
||||||
|
-
|
||||||
|
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ dateString(wine.dates[index]) }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Wine>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,6 +36,7 @@ import { page, event } from "vue-analytics";
|
|||||||
import Banner from "@/ui/Banner";
|
import Banner from "@/ui/Banner";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
import { overallWineStatistics } from "@/api";
|
import { overallWineStatistics } from "@/api";
|
||||||
|
import { dateString } from "@/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -41,133 +48,77 @@ export default {
|
|||||||
wines: []
|
wines: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
winDateUrl(date) {
|
||||||
|
const timestamp = new Date(date).getTime();
|
||||||
|
return `/history/${timestamp}`
|
||||||
|
},
|
||||||
|
dateString: dateString
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const wines = await overallWineStatistics();
|
this.wines = await overallWineStatistics();
|
||||||
this.wines = wines.sort((a, b) =>
|
|
||||||
a.rating > b.rating ? -1 : 1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "./src/styles/media-queries";
|
@import "./src/styles/media-queries";
|
||||||
|
@import "./src/styles/variables";
|
||||||
|
|
||||||
h1 {
|
.container {
|
||||||
font-family: knowit, Arial;
|
max-width: unset;
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wines-container {
|
#wines-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
@include desktop {
|
> div {
|
||||||
margin: 0 2rem;
|
justify-content: flex-start;
|
||||||
|
|
||||||
> div {
|
|
||||||
max-width: max-content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.winners-container {
|
.winners-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
margin-top: 2rem;
|
margin-top: 1rem;
|
||||||
|
|
||||||
@include mobile {
|
> div:not(:last-of-type) {
|
||||||
flex-direction: row;
|
margin-bottom: 1rem;
|
||||||
width: max-content;
|
|
||||||
margin: 0.75rem;
|
|
||||||
|
|
||||||
&:not(&:first-child) {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-wins {
|
.name-wins {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: max-content;
|
|
||||||
|
|
||||||
.names {
|
a {
|
||||||
margin-left: 0.5rem;
|
font-weight: normal;
|
||||||
|
&:not(:hover) {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "- ";
|
||||||
|
margin-left: -0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-wins {
|
.raffle-element {
|
||||||
display: flex;
|
padding: 1rem;
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
margin-left: -3px; // offset span.color-win margin
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: 25vw;
|
|
||||||
margin-left: -2px; // offset span.color-win margin
|
|
||||||
}
|
|
||||||
@include desktop {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-wins.big {
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.color-win {
|
|
||||||
border: 2px solid transparent;
|
|
||||||
color: #333;
|
|
||||||
display: block;
|
|
||||||
padding: 25px;
|
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
/* max-height: calc(3rem + 18px); */
|
|
||||||
/* max-width: calc(3rem + 18px); */
|
|
||||||
width: 1rem;
|
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
touch-action: manipulation;
|
|
||||||
height: 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 900;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
margin: 2px;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 1rem;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
grow: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.green {
|
|
||||||
background: #c8f9df;
|
|
||||||
}
|
|
||||||
&.blue {
|
|
||||||
background: #d4f2fe;
|
|
||||||
}
|
|
||||||
&.red {
|
|
||||||
background: #fbd7de;
|
|
||||||
}
|
|
||||||
&.yellow {
|
|
||||||
background: #fff6d6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title" @click="startCountdown">Loddgenerator</h1>
|
<h1 class="text-center title" @click="startCountdown">Loddgenerator</h1>
|
||||||
<p class="subtext">
|
<p class="subtext">
|
||||||
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<RaffleGenerator @numberOfBallots="val => this.numberOfBallots = val" />
|
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" />
|
||||||
|
|
||||||
<Vipps class="vipps" :amount="numberOfBallots" />
|
<Vipps class="vipps" :amount="numberOfRaffles" />
|
||||||
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -27,7 +27,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hardStart: false,
|
hardStart: false,
|
||||||
numberOfBallots: null
|
numberOfRaffles: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -54,13 +54,16 @@ export default {
|
|||||||
@import "../styles/variables.scss";
|
@import "../styles/variables.scss";
|
||||||
@import "../styles/global.scss";
|
@import "../styles/global.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "../styles/media-queries.scss";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.header-link {
|
|
||||||
color: #333333;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -74,10 +77,4 @@ p {
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,56 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="container">
|
||||||
<h1 class="text-center">Vinlottis highscore 🏆🍷</h1>
|
<h1>Vinlottis highscore</h1>
|
||||||
|
|
||||||
<div class="content-container">
|
<div class="filter flex el-spacing">
|
||||||
<div class="highscore">
|
<input type="text" v-model="filterInput" placeholder="Filtrer på navn" />
|
||||||
<div>
|
<button v-if="filterInput" @click="resetFilter" class="vin-button auto-height margin-left-sm">
|
||||||
<h3 >Finn ditt navn:</h3>
|
Reset
|
||||||
<input type="text" v-model="highscoreFilter" placeholder="Filtrer på navn" class="margin-bottom-sm" />
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ol v-if="highscore.length > 0">
|
<p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p>
|
||||||
<li v-for="person in highscore" :key="person" @click="selectWinner(person)">
|
|
||||||
{{ person.name }} - {{ person.wins.length }}
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div v-if="highscore.length != highscoreResponse.length" class="flex justify-center align-center">
|
<ol v-if="highscore.length > 0" class="highscore-list">
|
||||||
<i @click="resetFilter" @keyup.space="resetFilter"
|
<li v-for="person in filteredResults" @click="selectWinner(person)" @keydown.enter="selectWinner(person)" tabindex="0">
|
||||||
role="button" aria-pressed="false" tabindex="0">reset filter</i>
|
<b>{{ person.rank }}.</b> {{ person.name }} - {{ person.wins.length }}
|
||||||
</div>
|
</li>
|
||||||
</div>
|
</ol>
|
||||||
|
|
||||||
<div class="winners-vines" v-if="selectedWinner">
|
<div class="center desktop-only">
|
||||||
<h1>{{ selectedWinner.name }}</h1>
|
👈 Se dine vin(n), trykk på navnet ditt
|
||||||
|
|
||||||
<h2>Vinnende farger:</h2>
|
|
||||||
<div class="winning-colors">
|
|
||||||
<div v-for="win in selectedWinner.wins"
|
|
||||||
class="color-box" :class="win.color"
|
|
||||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Flasker vunnet:</h2>
|
|
||||||
<div v-for="win in selectedWinner.wins" class="single-win">
|
|
||||||
<div class="date-won"><b>{{ humanReadableDate(win.date) }}</b> - {{ daysAgo(win.date) }} dager siden</div>
|
|
||||||
<br/>
|
|
||||||
<div class="left">
|
|
||||||
<h3>Vunnet med:</h3>
|
|
||||||
|
|
||||||
<div class="color-box" :class="win.color"
|
|
||||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"></div>
|
|
||||||
</div>
|
|
||||||
<div class="left">
|
|
||||||
<Wine :wine="win.wine"></Wine>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="center">
|
|
||||||
<h1>👈 Se dine vinn, trykk på navnet ditt</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,16 +26,15 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import { highscoreStatistics } from "@/api";
|
import { highscoreStatistics } from "@/api";
|
||||||
|
import { humanReadableDate, daysAgo } from "@/utils";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Wine },
|
components: { Wine },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
highscoreResponse: [],
|
|
||||||
highscore: [],
|
highscore: [],
|
||||||
highscoreFilter: '',
|
filterInput: ''
|
||||||
selectedWinner: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -78,177 +45,110 @@ export default {
|
|||||||
response = response.filter(
|
response = response.filter(
|
||||||
person => person.name != null && person.name != ""
|
person => person.name != null && person.name != ""
|
||||||
);
|
);
|
||||||
this.highscoreResponse = response
|
this.highscore = this.generateScoreBoard(response);
|
||||||
this.highscore = this.highscoreResponse;
|
|
||||||
},
|
},
|
||||||
watch: {
|
computed: {
|
||||||
highscoreFilter(val) {
|
filteredResults() {
|
||||||
|
let highscore = this.highscore;
|
||||||
|
let val = this.filterInput;
|
||||||
|
|
||||||
if (val.length) {
|
if (val.length) {
|
||||||
val = val.toLowerCase();
|
val = val.toLowerCase()
|
||||||
this.highscore = this.highscoreResponse.filter(person => person.name.toLowerCase().includes(val))
|
const nameIncludesString = (person) => person.name.toLowerCase().includes(val);
|
||||||
} else {
|
highscore = highscore.filter(nameIncludesString)
|
||||||
this.highscore = this.highscoreResponse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return highscore
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
generateScoreBoard(highscore=this.highscore) {
|
||||||
|
let place = 0;
|
||||||
|
let highestWinCount = -1;
|
||||||
|
|
||||||
|
return highscore.map(win => {
|
||||||
|
const wins = win.wins.length
|
||||||
|
if (wins != highestWinCount) {
|
||||||
|
place += 1
|
||||||
|
highestWinCount = wins
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeString = place.toString().padStart(2, "0");
|
||||||
|
win.rank = placeString;
|
||||||
|
return win
|
||||||
|
})
|
||||||
|
},
|
||||||
resetFilter() {
|
resetFilter() {
|
||||||
this.highscore = this.highscoreResponse;
|
this.filterInput = '';
|
||||||
this.highscoreFilter = '';
|
|
||||||
document.getElementsByTagName('input')[0].focus();
|
document.getElementsByTagName('input')[0].focus();
|
||||||
},
|
},
|
||||||
humanReadableDate(date) {
|
|
||||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
|
||||||
return new Date(date).toLocaleDateString(undefined, options);
|
|
||||||
},
|
|
||||||
daysAgo(date) {
|
|
||||||
const day = 24 * 60 * 60 * 1000;
|
|
||||||
return Math.round(Math.abs((new Date() - new Date(date)) / day));
|
|
||||||
},
|
|
||||||
selectWinner(winner) {
|
selectWinner(winner) {
|
||||||
if (this.selectedWinner != null && this.selectedWinner["name"] == winner["name"]) {
|
const path = "/highscore/" + encodeURIComponent(winner.name)
|
||||||
this.selectedWinner = null;
|
this.$router.push(path);
|
||||||
} else {
|
|
||||||
let newestFirst = winner.wins.sort((a, b) => a.date < b.date);
|
|
||||||
winner.wins = newestFirst;
|
|
||||||
this.selectedWinner = { ...winner };
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getRotation: function() {
|
humanReadableDate: humanReadableDate,
|
||||||
let num = Math.floor(Math.random() * 12.5);
|
daysAgo: daysAgo
|
||||||
let neg = Math.floor(Math.random() * 2);
|
|
||||||
return neg == 0 ? -num : num;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../styles/media-queries.scss";
|
@import "./src/styles/media-queries.scss";
|
||||||
@import "../styles/variables.scss";
|
@import "./src/styles/variables.scss";
|
||||||
|
$elementSpacing: 3.5rem;
|
||||||
|
|
||||||
h1 {
|
.el-spacing {
|
||||||
text-align: center;
|
margin-bottom: $elementSpacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
.filter input {
|
||||||
width: 100%;
|
font-size: 1rem;
|
||||||
color: black;
|
width: 30%;
|
||||||
border-radius: 4px;
|
border-color: black;
|
||||||
padding: 1rem 1rem;
|
border-width: 1.5px;
|
||||||
border: 1px solid black;
|
padding: 0.75rem 1rem;
|
||||||
max-width: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-won {
|
.highscore-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
margin-top: 2rem;
|
color: $matte-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-box {
|
.highscore-list {
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
margin: 10px;
|
|
||||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
mask-image: url(/../../public/assets/images/lodd.svg);
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-win {
|
|
||||||
border-bottom: 1px solid rgb(237, 237, 237);
|
|
||||||
.left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.green {
|
|
||||||
background-color: $light-green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue {
|
|
||||||
background-color: $light-blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yellow {
|
|
||||||
background-color: $light-yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.red {
|
|
||||||
background-color: $light-red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-container {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column;
|
||||||
margin: 2em;
|
padding-left: 0;
|
||||||
|
|
||||||
.center {
|
li {
|
||||||
align-self: center;
|
width: fit-content;
|
||||||
h1 {
|
display: inline-block;
|
||||||
background-color: $primary;
|
margin-bottom: calc(1rem - 2px);
|
||||||
padding: 0.5rem 1rem;
|
font-size: 1.25rem;
|
||||||
border-radius: 8px;
|
color: $matte-text-color;
|
||||||
font-style: italic;
|
cursor: pointer;
|
||||||
|
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
&:hover, &:focus {
|
||||||
|
border-color: $link-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.winning-colors {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
.center {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include tablet {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: .3fr 1fr;
|
|
||||||
grid-template-rows: auto-flow min-content;
|
|
||||||
justify-items: center;
|
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
grid-gap: 1em;
|
|
||||||
max-width: 1600px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
.center {
|
||||||
padding-left: 1.375rem !important;
|
position: absolute;
|
||||||
margin-left: 0;
|
top: 40%;
|
||||||
margin: 0 0 1.5em;
|
right: 10vw;
|
||||||
padding: 0;
|
max-width: 50vw;
|
||||||
counter-reset: item;
|
|
||||||
& > li {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2.5px 0;
|
|
||||||
width: max-content;
|
|
||||||
margin: 0 0 0 -1.25rem;
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
counter-increment: item;
|
|
||||||
&:before {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
content: counter(item) ".";
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
font-size: 2.5rem;
|
||||||
padding: 5px 0;
|
background-color: $primary;
|
||||||
}
|
padding: 1rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
@include widescreen {
|
||||||
|
right: 20vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>Historie fra tidligere lotteri</h1>
|
<h1>Historie fra tidligere lotteri</h1>
|
||||||
|
|
||||||
<div v-if="lotteries.length" v-for="lottery in lotteries">
|
<div v-if="lotteries.length || lotteries != null" v-for="lottery in lotteries">
|
||||||
<Winners :winners="lottery.winners" :title="`Vinnere fra ${lottery.dateString}`" />
|
<Winners :winners="lottery.winners" :title="`Vinnere fra ${ humanReadableDate(lottery.date) }`" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { historyAll } from '@/api'
|
import { historyByDate, historyAll } from '@/api'
|
||||||
|
import { humanReadableDate } from "@/utils";
|
||||||
import Winners from '@/ui/Winners'
|
import Winners from '@/ui/Winners'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -20,9 +21,18 @@ export default {
|
|||||||
lotteries: [],
|
lotteries: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
humanReadableDate: humanReadableDate
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
historyAll()
|
const dateFromUrl = this.$route.params.date;
|
||||||
.then(history => this.lotteries = history.lotteries.reverse())
|
|
||||||
|
if (dateFromUrl !== undefined)
|
||||||
|
historyByDate(dateFromUrl)
|
||||||
|
.then(history => this.lotteries = { "lottery": history })
|
||||||
|
else
|
||||||
|
historyAll()
|
||||||
|
.then(history => this.lotteries = history.lotteries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
255
src/components/PersonalHighscorePage.vue
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Vinlottis highscore</h1>
|
||||||
|
|
||||||
|
<div class="backdrop">
|
||||||
|
<a @click="navigateBack" @keydown.enter="navigateBack" tabindex="0">
|
||||||
|
⬅ <span class="vin-link navigate-back">Tilbake til {{ previousRoute.name }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<section v-if="winner">
|
||||||
|
<h2 class="name">{{ winner.name }}</h2>
|
||||||
|
|
||||||
|
<p class="win-count el-spacing">{{ numberOfWins }} vinn</p>
|
||||||
|
|
|||||||
|
|
||||||
|
<h4 class="margin-bottom-0">Vinnende farger:</h4>
|
||||||
|
<div class="raffle-container el-spacing">
|
||||||
|
<div class="raffle-element" :class="color + `-raffle`" v-for="[color, occurences] in Object.entries(winningColors)">
|
||||||
|
{{ occurences }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="el-spacing">Flasker vunnet:</h4>
|
||||||
|
|
||||||
|
<div v-for="win in winner.highscore">
|
||||||
|
<router-link :to="winDateUrl(win.date)" class="days-ago">
|
||||||
|
{{ humanReadableDate(win.date) }} - {{ daysAgo(win.date) }} dager siden
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<div class="won-wine">
|
||||||
|
<img :src="smallerWineImage(win.wine.image)">
|
||||||
|
|
||||||
|
<div class="won-wine-details">
|
||||||
|
<h3>{{ win.wine.name }}</h3>
|
||||||
|
<a :href="win.wine.vivinoLink" class="vin-link no-margin">
|
||||||
|
Les mer på vinmonopolet.no
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="raffle-element small" :class="win.color + `-raffle`"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2 v-else-if="error" class="error">
|
||||||
|
{{ error }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getWinnerByName } from "@/api";
|
||||||
|
import { humanReadableDate, daysAgo } from "@/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
winner: undefined,
|
||||||
|
error: undefined,
|
||||||
|
previousRoute: {
|
||||||
|
default: true,
|
||||||
|
name: "topplisten",
|
||||||
|
path: "/highscore"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeRouteEnter(to, from, next) {
|
||||||
|
next(vm => {
|
||||||
|
if (from.name != null)
|
||||||
|
vm.previousRoute = from
|
||||||
|
})
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
numberOfWins() {
|
||||||
|
return this.winner.highscore.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
const nameFromURL = this.$route.params.name;
|
||||||
|
getWinnerByName(nameFromURL)
|
||||||
|
.then(winner => this.setWinner(winner))
|
||||||
|
.catch(err => this.error = `Ingen med navn: "${nameFromURL}" funnet.`)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setWinner(winner) {
|
||||||
|
this.winner = {
|
||||||
|
name: winner.name,
|
||||||
|
highscore: [],
|
||||||
|
...winner
|
||||||
|
}
|
||||||
|
this.winningColors = this.findWinningColors()
|
||||||
|
},
|
||||||
|
smallerWineImage(image) {
|
||||||
|
if (image && image.includes(`515x515`))
|
||||||
|
return image.replace(`515x515`, `175x175`)
|
||||||
|
return image
|
||||||
|
},
|
||||||
|
findWinningColors() {
|
||||||
|
const colors = this.winner.highscore.map(win => win.color)
|
||||||
|
const colorOccurences = {}
|
||||||
|
colors.forEach(color => {
|
||||||
|
if (colorOccurences[color] == undefined) {
|
||||||
|
colorOccurences[color] = 1
|
||||||
|
} else {
|
||||||
|
colorOccurences[color] += 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return colorOccurences
|
||||||
|
},
|
||||||
|
winDateUrl(date) {
|
||||||
|
const timestamp = new Date(date).getTime();
|
||||||
|
return `/history/${timestamp}`
|
||||||
|
},
|
||||||
|
navigateBack() {
|
||||||
|
if (this.previousRoute.default) {
|
||||||
|
this.$router.push({ path: this.previousRoute.path });
|
||||||
|
} else {
|
||||||
|
this.$router.go(-1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
humanReadableDate: humanReadableDate,
|
||||||
|
daysAgo: daysAgo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "./src/styles/variables";
|
||||||
|
@import "./src/styles/media-queries";
|
||||||
|
|
||||||
|
$elementSpacing: 3rem;
|
||||||
|
|
||||||
|
.el-spacing {
|
||||||
|
margin-bottom: $elementSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigate-back {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-family: "knowit";
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 2rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win-count {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raffle-container {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
div:not(:last-of-type) {
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.raffle-element {
|
||||||
|
width: 5rem;
|
||||||
|
height: 4rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-ago {
|
||||||
|
color: $matte-text-color;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.won-wine {
|
||||||
|
--spacing: 1rem;
|
||||||
|
background-color: white;
|
||||||
|
margin: var(--spacing) 0 3rem 0;
|
||||||
|
padding: var(--spacing);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin: 0 3rem;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-details {
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
width: calc(100% - 160px - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
color: $matte-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
border-width: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.raffle-element {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--spacing) * 2);
|
||||||
|
right: calc(var(--spacing) * 2);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
$background: rgb(244,244,244);
|
||||||
|
|
||||||
|
--padding: 2rem;
|
||||||
|
@include desktop {
|
||||||
|
--padding: 5rem;
|
||||||
|
}
|
||||||
|
background-color: $background;
|
||||||
|
padding: var(--padding);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<h3>Vinnere</h3>
|
<h3>Vinnere</h3>
|
||||||
<a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a>
|
<a class="wine-link" @click="fetchColorsAndWinners()">Refresh data fra virtuelt lotteri</a>
|
||||||
<div class="winner-container" v-if="winners.length > 0">
|
<div class="winner-container" v-if="winners.length > 0">
|
||||||
<wine v-for="winner in winners" :key="winner" :wine="winner.wine" :inlineSlot="true">
|
<wine v-for="winner in winners" :key="winner" :wine="winner.wine">
|
||||||
<div class="winner-element">
|
<div class="winner-element">
|
||||||
<div class="color-selector">
|
<div class="color-selector">
|
||||||
<div class="label-div">
|
<div class="label-div">
|
||||||
@@ -500,6 +500,10 @@ hr {
|
|||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.page-container {
|
.page-container {
|
||||||
padding: 0 1.5rem 3rem;
|
padding: 0 1.5rem 3rem;
|
||||||
|
|
||||||
@@ -537,10 +541,10 @@ hr {
|
|||||||
}
|
}
|
||||||
.winner-element {
|
.winner-element {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
|
|
||||||
@include desktop {
|
> div {
|
||||||
margin-top: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="outer">
|
<div class="container">
|
||||||
<div class="container">
|
<h1 class="title">Dagens viner</h1>
|
||||||
<h1 class="title">Dagens viner</h1>
|
<div class="wines-container">
|
||||||
<div class="wines-container">
|
<Wine :wine="wine" v-for="wine in wines" :key="wine" />
|
||||||
<Wine :wine="wine" v-for="wine in wines" :key="wine" :fullscreen="true" :inlineSlot="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { page, event } from "vue-analytics";
|
import { page, event } from "vue-analytics";
|
||||||
|
import { prelottery } from "@/api";
|
||||||
import Banner from "@/ui/Banner";
|
import Banner from "@/ui/Banner";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
|
|
||||||
@@ -25,8 +24,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const _wines = await fetch("/api/wines/prelottery");
|
prelottery().then(wines => this.wines = wines);
|
||||||
this.wines = await _wines.json();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -39,79 +37,10 @@ export default {
|
|||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-family: knowit, Arial;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wines-container {
|
.wines-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-evenly;
|
||||||
margin: 0 2rem;
|
align-items: flex-start;
|
||||||
|
|
||||||
@media (min-width: 1500px) {
|
|
||||||
max-width: 1500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
max-width: 30vw;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
max-width: 50vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-wine-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: auto;
|
|
||||||
width: 500px;
|
|
||||||
font-family: Arial;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 150px;
|
|
||||||
margin-left: 50px;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
a:focus,
|
|
||||||
a:hover,
|
|
||||||
a:visited {
|
|
||||||
color: #333333;
|
|
||||||
font-family: Arial;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wine-link {
|
|
||||||
color: #333333;
|
|
||||||
font-family: Arial;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 1px solid $link-color;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="container">
|
<section class="content-container">
|
||||||
|
|
||||||
<div class="scroll-info">
|
<div class="scroll-info">
|
||||||
<i class ="icon icon--arrow-long-right"></i>
|
<i class ="icon icon--arrow-long-right"></i>
|
||||||
@@ -312,7 +312,7 @@ h1 {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container{
|
.content-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
row-gap: 5em;
|
row-gap: 5em;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||||
<div class="winners" v-if="winners.length > 0">
|
<div class="winners" v-if="winners.length > 0">
|
||||||
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
||||||
<div :class="winner.color + '-ballot'" class="ballot-element">
|
<div :class="winner.color + '-raffle'" class="raffle-element">
|
||||||
<span>{{ winner.name }}</span>
|
<span>{{ winner.name }}</span>
|
||||||
<span>{{ winner.phoneNumber }}</span>
|
<span>{{ winner.phoneNumber }}</span>
|
||||||
<span>Rød: {{ winner.red }}</span>
|
<span>Rød: {{ winner.red }}</span>
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
<span class="name">{{ attendee.name }}</span>
|
<span class="name">{{ attendee.name }}</span>
|
||||||
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ballots-container">
|
<div class="raffles-container">
|
||||||
<div class="red-ballot ballot-element small">{{ attendee.red }}</div>
|
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||||
<div class="blue-ballot ballot-element small">{{ attendee.blue }}</div>
|
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||||
<div class="green-ballot ballot-element small">{{ attendee.green }}</div>
|
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||||
<div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div>
|
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +140,7 @@ export default {
|
|||||||
blue: 0,
|
blue: 0,
|
||||||
green: 0,
|
green: 0,
|
||||||
yellow: 0,
|
yellow: 0,
|
||||||
ballots: 0,
|
raffles: 0,
|
||||||
randomColors: false,
|
randomColors: false,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
winners: [],
|
winners: [],
|
||||||
@@ -197,7 +197,7 @@ export default {
|
|||||||
blue: this.blue,
|
blue: this.blue,
|
||||||
green: this.green,
|
green: this.green,
|
||||||
yellow: this.yellow,
|
yellow: this.yellow,
|
||||||
ballots: this.ballots
|
raffles: this.raffles
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
@@ -229,7 +229,8 @@ export default {
|
|||||||
this.drawingWinner = true;
|
this.drawingWinner = true;
|
||||||
let response = await getVirtualWinner();
|
let response = await getVirtualWinner();
|
||||||
|
|
||||||
if (response) {
|
if (response.success) {
|
||||||
|
console.log("Winner:", response.winner);
|
||||||
if (this.currentWinners < this.numberOfWinners) {
|
if (this.currentWinners < this.numberOfWinners) {
|
||||||
this.countdown();
|
this.countdown();
|
||||||
} else {
|
} else {
|
||||||
@@ -245,7 +246,7 @@ export default {
|
|||||||
this.getAttendees();
|
this.getAttendees();
|
||||||
} else {
|
} else {
|
||||||
this.drawingWinner = false;
|
this.drawingWinner = false;
|
||||||
alert("Noe gikk galt under trekningen..!");
|
alert("Noe gikk galt under trekningen..! " + response["message"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -354,7 +355,7 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
@@ -378,19 +379,19 @@ hr {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.green-ballot {
|
&.green-raffle {
|
||||||
background-color: $light-green;
|
background-color: $light-green;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.blue-ballot {
|
&.blue-raffle {
|
||||||
background-color: $light-blue;
|
background-color: $light-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.yellow-ballot {
|
&.yellow-raffle {
|
||||||
background-color: $light-yellow;
|
background-color: $light-yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red-ballot {
|
&.red-raffle {
|
||||||
background-color: $light-red;
|
background-color: $light-red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +423,7 @@ button {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
& .name-and-phone,
|
& .name-and-phone,
|
||||||
& .ballots-container {
|
& .raffles-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -431,7 +432,7 @@ button {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ballots-container {
|
& .raffles-container {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,18 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div v-if="!posted">
|
<div v-if="!posted">
|
||||||
<h1 v-if="name">Gratulerer {{name}}!</h1>
|
<h1 v-if="name">Gratulerer {{name}}!</h1>
|
||||||
<p
|
<p v-if="name">
|
||||||
v-if="name"
|
Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.
|
||||||
>Her er valgene for dagens lotteri, du har 10 minutter å velge etter du fikk SMS-en.</p>
|
</p>
|
||||||
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
|
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
|
||||||
<h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1>
|
<h1 v-else-if="!turn" class="sent-container">Du må vente på tur..</h1>
|
||||||
<div class="wines-container" v-if="name">
|
<div class="wines-container" v-if="name">
|
||||||
<br />
|
<Wine :wine="wine" v-for="wine in wines" :key="wine">
|
||||||
<br />
|
<button
|
||||||
<Wine
|
@click="chooseWine(wine.name)"
|
||||||
:wine="wine"
|
class="vin-button select-wine"
|
||||||
v-for="wine in wines"
|
>Velg denne vinnen</button>
|
||||||
:key="wine"
|
</Wine>
|
||||||
:winner="true"
|
|
||||||
:fullscreen="true"
|
|
||||||
:inlineSlot="true"
|
|
||||||
v-on:chosen="chosenWine"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="posted" class="sent-container">
|
<div v-else-if="posted" class="sent-container">
|
||||||
@@ -29,7 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getAmIWinner, postWineChosen } from "@/api";
|
import { getAmIWinner, postWineChosen, prelottery } from "@/api";
|
||||||
import Wine from "@/ui/Wine";
|
import Wine from "@/ui/Wine";
|
||||||
export default {
|
export default {
|
||||||
components: { Wine },
|
components: { Wine },
|
||||||
@@ -60,11 +55,10 @@ export default {
|
|||||||
}
|
}
|
||||||
this.turn = true;
|
this.turn = true;
|
||||||
this.name = winnerObject.name;
|
this.name = winnerObject.name;
|
||||||
const _wines = await fetch("/api/wines/prelottery");
|
this.wines = await prelottery();
|
||||||
this.wines = await _wines.json();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
chosenWine: async function(name) {
|
chooseWine: async function(name) {
|
||||||
let posted = await postWineChosen(this.id, name);
|
let posted = await postWineChosen(this.id, name);
|
||||||
console.log("response", posted);
|
console.log("response", posted);
|
||||||
if (posted.success) {
|
if (posted.success) {
|
||||||
@@ -83,6 +77,11 @@ export default {
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: $matte-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
.sent-container {
|
.sent-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
@@ -93,9 +92,14 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-wine {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.wines-container {
|
.wines-container {
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -12,6 +12,7 @@ import WinnerPage from "@/components/WinnerPage";
|
|||||||
import LotteryPage from "@/components/LotteryPage";
|
import LotteryPage from "@/components/LotteryPage";
|
||||||
import HistoryPage from "@/components/HistoryPage";
|
import HistoryPage from "@/components/HistoryPage";
|
||||||
import HighscorePage from "@/components/HighscorePage";
|
import HighscorePage from "@/components/HighscorePage";
|
||||||
|
import PersonalHighscorePage from "@/components/PersonalHighscorePage";
|
||||||
|
|
||||||
import RequestWine from "@/components/RequestWine";
|
import RequestWine from "@/components/RequestWine";
|
||||||
import AllRequestedWines from "@/components/AllRequestedWines";
|
import AllRequestedWines from "@/components/AllRequestedWines";
|
||||||
@@ -19,30 +20,37 @@ import AllRequestedWines from "@/components/AllRequestedWines";
|
|||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
|
name: "Hjem",
|
||||||
component: VinlottisPage
|
component: VinlottisPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/lottery",
|
path: "/lottery",
|
||||||
|
name: "Lotteri",
|
||||||
component: LotteryPage
|
component: LotteryPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dagens",
|
path: "/dagens",
|
||||||
|
name: "Dagens vin",
|
||||||
component: TodaysPage
|
component: TodaysPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/viner",
|
path: "/viner",
|
||||||
|
name: "All viner",
|
||||||
component: AllWinesPage
|
component: AllWinesPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
|
name: "Login",
|
||||||
component: LoginPage
|
component: LoginPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/create",
|
path: "/create",
|
||||||
|
name: "Registrer",
|
||||||
component: CreatePage
|
component: CreatePage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
|
name: "Admin side",
|
||||||
component: AdminPage
|
component: AdminPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -53,20 +61,34 @@ const routes = [
|
|||||||
path: "/winner/:id",
|
path: "/winner/:id",
|
||||||
component: WinnerPage
|
component: WinnerPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/history",
|
path: "/history/:date",
|
||||||
|
name: "Historie for dato",
|
||||||
component: HistoryPage
|
component: HistoryPage
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/history",
|
||||||
|
name: "Historie",
|
||||||
|
component: HistoryPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/highscore/:name",
|
||||||
|
name: "Personlig topplisten",
|
||||||
|
component: PersonalHighscorePage
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/highscore",
|
path: "/highscore",
|
||||||
|
name: "Topplisten",
|
||||||
component: HighscorePage
|
component: HighscorePage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/request",
|
path: "/request",
|
||||||
|
name: "Etterspør vin",
|
||||||
component: RequestWine
|
component: RequestWine
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/requested-wines",
|
path: "/requested-wines",
|
||||||
|
name: "Etterspurte vin",
|
||||||
component: AllRequestedWines
|
component: AllRequestedWines
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
@@ -33,6 +37,33 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 90vw;
|
||||||
|
margin: 3rem auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
max-width: 1700px;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: "knowit";
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 2rem;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.subtext {
|
.subtext {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
font-size: 1.22rem;
|
font-size: 1.22rem;
|
||||||
@@ -129,6 +160,15 @@ textarea {
|
|||||||
// disable-dbl-tap-zoom
|
// disable-dbl-tap-zoom
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
|
|
||||||
|
&.auto-height {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background-color: $red;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -172,7 +212,14 @@ textarea {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-bottom: 1px solid $link-color;
|
border-bottom: 1px solid $link-color;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-left: 15px;
|
cursor: pointer;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
color: $matte-text-color;
|
||||||
|
|
||||||
|
&:focus, &:hover {
|
||||||
|
border-color: $link-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -180,8 +227,35 @@ textarea {
|
|||||||
&-md {
|
&-md {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
&-sm {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
&-0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.margin-left {
|
||||||
|
&-md {
|
||||||
|
margin-left: 3rem;
|
||||||
|
}
|
||||||
|
&-sm {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
&-0 {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.margin-right {
|
||||||
|
&-md {
|
||||||
|
margin-right: 3rem;
|
||||||
|
}
|
||||||
|
&-sm {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
&-0 {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.margin-bottom {
|
.margin-bottom {
|
||||||
&-md {
|
&-md {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
@@ -199,7 +273,7 @@ textarea {
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
-webkit-mask-image: url(/../../public/assets/images/lodd.svg);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
@@ -208,19 +282,19 @@ textarea {
|
|||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
|
|
||||||
&.green-ballot {
|
&.green-raffle {
|
||||||
background-color: $light-green;
|
background-color: $light-green;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.blue-ballot {
|
&.blue-raffle {
|
||||||
background-color: $light-blue;
|
background-color: $light-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.yellow-ballot {
|
&.yellow-raffle {
|
||||||
background-color: $light-yellow;
|
background-color: $light-yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red-ballot {
|
&.red-raffle {
|
||||||
background-color: $light-red;
|
background-color: $light-red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,3 +320,15 @@ textarea {
|
|||||||
bottom: -25px
|
bottom: -25px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-only {
|
||||||
|
@include mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-only {
|
||||||
|
@include desktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
$mobile-width: 768px;
|
$mobile-width: 768px;
|
||||||
$tablet-max: 1200px;
|
$tablet-max: 1200px;
|
||||||
$desktop-max: 1704;
|
$desktop-max: 2004px;
|
||||||
|
|
||||||
|
|
||||||
@mixin mobile {
|
@mixin mobile {
|
||||||
@@ -15,7 +15,6 @@ $desktop-max: 1704;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@mixin desktop {
|
@mixin desktop {
|
||||||
@media (min-width: #{$tablet-max + 1px}) {
|
@media (min-width: #{$tablet-max + 1px}) {
|
||||||
@content;
|
@content;
|
||||||
|
|||||||
@@ -1,23 +1,46 @@
|
|||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
& .column {
|
&.column {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .row {
|
&.row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .wrap {
|
&.wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.justify-center {
|
&.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
&.justify-space-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
&.justify-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
&.justify-start {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
&.align-center {
|
&.align-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float {
|
||||||
|
&-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
$primary: #dbeede;
|
$primary: #b7debd;
|
||||||
|
|
||||||
$light-green: #c8f9df;
|
$light-green: #c8f9df;
|
||||||
$green: #0be881;
|
$green: #0be881;
|
||||||
@@ -17,3 +17,5 @@ $red: #ef5878;
|
|||||||
$dark-red: #ec3b61;
|
$dark-red: #ec3b61;
|
||||||
|
|
||||||
$link-color: #ff5fff;
|
$link-color: #ff5fff;
|
||||||
|
|
||||||
|
$matte-text-color: #333333;
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
color="#23101f"
|
color="#23101f"
|
||||||
/>
|
/>
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#dbeede" />
|
<meta name="theme-color" content="#b7debd" />
|
||||||
<meta
|
<meta
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
content="black-translucent"
|
content="black-translucent"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #dbeede;
|
background-color: #b7debd;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<div class="attendees-container" ref="attendees">
|
<div class="attendees-container" ref="attendees">
|
||||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||||
<span class="attendee-name">{{ attendee.name }}</span>
|
<span class="attendee-name">{{ attendee.name }}</span>
|
||||||
<div class="red-ballot ballot-element small">{{ attendee.red }}</div>
|
<div class="red-raffle raffle-element small">{{ attendee.red }}</div>
|
||||||
<div class="blue-ballot ballot-element small">{{ attendee.blue }}</div>
|
<div class="blue-raffle raffle-element small">{{ attendee.blue }}</div>
|
||||||
<div class="green-ballot ballot-element small">{{ attendee.green }}</div>
|
<div class="green-raffle raffle-element small">{{ attendee.green }}</div>
|
||||||
<div class="yellow-ballot ballot-element small">{{ attendee.yellow }}</div>
|
<div class="yellow-raffle raffle-element small">{{ attendee.yellow }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@ export default {
|
|||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
|
|||||||
@@ -36,6 +36,26 @@ export default {
|
|||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
Fixed Fixed
|
|||||||
person => person.name != null && person.name != ""
|
person => person.name != null && person.name != ""
|
||||||
);
|
);
|
||||||
this.highscore = response.slice(0, 5);
|
this.highscore = response.slice(0, 5);
|
||||||
|
// response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1)
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
// this.highscore = this.generateScoreBoard(response.slice(0, 10));
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
},
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
methods: {
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
generateScoreBoard(highscore=this.highscore) {
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
let place = 0;
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
let highestWinCount = -1;
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
return highscore.map(win => {
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
const wins = win.wins.length
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
if (wins != highestWinCount) {
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
place += 1
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
highestWinCount = wins
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
}
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
const placeString = place.toString().padStart(2, "0");
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
win.rank = placeString;
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
return win
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
})
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
|
}
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
Could also update api to take a Could also update api to take a `size` 😬
|
|||||||
@@ -98,6 +118,7 @@ ol {
|
|||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
Fixed Fixed
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
|
|||||||
.winner-icon {
|
.winner-icon {
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
|
|||||||
|
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Same as above. Try not to use entire objects as keys. Same as above. Try not to use entire objects as keys.
Fixed Fixed
Fixed Fixed
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="flex column">
|
||||||
<div class="input-line">
|
<div class="input-line">
|
||||||
<label for="redCheckbox">
|
<label for="redCheckbox">
|
||||||
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/>
|
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/>
|
||||||
@@ -208,12 +208,6 @@ export default {
|
|||||||
@import "../styles/global.scss";
|
@import "../styles/global.scss";
|
||||||
@import "../styles/media-queries.scss";
|
@import "../styles/media-queries.scss";
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-line {
|
.input-line {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
230
src/ui/Wine.vue
@@ -1,39 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wine-container" :class="{ 'big': fullscreen }">
|
<div class="wine">
|
||||||
<div class="left">
|
<div class="wine-image">
|
||||||
<img
|
<img
|
||||||
v-if="wine.image"
|
v-if="wine.image && loadImage"
|
||||||
:src="wine.image"
|
:src="wine.image"
|
||||||
class="wine-image"
|
|
||||||
:class="{ 'fullscreen': fullscreen }"
|
|
||||||
/>
|
/>
|
||||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
|
||||||
<div>
|
|
||||||
<h2 v-if="wine.name">{{ wine.name }}</h2>
|
|
||||||
<h2 v-else>(no name)</h2>
|
|
||||||
<span v-if="wine.rating">{{ wine.rating }} rating</span>
|
|
||||||
<span v-if="wine.price">{{ wine.price }} NOK</span>
|
|
||||||
<span v-if="wine.country">{{ wine.country }}</span>
|
|
||||||
|
|
||||||
<a
|
<div class="wine-details">
|
||||||
v-if="wine.vivinoLink"
|
<h2 v-if="wine.name">{{ wine.name }}</h2>
|
||||||
:href="wine.vivinoLink"
|
<span v-if="wine.rating"><b>Rating:</b> {{ wine.rating }} rating</span>
|
||||||
class="wine-link"
|
<span v-if="wine.price"><b>Pris:</b> {{ wine.price }} NOK</span>
|
||||||
>Les mer på {{ hostname(wine.vivinoLink) }}</a>
|
<span v-if="wine.country"><b>Land:</b> {{ wine.country }}</span>
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="winner"
|
|
||||||
@click="choseWine(wine.name)"
|
|
||||||
class="vin-button"
|
|
||||||
>Velg dette som din vin</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<slot v-if="shouldUseInlineSlot()"></slot>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot v-if="!shouldUseInlineSlot()"></slot>
|
<slot></slot>
|
||||||
|
|
||||||
|
<div class="bottom-section">
|
||||||
|
<a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="link float-right">
|
||||||
|
Les mer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -43,37 +31,30 @@ export default {
|
|||||||
wine: {
|
wine: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
}
|
||||||
fullscreen: {
|
},
|
||||||
type: Boolean,
|
data() {
|
||||||
required: false
|
return {
|
||||||
},
|
loadImage: false
|
||||||
inlineSlot: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
winner: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
shouldUseInlineSlot() {
|
setImage(entries) {
|
||||||
return this.inlineSlot && window.innerWidth > 768;
|
const { target, isIntersecting } = entries[0];
|
||||||
},
|
if (!isIntersecting) return;
|
||||||
hostname(url) {
|
|
||||||
const urlHostname = new URL(url).hostname;
|
this.loadImage = true;
|
||||||
return urlHostname.split(".")[
|
this.observer.unobserve(target);
|
||||||
(urlHostname.match(/\./g) || []).length - 1
|
|
||||||
];
|
|
||||||
},
|
|
||||||
choseWine(name) {
|
|
||||||
if (window.confirm(`Er du sikker på at du vil ha ${name}?`)) {
|
|
||||||
this.$emit("chosen", name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.observer = new IntersectionObserver(this.setImage, {
|
||||||
|
root: this.$el,
|
||||||
|
threshold: 0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.observer.observe(this.$el);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -83,100 +64,63 @@ export default {
|
|||||||
@import "./src/styles/global";
|
@import "./src/styles/global";
|
||||||
@import "./src/styles/variables";
|
@import "./src/styles/variables";
|
||||||
|
|
||||||
.wine-image {
|
.wine {
|
||||||
height: 250px;
|
padding: 2rem;
|
||||||
|
margin: 1rem 0rem;
|
||||||
|
position: relative;
|
||||||
|
-webkit-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
|
-moz-box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
@include mobile {
|
@include tablet {
|
||||||
max-width: 90px;
|
width: 250px;
|
||||||
object-fit: cover;
|
height: 100%;
|
||||||
|
margin: 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.fullscreen {
|
.wine-image {
|
||||||
@include desktop {
|
display: flex;
|
||||||
height: 100%;
|
justify-content: center;
|
||||||
max-height: 65vh;
|
|
||||||
max-width: 275px;
|
img {
|
||||||
|
height: 250px;
|
||||||
|
@include mobile {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
max-width: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.wine-placeholder {
|
||||||
|
height: 250px;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wine-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-section {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: $matte-text-color;
|
||||||
|
font-family: Arial;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: normal;
|
||||||
|
border-bottom: 2px solid $matte-text-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
font-weight: normal;
|
||||||
|
border-color: $link-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.wine-placeholder {
|
|
||||||
height: 250px;
|
|
||||||
width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 30vw;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
max-width: 50vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wine-container {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-family: Arial;
|
|
||||||
width: 100%;
|
|
||||||
max-width: max-content;
|
|
||||||
|
|
||||||
&.big {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include desktop {
|
|
||||||
max-width: 550px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.left {
|
|
||||||
float: left;
|
|
||||||
margin-right: 3rem;
|
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
margin-right: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: max-content;
|
|
||||||
|
|
||||||
> div:first-of-type {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
a:focus,
|
|
||||||
a:hover,
|
|
||||||
a:visited {
|
|
||||||
color: #333333;
|
|
||||||
font-family: Arial;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wine-link {
|
|
||||||
color: #333333;
|
|
||||||
font-family: Arial;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 1px solid $link-color;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
& button.red {
|
|
||||||
background-color: $light-red;
|
|
||||||
color: $red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vin-button {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
<div class="current-draw" v-if="drawing">
|
<div class="current-draw" v-if="drawing">
|
||||||
<h2>TREKKER</h2>
|
<h2>TREKKER</h2>
|
||||||
<div
|
<div
|
||||||
:class="currentColor + '-ballot'"
|
:class="currentColor + '-raffle'"
|
||||||
class="ballot-element center-new-winner"
|
class="raffle-element center-new-winner"
|
||||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||||
>
|
>
|
||||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
<div class="current-draw" v-if="drawingDone">
|
<div class="current-draw" v-if="drawingDone">
|
||||||
<h2>VINNER</h2>
|
<h2>VINNER</h2>
|
||||||
<div
|
<div
|
||||||
:class="currentColor + '-ballot'"
|
:class="currentColor + '-raffle'"
|
||||||
class="ballot-element center-new-winner"
|
class="raffle-element center-new-winner"
|
||||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||||
>
|
>
|
||||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||||
@@ -204,7 +204,7 @@ h2 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 v-if="winners.length > 0"> {{ title ? title : 'Vinnere' }}</h2>
|
<h2 v-if="winners.length > 0"> {{ title ? title : 'Vinnere' }}</h2>
|
||||||
<div class="winners" v-if="winners.length > 0">
|
<div class="winners" v-if="winners.length > 0">
|
||||||
<div class="winner" v-for="(winner, index) in winners" :key="index">
|
<div v-for="(winner, index) in winners" :key="index">
|
||||||
<div :class="winner.color + '-ballot'" class="ballot-element">{{ winner.name }}</div>
|
<router-link :to="`/highscore/${ encodeURIComponent(winner.name) }`">
|
||||||
|
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +42,7 @@ h2 {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ballot-element {
|
.raffle-element {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
width: 145px;
|
width: 145px;
|
||||||
height: 145px;
|
height: 145px;
|
||||||
|
|||||||
19
src/utils.js
@@ -1,5 +1,8 @@
|
|||||||
|
|
||||||
const dateString = (date) => {
|
const dateString = (date) => {
|
||||||
|
if (typeof(date) == "string") {
|
||||||
|
date = new Date(date);
|
||||||
|
}
|
||||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
|
||||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
|
||||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
|
||||||
@@ -7,6 +10,18 @@ const dateString = (date) => {
|
|||||||
return `${ye}-${mo}-${da}`
|
return `${ye}-${mo}-${da}`
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
function humanReadableDate(date) {
|
||||||
dateString
|
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||||
|
return new Date(date).toLocaleDateString(undefined, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysAgo(date) {
|
||||||
|
const day = 24 * 60 * 60 * 1000;
|
||||||
|
return Math.round(Math.abs((new Date() - new Date(date)) / day));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dateString,
|
||||||
|
humanReadableDate,
|
||||||
|
daysAgo
|
||||||
}
|
}
|
||||||
|
|||||||
Keys don't need to be entire objects. Does this object have a unique property we can use instead?
Fixed