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