Renamed /src to /frontend.

This commit is contained in:
2020-12-06 21:48:21 +01:00
parent 913268b01c
commit ce7e05fd43
57 changed files with 8 additions and 8 deletions

View File

@@ -0,0 +1,34 @@
<template>
<div>
<h1>Admin-side</h1>
<Tabs :tabs="tabs" />
</div>
</template>
<script>
import Tabs from "@/ui/Tabs";
import RegisterPage from "@/components/RegisterPage";
import VirtualLotteryRegistrationPage from "@/components/VirtualLotteryRegistrationPage";
export default {
components: {
Tabs,
RegisterPage,
VirtualLotteryRegistrationPage
},
data() {
return {
tabs: [
{ name: "Registrering", component: RegisterPage },
{ name: "Virtuelt lotteri", component: VirtualLotteryRegistrationPage }
]
};
}
};
</script>
<style lang="scss" scoped>
h1 {
text-align: center;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<main class="container">
<h1>Alle foreslåtte viner</h1>
<section class="requested-wines-container">
<p v-if="wines == undefined || wines.length == 0">Ingen har foreslått noe enda!</p>
<RequestedWineCard v-for="requestedEl in wines" :key="requestedEl.wine._id" :requestedElement="requestedEl" @wineDeleted="filterOutDeletedWine" :showDeleteButton="isAdmin"/>
</section>
</main>
</template>
<script>
import { allRequestedWines } from "@/api";
import RequestedWineCard from "@/ui/RequestedWineCard";
export default {
components: {
RequestedWineCard
},
data(){
return{
wines: undefined,
canRequest: true,
isAdmin: false
}
},
methods: {
filterOutDeletedWine(wine){
this.wines = this.wines.filter(item => item.wine._id !== wine._id)
},
async refreshData(){
[this.wines, this.isAdmin] = await allRequestedWines() || [[], false]
}
},
mounted() {
this.refreshData()
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/media-queries.scss";
@import "@/styles/variables.scss";
.container {
width: 90vw;
margin: 3rem auto;
margin-bottom: 0;
padding-bottom: 3rem;
}
h1 {
font-size: 3rem;
font-family: "knowit";
color: $matte-text-color;
font-weight: normal;
}
.requested-wines-container{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 2rem;
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<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>
<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>
-&nbsp;
<router-link class="vin-link" :to="winDateUrl(wine.dates[index])">{{ dateString(wine.dates[index]) }}</router-link>
</li>
</ul>
</div>
</div>
</Wine>
</div>
</div>
</template>
<script>
import Banner from "@/ui/Banner";
import Wine from "@/ui/Wine";
import { overallWineStatistics } from "@/api";
import { dateString } from "@/utils";
export default {
components: {
Banner,
Wine
},
data() {
return {
wines: []
};
},
methods: {
winDateUrl(date) {
const timestamp = new Date(date).getTime();
return `/history/${timestamp}`
},
dateString: dateString
},
async mounted() {
this.wines = await overallWineStatistics();
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/media-queries";
@import "@/styles/variables";
.container {
width: 90vw;
margin: 3rem auto;
margin-bottom: 0;
padding-bottom: 4rem;
}
h1 {
font-size: 3rem;
font-family: "knowit";
font-weight: normal;
font-family: knowit, Arial;
margin-bottom: 25px;
}
.label {
font-weight: 600;
}
#wines-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 2rem;
> div {
justify-content: flex-start;
margin-bottom: 2rem;
}
}
.name-wins {
display: flex;
flex-direction: column;
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;
}
}
}
}
.raffle-element {
padding: 1rem;
font-size: 1.3rem;
margin: 3px;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="outer">
<h2>Vinlottis brukerregistering</h2>
<form aria-label="User registration" @submit.prevent>
<div class="label-div">
<label>Brukernavn</label>
<input type="text"
v-model="username"
placeholder="Brukernavn"
autocapitalize="none"
@keyup.enter="submit" />
</div>
<div class="label-div row">
<label>Passord</label>
<input type="password" v-model="password" placeholder="Passord" @keyup.enter="submit" />
</div>
<button class="vin-button" @click="submit">Registrer bruker</button>
<div v-if="error" class="error">{{ error }}</div>
</form>
</div>
</template>
<script>
import { register } from "@/api";
export default {
data() {
return {
username: undefined,
password: undefined,
error: undefined
}
},
methods: {
submit() {
register(this.username, this.password)
.then(resp => this.$router.push("/"))
.catch(error => this.error = error.message || error)
}
}
};
</script>
<style lang="scss" scoped>
@import "../styles/loginAndRegister";
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="container">
<h1 class="title" @click="startCountdown">Loddgenerator</h1>
<p class="subtext">
Velg hvilke farger du vil ha, fyll inn antall lodd og klikk 'generer'
</p>
<RaffleGenerator @numberOfRaffles="val => this.numberOfRaffles = val" />
<Vipps class="vipps" :amount="numberOfRaffles" />
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
</div>
</template>
<script>
import RaffleGenerator from "@/ui/RaffleGenerator";
import Vipps from "@/ui/Vipps";
import Countdown from "@/ui/Countdown";
export default {
components: {
RaffleGenerator,
Vipps,
Countdown
},
data() {
return {
hardStart: false,
numberOfRaffles: null
};
},
mounted() {
if (window.location.hostname == "localhost") {
return;
}
this.track();
},
methods: {
changeEnabled: function(way) {
this.hardStart = way;
},
startCountdown: function() {
this.hardStart = true;
},
track() {
window.ga('send', 'pageview', '/lottery/generate');
}
}
};
</script>
<style lang="scss" scoped>
@import "../styles/variables.scss";
@import "../styles/global.scss";
@import "../styles/media-queries.scss";
h1 {
cursor: pointer;
}
.header-link {
color: #333333;
text-decoration: none;
}
p {
text-align: center;
margin: 0;
}
.vipps {
margin: 5rem auto 2.5rem auto;
@include mobile {
margin-top: 2rem;
}
}
.container {
margin: auto;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,177 @@
<template>
<div class="container">
<h1>Vinlottis highscore</h1>
<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>
<p class="highscore-header margin-bottom-md"><b>Plassering.</b> Navn - Antall vinn</p>
<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>&nbsp;&nbsp;&nbsp;{{ person.name }} - {{ person.wins.length }}
</li>
</ol>
<div class="center desktop-only">
👈 Se dine vin(n), trykk navnet ditt
</div>
</div>
</template>
<script>
import { highscoreStatistics } from "@/api";
import { humanReadableDate, daysAgo } from "@/utils";
import Wine from "@/ui/Wine";
export default {
components: { Wine },
data() {
return {
highscore: [],
filterInput: ''
}
},
async mounted() {
let response = await highscoreStatistics();
response.sort((a, b) => {
return a.wins.length > b.wins.length ? -1 : 1;
});
response = response.filter(
person => person.name != null && person.name != ""
);
this.highscore = this.generateScoreBoard(response);
},
computed: {
filteredResults() {
let highscore = this.highscore;
let val = this.filterInput;
if (val.length) {
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.filterInput = '';
document.getElementsByTagName('input')[0].focus();
},
selectWinner(winner) {
const path = "/highscore/" + encodeURIComponent(winner.name)
this.$router.push(path);
},
humanReadableDate: humanReadableDate,
daysAgo: daysAgo
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/media-queries.scss";
@import "@/styles/variables.scss";
$elementSpacing: 3.5rem;
.el-spacing {
margin-bottom: $elementSpacing;
}
.container {
width: 90vw;
margin: 3rem auto;
max-width: 1200px;
margin-bottom: 0;
padding-bottom: 3rem;
@include desktop {
width: 80vw;
}
}
h1 {
font-size: 3rem;
font-family: "knowit";
color: $matte-text-color;
font-weight: normal;
}
.filter input {
font-size: 1rem;
width: 100%;
border-color: black;
border-width: 1.5px;
padding: 0.75rem 1rem;
@include desktop {
width: 30%;
}
}
.highscore-header {
margin-bottom: 2rem;
font-size: 1.3rem;
color: $matte-text-color;
}
.highscore-list {
display: flex;
flex-direction: column;
padding-left: 0;
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;
}
}
}
.center {
position: absolute;
top: 40%;
right: 10vw;
max-width: 50vw;
font-size: 2.5rem;
background-color: $primary;
padding: 1rem 1rem;
border-radius: 8px;
font-style: italic;
@include widescreen {
right: 20vw;
}
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div>
<h1>Historie fra tidligere lotteri</h1>
<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 { historyByDate, historyAll } from '@/api'
import { humanReadableDate } from "@/utils";
import Winners from '@/ui/Winners'
export default {
name: 'History page of prev lotteries',
components: { Winners },
data() {
return {
lotteries: [],
}
},
methods: {
humanReadableDate: humanReadableDate
},
created() {
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>
<style lang="scss" scoped>
h1 {
text-align: center;
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<div class="outer">
<h2>Vinlottis brukerinnlogging</h2>
<form aria-label="User signin" @submit.prevent>
<div class="label-div">
<label>Brukernavn</label>
<input
type="text"
v-model="username"
placeholder="Brukernavn"
autocapitalize="none"
@keyup.enter="submit"
/>
</div>
<div class="label-div row">
<label>Passord</label>
<input type="password" v-model="password" placeholder="Passord" @keyup.enter="submit" />
</div>
<button class="vin-button" @click="submit">Logg inn</button>
<div v-if="error" class="error">{{ error }}</div>
</form>
</div>
</template>
<script>
import { login } from "@/api";
export default {
data() {
return {
username: undefined,
password: undefined,
error: undefined
};
},
methods: {
submit() {
login(this.username, this.password)
.then(resp => this.$router.push("admin"))
.catch(error => (this.error = error.message || error));
}
}
};
</script>
<style lang="scss" scoped>
@import "../styles/loginAndRegister";
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div>
<Tabs :tabs="tabs" :active="startRoute" v-on:tabChange="tabChanged" />
</div>
</template>
<script>
import Tabs from "@/ui/Tabs";
import GeneratePage from "@/components/GeneratePage";
import VirtualLotteryPage from "@/components/VirtualLotteryPage";
export default {
components: {
Tabs
},
data() {
return {
startRoute: 0
};
},
beforeMount() {
switch (this.$router.currentRoute.params.tab) {
case "generate":
this.startRoute = 0;
break;
case "game":
this.startRoute = 1;
break;
default:
this.startRoute = 0;
}
},
data() {
return {
tabs: [
{ name: "Loddgenerator", component: GeneratePage },
{ name: "Virtuelt lotteri", component: VirtualLotteryPage }
]
};
},
methods: {
tabChanged: function(num) {
if (num == 0) {
this.$router.push("/lottery/generate");
} else if (num == 1) {
this.$router.push("/lottery/game");
}
}
}
};
</script>
<style lang="scss" scoped>
h1 {
text-align: center;
}
</style>

View File

@@ -0,0 +1,273 @@
<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)" :key="color">
{{ occurences }}
</div>
</div>
<h4 class="el-spacing">Flasker vunnet:</h4>
<div v-for="win in winner.highscore" :key="win._id">
<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 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 "@/styles/variables";
@import "@/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;
}
.container {
width: 90vw;
margin: 3rem auto;
margin-bottom: 0;
padding-bottom: 3rem;
max-width: 1200px;
@include desktop {
width: 80vw;
}
}
h1 {
font-size: 3rem;
font-family: "knowit";
font-weight: normal;
}
.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>

View File

@@ -0,0 +1,723 @@
<template>
<div class="page-container">
<h1>Registrering</h1>
<br />
<br />
<div class="notification-element">
<div class="label-div">
<label for="notification">Push-melding</label>
<textarea
id="notification"
type="text"
rows="3"
v-model="pushMessage"
placeholder="Push meldingtekst"
/>
<input id="notification-link" type="text" v-model="pushLink" placeholder="Push-click link" />
</div>
</div>
<div class="button-container">
<button class="vin-button" @click="sendPush">Send push</button>
</div>
<hr />
<h2 id="addwine-title">Prelottery</h2>
<ScanToVinmonopolet @wine="wineFromVinmonopoletScan" v-if="showCamera" />
<div class="button-container">
<button
class="vin-button"
@click="showCamera = !showCamera"
>{{ showCamera ? "Skjul camera" : "Legg til vin med camera" }}</button>
<button class="vin-button" @click="addWine">Legg til en vin manuelt</button>
</div>
<div v-if="wines.length > 0" class="edit-container">
<wine v-for="wine in wines" :key="key" :wine="wine">
<div class="edit">
<div class="button-container row">
<button
class="vin-button"
@click="editWine = amIBeingEdited(wine) ? false : wine"
>{{ amIBeingEdited(wine) ? "Lukk" : "Rediger" }}</button>
<button class="red vin-button" @click="deleteWine(wine)">Slett</button>
</div>
<div v-if="amIBeingEdited(wine)" class="wine-edit">
<div class="label-div" v-for="key in Object.keys(wine)" :key="key">
<label>{{ key }}</label>
<input type="text" v-model="wine[key]" :placeholder="key" />
</div>
</div>
</div>
</wine>
</div>
<div class="button-container" v-if="wines.length > 0">
<button class="vin-button" @click="sendWines">Send inn viner</button>
</div>
<hr />
<h2>Lottery</h2>
<h3>Legg til lodd kjøpt</h3>
<div class="colors">
<div v-for="color in lotteryColors" :class="color.css + ' colors-box'" :key="color">
<div class="colors-overlay">
<p>{{ color.name }} kjøpt</p>
<input v-model="color.value" min="0" :placeholder="0" type="number" />
</div>
</div>
<div class="label-div">
<label>Totalt kjøpt for:</label>
<input v-model="payed" placeholder="NOK" type="number" :step="price || 1" min="0" />
</div>
</div>
<div class="button-container">
<button class="vin-button" @click="submitLottery">Send inn lotteri</button>
</div>
<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">
<div class="winner-element">
<div class="color-selector">
<div class="label-div">
<label>Farge vunnet</label>
</div>
<button
class="blue"
:class="{ active: winner.color == 'blue' }"
@click="winner.color = 'blue'"
></button>
<button
class="red"
:class="{ active: winner.color == 'red' }"
@click="winner.color = 'red'"
></button>
<button
class="green"
:class="{ active: winner.color == 'green' }"
@click="winner.color = 'green'"
></button>
<button
class="yellow"
:class="{ active: winner.color == 'yellow' }"
@click="winner.color = 'yellow'"
></button>
</div>
<div class="label-div">
<label for="winner-name">Navn vinner</label>
<input id="winner-name" type="text" placeholder="Navn" v-model="winner.name" />
</div>
</div>
<div class="label-div">
<label for="potential-winner-name">Virtuelle vinnere</label>
<select
id="potential-winner-name"
type="text"
placeholder="Navn"
v-model="winner.potentialWinner"
@change="potentialChange($event, winner)"
>
<option
v-for="fetchedWinner in fetchedWinners"
:value="stringify(fetchedWinner)"
>{{fetchedWinner.name}}</option>
</select>
</div>
</wine>
<div class="button-container column">
<button class="vin-button" @click="submitLotteryWinners">Send inn vinnere</button>
<button class="vin-button" @click="resetWinnerDataInStorage">Reset local wines</button>
</div>
</div>
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
</div>
</template>
<script>
import eventBus from "@/mixins/EventBus";
import { dateString } from '@/utils'
import {
prelottery,
sendLotteryWinners,
sendLottery,
logWines,
wineSchema,
winnersSecure,
attendees
} from "@/api";
import TextToast from "@/ui/TextToast";
import Wine from "@/ui/Wine";
import ScanToVinmonopolet from "@/ui/ScanToVinmonopolet";
export default {
components: { TextToast, Wine, ScanToVinmonopolet },
data() {
return {
payed: undefined,
winners: [],
fetchedWinners: [],
wines: [],
pushMessage: "",
pushLink: "/",
toastText: undefined,
showToast: false,
showCamera: false,
editWine: false,
lotteryColors: [
{ value: null, name: "Blå", css: "blue" },
{ value: null, name: "Rød", css: "red" },
{ value: null, name: "Grønn", css: "green" },
{ value: null, name: "Gul", css: "yellow" }
],
price: __PRICE__
};
},
created() {
this.fetchAndAddPrelotteryWines().then(this.getWinnerdataFromStorage);
window.addEventListener("unload", this.setWinnerdataToStorage);
},
beforeDestroy() {
this.setWinnerdataToStorage();
eventBus.$off("tab-change", () => {
this.fetchColorsAndWinners();
});
},
mounted() {
this.fetchColorsAndWinners();
eventBus.$on("tab-change", () => {
this.fetchColorsAndWinners();
});
},
methods: {
stringify(json) {
return JSON.stringify(json);
},
potentialChange(event, winner) {
let data = JSON.parse(event.target.value);
winner.name = data.name;
winner.color = data.color;
},
async fetchColorsAndWinners() {
let winners = await winnersSecure();
let _attendees = await attendees();
let colors = {
red: 0,
blue: 0,
green: 0,
yellow: 0
};
this.payed = 0;
for (let i = 0; i < _attendees.length; i++) {
let attendee = _attendees[i];
colors.red += attendee.red;
colors.blue += attendee.blue;
colors.green += attendee.green;
colors.yellow += attendee.yellow;
this.payed +=
(attendee.red + attendee.blue + attendee.green + attendee.yellow) *
10;
}
for (let i = 0; i < this.lotteryColors.length; i++) {
let currentColor = this.lotteryColors[i];
switch (currentColor.css) {
case "red":
currentColor.value = colors.red;
break;
case "blue":
currentColor.value = colors.blue;
break;
a;
case "green":
currentColor.value = colors.green;
break;
case "yellow":
currentColor.value = colors.yellow;
break;
}
}
this.fetchedWinners = winners;
},
amIBeingEdited(wine) {
return this.editWine.id == wine.id && this.editWine.name == wine.name;
},
async fetchAndAddPrelotteryWines() {
const wines = await prelottery();
for (let i = 0; i < wines.length; i++) {
let wine = wines[i];
this.winners.push({
name: "",
color: "",
potentialWinner: "",
wine: {
name: wine.name,
vivinoLink: wine.vivinoLink,
rating: wine.rating,
image: wine.image,
id: wine.id
}
});
}
},
wineFromVinmonopoletScan(wineResponse) {
if (this.wines.map(wine => wine.name).includes(wineResponse.name)) {
this.toastText = "Vinen er allerede lagt til.";
this.showToast = true;
return;
}
this.toastText = "Fant og la til vin:<br>" + wineResponse.name;
this.showToast = true;
this.wines.unshift(wineResponse);
},
sendPush: async function() {
let _response = await fetch("/subscription/send-notification", {
headers: {
"Content-Type": "application/json"
// 'Content-Type': 'application/x-www-form-urlencoded',
},
method: "POST",
body: JSON.stringify({ message: this.pushMessage, link: this.pushLink })
});
let response = await _response.json();
if (response) {
alert("Sendt!");
} else {
alert("Noe gikk galt!");
}
},
addWine: async function(event) {
const wine = await wineSchema();
this.editWine = wine;
this.wines.unshift(wine);
},
deleteWine(deletedWine) {
this.wines = this.wines.filter(wine => wine.name != deletedWine.name);
},
sendWines: async function() {
let response = await logWines(this.wines);
if (response.success == true) {
alert("Sendt!");
window.location.reload();
} else {
alert("Noe gikk galt under innsending");
}
},
addWinner: function(event) {
this.winners.push({
name: "",
color: "",
wine: {
name: "",
vivinoLink: "",
rating: ""
}
});
},
submitLottery: async function(event) {
const colors = {
red: this.lotteryColors.filter(c => c.css == "red")[0].value,
green: this.lotteryColors.filter(c => c.css == "green")[0].value,
blue: this.lotteryColors.filter(c => c.css == "blue")[0].value,
yellow: this.lotteryColors.filter(c => c.css == "yellow")[0].value
};
let sendObject = {
lottery: {
date: dateString(new Date()),
...colors
}
};
if (sendObject.lottery.red == undefined) {
alert("Rød må defineres");
return;
}
if (sendObject.lottery.green == undefined) {
alert("Grønn må defineres");
return;
}
if (sendObject.lottery.yellow == undefined) {
alert("Gul må defineres");
return;
}
if (sendObject.lottery.blue == undefined) {
alert("Blå må defineres");
return;
}
sendObject.lottery.bought =
parseInt(colors.blue) +
parseInt(colors.red) +
parseInt(colors.green) +
parseInt(colors.yellow);
const stolen = sendObject.lottery.bought - parseInt(this.payed) / 10;
if (isNaN(stolen) || stolen == undefined) {
alert("Betalt må registreres");
return;
}
sendObject.lottery.stolen = stolen;
let response = await sendLottery(sendObject);
if (response == true) {
alert("Sendt!");
window.location.reload();
} else {
alert(response.message || "Noe gikk galt under innsending");
}
},
submitLotteryWinners: async function(event) {
let sendObject = {
lottery: {
date: dateString(new Date()),
winners: this.winners
}
}
if (sendObject.lottery.winners.length == 0) {
alert("Det må være med vinnere");
return;
}
for (let i = 0; i < sendObject.lottery.winners.length; i++) {
let currentWinner = sendObject.lottery.winners[i];
if (currentWinner.name == undefined || currentWinner.name == "") {
alert("Navn må defineres");
return;
}
if (currentWinner.color == undefined || currentWinner.color == "") {
alert("Farge må defineres");
return;
}
}
let response = await sendLotteryWinners(sendObject);
if (response == true) {
alert("Sendt!");
window.location.reload();
} else {
alert(response.message || "Noe gikk galt under innsending");
}
},
getWinnerdataFromStorage() {
let localWinners = localStorage.getItem("winners");
if (localWinners && this.winners.length) {
localWinners = JSON.parse(localWinners);
this.winners = this.winners.map(winner => {
const localWinnerMatch = localWinners.filter(
localWinner =>
localWinner.wine.name == winner.wine.name ||
localWinner.wine.id == winner.wine.id
);
if (localWinnerMatch.length > 0) {
winner.name = localWinnerMatch[0].name || winner.name;
winner.color = localWinnerMatch[0].color || winner.name;
}
return winner;
});
}
let localColors = localStorage.getItem("colorValues");
if (localColors) {
localColors = localColors.split(",");
this.lotteryColors.forEach((color, i) => {
const localColorValue = Number(localColors[i]);
color.value = localColorValue == 0 ? null : localColorValue;
});
}
},
setWinnerdataToStorage() {
localStorage.setItem("winners", JSON.stringify(this.winners));
localStorage.setItem(
"colorValues",
this.lotteryColors.map(color => Number(color.value))
);
window.removeEventListener("unload", this.setWinnerdataToStorage);
},
resetWinnerDataInStorage() {
this.winners = [];
this.fetchAndAddPrelotteryWines().then(resp => (this.winners = resp));
this.lotteryColors.map(color => (color.value = null));
window.location.reload();
}
}
};
</script>
<style lang="scss" scoped>
@import "../styles/global.scss";
@import "../styles/media-queries.scss";
select {
margin: 0 0 auto;
height: 2rem;
min-width: 0;
width: 98%;
padding: 1%;
}
h1 {
width: 100%;
text-align: center;
font-family: knowit, Arial;
}
h2 {
width: 100%;
text-align: center;
font-size: 1.6rem;
font-family: knowit, Arial;
}
.wine-link {
color: #333333;
text-decoration: none;
font-weight: bold;
cursor: pointer;
border-bottom: 1px solid $link-color;
}
hr {
width: 90%;
margin: 2rem auto;
color: grey;
}
.button-container {
margin-top: 1rem;
}
.page-container {
padding: 0 1.5rem 3rem;
@include desktop {
max-width: 60vw;
margin: 0 auto;
}
}
.winner-container {
width: max-content;
max-width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.button-container {
width: 100%;
}
}
.edit-container {
margin-top: 2rem;
display: flex;
justify-content: center;
flex-direction: column;
}
.edit {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.notification-element {
margin-bottom: 2rem;
}
.winner-element {
display: flex;
flex-direction: column;
> div {
margin-bottom: 1rem;
}
@include mobile {
width: 100%;
}
}
.wine-element {
align-items: flex-start;
}
.generate-link {
color: #333333;
text-decoration: none;
display: block;
text-align: center;
margin-bottom: 0px;
}
.wine-edit {
width: 100%;
margin-top: 1.5rem;
label {
margin-top: 0.75rem;
margin-bottom: 0;
}
}
.color-selector {
margin-bottom: 0.65rem;
margin-right: 1rem;
@include desktop {
min-width: 175px;
}
@include mobile {
max-width: 25vw;
}
.active {
border: 2px solid unset;
&.green {
border-color: $green;
}
&.blue {
border-color: $dark-blue;
}
&.red {
border-color: $red;
}
&.yellow {
border-color: $dark-yellow;
}
}
button {
border: 2px solid transparent;
display: inline-flex;
flex-wrap: wrap;
flex-direction: row;
height: 2.5rem;
width: 2.5rem;
// disable-dbl-tap-zoom
touch-action: manipulation;
@include mobile {
margin: 2px;
}
&.green {
background: #c8f9df;
}
&.blue {
background: #d4f2fe;
}
&.red {
background: #fbd7de;
}
&.yellow {
background: #fff6d6;
}
}
}
.colors {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
max-width: 1400px;
margin: 3rem auto 1rem;
@include mobile {
margin: 1.8rem auto 0;
}
.label-div {
margin-top: 0.5rem;
width: 100%;
}
}
.colors-box {
width: 150px;
height: 150px;
margin: 20px;
-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: 120px;
height: 120px;
margin: 10px;
}
}
.colors-overlay {
display: flex;
justify-content: center;
height: 100%;
padding: 0 0.5rem;
position: relative;
p {
margin: 0;
font-size: 0.8rem;
margin-bottom: 0.5rem;
text-transform: uppercase;
font-weight: 600;
position: absolute;
top: 0.4rem;
left: 0.5rem;
}
input {
width: 70%;
border: 0;
padding: 0;
font-size: 3rem;
height: unset;
max-height: unset;
position: absolute;
bottom: 1.5rem;
}
}
.green,
.green .colors-overlay > input {
background-color: $light-green;
color: $green;
}
.blue,
.blue .colors-overlay > input {
background-color: $light-blue;
color: $blue;
}
.yellow,
.yellow .colors-overlay > input {
background-color: $light-yellow;
color: $yellow;
}
.red,
.red .colors-overlay > input {
background-color: $light-red;
color: $red;
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<section class="main-container">
<Modal
v-if="showModal"
modalText="Ønsket ditt har blitt lagt til"
:buttons="modalButtons"
@click="emitFromModalButton"
></Modal>
<h1>
Foreslå en vin!
</h1>
<section class="search-container">
<section class="search-section">
<input type="text" v-model="searchString" @keyup.enter="fetchWineFromVin()" placeholder="Søk etter en vin du liker her!🍷" class="search-input-field">
<button :disabled="!searchString" @click="fetchWineFromVin()" class="vin-button">Søk</button>
</section>
<section v-for="(wine, index) in this.wines" :key="index" class="single-result">
<img
v-if="wine.image"
:src="wine.image"
class="wine-image"
:class="{ 'fullscreen': fullscreen }"
/>
<img v-else class="wine-placeholder" alt="Wine image" />
<section class="wine-info">
<h2 v-if="wine.name">{{ wine.name }}</h2>
<h2 v-else>(no name)</h2>
<div class="details">
<span v-if="wine.rating">{{ wine.rating }}%</span>
<span v-if="wine.price">{{ wine.price }} NOK</span>
<span v-if="wine.country">{{ wine.country }}</span>
</div>
</section>
<button class="vin-button" @click="request(wine)">Foreslå denne</button>
<a
v-if="wine.vivinoLink"
:href="wine.vivinoLink"
class="wine-link"
>Les mer</a>
</section>
<p v-if="this.wines && this.wines.length == 0">
Fant ingen viner med det navnet!
</p>
</section>
</section>
</template>
<script>
import { searchForWine, requestNewWine } from "@/api";
import Wine from "@/ui/Wine";
import Modal from "@/ui/Modal";
export default {
components: {
Wine,
Modal
},
data() {
return {
searchString: undefined,
wines: undefined,
showModal: false,
modalButtons: [
{
text: "Legg til flere viner",
action: "stay"
},
{
text: "Se alle viner",
action: "move"
}
]
}
},
methods: {
fetchWineFromVin(){
if(this.searchString){
this.wines = []
let localSearchString = this.searchString.replace(/ /g,"_");
searchForWine(localSearchString)
.then(res => this.wines = res)
}
},
request(wine){
requestNewWine(wine)
.then(() => this.showModal = true)
},
emitFromModalButton(action){
if(action == "stay"){
this.showModal = false
} else {
this.$router.push("/requested-wines");
}
}
},
}
</script>
<style lang="scss" scoped>
@import "@/styles/media-queries";
@import "@/styles/global";
@import "@/styles/variables";
h1{
text-align: center;
}
.main-container{
margin: auto;
max-width: 1200px;
}
input[type="text"] {
width: 90%;
color: black;
border-radius: 4px;
padding: 1rem 1rem;
border: 1px solid black;
max-width: 90%;
}
.search-container{
margin: 1rem;
}
.search-section{
display: grid;
grid: 1fr / 1fr .2fr;
@include mobile{
.vin-button{
display: none;
}
.search-input-field{
grid-column: 1 / -1;
}
}
}
.single-result{
margin-top: 1rem;
display: grid;
grid: 1fr / .5fr 2fr .5fr .5fr;
grid-template-areas: "picture details button-left button-right";
justify-items: center;
align-items: center;
grid-gap: 1em;
padding-bottom: 1em;
margin-bottom: 1em;
box-shadow: 0 1px 0 0 rgba(0,0,0,0.2);
@include mobile{
grid: 1fr .5fr / .5fr 1fr;
grid-template-areas: "picture details"
"button-left button-right";
grid-gap: .5em;
.vin-button{
grid-area: button-right;
padding: .5em;
font-size: 1em;
line-height: 1em;
height: 2em;
}
.wine-link{
grid-area: button-left;
}
h2{
font-size: 1em;
max-width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis
}
}
.wine-image {
height: 100px;
grid-area: picture;
}
.wine-placeholder {
height: 100px;
width: 70px;
grid-area: picture;
}
.wine-info{
grid-area: details;
width: 100%;
h2{
margin: 0;
}
.details{
top: 0;
display: flex;
flex-direction: column;
}
}
.wine-link {
grid-area: button-left;
color: #333333;
font-family: Arial;
text-decoration: none;
font-weight: bold;
border-bottom: 1px solid $link-color;
height: 1.2em;
width: max-content;
}
.vin-button{
grid-area: button-right;
}
@include tablet{
h2{
font-size: 1.2em;
}
}
@include desktop{
h2{
font-size: 1.6em;
}
}
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<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 { prelottery } from "@/api";
import Banner from "@/ui/Banner";
import Wine from "@/ui/Wine";
export default {
components: {
Banner,
Wine
},
data() {
return {
wines: []
};
},
async mounted() {
prelottery().then(wines => this.wines = wines);
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/media-queries";
@import "@/styles/variables";
.wine-image {
height: 250px;
}
h1 {
font-family: knowit, Arial;
margin-bottom: 25px;
}
.wines-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
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;
}
</style>

View File

@@ -0,0 +1,382 @@
<template>
<main class="main-container">
<section class="top-container">
<div class="want-to-win">
<h1>
Vil du også vinne?
</h1>
<img
src="/public/assets/images/notification.svg"
alt="Notification-bell"
@click="requestNotificationAccess"
class="notification-request-button"
role="button"
v-if="notificationAllowed"
/>
</div>
<router-link to="/lottery/game" class="participate-button">
<i class="icon icon--arrow-right"></i>
<p>Trykk her for å delta</p>
</router-link>
<router-link to="/lottery/generate" class="see-details-link">
Se vipps detaljer og QR-kode
</router-link>
<div class="icons-container">
<i class="icon icon--heart-sparks"></i>
<i class="icon icon--face-1"></i>
<i class="icon icon--face-3"></i>
<i class="icon icon--ballon"></i>
<i class="icon icon--bottle"></i>
<i class="icon icon--bottle"></i>
<i class="icon icon--bottle"></i>
<i class="icon icon--bottle"></i>
<i class="icon icon--bottle"></i>
</div>
</section>
<section class="content-container">
<div class="scroll-info">
<i class ="icon icon--arrow-long-right"></i>
<p>Scroll for å se vinnere og annen gøy statistikk</p>
</div>
<Highscore class="highscore"/>
<TotalBought class="total-bought" />
<section class="chart-container">
<PurchaseGraph class="purchase" />
<WinGraph class="win" />
</section>
<Wines class="wines-container" />
</section>
<Countdown :hardEnable="hardStart" @countdown="changeEnabled" />
</main>
</template>
<script>
import PurchaseGraph from "@/ui/PurchaseGraph";
import TotalBought from "@/ui/TotalBought";
import Highscore from "@/ui/Highscore";
import WinGraph from "@/ui/WinGraph";
import Wines from "@/ui/Wines";
import Vipps from "@/ui/Vipps";
import Countdown from "@/ui/Countdown";
import { prelottery } from "@/api";
export default {
components: {
PurchaseGraph,
TotalBought,
Highscore,
WinGraph,
Wines,
Vipps,
Countdown
},
data() {
return {
hardStart: false,
pushAllowed: false
};
},
computed: {
notificationAllowed: function() {
if (!("PushManager" in window)) {
return false;
}
return (
Notification.permission !== "granted" ||
!this.pushAllowed ||
localStorage.getItem("push") == null
);
}
},
async mounted() {
this.$on("push-allowed", () => {
this.pushAllowed = true;
});
if (window.location.hostname == "localhost") {
return;
}
this.track();
},
methods: {
requestNotificationAccess() {
this.$root.$children[0].registerServiceWorkerPushNotification();
},
changeEnabled(way) {
this.hardStart = way;
},
track() {
window.ga('send', 'pageview', '/');
},
startCountdown() {
this.hardStart = true;
}
}
};
</script>
<style lang="scss" scoped>
@import "../styles/media-queries.scss";
@import "../styles/variables.scss";
.top-container {
height: 30em;
background-color: $primary;
width: 100vw;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: repeat(12, 1fr);
align-items: center;
justify-items: start;
@include mobile{
padding-bottom: 2em;
height: 15em;
grid-template-rows: repeat(7, 1fr);
}
.want-to-win {
grid-row: 2 / 4;
grid-column: 2 / -1;
display: flex;
h1{
font-size: 2em;
font-weight: 400;
}
@include tablet {
h1{
font-size: 3em;
}
grid-row: 2 / 4;
grid-column: 3 / -3;
}
}
.notification-request-button{
cursor: pointer;
}
.participate-button {
grid-row: 4 / 6;
grid-column: 2 / -1;
background: inherit;
border: 4px solid black;
padding: 0 1em 0 1em;
display: flex;
width: 12.5em;
align-items: center;
text-decoration: none;
color: black;
i {
color: $link-color;
margin-left: 5px;
}
p {
font-size: 16px;
margin-left: 15px;
}
@include tablet {
grid-row: 4 / 6;
grid-column: 3 / -3;
}
}
.see-details-link {
grid-row: 6 / 8;
grid-column: 2 / -1;
@include tablet {
grid-row: 6 / 8;
grid-column: 2 / 10;
}
@include tablet {
grid-column: 3 / -3;
}
font-weight: bold;
color: black;
font-weight: 200;
font-size: 1.3em;
text-decoration: underline;
text-decoration-color: $link-color;
text-underline-position: under;
}
.icons-container {
grid-column: 1 / -1;
grid-row: 7 / -1;
@include mobile{
margin-top: 2em;
display: none;
}
@include tablet {
grid-row: 6 / -1;
grid-column: 7 / -1;
}
@include desktop{
grid-row: 4 / -3;
grid-column: 7 / 11;
}
@include widescreen {
grid-column: 6 / 10;
}
width: 100%;
min-width: 375px;
height: 100%;
display: grid;
grid: repeat(6, 1fr) / repeat(12, 1fr);
i {
font-size: 5em;
&.icon--heart-sparks{
grid-column: 2 / 4;
grid-row: 2 / 4;
align-self: center;
justify-self: center;
}
&.icon--face-1{
grid-column: 4 / 7;
grid-row: 2 / 4;
justify-self: center;
}
&.icon--face-3{
grid-column: 7 / 10;
grid-row: 1 / 4;
align-self: center;
}
&.icon--ballon{
grid-column: 9 / 11;
grid-row: 3 / 5;
}
&.icon--bottle{
grid-row: 4 / -1;
&:nth-of-type(5) {
grid-column: 4 / 5;
align-self: center;
}
&:nth-of-type(6) {
grid-column: 5 / 6;
}
&:nth-of-type(7) {
grid-column: 6 / 7;
align-self: center;
}
&:nth-of-type(8) {
grid-column: 7 / 8;
}
&:nth-of-type(9){
grid-column: 8 / 9;
align-self: center;
}
}
}
}
}
h1 {
text-align: center;
font-family: "knowit";
}
.to-lottery{
color: #333;
text-decoration: none;
display: block;
text-align: center;
margin-bottom: 0;
}
.content-container {
display: grid;
grid-template-columns: repeat(12, 1fr);
row-gap: 5em;
.scroll-info {
display: flex;
align-items: center;
column-gap: 10px;
grid-column: 2 / -2;
}
.chart-container {
display: flex;
width: 100%;
flex-direction: column;
grid-column: 2 / -2;
}
.total-bought {
grid-column: 2 / -2;
}
.highscore {
grid-column: 2 / -2;
}
.wines-container {
grid-column: 2 / -2;
}
.icon--arrow-long-right {
transform: rotate(90deg);
color: $link-color;
}
@include tablet {
.scroll-info{
grid-column: 3 / -3;
}
.chart-container {
grid-column: 3 / -3;
flex-direction: row;
}
.total-bought {
grid-column: 3 / -3;
}
.highscore {
grid-column: 3 / -3;
}
.wines-container {
grid-column: 3 / -3;
}
}
}
</style>

View File

@@ -0,0 +1,387 @@
<template>
<div>
<h1 class="title">Virtuelt lotteri</h1>
<h2
v-if="
attendees.length <= 0 &&
winners.length <= 0 &&
attendeesFetched &&
winnersFetched
"
>Her var det lite.. Sikker på at det er en virtuell trekning nå?</h2>
<div class="title-info">
<h2>Send vipps med melding "Vinlotteri" for å bli registrert til virtuelt lotteri</h2>
<p>Send gjerne melding om fargeønsker også</p>
</div>
<router-link to="/dagens" class="generate-link" v-if="todayExists">
Lurer du på dagens fangst?
<span class="subtext generator-link">Se her</span>
</router-link>
<hr />
<h2>Live oversikt av lodd kjøp i dag</h2>
<div class="colors">
<div v-for="color in Object.keys(ticketsBought)" :class="color + ' colors-box'" :key="color">
<div class="colors-overlay">
<p>{{ ticketsBought[color] }} kjøpt</p>
</div>
</div>
</div>
<WinnerDraw
:currentWinnerDrawn="currentWinnerDrawn"
:currentWinner="currentWinner"
:attendees="attendees"
/>
<Winners :winners="winners" />
<hr />
<div class="middle-elements">
<Attendees :attendees="attendees" class="outer-attendees" />
<Chat class="outer-chat" />
</div>
<Vipps class="vipps" :amount="1" />
</div>
</template>
<script>
import { attendees, winners, prelottery } from "@/api";
import Chat from "@/ui/Chat";
import Vipps from "@/ui/Vipps";
import Attendees from "@/ui/Attendees";
import Winners from "@/ui/Winners";
import WinnerDraw from "@/ui/WinnerDraw";
import io from "socket.io-client";
export default {
components: { Chat, Attendees, Winners, WinnerDraw, Vipps },
data() {
return {
attendees: [],
winners: [],
currentWinnerDrawn: false,
currentWinner: {},
socket: null,
attendeesFetched: false,
winnersFetched: false,
wasDisconnected: false,
ticketsBought: {}
};
},
mounted() {
this.track();
this.getAttendees();
this.getWinners();
const BASE_URL = __APIURL__ || window.location.origin;
this.socket = io(`${BASE_URL}`);
this.socket.on("color_winner", msg => {});
this.socket.on("disconnect", msg => {
this.wasDisconnected = true;
});
this.socket.on("winner", async msg => {
this.currentWinnerDrawn = true;
this.currentWinner = { name: msg.name, color: msg.color };
setTimeout(() => {
this.getWinners();
this.getAttendees();
this.currentWinner = null;
this.currentWinnerDrawn = false;
}, 12000);
});
this.socket.on("refresh_data", async msg => {
this.getAttendees();
this.getWinners();
});
this.socket.on("new_attendee", async msg => {
this.getAttendees();
});
},
beforeDestroy() {
this.socket.disconnect();
this.socket = null;
},
computed: {
todayExists: () => {
return prelottery()
.then(wines => wines.length > 0)
.catch(() => false);
}
},
methods: {
getWinners: async function() {
let response = await winners();
if (response) {
this.winners = response;
}
this.winnersFetched = true;
},
getAttendees: async function() {
let response = await attendees();
if (response) {
this.attendees = response;
if (this.attendees == undefined || this.attendees.length == 0) {
this.attendeesFetched = true;
return;
}
const addValueOfListObjectByKey = (list, key) =>
list.map(object => object[key]).reduce((a, b) => a + b);
this.ticketsBought = {
red: addValueOfListObjectByKey(response, "red"),
blue: addValueOfListObjectByKey(response, "blue"),
green: addValueOfListObjectByKey(response, "green"),
yellow: addValueOfListObjectByKey(response, "yellow")
};
}
this.attendeesFetched = true;
},
track() {
window.ga('send', 'pageview', '/lottery/game');
}
}
};
</script>
<!-- TODO move link styling to global with more generic name -->
<style lang="scss" scoped>
@import "../styles/global.scss";
@import "../styles/variables.scss";
@import "../styles/media-queries.scss";
.generate-link {
color: #333333;
text-decoration: none;
display: block;
width: 100vw;
text-align: center;
margin-bottom: 0px;
@include mobile {
width: 60vw;
margin: auto;
}
}
.vipps-image {
width: 250px;
margin: auto;
display: block;
margin-top: 30px;
}
.generator-link {
font-weight: bold;
border-bottom: 1px solid $link-color;
}
</style>
<style lang="scss" scoped>
@import "../styles/global.scss";
@import "../styles/variables.scss";
@import "../styles/media-queries.scss";
.color-selector {
margin-bottom: 0.65rem;
margin-right: 1rem;
@include desktop {
min-width: 175px;
}
@include mobile {
max-width: 25vw;
}
.active {
border: 2px solid unset;
&.green {
border-color: $green;
}
&.blue {
border-color: $dark-blue;
}
&.red {
border-color: $red;
}
&.yellow {
border-color: $dark-yellow;
}
}
button {
border: 2px solid transparent;
display: inline-flex;
flex-wrap: wrap;
flex-direction: row;
height: 2.5rem;
width: 2.5rem;
// disable-dbl-tap-zoom
touch-action: manipulation;
@include mobile {
margin: 2px;
}
&.green {
background: #c8f9df;
}
&.blue {
background: #d4f2fe;
}
&.red {
background: #fbd7de;
}
&.yellow {
background: #fff6d6;
}
}
}
.colors {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
max-width: 1400px;
margin: 0 auto;
@include mobile {
margin: 1.8rem auto 0;
}
.label-div {
margin-top: 0.5rem;
width: 100%;
}
}
.colors-box {
width: 150px;
height: 150px;
margin: 20px;
-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: 120px;
height: 120px;
margin: 10px;
}
}
.colors-overlay {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 0 0.25rem;
position: relative;
p {
width: 70%;
border: 0;
padding: 0;
font-size: 1.5rem;
height: unset;
max-height: unset;
@include mobile {
font-size: 1.3rem;
}
}
}
.green,
.green .colors-overlay > input {
background-color: $light-green;
color: $green;
}
.blue,
.blue .colors-overlay > input {
background-color: $light-blue;
color: $blue;
}
.yellow,
.yellow .colors-overlay > input {
background-color: $light-yellow;
color: $yellow;
}
.red,
.red .colors-overlay > input {
background-color: $light-red;
color: $red;
}
</style>
<style lang="scss" scoped>
@import "../styles/global.scss";
@import "../styles/variables.scss";
@import "../styles/media-queries.scss";
hr {
width: 80%;
}
h1,
h2 {
text-align: center;
}
.current-draw {
margin: auto;
}
.title-info {
width: 100%;
text-align: center;
}
.outer-chat {
margin: 0 60px 0 10px;
@include mobile {
margin: 0;
}
}
.outer-attendees {
margin: 0 10px 0 45px;
@include mobile {
margin: 0;
}
}
.center-new-winner {
margin: auto !important;
}
.middle-elements {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 400px;
@include mobile {
height: auto;
flex-direction: column;
}
}
.vipps {
margin-top: 70px;
display: flex;
padding-bottom: 50px;
justify-content: center;
@include mobile {
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,439 @@
<template>
<div class="page-container">
<h1 class="title">Virtuelt lotteri registrering</h1>
<br />
<div class="draw-winner-container" v-if="attendees.length > 0">
<div v-if="drawingWinner">
<span>
Trekker {{ currentWinners }} av {{ numberOfWinners }} vinnere.
{{ secondsLeft }} sekunder av {{ drawTime }} igjen
</span>
<button class="vin-button no-margin" @click="stopDraw">Stopp trekning</button>
</div>
<div class="draw-container" v-if="!drawingWinner">
<button class="vin-button no-margin" @click="drawWinner">Trekk vinnere</button>
<input type="number" v-model="numberOfWinners" />
</div>
</div>
<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 + '-raffle'" class="raffle-element">
<span>{{ winner.name }}</span>
<span>{{ winner.phoneNumber }}</span>
<span>Rød: {{ winner.red }}</span>
<span>Blå: {{ winner.blue }}</span>
<span>Grønn: {{ winner.green }}</span>
<span>Gul: {{ winner.yellow }}</span>
</div>
</div>
</div>
<div class="delete-buttons" v-if="attendees.length > 0 || winners.length > 0">
<button
class="vin-button"
v-if="winners.length > 0"
@click="deleteAllWinners"
>Slett virtuelle vinnere</button>
<button
class="vin-button"
v-if="attendees.length > 0"
@click="deleteAllAttendees"
>Slett virtuelle deltakere</button>
</div>
<div class="attendees" v-if="attendees.length > 0">
<h2>Deltakere ({{ attendees.length }})</h2>
<div class="attendee" v-for="(attendee, index) in attendees" :key="index">
<div class="name-and-phone">
<span class="name">{{ attendee.name }}</span>
<span class="phoneNumber">{{ attendee.phoneNumber }}</span>
</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>
<div class="attendee-registration-container">
<h2>Legg til deltaker</h2>
<div class="label-div">
<label for="name">Navn</label>
<input id="name" type="text" placeholder="Navn" v-model="name" />
</div>
<br />
<div class="label-div">
<label for="phoneNumber">Telefonnummer</label>
<input id="phoneNumber" type="text" placeholder="Telefonnummer" v-model="phoneNumber" />
</div>
<br />
<br />
<div class="label-div">
<label for="randomColors">Tilfeldig farger?</label>
<input
id="randomColors"
type="checkbox"
placeholder="Tilfeldig farger"
v-model="randomColors"
/>
</div>
<div v-if="!randomColors">
<br />
<br />
<div class="label-div">
<label for="red">Rød</label>
<input id="red" type="number" placeholder="Rød" v-model="red" />
</div>
<br />
<div class="label-div">
<label for="blue">Blå</label>
<input id="blue" type="number" placeholder="Blå" v-model="blue" />
</div>
<br />
<div class="label-div">
<label for="green">Grønn</label>
<input id="green" type="number" placeholder="Grønn" v-model="green" />
</div>
<br />
<div class="label-div">
<label for="yellow">Gul</label>
<input id="yellow" type="number" placeholder="Gul" v-model="yellow" />
</div>
</div>
<div v-else>
<RaffleGenerator @colors="setWithRandomColors" :generateOnInit="true" />
</div>
</div>
<br />
<button class="vin-button" @click="sendAttendee">Send deltaker</button>
<TextToast v-if="showToast" :text="toastText" v-on:closeToast="showToast = false" />
</div>
</template>
<script>
import io from "socket.io-client";
import {
addAttendee,
getVirtualWinner,
attendeesSecure,
attendees,
winnersSecure,
deleteWinners,
deleteAttendees,
finishedDraw,
prelottery
} from "@/api";
import TextToast from "@/ui/TextToast";
import RaffleGenerator from "@/ui/RaffleGenerator";
export default {
components: {
RaffleGenerator,
TextToast
},
data() {
return {
name: null,
phoneNumber: null,
red: 0,
blue: 0,
green: 0,
yellow: 0,
raffles: 0,
randomColors: false,
attendees: [],
winners: [],
drawingWinner: false,
secondsLeft: 20,
drawTime: 20,
currentWinners: 1,
numberOfWinners: 4,
socket: null,
toastText: undefined,
showToast: false
};
},
mounted() {
this.getAttendees();
this.getWinners();
this.socket = io(`${window.location.hostname}:${window.location.port}`);
this.socket.on("winner", async msg => {
this.getWinners();
this.getAttendees();
});
this.socket.on("refresh_data", async msg => {
this.getAttendees();
this.getWinners();
});
this.socket.on("new_attendee", async msg => {
this.getAttendees();
});
window.finishedDraw = finishedDraw;
},
methods: {
setWithRandomColors(colors) {
Object.keys(colors).forEach(color => (this[color] = colors[color]));
},
sendAttendee: async function() {
if (this.red == 0 && this.blue == 0 && this.green == 0 && this.yellow == 0) {
alert('Ingen farger valgt!')
return;
}
if (this.name == 0 && this.phoneNumber) {
alert('Ingen navn eller tlf satt!')
return;
}
let response = await addAttendee({
name: this.name,
phoneNumber: this.phoneNumber,
red: this.red,
blue: this.blue,
green: this.green,
yellow: this.yellow,
raffles: this.raffles
});
if (response == true) {
this.toastText = `Sendt inn deltaker: ${this.name}`;
this.showToast = true;
this.name = null;
this.phoneNumber = null;
this.yellow = 0;
this.green = 0;
this.red = 0;
this.blue = 0;
this.getAttendees();
} else {
alert("Klarte ikke sende inn.. Er du logget inn?");
}
},
getAttendees: async function() {
let response = await attendeesSecure();
this.attendees = response;
},
stopDraw: function() {
this.drawingWinner = false;
this.secondsLeft = this.drawTime;
},
drawWinner: async function() {
if (window.confirm("Er du sikker på at du vil trekke vinnere?")) {
this.drawingWinner = true;
let response = await getVirtualWinner();
if (response.success) {
console.log("Winner:", response.winner);
if (this.currentWinners < this.numberOfWinners) {
this.countdown();
} else {
this.drawingWinner = false;
let finished = await finishedDraw();
if(finished) {
alert("SMS'er er sendt ut!");
} else {
alert("Noe gikk galt under SMS utsendelser.. Sjekk logg og database for id'er.");
}
}
this.getWinners();
this.getAttendees();
} else {
this.drawingWinner = false;
alert("Noe gikk galt under trekningen..! " + response["message"]);
}
}
},
countdown: function() {
this.secondsLeft -= 1;
if (!this.drawingWinner) {
return;
}
if (this.secondsLeft <= 0) {
this.secondsLeft = this.drawTime;
this.currentWinners += 1;
if (this.currentWinners <= this.numberOfWinners) {
this.drawWinner();
} else {
this.drawingWinner = false;
}
return;
}
setTimeout(() => {
this.countdown();
}, 1000);
},
deleteAllWinners: async function() {
if (window.confirm("Er du sikker på at du vil slette vinnere?")) {
let response = await deleteWinners();
if (response) {
this.getWinners();
} else {
alert("Klarte ikke hente ut vinnere");
}
}
},
deleteAllAttendees: async function() {
if (window.confirm("Er du sikker på at du vil slette alle deltakere?")) {
let response = await deleteAttendees();
if (response) {
this.getAttendees();
} else {
alert("Klarte ikke hente ut vinnere");
}
}
},
getWinners: async function() {
let response = await winnersSecure();
if (response) {
this.winners = response;
} else {
alert("Klarte ikke hente ut vinnere");
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../styles/global.scss";
@import "../styles/media-queries.scss";
.draw-container {
display: flex;
justify-content: space-around;
}
.draw-winner-container,
.delete-buttons {
margin-bottom: 20px;
}
.delete-buttons {
display: flex;
}
h1 {
width: 100%;
text-align: center;
font-family: knowit, Arial;
}
h2 {
width: 100%;
text-align: center;
font-size: 1.6rem;
font-family: knowit, Arial;
}
hr {
width: 90%;
margin: 2rem auto;
color: grey;
}
.page-container {
padding: 0 1.5rem 3rem;
@include desktop {
max-width: 60vw;
margin: 0 auto;
}
}
#randomColors {
width: 40px;
height: 40px;
&:checked {
background: green;
}
}
.raffle-element {
width: 140px;
height: 150px;
margin: 20px 0;
-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;
color: #333333;
font-size: 0.75rem;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-direction: column;
&.small {
width: 45px;
height: 45px;
font-size: 1rem;
}
&.green-raffle {
background-color: $light-green;
}
&.blue-raffle {
background-color: $light-blue;
}
&.yellow-raffle {
background-color: $light-yellow;
}
&.red-raffle {
background-color: $light-red;
}
}
button {
display: flex !important;
margin: auto !important;
}
.winners {
display: flex;
justify-content: space-around;
align-items: center;
}
.attendees {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.attendee {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 50%;
margin: 0 auto;
& .name-and-phone,
& .raffles-container {
display: flex;
justify-content: center;
}
& .name-and-phone {
flex-direction: column;
}
& .raffles-container {
flex-direction: row;
}
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<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>
<h1 v-else-if="!turn && !existing" class="sent-container">Finner ikke noen vinner her..</h1>
<h1 v-else-if="!turn" class="sent-container">Du vente tur..</h1>
<div class="wines-container" v-if="name">
<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">
<h1>Valget ditt er sendt inn!</h1>
<p>Du får mer info om henting snarest!</p>
</div>
</div>
</template>
<script>
import { getAmIWinner, postWineChosen, prelottery } from "@/api";
import Wine from "@/ui/Wine";
export default {
components: { Wine },
data() {
return {
id: null,
existing: false,
fetched: false,
turn: false,
name: null,
wines: [],
posted: false
};
},
async mounted() {
this.id = this.$router.currentRoute.params.id;
let winnerObject = await getAmIWinner(this.id);
this.fetched = true;
if (!winnerObject || !winnerObject.existing) {
console.error("non existing", winnerObject);
return;
}
this.existing = true;
if (winnerObject.existing && !winnerObject.turn) {
console.error("not your turn yet", winnerObject);
return;
}
this.turn = true;
this.name = winnerObject.name;
this.wines = await prelottery();
},
methods: {
chooseWine: async function(name) {
let posted = await postWineChosen(this.id, name);
console.log("response", posted);
if (posted.success) {
this.posted = true;
}
}
}
};
</script>
<style lang="scss" scoped>
@import "@/styles/global";
.container {
display: flex;
justify-content: center;
margin-top: 2rem;
padding: 2rem;
}
.sent-container {
width: 100%;
height: 90vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-direction: column;
}
.select-wine {
margin-top: 1rem;
}
.wines-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: flex-start;
}
</style>