Merged master into feature branch.
This commit is contained in:
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<banner :routes="routes"/>
|
||||
<router-view />
|
||||
<Footer />
|
||||
<UpdateToast
|
||||
v-if="showToast"
|
||||
:text="toastText"
|
||||
:refreshButton="refreshToast"
|
||||
v-on:closeToast="closeToast"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ServiceWorkerMixin from "@/mixins/ServiceWorkerMixin";
|
||||
import banner from "@/ui/Banner";
|
||||
import Footer from "@/ui/Footer";
|
||||
import UpdateToast from "@/ui/UpdateToast";
|
||||
|
||||
export default {
|
||||
name: "vinlottis",
|
||||
components: { banner, UpdateToast, Footer },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
showToast: false,
|
||||
toastText: null,
|
||||
refreshToast: false,
|
||||
routes: [
|
||||
{
|
||||
name: "Dagens viner",
|
||||
route: "/dagens/"
|
||||
},
|
||||
{
|
||||
name: "Historie",
|
||||
route: "/history/"
|
||||
},
|
||||
{
|
||||
name: "Lotteriet",
|
||||
route: "/lottery/game/"
|
||||
},
|
||||
{
|
||||
name: "Foreslå vin",
|
||||
route: "/request"
|
||||
},
|
||||
{
|
||||
name: "Foreslåtte viner",
|
||||
route: "/requested-wines"
|
||||
},
|
||||
{
|
||||
name: "Highscore",
|
||||
route: "/highscore"
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log("SNEAKY PETE!");
|
||||
this.$on("service-worker-updated", () => {
|
||||
this.toastText = "Det er ny oppdatering av siden, vil du oppdatere?";
|
||||
this.showToast = true;
|
||||
this.refreshToast = true;
|
||||
});
|
||||
this.$on("push-allowed", () => {
|
||||
this.toastText = "Push-notifications er skrudd på!";
|
||||
this.refreshToast = false;
|
||||
this.showToast = true;
|
||||
});
|
||||
},
|
||||
computed: {},
|
||||
mixins: [ServiceWorkerMixin],
|
||||
methods: {
|
||||
closeToast: function() {
|
||||
this.showToast = false;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "styles/global.scss";
|
||||
@import "styles/positioning.scss";
|
||||
@import "styles/vinlottis-icons";
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 600;
|
||||
src: url("/../public/assets/fonts/bold.woff"),
|
||||
url("/../public/assets/fonts/bold.woff") format("woff"), local("Arial");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 300;
|
||||
src: url("/../public/assets/fonts/regular.eot"),
|
||||
url("/../public/assets/fonts/regular.woff") format("woff"), local("Arial");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: $primary;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
background-color: white;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: 80px auto 100px;
|
||||
|
||||
.main-container{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
395
src/api.js
395
src/api.js
@@ -1,395 +0,0 @@
|
||||
import fetch from "node-fetch";
|
||||
|
||||
const BASE_URL = __APIURL__ || window.location.origin;
|
||||
|
||||
const statistics = () => {
|
||||
const url = new URL("/api/purchase/statistics", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const colorStatistics = () => {
|
||||
const url = new URL("/api/purchase/statistics/color", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const highscoreStatistics = () => {
|
||||
const url = new URL("/api/highscore/statistics", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const overallWineStatistics = () => {
|
||||
const url = new URL("/api/wines/statistics/overall", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const allRequestedWines = () => {
|
||||
const url = new URL("/api/request/all", BASE_URL);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => {
|
||||
const isAdmin = resp.headers.get("vinlottis-admin") == "true";
|
||||
return Promise.all([resp.json(), isAdmin]);
|
||||
});
|
||||
};
|
||||
|
||||
const chartWinsByColor = () => {
|
||||
const url = new URL("/api/purchase/statistics/color", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const chartPurchaseByColor = () => {
|
||||
const url = new URL("/api/purchase/statistics", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const prelottery = () => {
|
||||
const url = new URL("/api/wines/prelottery", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const sendLottery = sendObject => {
|
||||
const url = new URL("/api/lottery", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const sendLotteryWinners = sendObject => {
|
||||
const url = new URL("/api/lottery/winners", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const addAttendee = sendObject => {
|
||||
const url = new URL("/api/virtual/attendee/add", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(sendObject)
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const getVirtualWinner = () => {
|
||||
const url = new URL("/api/virtual/winner/draw", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const attendeesSecure = () => {
|
||||
const url = new URL("/api/virtual/attendee/all/secure", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const winnersSecure = () => {
|
||||
const url = new URL("/api/virtual/winner/all/secure", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const winners = () => {
|
||||
const url = new URL("/api/virtual/winner/all", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const deleteRequestedWine = wineToBeDeleted => {
|
||||
|
||||
const url = new URL("api/request/"+ wineToBeDeleted.id, BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(wineToBeDeleted)
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json())
|
||||
}
|
||||
|
||||
const deleteWinners = () => {
|
||||
const url = new URL("/api/virtual/winner/all", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const deleteAttendees = () => {
|
||||
const url = new URL("/api/virtual/attendee/all", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "DELETE"
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const attendees = () => {
|
||||
const url = new URL("/api/virtual/attendee/all", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const requestNewWine = (wine) => {
|
||||
const options = {
|
||||
body: JSON.stringify({
|
||||
wine: wine
|
||||
}),
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "post"
|
||||
}
|
||||
|
||||
const url = new URL("/api/request/new-wine", BASE_URL)
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json())
|
||||
}
|
||||
|
||||
const logWines = wines => {
|
||||
const url = new URL("/api/log/wines", BASE_URL);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify(wines)
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const wineSchema = () => {
|
||||
const url = new URL("/api/wineinfo/schema", BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const barcodeToVinmonopolet = id => {
|
||||
const url = new URL("/api/wineinfo/" + id, BASE_URL);
|
||||
|
||||
return fetch(url.href).then(async resp => {
|
||||
if (!resp.ok) {
|
||||
if (resp.status == 404) {
|
||||
throw await resp.json();
|
||||
}
|
||||
} else {
|
||||
return resp.json();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const searchForWine = searchString => {
|
||||
const url = new URL("/api/wineinfo/search?query=" + searchString, BASE_URL);
|
||||
|
||||
return fetch(url.href).then(async resp => {
|
||||
if (!resp.ok) {
|
||||
if (resp.status == 404) {
|
||||
throw await resp.json();
|
||||
}
|
||||
} else {
|
||||
return resp.json();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleErrors = async resp => {
|
||||
if ([400, 409].includes(resp.status)) {
|
||||
throw await resp.json();
|
||||
} else {
|
||||
console.error("Unexpected error occured when login/register user:", resp);
|
||||
throw await resp.json();
|
||||
}
|
||||
};
|
||||
|
||||
const login = (username, password) => {
|
||||
const url = new URL("/login", BASE_URL);
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const register = (username, password) => {
|
||||
const url = new URL("/register", BASE_URL);
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getChatHistory = (skip = null, take = null) => {
|
||||
const url = new URL("/api/chat/history", BASE_URL);
|
||||
if (!isNaN(skip)) url.searchParams.append("skip", skip);
|
||||
if (!isNaN(take)) url.searchParams.append("take", take);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const finishedDraw = () => {
|
||||
const url = new URL("/api/virtual/finish", BASE_URL);
|
||||
const options = {
|
||||
method: 'POST'
|
||||
}
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const getAmIWinner = id => {
|
||||
const url = new URL(`/api/winner/${id}`, BASE_URL);
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
const postWineChosen = (id, wineName) => {
|
||||
const url = new URL(`/api/winner/${id}`, BASE_URL);
|
||||
const options = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({ wineName: wineName })
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const historyAll = () => {
|
||||
const url = new URL(`/api/lottery/all`, BASE_URL);
|
||||
|
||||
return fetch(url.href).then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
} else {
|
||||
return handleErrors(resp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
highscoreStatistics,
|
||||
overallWineStatistics,
|
||||
chartWinsByColor,
|
||||
chartPurchaseByColor,
|
||||
prelottery,
|
||||
sendLottery,
|
||||
sendLotteryWinners,
|
||||
logWines,
|
||||
wineSchema,
|
||||
barcodeToVinmonopolet,
|
||||
searchForWine,
|
||||
requestNewWine,
|
||||
allRequestedWines,
|
||||
login,
|
||||
register,
|
||||
addAttendee,
|
||||
getVirtualWinner,
|
||||
attendeesSecure,
|
||||
attendees,
|
||||
winners,
|
||||
winnersSecure,
|
||||
deleteWinners,
|
||||
deleteAttendees,
|
||||
deleteRequestedWine,
|
||||
getChatHistory,
|
||||
finishedDraw,
|
||||
getAmIWinner,
|
||||
postWineChosen,
|
||||
historyAll,
|
||||
historyByDate,
|
||||
getWinnerByName
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
<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>
|
||||
@@ -1,64 +0,0 @@
|
||||
<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 "./src/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>
|
||||
@@ -1,131 +0,0 @@
|
||||
<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>
|
||||
-
|
||||
<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 { 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: {
|
||||
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 "./src/styles/media-queries";
|
||||
@import "./src/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>
|
||||
@@ -1,47 +0,0 @@
|
||||
<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>
|
||||
@@ -1,83 +0,0 @@
|
||||
<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 { page, event } from "vue-analytics";
|
||||
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() {
|
||||
this.$ga.page("/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>
|
||||
@@ -1,177 +0,0 @@
|
||||
<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> {{ person.name }} - {{ person.wins.length }}
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="center desktop-only">
|
||||
👈 Se dine vin(n), trykk på 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 "./src/styles/media-queries.scss";
|
||||
@import "./src/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>
|
||||
@@ -1,44 +0,0 @@
|
||||
<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>
|
||||
@@ -1,49 +0,0 @@
|
||||
<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>
|
||||
@@ -1,57 +0,0 @@
|
||||
<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>
|
||||
@@ -1,273 +0,0 @@
|
||||
<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 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;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -1,723 +0,0 @@
|
||||
<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>
|
||||
@@ -1,237 +0,0 @@
|
||||
<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 "./src/styles/media-queries";
|
||||
@import "./src/styles/global";
|
||||
@import "./src/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>
|
||||
@@ -1,207 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>Slagsbetingelser</h1>
|
||||
<section class="chapter cf" id="chapter-1">
|
||||
<h2 class="h2-title">Innledning</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dette kjøpet er regulert av de nedenstående standard salgsbetingelser for forbrukerkjøp av varer over Internett. Forbrukerkjøp over internett reguleres hovedsakelig av avtaleloven, forbrukerkjøpsloven, markedsføringsloven, angrerettloven og ehandelsloven, og disse lovene gir forbrukeren ufravikelige rettigheter. Lovene er tilgjengelig på
|
||||
<a target="_blank" class="vin-link" href="http://www.lovdata.no/" rel="noopener">www.lovdata.no.</a>
|
||||
Vilkårene i denne avtalen skal ikke forstås som noen begrensning i de lovbestemte rettighetene, men oppstiller partenes viktigste rettigheter og plikter for handelen.
|
||||
</p>
|
||||
<p>
|
||||
Salgsbetingelsene er utarbeidet og anbefalt av Forbrukertilsynet.
|
||||
<a class="vin-link" href="https://forbrukertilsynet.no/lov-og-rett/veiledninger-og-retningslinjer/veiledning-standard-salgsbetingelser-forbrukerkjop-varer-internett">For en bedre forståelse av disse salgsbetingelsene, se Forbrukertilsynets veileder her. </a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-2">
|
||||
<h2 class="h2-title">1. Avtalen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Avtalen består av disse salgsbetingelsene, opplysninger gitt i bestillingsløsningen og eventuelt særskilt avtalte vilkår. Ved eventuell motstrid mellom opplysningene, går det som særskilt er avtalt mellom partene foran, så fremt det ikke strider mot ufravikelig lovgivning.</p>
|
||||
<p>Avtalen vil i tillegg bli utfylt av relevante lovbestemmelser som regulerer kjøp av varer mellom næringsdrivende og forbrukere.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-3">
|
||||
<h2 class="h2-title">2. Partene</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Selger er KEVIN MIDBØE, Schleppegrells gate 18, questions@vinlottis.no/kevin.midboe@gmail.com, 926432478, og betegnes i det følgende som selger/selgeren.</p>
|
||||
<p>Kjøper er den forbrukeren som foretar bestillingen, og betegnes i det følgende som kjøper/kjøperen.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-4">
|
||||
<h2 class="h2-title">3. Pris</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Den oppgitte prisen for varen og tjenester er den totale prisen kjøper skal betale. Denne prisen inkluderer alle avgifter og tilleggskostnader. Ytterligere kostnader som selger før kjøpet ikke har informert om, skal kjøper ikke bære.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-5">
|
||||
<h2 class="h2-title">4. Avtaleinngåelse</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Avtalen er bindende for begge parter når kjøperen har sendt sin bestilling til selgeren.</p>
|
||||
<p>Avtalen er likevel ikke bindende hvis det har forekommet skrive- eller tastefeil i tilbudet fra selgeren i bestillingsløsningen i nettbutikken eller i kjøperens bestilling, og den annen part innså eller burde ha innsett at det forelå en slik feil.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-6">
|
||||
<h2 class="h2-title">5. Betalingen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Selgeren kan kreve betaling for varen fra det tidspunkt den blir sendt fra selgeren til kjøperen.</p>
|
||||
<p>Dersom kjøperen bruker kredittkort eller debetkort ved betaling, kan selgeren reservere kjøpesummen på kortet ved bestilling. Kortet blir belastet samme dag som varen sendes.</p>
|
||||
<p>Ved betaling med faktura, blir fakturaen til kjøperen utstedt ved forsendelse av varen. Betalingsfristen fremgår av fakturaen og er på minimum 14 dager fra mottak.</p>
|
||||
<p>Kjøpere under 18 år kan ikke betale med etterfølgende faktura.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-7">
|
||||
<h2 class="h2-title">6. Levering</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Levering er skjedd når kjøperen, eller hans representant, har overtatt tingen.</p>
|
||||
<p>Hvis ikke leveringstidspunkt fremgår av bestillingsløsningen, skal selgeren levere varen til kjøper uten unødig opphold og senest 30 dager etter bestillingen fra kunden. Varen skal leveres hos kjøperen med mindre annet er særskilt avtalt mellom partene.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-8">
|
||||
<h2 class="h2-title">7. Risikoen for varen</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Risikoen for varen går over på kjøper når han, eller hans representant, har fått varene levert i tråd med punkt 6.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-9">
|
||||
<h2 class="h2-title">8. Angrerett</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Med mindre avtalen er unntatt fra angrerett, kan kjøperen angre kjøpet av varen i henhold til angrerettloven.</p>
|
||||
<p>Kjøperen må gi selger melding om bruk av angreretten innen 14 dager fra fristen begynner å løpe. I fristen inkluderes alle kalenderdager. Dersom fristen ender på en lørdag, helligdag eller høytidsdag forlenges fristen til nærmeste virkedag.</p>
|
||||
<p>Angrefristen anses overholdt dersom melding er sendt før utløpet av fristen. Kjøper har bevisbyrden for at angreretten er blitt gjort gjeldende, og meldingen bør derfor skje skriftlig (angrerettskjema, e-post eller brev).</p>
|
||||
<p>Angrefristen begynner å løpe:</p>
|
||||
<ul>
|
||||
<li>Ved kjøp av enkeltstående varer vil angrefristen løpe fra dagen etter varen(e) er mottatt.</li>
|
||||
<li>Selges et abonnement, eller innebærer avtalen regelmessig levering av identiske varer, løper fristen fra dagen etter første forsendelse er mottatt.</li>
|
||||
<li>Består kjøpet av flere leveranser, vil angrefristen løpe fra dagen etter siste leveranse er mottatt.</li>
|
||||
</ul>
|
||||
<p>Angrefristen utvides til 12 måneder etter utløpet av den opprinnelige fristen dersom selger ikke før avtaleinngåelsen opplyser om at det foreligger angrerett og standardisert angreskjema. Tilsvarende gjelder ved manglende opplysning om vilkår, tidsfrister og fremgangsmåte for å benytte angreretten. Sørger den næringsdrivende for å gi opplysningene i løpet av disse 12 månedene, utløper angrefristen likevel 14 dager etter den dagen kjøperen mottok opplysningene.</p>
|
||||
<p>Ved bruk av angreretten må varen leveres tilbake til selgeren uten unødig opphold og senest 14 dager fra melding om bruk av angreretten er gitt. Kjøper dekker de direkte kostnadene ved å returnere varen, med mindre annet er avtalt eller selger har unnlatt å opplyse om at kjøper skal dekke returkostnadene. Selgeren kan ikke fastsette gebyr for kjøperens bruk av angreretten.</p>
|
||||
<p>Kjøper kan prøve eller teste varen på en forsvarlig måte for å fastslå varens art, egenskaper og funksjon, uten at angreretten faller bort. Dersom prøving eller test av varen går utover hva som er forsvarlig og nødvendig, kan kjøperen bli ansvarlig for eventuell redusert verdi på varen.</p>
|
||||
<p>Selgeren er forpliktet til å tilbakebetale kjøpesummen til kjøperen uten unødig opphold, og senest 14 dager fra selgeren fikk melding om kjøperens beslutning om å benytte angreretten. Selger har rett til å holde tilbake betalingen til han har mottatt varene fra kjøperen, eller til kjøper har lagt frem dokumentasjon for at varene er sendt tilbake.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-10">
|
||||
<h2 class="h2-title">9. Forsinkelse og manglende levering - kjøpernes rettigheter og frist for å melde krav</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dersom selgeren ikke leverer varen eller leverer den for sent i henhold til avtalen mellom partene, og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold til reglene i forbrukerkjøpslovens kapittel 5 etter omstendighetene
|
||||
<em>holde kjøpesummen tilbake</em>
|
||||
, kreve
|
||||
<em>oppfyl</em>
|
||||
<em>lelse</em>
|
||||
,
|
||||
<em>heve </em>
|
||||
avtalen og/eller kreve
|
||||
<em>erstatning </em>
|
||||
fra selgeren.
|
||||
</p>
|
||||
<p>Ved krav om misligholdsbeføyelser bør meldingen av bevishensyn være skriftlig (for eksempel e-post).</p>
|
||||
<h3>Oppfyllelse</h3>
|
||||
<p>Kjøper kan fastholde kjøpet og kreve oppfyllelse fra selger. Kjøper kan imidlertid ikke kreve oppfyllelse dersom det foreligger en hindring som selgeren ikke kan overvinne, eller dersom oppfyllelse vil medføre en så stor ulempe eller kostnad for selger at det står i vesentlig misforhold til kjøperens interesse i at selgeren oppfyller. Skulle vanskene falle bort innen rimelig tid, kan kjøper likevel kreve oppfyllelse.</p>
|
||||
<p>Kjøperen taper sin rett til å kreve oppfyllelse om han eller hun venter urimelig lenge med å fremme kravet.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Dersom selgeren ikke leverer varen på leveringstidspunktet, skal kjøperen oppfordre selger til å levere innen en rimelig tilleggsfrist for oppfyllelse. Dersom selger ikke leverer varen innen tilleggsfristen, kan kjøperen heve kjøpet.</p>
|
||||
<p>Kjøper kan imidlertid heve kjøpet umiddelbart hvis selger nekter å levere varen. Tilsvarende gjelder dersom levering til avtalt tid var avgjørende for inngåelsen av avtalen, eller dersom kjøperen har underrettet selger om at leveringstidspunktet er avgjørende.</p>
|
||||
<p>Leveres tingen etter tilleggsfristen forbrukeren har satt eller etter leveringstidspunktet som var avgjørende for inngåelsen av avtalen, må krav om heving gjøres gjeldende innen rimelig tid etter at kjøperen fikk vite om leveringen.</p>
|
||||
<h3>Erstatning</h3>
|
||||
<p>Kjøperen kan kreve erstatning for lidt tap som følge av forsinkelsen. Dette gjelder imidlertid ikke dersom selgeren godtgjør at forsinkelsen skyldes hindring utenfor selgers kontroll som ikke med rimelighet kunne blitt tatt i betraktning på avtaletiden, unngått, eller overvunnet følgene av.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-11">
|
||||
<h2 class="h2-title">10. Mangel ved varen - kjøperens rettigheter og reklamasjonsfrist</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Hvis det foreligger en mangel ved varen må kjøper innen rimelig tid etter at den ble oppdaget eller burde ha blitt oppdaget, gi selger melding om at han eller hun vil påberope seg mangelen. Kjøper har alltid reklamert tidsnok dersom det skjer innen 2 mnd. fra mangelen ble oppdaget eller burde blitt oppdaget. Reklamasjon kan skje senest to år etter at kjøper overtok varen. Dersom varen eller deler av den er ment å vare vesentlig lenger enn to år, er reklamasjonsfristen fem år.</p>
|
||||
<p>
|
||||
Dersom varen har en mangel og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold til reglene i forbrukerkjøpsloven kapittel 6 etter omstendighetene
|
||||
<em>holde kjøpesummen tilbake</em>
|
||||
, velge mellom
|
||||
<em>retting </em>
|
||||
og
|
||||
<em>omlevering</em>
|
||||
, kreve
|
||||
<em>prisavslag</em>
|
||||
, kreve avtalen hevet og/eller kreve
|
||||
<em>erstatning </em>
|
||||
fra selgeren.
|
||||
</p>
|
||||
<p>Reklamasjon til selgeren bør skje skriftlig.</p>
|
||||
<h3>Retting eller omlevering</h3>
|
||||
<p>Kjøperen kan velge mellom å kreve mangelen rettet eller levering av tilsvarende ting. Selger kan likevel motsette seg kjøperens krav dersom gjennomføringen av kravet er umulig eller volder selgeren urimelige kostnader. Retting eller omlevering skal foretas innen rimelig tid. Selger har i utgangspunktet ikke rett til å foreta mer enn to avhjelpsforsøk for samme mangel.</p>
|
||||
<h3>Prisavslag</h3>
|
||||
<p>Kjøper kan kreve et passende prisavslag dersom varen ikke blir rettet eller omlevert. Dette innebærer at forholdet mellom nedsatt og avtalt pris svarer til forholdet mellom tingens verdi i mangelfull og kontraktsmessig stand. Dersom særlige grunner taler for det, kan prisavslaget i stedet settes lik mangelens betydning for kjøperen.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Dersom varen ikke er rettet eller omlevert, kan kjøperen også heve kjøpet når mangelen ikke er uvesentlig.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-12">
|
||||
<h2 class="h2-title">11. Selgerens rettigheter ved kjøperens mislighold</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Dersom kjøperen ikke betaler eller oppfyller de øvrige pliktene etter avtalen eller loven, og dette ikke skyldes selgeren eller forhold på selgerens side, kan selgeren i henhold til reglene i forbrukerkjøpsloven kapittel 9 etter omstendighetene
|
||||
<em>holde</em>
|
||||
<em>varen tilbake</em>
|
||||
, kreve
|
||||
<em>oppfyllelse </em>
|
||||
av avtalen, kreve avtalen
|
||||
<em>hevet </em>
|
||||
samt kreve
|
||||
<em>erstatning </em>
|
||||
fra kjøperen. Selgeren vil også etter omstendighetene kunne kreve
|
||||
<em>renter ved forsinket betaling, inkassogebyr</em>
|
||||
og et rimelig
|
||||
<em>gebyr ved uavhentede varer</em>
|
||||
.
|
||||
</p>
|
||||
<h3>Oppfyllelse</h3>
|
||||
<p>Selger kan fastholde kjøpet og kreve at kjøperen betaler kjøpesummen. Er varen ikke levert, taper selgeren sin rett dersom han venter urimelig lenge med å fremme kravet.</p>
|
||||
<h3>Heving</h3>
|
||||
<p>Selger kan heve avtalen dersom det foreligger vesentlig betalingsmislighold eller annet vesentlig mislighold fra kjøperens side. Selger kan likevel ikke heve dersom hele kjøpesummen er betalt. Fastsetter selger en rimelig tilleggsfrist for oppfyllelse og kjøperen ikke betaler innen denne fristen, kan selger heve kjøpet.</p>
|
||||
<h3>Renter ved forsinket betaling/inkassogebyr</h3>
|
||||
<p>Dersom kjøperen ikke betaler kjøpesummen i henhold til avtalen, kan selger kreve renter av kjøpesummen etter forsinkelsesrenteloven. Ved manglende betaling kan kravet, etter forutgående varsel, bli sendt til Kjøper kan da bli holdt ansvarlig for gebyr etter inkassoloven.</p>
|
||||
<h3>Gebyr ved uavhentede ikke-forskuddsbetalte varer</h3>
|
||||
<p>Dersom kjøperen unnlater å hente ubetalte varer, kan selger belaste kjøper med et gebyr. Gebyret skal maksimalt dekke selgerens faktiske utlegg for å levere varen til kjøperen. Et slikt gebyr kan ikke belastes kjøpere under 18 år.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-13">
|
||||
<h2 class="h2-title">12. Garanti</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Garanti som gis av selgeren eller produsenten, gir kjøperen rettigheter i tillegg til de kjøperen allerede har etter ufravikelig lovgivning. En garanti innebærer dermed ingen begrensninger i kjøperens rett til reklamasjon og krav ved forsinkelse eller mangler etter punkt 9 og 10.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-14">
|
||||
<h2 class="h2-title">13. Personopplysninger</h2>
|
||||
<div class="content-wrap">
|
||||
<p>Behandlingsansvarlig for innsamlede personopplysninger er selger. Med mindre kjøperen samtykker til noe annet, kan selgeren, i tråd med personopplysningsloven, kun innhente og lagre de personopplysninger som er nødvendig for at selgeren skal kunne gjennomføre forpliktelsene etter avtalen. Kjøperens personopplysninger vil kun bli utlevert til andre hvis det er nødvendig for at selger skal få gjennomført avtalen med kjøperen, eller i lovbestemte tilfelle.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="chapter cf" id="chapter-15">
|
||||
<h2 class="h2-title">14. Konfliktløsning</h2>
|
||||
<div class="content-wrap">
|
||||
<p>
|
||||
Klager rettes til selger innen rimelig tid, jf. punkt 9 og 10. Partene skal forsøke å løse eventuelle tvister i minnelighet. Dersom dette ikke lykkes, kan kjøperen ta kontakt med Forbrukerrådet for mekling. Forbrukerrådet er tilgjengelig på telefon 23 400 500 eller
|
||||
<a target="_blank" class="vin-link" href="http://www.forbrukerradet.no/" rel="noopener">www.forbrukerradet.no.</a>
|
||||
</p>
|
||||
<p>
|
||||
Europa-Kommisjonens klageportal kan også brukes hvis du ønsker å inngi en klage. Det er særlig relevant, hvis du er forbruker bosatt i et annet EU-land. Klagen inngis her:
|
||||
<a class="vin-link" href="http://ec.europa.eu/odr">http://ec.europa.eu/odr</a>
|
||||
.
|
||||
</p>
|
||||
<p> </p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.container {
|
||||
margin: 3rem;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<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 { page, event } from "vue-analytics";
|
||||
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 "./src/styles/media-queries";
|
||||
@import "./src/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>
|
||||
@@ -1,383 +0,0 @@
|
||||
<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 { page, event } from "vue-analytics";
|
||||
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() {
|
||||
this.$ga.page("/");
|
||||
},
|
||||
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>
|
||||
@@ -1,446 +0,0 @@
|
||||
<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"
|
||||
:chatHistory="chatHistory"
|
||||
:historyPageSize="historyPageSize"
|
||||
:usernameAllowed="usernameAllowed"
|
||||
@loadMoreHistory="loadMoreHistory"
|
||||
@message="sendMessage"
|
||||
@username="setUsername"
|
||||
/>
|
||||
</div>
|
||||
<Vipps class="vipps" :amount="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { page, event } from "vue-analytics";
|
||||
import { attendees, winners, getChatHistory, 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,
|
||||
chatHistory: [],
|
||||
historyPage: 0,
|
||||
historyPageSize: 100,
|
||||
lastHistoryPage: false,
|
||||
usernameAccepted: false,
|
||||
username: null,
|
||||
wasDisconnected: false,
|
||||
emitUsernameOnConnect: false,
|
||||
ticketsBought: {}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
getChatHistory(0, 100).then(messages => (this.chatHistory = messages));
|
||||
},
|
||||
mounted() {
|
||||
this.track();
|
||||
this.getAttendees();
|
||||
this.getWinners();
|
||||
this.socket = io(`${window.location.hostname}:${window.location.port}`);
|
||||
this.socket.on("color_winner", msg => {});
|
||||
|
||||
this.socket.on("chat", msg => {
|
||||
this.chatHistory.push(msg);
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", msg => {
|
||||
this.wasDisconnected = true;
|
||||
});
|
||||
|
||||
this.socket.on("connect", msg => {
|
||||
if (
|
||||
this.emitUsernameOnConnect ||
|
||||
(this.wasDisconnected && this.username != null)
|
||||
) {
|
||||
this.setUsername(this.username);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("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();
|
||||
});
|
||||
this.socket.on("accept_username", accepted => {
|
||||
this.usernameAccepted = accepted;
|
||||
if (!accepted) {
|
||||
this.username = null;
|
||||
} else {
|
||||
window.localStorage.setItem("username", this.username);
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
},
|
||||
computed: {
|
||||
todayExists: () => {
|
||||
return prelottery()
|
||||
.then(wines => wines.length > 0)
|
||||
.catch(() => false);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUsername: function(username) {
|
||||
this.username = username;
|
||||
if (!this.socket || !this.socket.emit) {
|
||||
this.emitUsernameOnConnect = true;
|
||||
return;
|
||||
}
|
||||
this.socket.emit("username", { username });
|
||||
},
|
||||
sendMessage: function(msg) {
|
||||
this.socket.emit("chat", { message: msg });
|
||||
},
|
||||
loadMoreHistory: function() {
|
||||
const { historyPage, historyPageSize } = this;
|
||||
const page = historyPage + 1;
|
||||
|
||||
getChatHistory(page * historyPageSize, historyPageSize).then(messages => {
|
||||
this.chatHistory = messages.concat(this.chatHistory);
|
||||
this.historyPage = page;
|
||||
});
|
||||
},
|
||||
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() {
|
||||
this.$ga.page("/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>
|
||||
@@ -1,439 +0,0 @@
|
||||
<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>
|
||||
@@ -1,100 +0,0 @@
|
||||
<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 må vente på 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 "./src/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>
|
||||
@@ -1,2 +0,0 @@
|
||||
import Vue from "vue";
|
||||
export default new Vue();
|
||||
@@ -1,95 +0,0 @@
|
||||
var serviceWorkerRegistrationMixin = {
|
||||
created: function() {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
console.log("Nettleseren din støtter ikke service-workers.");
|
||||
return;
|
||||
}
|
||||
if ("PushManager" in window && __PUSHENABLED__) {
|
||||
if (Notification.permission !== "granted") {
|
||||
localStorage.removeItem("push");
|
||||
}
|
||||
}
|
||||
this.registerPushListener();
|
||||
this.registerServiceWorker();
|
||||
},
|
||||
methods: {
|
||||
registerPushListener: function() {
|
||||
try {
|
||||
const channel = new BroadcastChannel("updatePush");
|
||||
channel.addEventListener("message", event => {
|
||||
if (event.data.success) {
|
||||
localStorage.setItem("push", true);
|
||||
this.$emit("push-allowed");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Using safari 'eh? No notifications for you.");
|
||||
}
|
||||
},
|
||||
sendMessage: function(message) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = function(event) {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error);
|
||||
} else {
|
||||
resolve(event.data);
|
||||
}
|
||||
};
|
||||
if (navigator.serviceWorker.controller == null) {
|
||||
resolve();
|
||||
} else {
|
||||
navigator.serviceWorker.controller.postMessage(message, [
|
||||
messageChannel.port2
|
||||
]);
|
||||
}
|
||||
});
|
||||
},
|
||||
serviceWorkerUpdateFoundListener: function(serviceWorker) {
|
||||
const installingWorker = serviceWorker.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (
|
||||
installingWorker.state === "installed" &&
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
this.$emit("service-worker-updated");
|
||||
}
|
||||
};
|
||||
},
|
||||
registerServiceWorkerPushNotification: function() {
|
||||
if (!("PushManager" in window)) {
|
||||
throw new Error("No Push API Support!");
|
||||
}
|
||||
window.Notification.requestPermission().then(permission => {
|
||||
if (permission !== "granted") {
|
||||
console.log(
|
||||
"Du valgte å ikke ha arbeids-arbeideren til å sende deg dytte-meldinger :'('"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (localStorage.getItem("push") == null) {
|
||||
this.sendMessage("updatePush");
|
||||
}
|
||||
});
|
||||
},
|
||||
registerServiceWorker: function() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then(serviceWorker => {
|
||||
console.log(
|
||||
"Arbeids arbeideren din er installert. Du kan nå gå offline frem til neste trekning."
|
||||
);
|
||||
serviceWorker.onupdatefound = () => {
|
||||
this.serviceWorkerUpdateFoundListener(serviceWorker);
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Arbeids arbeideren klarer ikke arbeide.", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = serviceWorkerRegistrationMixin;
|
||||
@@ -1,205 +0,0 @@
|
||||
var version = "v1.0" + __DATE__;
|
||||
var cacheName = "vinlottis";
|
||||
var CACHE_NAME = cacheName;
|
||||
var CACHE_NAME_API = cacheName + "::api";
|
||||
var STATIC_CACHE_URLS = ["/"];
|
||||
|
||||
console.log("Nåværende versjon:", version);
|
||||
self.addEventListener("activate", event => {
|
||||
console.log("Aktiverer");
|
||||
|
||||
event.waitUntil(self.clients.claim());
|
||||
event.waitUntil(removeCache(CACHE_NAME));
|
||||
event.waitUntil(removeCache(CACHE_NAME_API));
|
||||
event.waitUntil(addCache(CACHE_NAME, STATIC_CACHE_URLS));
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", function(event) {
|
||||
event.notification.close();
|
||||
if (
|
||||
event.notification.data != undefined &&
|
||||
event.notification.data.link != undefined
|
||||
) {
|
||||
event.waitUntil(clients.openWindow(event.notification.data.link));
|
||||
} else {
|
||||
event.waitUntil(clients.openWindow("/"));
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("message", event => {
|
||||
if (!__PUBLICKEY__) {
|
||||
return;
|
||||
}
|
||||
if (event.data === "updatePush") {
|
||||
event.waitUntil(
|
||||
new Promise((resolve, reject) => {
|
||||
const applicationServerKey = urlB64ToUint8Array(__PUBLICKEY__);
|
||||
const options = { applicationServerKey, userVisibleOnly: true };
|
||||
self.registration.pushManager
|
||||
.subscribe(options)
|
||||
.then(subscription =>
|
||||
saveSubscription(subscription)
|
||||
.then(() => {
|
||||
try {
|
||||
const channel = new BroadcastChannel("updatePush");
|
||||
channel.postMessage({ success: true });
|
||||
} catch (e) {
|
||||
console.log("Using safari 'eh? No notifications for you.");
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(() => {
|
||||
resolve();
|
||||
})
|
||||
)
|
||||
.catch(() => {
|
||||
console.log("Kunne ikke legge til pushnotifications");
|
||||
reject();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("push", function(event) {
|
||||
if (event.data) {
|
||||
var message = JSON.parse(event.data.text());
|
||||
var link = "/";
|
||||
if (message.link != undefined) {
|
||||
link = message.link;
|
||||
}
|
||||
|
||||
showLocalNotification(
|
||||
message.title,
|
||||
message.message,
|
||||
link,
|
||||
self.registration
|
||||
);
|
||||
} else {
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("install", event => {
|
||||
console.log("Arbeids arbeideren installerer seg.");
|
||||
self.skipWaiting();
|
||||
event.waitUntil(addCache(CACHE_NAME, STATIC_CACHE_URLS));
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", event => {
|
||||
if (
|
||||
event.request.url.includes("/login") ||
|
||||
event.request.url.includes("/update") ||
|
||||
event.request.url.includes("/register") ||
|
||||
event.request.method == "POST" ||
|
||||
event.request.url.includes("/api/wines/prelottery") ||
|
||||
event.request.url.includes("/api/virtual") ||
|
||||
event.request.url.includes("/socket.io")
|
||||
) {
|
||||
event.respondWith(fetch(event.request));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.request.cache === "only-if-cached" &&
|
||||
event.request.mode !== "same-origin"
|
||||
)
|
||||
return;
|
||||
|
||||
if (event.request.url.includes("/api/")) {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then(response => cache(event.request, response))
|
||||
.catch(function() {
|
||||
return caches.match(event.request);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
event.respondWith(
|
||||
caches
|
||||
.match(event.request) // check if the request has already been cached
|
||||
.then(cached => cached || fetch(event.request)) // otherwise request network
|
||||
.then(
|
||||
response =>
|
||||
staticCache(event.request, response) // put response in cache
|
||||
.then(() => response) // resolve promise with the network response
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function showLocalNotification(title, body, link, swRegistration) {
|
||||
const options = {
|
||||
body,
|
||||
icon: "https://lottis.vin/public/assets/images/favicon.png",
|
||||
image: "https://lottis.vin/public/assets/images/favicon.png",
|
||||
vibrate: [300],
|
||||
data: { link: link }
|
||||
};
|
||||
swRegistration.showNotification(title, options);
|
||||
}
|
||||
|
||||
async function saveSubscription(subscription) {
|
||||
const SERVER_URL = "/subscription/save-subscription";
|
||||
const response = await fetch(SERVER_URL, {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(subscription)
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const urlB64ToUint8Array = base64String => {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/\-/g, "+")
|
||||
.replace(/_/g, "/");
|
||||
const rawData = atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
};
|
||||
|
||||
function addCache(cacheKey, cacheUrls) {
|
||||
return caches.open(cacheKey).then(cache => {
|
||||
console.log("Legger til cache", cache);
|
||||
return cache.addAll(cacheUrls);
|
||||
});
|
||||
}
|
||||
|
||||
function removeCache(cacheKey) {
|
||||
return caches
|
||||
.keys()
|
||||
.then(keys => keys.filter(key => key !== cacheKey))
|
||||
.then(keys =>
|
||||
Promise.all(
|
||||
keys.map(key => {
|
||||
console.log(`Sletter mellom-lager på nøkkel ${key}`);
|
||||
return caches.delete(key);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function staticCache(request, response) {
|
||||
if (response.type === "error" || response.type === "opaque") {
|
||||
return Promise.resolve(); // do not put in cache network errors
|
||||
}
|
||||
|
||||
return caches
|
||||
.open(CACHE_NAME)
|
||||
.then(cache => cache.put(request, response.clone()));
|
||||
}
|
||||
|
||||
function cache(request, response) {
|
||||
if (response.type === "error" || response.type === "opaque") {
|
||||
return response;
|
||||
}
|
||||
|
||||
return caches.open(CACHE_NAME_API).then(cache => {
|
||||
cache.put(request, response.clone());
|
||||
return response;
|
||||
});
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
@import "./media-queries.scss";
|
||||
@import "./variables.scss";
|
||||
|
||||
.top-banner{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 1fr 0.5fr;
|
||||
grid-template-areas: "menu logo clock";
|
||||
grid-gap: 1em;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
background-color: $primary;
|
||||
|
||||
// ios homescreen app whitespace above header fix.
|
||||
&::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
position: absolute;
|
||||
top: -3rem;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.company-logo{
|
||||
grid-area: logo;
|
||||
}
|
||||
|
||||
.menu-toggle-container{
|
||||
grid-area: menu;
|
||||
color: #1e1e1e;
|
||||
border-radius: 50% 50%;
|
||||
z-index: 3;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span{
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
height: 3px;
|
||||
width: 18px;
|
||||
background: #111;
|
||||
z-index: 1;
|
||||
transform-origin: 4px 0px;
|
||||
transition:
|
||||
transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
|
||||
background 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
|
||||
opacity 0.55s ease;
|
||||
}
|
||||
|
||||
span:first-child{
|
||||
transform-origin: 0% 0%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
span:nth-last-child(2){
|
||||
transform-origin: 0% 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&.open{
|
||||
span{
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg) translate(2px, -2px);
|
||||
background: #232323;
|
||||
}
|
||||
|
||||
span:nth-last-child(2){
|
||||
opacity: 0;
|
||||
transform: rotate(0deg) scale(0.2, 0.2);
|
||||
}
|
||||
|
||||
span:nth-last-child(3){
|
||||
transform: rotate(45deg) translate(3.5px, -2px);
|
||||
}
|
||||
}
|
||||
|
||||
&.open{
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.menu{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background-color: $primary;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-out;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
row-gap: 3em;
|
||||
|
||||
&.collapsed{
|
||||
max-height: 0%;
|
||||
}
|
||||
|
||||
a{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.single-route{
|
||||
font-size: 3em;
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
color: #1e1e1e;
|
||||
border-bottom: 4px solid transparent;
|
||||
display: block;
|
||||
|
||||
&.open{
|
||||
-webkit-animation: fadeInFromNone 3s ease-out;
|
||||
-moz-animation: fadeInFromNone 3s ease-out;
|
||||
-o-animation: fadeInFromNone 3s ease-out;
|
||||
animation: fadeInFromNone 3s ease-out;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInFromNone {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.clock {
|
||||
grid-area: clock;
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
display: flex;
|
||||
font-family: Arial;
|
||||
@include mobile {
|
||||
font-size: 0.8em;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
h2 {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
@import "./media-queries.scss";
|
||||
@import "./variables.scss";
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 600;
|
||||
src: url("/../../public/assets/fonts/bold.woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "knowit";
|
||||
font-weight: 300;
|
||||
src: url("/../../public/assets/fonts/regular.eot");
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
margin: 2rem auto;
|
||||
text-align: center;
|
||||
font-family: knowit, Arial;
|
||||
margin-top: 3.8rem;
|
||||
font-weight: 600;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subtext {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 1.22rem;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
.label-div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
label {
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
margin-bottom: auto;
|
||||
height: 2rem;
|
||||
padding: 0.5rem;
|
||||
min-width: 0;
|
||||
width: 98%;
|
||||
padding: 1%;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin-right: unset;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
&:not(.row) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: unset;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: none;
|
||||
font-size: 1.1rem;
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
}
|
||||
|
||||
.vin-button {
|
||||
font-family: Arial;
|
||||
$color: #b7debd;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background: $color;
|
||||
color: #333;
|
||||
padding: 10px 30px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
width: fit-content;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.3rem;
|
||||
height: 4rem;
|
||||
max-height: 4rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: transform 0.5s ease;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
// disable-dbl-tap-zoom
|
||||
touch-action: manipulation;
|
||||
|
||||
&.auto-height {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: $red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
|
||||
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
|
||||
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: scale(1.02) translateZ(0);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:disabled{
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: min-content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vin-link {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $link-color;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
text-decoration: none;
|
||||
color: $matte-text-color;
|
||||
|
||||
&:focus, &:hover {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.margin-top {
|
||||
&-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;
|
||||
}
|
||||
&-sm {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.width {
|
||||
&-100 {
|
||||
width: 100%;
|
||||
}
|
||||
&-75 {
|
||||
width: 75%;
|
||||
}
|
||||
&-50 {
|
||||
width: 50%;
|
||||
}
|
||||
&-25 {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor {
|
||||
&-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
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;
|
||||
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-raffle {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-raffle {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin raffle {
|
||||
padding-bottom: 50px;
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 25px;
|
||||
height: 50px;
|
||||
background: radial-gradient(closest-side, #fff, #fff 50%, transparent 50%);
|
||||
background-size: 50px 50px;
|
||||
background-position: 0 25px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
&::after{
|
||||
background: radial-gradient(closest-side, transparent, transparent 50%, #fff 50%);
|
||||
background-size: 50px 50px;
|
||||
background-position: 25px -25px;
|
||||
bottom: -25px
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
@include mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
@include desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
@import "./media-queries.scss";
|
||||
@import "./variables.scss";
|
||||
|
||||
.outer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include desktop {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: knowit, Arial;
|
||||
text-align: center;
|
||||
font-size: 3rem;
|
||||
width: 100vw;
|
||||
|
||||
@include mobile {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin: 2rem auto 0;
|
||||
width: 80vw;
|
||||
max-width: 1100px;
|
||||
|
||||
@include mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.label-div input {
|
||||
font-size: 2rem;
|
||||
min-height: 2rem;
|
||||
line-height: 2rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid black;
|
||||
max-width: 30vw;
|
||||
|
||||
@include mobile {
|
||||
max-width: unset;
|
||||
font-size: 2rem;
|
||||
min-height: 2rem;
|
||||
line-height: 2rem;
|
||||
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vin-button {
|
||||
margin-bottom: 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 1.25rem;
|
||||
margin: 1.25rem;
|
||||
width: calc(100% - 5rem);
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
$mobile-width: 768px;
|
||||
$tablet-max: 1200px;
|
||||
$desktop-max: 2004px;
|
||||
|
||||
|
||||
@mixin mobile {
|
||||
@media (max-width: #{$mobile-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tablet {
|
||||
@media (min-width: #{$mobile-width + 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: #{$tablet-max + 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin widescreen {
|
||||
@media (min-width: #{$desktop-max + 1px}){
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
.flex {
|
||||
display: flex;
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.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,21 +0,0 @@
|
||||
$primary: #b7debd;
|
||||
|
||||
$light-green: #c8f9df;
|
||||
$green: #0be881;
|
||||
$dark-green: #0ed277;
|
||||
|
||||
$light-blue: #d4f2fe;
|
||||
$blue: #4bcffa;
|
||||
$dark-blue: #24acda;
|
||||
|
||||
$light-yellow: #fff6d6;
|
||||
$yellow: #ffde5d;
|
||||
$dark-yellow: #ecc31d;
|
||||
|
||||
$light-red: #fbd7de;
|
||||
$red: #ef5878;
|
||||
$dark-red: #ec3b61;
|
||||
|
||||
$link-color: #ff5fff;
|
||||
|
||||
$matte-text-color: #333333;
|
||||
@@ -1,122 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'vinlottis-icons';
|
||||
src:
|
||||
url('/public/assets/fonts/vinlottis-icons/vinlottis-icons.ttf?95xu5r') format('truetype'),
|
||||
url('/public/assets/fonts/vinlottis-icons/vinlottis-icons.woff?95xu5r') format('woff'),
|
||||
url('/public/assets/fonts/vinlottis-icons/vinlottis-icons.svg?95xu5r#vinlottis-icons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'vinlottis-icons' !important;
|
||||
speak: never;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon--arrow-long-right:before {
|
||||
content: "\e907";
|
||||
}
|
||||
.icon--arrow-long-left:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.icon--arrow-right:before {
|
||||
content: "\e909";
|
||||
}
|
||||
.icon--arrow-left:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon--ballon:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon--bars:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon--bottle:before {
|
||||
content: "\e90d";
|
||||
}
|
||||
.icon--cake-chart:before {
|
||||
content: "\e90f";
|
||||
}
|
||||
.icon--stopwatch:before {
|
||||
content: "\e911";
|
||||
}
|
||||
.icon--cloud:before {
|
||||
content: "\e912";
|
||||
}
|
||||
.icon--dart:before {
|
||||
content: "\e914";
|
||||
}
|
||||
.icon--eye-1:before {
|
||||
content: "\e919";
|
||||
}
|
||||
.icon--eye-2:before {
|
||||
content: "\e91a";
|
||||
}
|
||||
.icon--eye-3:before {
|
||||
content: "\e91b";
|
||||
}
|
||||
.icon--eye-4:before {
|
||||
content: "\e91c";
|
||||
}
|
||||
.icon--eye-5:before {
|
||||
content: "\e91d";
|
||||
}
|
||||
.icon--eye-6:before {
|
||||
content: "\e91e";
|
||||
}
|
||||
.icon--eye-7:before {
|
||||
content: "\e91f";
|
||||
}
|
||||
.icon--eye-8:before {
|
||||
content: "\e920";
|
||||
}
|
||||
.icon--face-1:before {
|
||||
content: "\e922";
|
||||
}
|
||||
.icon--face-2:before {
|
||||
content: "\e923";
|
||||
}
|
||||
.icon--face-3:before {
|
||||
content: "\e924";
|
||||
}
|
||||
.icon--heart-sparks:before {
|
||||
content: "\e928";
|
||||
}
|
||||
.icon--heart:before {
|
||||
content: "\e929";
|
||||
}
|
||||
.icon--medal:before {
|
||||
content: "\e936";
|
||||
}
|
||||
.icon--megaphone:before {
|
||||
content: "\e937";
|
||||
}
|
||||
.icon--phone:before {
|
||||
content: "\e93a";
|
||||
}
|
||||
.icon--plus:before {
|
||||
content: "\e93e";
|
||||
}
|
||||
.icon--spark:before {
|
||||
content: "\e946";
|
||||
}
|
||||
.icon--tag:before {
|
||||
content: "\e949";
|
||||
}
|
||||
.icon--talk:before {
|
||||
content: "\e94b";
|
||||
}
|
||||
.icon--cross:before {
|
||||
content: "\e952";
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nb">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Vinlottis</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Knowits ukentlige vinlotteri-statistikk-side."
|
||||
/>
|
||||
<meta name="keywords" content="vin,vinlotteri,knowit,statistikk" />
|
||||
<meta name="author" content="Kasper Rynning-Tønnesen & Kevin Midbøe" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/public/assets/fonts/bold.woff"
|
||||
as="font"
|
||||
crossorigin
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Knowits ukentlige vinlotteri-statistikk-side."
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="152x152"
|
||||
href="/public/assets/images/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/public/assets/images/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/public/assets/images/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/public/assets/manifest.json" />
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/public/assets/images/safari-pinned-tab.svg"
|
||||
color="#23101f"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#b7debd" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<noscript>Du trenger vin, jeg trenger javascript!</noscript>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
noscript {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 100vh;
|
||||
background-color: #b7debd;
|
||||
font-size: 1.5rem;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
<div id="app"></div>
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<div class="attendees" v-if="attendees.length > 0">
|
||||
<h2>Deltakere ({{ attendees.length }})</h2>
|
||||
<div class="attendees-container" ref="attendees">
|
||||
<div class="attendee" v-for="(attendee, index) in flipList(attendees)" :key="index">
|
||||
<span class="attendee-name">{{ attendee.name }}</span>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
attendees: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
flipList: (list) => list.slice().reverse()
|
||||
},
|
||||
watch: {
|
||||
attendees: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.$refs && this.$refs.history) {
|
||||
setTimeout(() => {
|
||||
this.$refs.attendees.scrollTop = this.$refs.attendees.scrollHeight;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
.attendee-name {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
font-size: 0.75rem;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.attendees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 65%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.attendees-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.attendee {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<header class="top-banner">
|
||||
<!-- Mobile -->
|
||||
<router-link to="/" class="company-logo">
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo" />
|
||||
</router-link>
|
||||
|
||||
<a class="menu-toggle-container" aria-label="show-menu" @click="toggleMenu" :class="isOpen ? 'open' : 'collapsed'" >
|
||||
<span class="menu-toggle"></span>
|
||||
<span class="menu-toggle"></span>
|
||||
<span class="menu-toggle"></span>
|
||||
</a>
|
||||
|
||||
<nav class="menu" :class="isOpen ? 'open' : 'collapsed'" >
|
||||
<router-link v-for="(route, index) in routes" :key="index" :to="route.route" class="menu-item-link" >
|
||||
<a @click="toggleMenu" class="single-route" :class="isOpen ? 'open' : 'collapsed'">{{route.name}}</a>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<div class="clock">
|
||||
<h2 v-if="!fiveMinutesLeft || !tenMinutesOver">
|
||||
<span v-if="days > 0">{{ pad(days) }}:</span>
|
||||
<span>{{ pad(hours) }}</span>:
|
||||
<span>{{ pad(minutes) }}</span>:
|
||||
<span>{{ pad(seconds) }}</span>
|
||||
</h2>
|
||||
<h2 v-if="twoMinutesLeft || tenMinutesOver">Lotteriet er i gang!</h2>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
nextLottery: null,
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
distance: 0,
|
||||
interval: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
routes: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initialize(), this.countdown();
|
||||
},
|
||||
computed: {
|
||||
fiveMinutesLeft: function() {
|
||||
if (this.days == 0 && this.hours == 0 && this.minutes <= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
tenMinutesOver: function() {
|
||||
if (this.days == 6 && this.hours >= 23 && this.minutes >= 50) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMenu(){
|
||||
this.isOpen = this.isOpen ? false : true;
|
||||
},
|
||||
pad: function(num) {
|
||||
if (num < 10) {
|
||||
return `0${num}`;
|
||||
}
|
||||
return num;
|
||||
},
|
||||
initialize: function() {
|
||||
let d = new Date();
|
||||
let dayOfLottery = __DATE__;
|
||||
let dayDifference = (dayOfLottery + 7 - d.getDay()) % 7;
|
||||
if (dayDifference == 0) {
|
||||
dayDifference = 7;
|
||||
}
|
||||
let nextDayOfLottery = new Date(d.setDate(d.getDate() + dayDifference));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setHours(__HOURS__));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setMinutes(0));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setSeconds(0));
|
||||
let nowDate = new Date();
|
||||
let now = nowDate.getTime();
|
||||
if (nextDayOfLottery.getTimezoneOffset() != nowDate.getTimezoneOffset()) {
|
||||
let _diff =
|
||||
(nextDayOfLottery.getTimezoneOffset() - nowDate.getTimezoneOffset()) *
|
||||
60 *
|
||||
-1;
|
||||
nextDayOfLottery.setSeconds(nextDayOfLottery.getSeconds() + _diff);
|
||||
}
|
||||
this.nextLottery = nextDayOfLottery;
|
||||
|
||||
this.distance = new Date(this.nextLottery).getTime() - now;
|
||||
},
|
||||
countdown: function() {
|
||||
// Get today's date and time
|
||||
let now = new Date().getTime();
|
||||
|
||||
// Find the distance between now and the count down date
|
||||
this.distance = new Date(this.nextLottery).getTime() - now;
|
||||
|
||||
// Time calculations for days, hours, minutes and seconds
|
||||
this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24));
|
||||
this.hours = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
||||
);
|
||||
this.minutes = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60)) / (1000 * 60)
|
||||
);
|
||||
this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000);
|
||||
if (this.days == 7) {
|
||||
this.days = 0;
|
||||
}
|
||||
if (this.distance < 0) {
|
||||
this.initialize();
|
||||
}
|
||||
this.interval = setTimeout(this.countdown, 500);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/banner.scss";
|
||||
</style>
|
||||
252
src/ui/Chat.vue
252
src/ui/Chat.vue
@@ -1,252 +0,0 @@
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<hr />
|
||||
<h2>Chat</h2>
|
||||
<div class="history" ref="history">
|
||||
<div class="opaque-skirt"></div>
|
||||
<div v-if="existsMore" class="fetch-older-history">
|
||||
<button @click="$emit('loadMoreHistory')">Hent eldre meldinger</button>
|
||||
</div>
|
||||
<div class="history-message"
|
||||
v-for="(history, index) in chatHistory"
|
||||
:key="`${history.username}-${history.timestamp}-${index}`"
|
||||
>
|
||||
<div>
|
||||
<span class="user-name">{{ history.username }}</span>
|
||||
<span class="timestamp">{{ getTime(history.timestamp) }}</span>
|
||||
</div>
|
||||
<span class="message">{{ history.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="usernameSet" class="input">
|
||||
<input @keyup.enter="sendMessage" type="text" v-model="message" placeholder="Melding.." />
|
||||
<button @click="sendMessage">Send</button>
|
||||
<button @click="removeUsername">Logg ut</button>
|
||||
</div>
|
||||
<div v-else class="username-dialog">
|
||||
<input
|
||||
type="text"
|
||||
@keyup.enter="setUsername"
|
||||
v-model="temporaryUsername"
|
||||
maxlength="30"
|
||||
placeholder="Ditt navn.."
|
||||
/>
|
||||
<button @click="setUsername">Lagre brukernavn</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
usernameAllowed: {
|
||||
type: Boolean
|
||||
},
|
||||
chatHistory: {
|
||||
type: Array
|
||||
},
|
||||
historyPageSize: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: "",
|
||||
temporaryUsername: null,
|
||||
username: null,
|
||||
usernameSet: false,
|
||||
existsMore: true
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
chatHistory: {
|
||||
handler: function(newVal, oldVal) {
|
||||
if (this.$refs && this.$refs.history) {
|
||||
const firstMessages = oldVal.length == 0;
|
||||
const diffLargerThanOne = newVal.length - oldVal.length > 1;
|
||||
|
||||
setTimeout(() => {
|
||||
if (firstMessages || diffLargerThanOne == false) {
|
||||
this.scrollToBottomOfHistory();
|
||||
} else {
|
||||
this.scrollToStartOfNewMessages();
|
||||
// what shows the load more button - if we scroll page and less than page size
|
||||
// come back we have reached a limit
|
||||
this.existsMore = newVal.length - oldVal.length == this.historyPageSize
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let username = window.localStorage.getItem("username");
|
||||
if (username) {
|
||||
this.username = username;
|
||||
this.usernameSet = true;
|
||||
this.$emit("username", username);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pad: function(num) {
|
||||
if (num > 9) return num;
|
||||
return `0${num}`;
|
||||
},
|
||||
getTime: function(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
||||
date.getMinutes()
|
||||
)}:${this.pad(date.getSeconds())}`;
|
||||
|
||||
if (date.getDate() == new Date().getDate()) {
|
||||
return timeString;
|
||||
}
|
||||
return `${date.toLocaleDateString()} ${timeString}`;
|
||||
},
|
||||
sendMessage: function() {
|
||||
this.$emit("message", this.message);
|
||||
this.message = "";
|
||||
},
|
||||
removeUsername: function() {
|
||||
this.username = null;
|
||||
this.temporaryUsername = null;
|
||||
this.usernameSet = false;
|
||||
window.localStorage.removeItem("username");
|
||||
this.$emit("username", null);
|
||||
},
|
||||
setUsername: function() {
|
||||
if (
|
||||
this.temporaryUsername.length > 3 &&
|
||||
this.temporaryUsername.length < 30
|
||||
) {
|
||||
this.username = this.temporaryUsername;
|
||||
this.usernameSet = true;
|
||||
this.$emit("username", this.username);
|
||||
}
|
||||
},
|
||||
scrollToBottomOfHistory() {
|
||||
if (this.$refs && this.$refs.history) {
|
||||
const { history } = this.$refs;
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}
|
||||
},
|
||||
scrollToStartOfNewMessages() {
|
||||
const { history } = this.$refs;
|
||||
const histLength = history.children.length;
|
||||
const pages = Math.floor(histLength / 100);
|
||||
|
||||
const messageToScrollTo = history.children[histLength - ((pages * 100) + 3)]
|
||||
history.scrollTop = messageToScrollTo.offsetTop;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: none;
|
||||
|
||||
@include mobile {
|
||||
display: block;
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
position: relative;
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.history {
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
|
||||
&-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.35rem 0;
|
||||
position: relative;
|
||||
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.05rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.9rem;
|
||||
top: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&-message:nth-of-type(2) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
& .opaque-skirt {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: 1rem;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
white,
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
}
|
||||
|
||||
& .fetch-older-history {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0.2rem 0 0.5rem;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.username-dialog {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background: #b7debd;
|
||||
color: #333;
|
||||
padding: 10px 30px;
|
||||
border: 0;
|
||||
width: fit-content;
|
||||
font-size: 1rem;
|
||||
/* height: 1.5rem; */
|
||||
/* max-height: 1.5rem; */
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: transform 0.5s ease;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
</style>
|
||||
@@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="clock" v-if="enabled">
|
||||
<h2 cv-if="distance > 0">
|
||||
<span v-if="days > 0">{{ pad(days) }}:</span>
|
||||
<span>{{ pad(hours) }}</span
|
||||
>: <span>{{ pad(minutes) }}</span
|
||||
>:
|
||||
<span>{{ pad(seconds) }}</span>
|
||||
</h2>
|
||||
<div class="cross" @click="stopClock">X</div>
|
||||
<h2 v-if="distance <= 0">Lotteriet har begynt!</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
hardEnable: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nextLottery: null,
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
distance: 0,
|
||||
enabled: false,
|
||||
code: "38384040373937396665",
|
||||
codeDone: "",
|
||||
interval: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
hardEnable: function(hardEnable) {
|
||||
if (hardEnable) {
|
||||
this.enabled = true;
|
||||
this.initialize();
|
||||
this.countdown();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.listenerFunction);
|
||||
},
|
||||
methods: {
|
||||
pad: function(num) {
|
||||
if (num < 10) {
|
||||
return `0${num}`;
|
||||
}
|
||||
return num;
|
||||
},
|
||||
listenerFunction: function(event) {
|
||||
this.codeDone += event.keyCode;
|
||||
if (this.code.substring(0, this.codeDone.length) == this.codeDone) {
|
||||
if (this.code == this.codeDone && !this.enabled) {
|
||||
this.enabled = true;
|
||||
this.initialize();
|
||||
this.countdown();
|
||||
this.codeDone = "";
|
||||
}
|
||||
} else {
|
||||
this.codeDone = "";
|
||||
}
|
||||
},
|
||||
initialize: function() {
|
||||
let d = new Date();
|
||||
let dayOfLottery = __DATE__;
|
||||
let dayDifference = (dayOfLottery + 7 - d.getDay()) % 7;
|
||||
if (dayDifference == 0) {
|
||||
dayDifference = 7;
|
||||
}
|
||||
let nextDayOfLottery = new Date(d.setDate(d.getDate() + dayDifference));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setHours(__HOURS__));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setMinutes(0));
|
||||
nextDayOfLottery = new Date(nextDayOfLottery.setSeconds(0));
|
||||
this.nextLottery = nextDayOfLottery;
|
||||
window.scrollTo(0, 0);
|
||||
document.querySelector("body").style.overflow = "hidden";
|
||||
document.querySelector("body").style.height = "100vh";
|
||||
let now = new Date().getTime();
|
||||
this.distance = new Date(this.nextLottery).getTime() - now;
|
||||
},
|
||||
stopClock: function() {
|
||||
clearInterval(this.interval);
|
||||
this.enabled = false;
|
||||
document.querySelector("body").style.overflow = "auto";
|
||||
document.querySelector("body").style.height = "initial";
|
||||
this.$emit("countdown", false);
|
||||
},
|
||||
countdown: function() {
|
||||
// Get today's date and time
|
||||
let now = new Date().getTime();
|
||||
|
||||
// Find the distance between now and the count down date
|
||||
this.distance = new Date(this.nextLottery).getTime() - now;
|
||||
|
||||
// Time calculations for days, hours, minutes and seconds
|
||||
this.days = Math.floor(this.distance / (1000 * 60 * 60 * 24));
|
||||
this.hours = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
|
||||
);
|
||||
this.minutes = Math.floor(
|
||||
(this.distance % (1000 * 60 * 60)) / (1000 * 60)
|
||||
);
|
||||
this.seconds = Math.floor((this.distance % (1000 * 60)) / 1000);
|
||||
if (this.days == 7) {
|
||||
this.days = 0;
|
||||
}
|
||||
if (this.distance < 0) {
|
||||
clearTimeout(this.interval);
|
||||
return;
|
||||
}
|
||||
this.interval = setTimeout(this.countdown, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
.clock {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
background: white;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@include mobile {
|
||||
width: 105vw;
|
||||
height: 110vh;
|
||||
}
|
||||
}
|
||||
.cross {
|
||||
top: 15px;
|
||||
right: 23px;
|
||||
font-size: 2rem;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
|
||||
@include mobile {
|
||||
right: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: auto;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<footer>
|
||||
<router-link to="/" class="company-logo">
|
||||
<img src="/public/assets/images/knowit.svg" alt="knowit logo">
|
||||
</router-link>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WineFooter'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
background: #f4f4f4;
|
||||
.company-logo{
|
||||
padding: 0 5em 0 0;
|
||||
img{
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<div class="highscores" v-if="highscore.length > 0">
|
||||
|
||||
<section class="heading">
|
||||
<h3>
|
||||
Topp 5 vinnere
|
||||
</h3>
|
||||
<router-link to="highscore" class="">
|
||||
<span class="vin-link">Se alle vinnere</span>
|
||||
</router-link>
|
||||
</section>
|
||||
<ol class="winner-list-container">
|
||||
<li v-for="(person, index) in highscore" :key="person._id" class="single-winner">
|
||||
<span class="placement">{{index + 1}}.</span>
|
||||
<i class="icon icon--medal"></i>
|
||||
<p class="winner-name">{{ person.name }}</p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { highscoreStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { highscore: [] };
|
||||
},
|
||||
async mounted() {
|
||||
let response = await highscoreStatistics();
|
||||
response.sort((a, b) => a.wins.length < b.wins.length ? 1 : -1)
|
||||
this.highscore = this.generateScoreBoard(response.slice(0, 5));
|
||||
},
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:visited {
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: none;
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.winner-list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12.5em, 1fr));
|
||||
gap: 5%;
|
||||
|
||||
.single-winner {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: $primary;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
|
||||
i {
|
||||
font-size: 3em;
|
||||
width: max-content;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.placement {
|
||||
grid-row: 1;
|
||||
grid-column: 1 / 3;
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.winner-name {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
|
||||
.winner-icon {
|
||||
grid-row: 1;
|
||||
grid-column: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
src/ui/Modal.vue
101
src/ui/Modal.vue
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<main class="modal-backdrop">
|
||||
<section class="modal">
|
||||
<header class="modal-header" v-if="headerText">
|
||||
{{headerText}}
|
||||
</header>
|
||||
<section class="modal-body">
|
||||
<p>
|
||||
{{modalText}}
|
||||
</p>
|
||||
<section class="button-container">
|
||||
<button v-for="(button, index) in buttons" :key="index" @click="modalButtonClicked(button.action)" class="vin-button">
|
||||
{{button.text}}
|
||||
</button>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerText: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
modalText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
buttons: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
methods:{
|
||||
modalButtonClicked(action){
|
||||
this.$emit('click', action)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
|
||||
.modal-fade-enter,
|
||||
.modal-fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity .5s ease
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #FFFFFF;
|
||||
-webkit-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
|
||||
-moz-box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
|
||||
box-shadow: 0px 0px 22px 1px rgba(0, 0, 0, 0.65);
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
color: #4AAE9B;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
position: relative;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,146 +0,0 @@
|
||||
<template>
|
||||
<div class="chart">
|
||||
<canvas ref="purchase-chart" width="100" height="50"></canvas>
|
||||
<div ref="chartjsLegend" class="chartjsLegend"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chartjs from "chart.js";
|
||||
import { chartPurchaseByColor } from "@/api";
|
||||
|
||||
export default {
|
||||
async mounted() {
|
||||
let canvas = this.$refs["purchase-chart"].getContext("2d");
|
||||
|
||||
let response = await chartPurchaseByColor();
|
||||
let labels = [];
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
borderColor: "#57d2fb",
|
||||
backgroundColor: "#d4f2fe",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let yellow = {
|
||||
label: "Gul",
|
||||
borderColor: "#ffde5d",
|
||||
backgroundColor: "#fff6d6",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let red = {
|
||||
label: "Rød",
|
||||
borderColor: "#ef5878",
|
||||
backgroundColor: "#fbd7de",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let green = {
|
||||
label: "Grønn",
|
||||
borderColor: "#10e783",
|
||||
backgroundColor: "#c8f9df",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
|
||||
if (response.length == 1) {
|
||||
labels.push("");
|
||||
blue.data.push(0);
|
||||
yellow.data.push(0);
|
||||
red.data.push(0);
|
||||
green.data.push(0);
|
||||
}
|
||||
|
||||
let highestNumber = 0;
|
||||
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
let thisDate = response[i];
|
||||
let dateObject = new Date(thisDate.date);
|
||||
labels.push(this.getPrettierDateString(dateObject));
|
||||
|
||||
blue.data.push(thisDate.blue);
|
||||
yellow.data.push(thisDate.yellow);
|
||||
red.data.push(thisDate.red);
|
||||
green.data.push(thisDate.green);
|
||||
|
||||
if (thisDate.blue > highestNumber) {
|
||||
highestNumber = thisDate.blue;
|
||||
}
|
||||
if (thisDate.yellow > highestNumber) {
|
||||
highestNumber = thisDate.yellow;
|
||||
}
|
||||
if (thisDate.green > highestNumber) {
|
||||
highestNumber = thisDate.green;
|
||||
}
|
||||
if (thisDate.red > highestNumber) {
|
||||
highestNumber = thisDate.red;
|
||||
}
|
||||
}
|
||||
let datasets = [blue, yellow, green, red];
|
||||
let chartdata = {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
let chart = new Chart(canvas, {
|
||||
type: "line",
|
||||
data: chartdata,
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // general animation time
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Antall kjøpt",
|
||||
fontSize: 20
|
||||
},
|
||||
legend: {
|
||||
display: true,
|
||||
boxWidth: 3,
|
||||
usePointStyle: true,
|
||||
borderRadius: 10,
|
||||
labels: {
|
||||
padding: 12,
|
||||
boxWidth: 20,
|
||||
usePointStyle: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
suggestedMax: highestNumber + 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getPrettierDateString(date) {
|
||||
return `${this.pad(date.getDate())}.${this.pad(
|
||||
date.getMonth() + 1
|
||||
)}.${this.pad(date.getYear() - 100)}`;
|
||||
},
|
||||
pad(num) {
|
||||
if (num < 10) {
|
||||
return `0${num}`;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,429 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="input-line">
|
||||
<label for="redCheckbox">
|
||||
<input type="checkbox" id="redCheckbox" v-model="redCheckbox" @click="generateColors"/>
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Rød</span>
|
||||
</label>
|
||||
<label for="blueCheckbox">
|
||||
<input type="checkbox" id="blueCheckbox" v-model="blueCheckbox" @click="generateColors"/>
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Blå</span>
|
||||
</label>
|
||||
<label for="yellowCheckbox">
|
||||
<input type="checkbox" id="yellowCheckbox" v-model="yellowCheckbox" @click="generateColors"/>
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Gul</span>
|
||||
</label>
|
||||
<label for="greenCheckbox">
|
||||
<input type="checkbox" id="greenCheckbox" v-model="greenCheckbox" @click="generateColors"/>
|
||||
<span class="border">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
<span class="text">Grønn</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Antall lodd"
|
||||
@keyup.enter="generateColors"
|
||||
v-model="numberOfRaffles"
|
||||
/>
|
||||
<button class="vin-button" @click="generateColors">Generer</button>
|
||||
</div>
|
||||
<div class="colors">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:class="getColorClass(color)"
|
||||
class="color-box"
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="color-count-container" v-if="generated">
|
||||
<span>Rød: {{ red }}</span>
|
||||
<span>Blå: {{ blue }}</span>
|
||||
<span>Gul: {{ yellow }}</span>
|
||||
<span>Grønn: {{ green }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
generateOnInit: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
numberOfRaffles: 4,
|
||||
colors: [],
|
||||
blue: 0,
|
||||
red: 0,
|
||||
green: 0,
|
||||
yellow: 0,
|
||||
colorTimeout: null,
|
||||
redCheckbox: true,
|
||||
greenCheckbox: true,
|
||||
yellowCheckbox: true,
|
||||
blueCheckbox: true,
|
||||
generated: false,
|
||||
generating: false
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
this.$emit("numberOfRaffles", this.numberOfRaffles);
|
||||
if (this.generateOnInit) {
|
||||
this.generateColors();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
numberOfRaffles: function() {
|
||||
this.$emit("numberOfRaffles", this.numberOfRaffles);
|
||||
this.generateColors();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generateColors: function(event, time) {
|
||||
this.generating = true;
|
||||
if (time == 5) {
|
||||
this.generating = false;
|
||||
this.generated = true;
|
||||
if (this.numberOfRaffles > 1 &&
|
||||
[this.redCheckbox, this.greenCheckbox, this.yellowCheckbox, this.blueCheckbox].filter(value => value == true).length == 1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (new Set(this.colors).size == 1) {
|
||||
alert("BINGO");
|
||||
}
|
||||
|
||||
this.emitColors()
|
||||
|
||||
if (window.location.hostname == "localhost") {
|
||||
return;
|
||||
}
|
||||
this.$ga.event({
|
||||
eventCategory: "Raffles",
|
||||
eventAction: "Generate",
|
||||
eventValue: JSON.stringify(this.colors)
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (time == undefined) {
|
||||
time = 1;
|
||||
}
|
||||
this.colors = [];
|
||||
this.blue = 0;
|
||||
this.red = 0;
|
||||
this.green = 0;
|
||||
this.yellow = 0;
|
||||
let randomArray = [];
|
||||
if (this.redCheckbox) {
|
||||
randomArray.push(1);
|
||||
}
|
||||
if (this.greenCheckbox) {
|
||||
randomArray.push(2);
|
||||
}
|
||||
if (this.yellowCheckbox) {
|
||||
randomArray.push(3);
|
||||
}
|
||||
if (this.blueCheckbox) {
|
||||
randomArray.push(4);
|
||||
}
|
||||
if (randomArray.length == 0) {
|
||||
alert("Du må velge MINST 1 farge");
|
||||
return;
|
||||
}
|
||||
if (this.numberOfRaffles > 0) {
|
||||
for (let i = 0; i < this.numberOfRaffles; i++) {
|
||||
let color =
|
||||
randomArray[Math.floor(Math.random() * randomArray.length)];
|
||||
this.colors.push(color);
|
||||
if (color == 1) {
|
||||
this.red += 1;
|
||||
}
|
||||
if (color == 2) {
|
||||
this.green += 1;
|
||||
}
|
||||
if (color == 3) {
|
||||
this.yellow += 1;
|
||||
}
|
||||
if (color == 4) {
|
||||
this.blue += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
clearTimeout(this.colorTimeout);
|
||||
this.colorTimeout = setTimeout(() => {
|
||||
this.generateColors(event, time + 1);
|
||||
}, 50);
|
||||
},
|
||||
getRotation: function() {
|
||||
let num = Math.floor(Math.random() * 15);
|
||||
let neg = Math.floor(Math.random() * 2);
|
||||
return neg == 0 ? -num : num;
|
||||
},
|
||||
getColorClass: function(number) {
|
||||
if (number == 1) {
|
||||
return "red";
|
||||
}
|
||||
if (number == 2) {
|
||||
return "green";
|
||||
}
|
||||
if (number == 3) {
|
||||
return "yellow";
|
||||
}
|
||||
if (number == 4) {
|
||||
return "blue";
|
||||
}
|
||||
},
|
||||
emitColors() {
|
||||
this.$emit("colors", {
|
||||
blue: this.blue,
|
||||
red: this.red,
|
||||
green: this.green,
|
||||
yellow: this.yellow
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.container {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin-top: 2.4rem;
|
||||
|
||||
@include mobile {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input-line label {
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 1.22rem;
|
||||
margin: 0 0.6rem;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 1.5rem;
|
||||
padding: 7px;
|
||||
margin: 0;
|
||||
height: 3rem;
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label .border {
|
||||
border: 1px solid rgba(#333333, 0.3);
|
||||
border-spacing: 2px;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
label .checkmark {
|
||||
background: none;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
label .text {
|
||||
margin-left: 0.15rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.colors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 1400px;
|
||||
margin: 3rem auto 0;
|
||||
|
||||
@include mobile {
|
||||
margin: 1.8rem auto 0;
|
||||
}
|
||||
}
|
||||
.color-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: 60px;
|
||||
height: 60px;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.colors-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.color-count-container {
|
||||
margin: auto;
|
||||
width: 300px;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-family: Arial;
|
||||
margin-top: 35px;
|
||||
|
||||
@include mobile {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
.yellow {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
.red {
|
||||
background-color: $light-red;
|
||||
}
|
||||
|
||||
.input-line label {
|
||||
& input {
|
||||
&#greenCheckbox:checked ~ .border .checkmark {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&#redCheckbox:checked ~ .border .checkmark {
|
||||
background-color: $light-red;
|
||||
}
|
||||
|
||||
&#yellowCheckbox:checked ~ .border .checkmark {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&#blueCheckbox:checked ~ .border .checkmark {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
&:hover input {
|
||||
&#greenCheckbox ~ .border .checkmark {
|
||||
background-color: $green;
|
||||
}
|
||||
|
||||
&#redCheckbox ~ .border .checkmark {
|
||||
background-color: $red;
|
||||
}
|
||||
|
||||
&#yellowCheckbox ~ .border .checkmark {
|
||||
background-color: $yellow;
|
||||
}
|
||||
|
||||
&#blueCheckbox ~ .border .checkmark {
|
||||
background-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus input,
|
||||
&:active input {
|
||||
&#greenCheckbox ~ .border .checkmark {
|
||||
background-color: $dark-green;
|
||||
}
|
||||
|
||||
&#redCheckbox ~ .border .checkmark {
|
||||
background-color: $dark-red;
|
||||
}
|
||||
|
||||
&#yellowCheckbox ~ .border .checkmark {
|
||||
background-color: $dark-yellow;
|
||||
}
|
||||
|
||||
&#blueCheckbox ~ .border .checkmark {
|
||||
background-color: $dark-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
input,
|
||||
button {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 45vw;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
width: 30%;
|
||||
margin-top: 15px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<Wine :wine="wine">
|
||||
<template v-slot:top>
|
||||
<div class="flex justify-end">
|
||||
<div class="requested-count cursor-pointer" @click="request">
|
||||
<span>{{ requestedElement.count }}</span>
|
||||
<i class="icon icon--heart" :class="{ 'active': locallyRequested }" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:default>
|
||||
<button @click="deleteWine(wine)" v-if="showDeleteButton == true" class="vin-button small danger width-100">
|
||||
Slett vinen
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-slot:bottom>
|
||||
<div class="float-left request">
|
||||
<i class="icon icon--heart request-icon" :class="{ 'active': locallyRequested }"></i>
|
||||
<a aria-role="button" tabindex="0" class="link" @click="request"
|
||||
:class="{ 'active': locallyRequested }">
|
||||
{{ locallyRequested ? 'Anbefalt' : 'Anbefal' }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</Wine>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deleteRequestedWine, requestNewWine } from "@/api";
|
||||
import Wine from "@/ui/Wine";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Wine
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
wine: this.requestedElement.wine,
|
||||
locallyRequested: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
requestedElement: {
|
||||
required: true,
|
||||
type: Object
|
||||
},
|
||||
showDeleteButton: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
request(){
|
||||
if (this.locallyRequested)
|
||||
return
|
||||
console.log("requesting", this.wine)
|
||||
this.locallyRequested = true
|
||||
this.requestedElement.count = this.requestedElement.count +1
|
||||
requestNewWine(this.wine)
|
||||
},
|
||||
async deleteWine() {
|
||||
const wine = this.wine
|
||||
if (window.confirm("Er du sikker på at du vil slette vinen?")) {
|
||||
let response = await deleteRequestedWine(wine);
|
||||
if (response['success'] == true) {
|
||||
this.$emit('wineDeleted', wine);
|
||||
} else {
|
||||
alert("Klarte ikke slette vinen");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/styles/variables";
|
||||
|
||||
.requested-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
background-color: rgb(244,244,244);
|
||||
border-radius: 1.1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
font-size: 1.25em;
|
||||
|
||||
span {
|
||||
padding-right: 0.5rem;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.icon--heart{
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
&.link {
|
||||
border-color: $link-color
|
||||
}
|
||||
|
||||
&.icon--heart {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.request {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 1.5rem;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 v-if="errorMessage">{{ errorMessage }}</h2>
|
||||
<video playsinline autoplay class="hidden"></video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BrowserBarcodeReader } from "@zxing/library";
|
||||
import { barcodeToVinmonopolet } from "@/api";
|
||||
|
||||
export default {
|
||||
name: "Scan to vinnopolet",
|
||||
data() {
|
||||
return {
|
||||
video: undefined,
|
||||
errorMessage: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (navigator.mediaDevices == undefined) {
|
||||
this.errorMessage = "Feil: Ingen kamera funnet.";
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.video = document.querySelector("video");
|
||||
this.scrollIntoView();
|
||||
|
||||
const constraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
facingMode: "environment"
|
||||
}
|
||||
};
|
||||
|
||||
navigator.mediaDevices
|
||||
.getUserMedia(constraints)
|
||||
.then(this.handleSuccess)
|
||||
.catch(this.handleError);
|
||||
}, 10);
|
||||
},
|
||||
methods: {
|
||||
handleSuccess(stream) {
|
||||
this.video.classList.remove("hidden");
|
||||
this.video.srcObject = stream;
|
||||
this.searchVideoForBarcode(this.video);
|
||||
},
|
||||
handleError(error) {
|
||||
console.log(
|
||||
"navigator.MediaDevices.getUserMedia error: ",
|
||||
error.message,
|
||||
error.name
|
||||
);
|
||||
this.errorMessage =
|
||||
"Feil ved oppstart av kamera! Feilmelding: " + error.message;
|
||||
},
|
||||
searchVideoForBarcode(video) {
|
||||
const codeReader = new BrowserBarcodeReader();
|
||||
|
||||
codeReader
|
||||
.decodeOnceFromVideoDevice(undefined, video)
|
||||
.then(result => {
|
||||
barcodeToVinmonopolet(result.text)
|
||||
.then(this.emitWineFromVinmonopolet)
|
||||
.catch(this.catchVinmonopoletError)
|
||||
.then(this.searchVideoForBarcode(video));
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
},
|
||||
emitWineFromVinmonopolet(response) {
|
||||
this.errorMessage = "";
|
||||
|
||||
this.$emit("wine", {
|
||||
name: response.name,
|
||||
vivinoLink: "https://vinmonopolet.no" + response.url,
|
||||
price: response.price.value,
|
||||
image: response.images[1].url,
|
||||
country: response.main_country.name,
|
||||
id: Number(response.code)
|
||||
});
|
||||
},
|
||||
catchVinmonopoletError(error) {
|
||||
this.errorMessage = "Feil! " + error.message || error;
|
||||
},
|
||||
scrollIntoView() {
|
||||
window.scrollTo(
|
||||
0,
|
||||
document.getElementById("addwine-title").offsetTop - 10
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/styles/variables";
|
||||
@import "./src/styles/global";
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="tab-container">
|
||||
<div
|
||||
class="tab"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="changeTab(index)"
|
||||
:class="chosenTab == index ? 'active' : null"
|
||||
>{{ tab.name }}</div>
|
||||
</div>
|
||||
<div class="tab-elements">
|
||||
<component :is="tabs[chosenTab].component" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventBus from "@/mixins/EventBus";
|
||||
export default {
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array
|
||||
},
|
||||
active: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.chosenTab = this.active;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chosenTab: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeTab: function(num) {
|
||||
this.chosenTab = num;
|
||||
this.$emit("tabChange", num);
|
||||
eventBus.$emit("tab-change");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 25px;
|
||||
border-bottom: 1px solid #333333;
|
||||
}
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin: 0 15px;
|
||||
border: 1px solid #333333;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
background: #00000008;
|
||||
border-bottom: 1px solid #333333;
|
||||
margin-bottom: -1px;
|
||||
|
||||
&.active {
|
||||
border-bottom: 1px solid white;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="update-toast" :class="showClass">
|
||||
<span v-html="text"></span>
|
||||
<div class="button-container">
|
||||
<button @click="closeToast">Lukk</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: { type: String, required: true },
|
||||
refreshButton: { type: Boolean, required: false }
|
||||
},
|
||||
data() {
|
||||
return { showClass: null };
|
||||
},
|
||||
created() {
|
||||
this.showClass = "show";
|
||||
},
|
||||
mounted() {
|
||||
if (this.refreshButton) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$emit("closeToast");
|
||||
}, 5000);
|
||||
},
|
||||
methods: {
|
||||
refresh: function() {
|
||||
location.reload();
|
||||
},
|
||||
closeToast: function() {
|
||||
this.$emit("closeToast");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.update-toast {
|
||||
position: fixed;
|
||||
bottom: 1.3rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
background: #2d2d2d;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 80vw;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.show {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
-moz-transition: opacity 0.5s ease-in-out;
|
||||
-ms-transition: opacity 0.5s ease-in-out;
|
||||
-o-transition: opacity 0.5s ease-in-out;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
@include mobile {
|
||||
width: 85vw;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
& span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
& .button-container {
|
||||
& button {
|
||||
color: #2d2d2d;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 0 3px;
|
||||
font-size: 0.8rem;
|
||||
height: max-content;
|
||||
|
||||
&:active {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,216 +0,0 @@
|
||||
<template>
|
||||
<section class="outer-bought">
|
||||
<h3>Loddstatistikk</h3>
|
||||
|
||||
<div class="total-raffles">
|
||||
Totalt
|
||||
<span class="total">{{ total }}</span>
|
||||
kjøpte,
|
||||
<span>{{ totalWin }} vinn og </span>
|
||||
<span> {{ stolen }} stjålet </span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bought-container">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:class="
|
||||
color.name +
|
||||
'-container ' +
|
||||
color.name +
|
||||
'-raffle raffle-element-local'
|
||||
"
|
||||
:key="color.name"
|
||||
>
|
||||
<p class="winner-chance">
|
||||
{{translate(color.name)}} vinnersjanse
|
||||
</p>
|
||||
<span class="win-percentage">{{ color.totalPercentage }}% </span>
|
||||
<p class="total-bought-color">{{ color.total }} kjøpte</p>
|
||||
<p class="amount-of-wins"> {{ color.win }} vinn </p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
import { colorStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
colors: [],
|
||||
red: 0,
|
||||
blue: 0,
|
||||
yellow: 0,
|
||||
green: 0,
|
||||
total: 0,
|
||||
totalWin: 0,
|
||||
stolen: 0,
|
||||
wins: 0,
|
||||
redPercentage: 0,
|
||||
yellowPercentage: 0,
|
||||
greenPercentage: 0,
|
||||
bluePercentage: 0
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await colorStatistics();
|
||||
|
||||
this.red = response.red;
|
||||
this.blue = response.blue;
|
||||
this.green = response.green;
|
||||
this.yellow = response.yellow;
|
||||
this.total = response.total;
|
||||
|
||||
this.totalWin =
|
||||
this.red.win + this.yellow.win + this.blue.win + this.green.win;
|
||||
this.stolen = response.stolen;
|
||||
|
||||
this.redPercentage = this.round(
|
||||
this.red.win == 0 ? 0 : (this.red.win / this.totalWin) * 100
|
||||
);
|
||||
this.greenPercentage = this.round(
|
||||
this.green.win == 0 ? 0 : (this.green.win / this.totalWin) * 100
|
||||
);
|
||||
this.bluePercentage = this.round(
|
||||
this.blue.win == 0 ? 0 : (this.blue.win / this.totalWin) * 100
|
||||
);
|
||||
this.yellowPercentage = this.round(
|
||||
this.yellow.win == 0 ? 0 : (this.yellow.win / this.totalWin) * 100
|
||||
);
|
||||
|
||||
this.colors.push({
|
||||
name: "red",
|
||||
total: this.red.total,
|
||||
win: this.red.win,
|
||||
totalPercentage: this.getPercentage(this.red.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.red.win, this.red.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "blue",
|
||||
total: this.blue.total,
|
||||
win: this.blue.win,
|
||||
totalPercentage: this.getPercentage(this.blue.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.blue.win, this.blue.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "yellow",
|
||||
total: this.yellow.total,
|
||||
win: this.yellow.win,
|
||||
totalPercentage: this.getPercentage(this.yellow.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.yellow.win, this.yellow.total)
|
||||
});
|
||||
this.colors.push({
|
||||
name: "green",
|
||||
total: this.green.total,
|
||||
win: this.green.win,
|
||||
totalPercentage: this.getPercentage(this.green.win, this.totalWin),
|
||||
percentage: this.getPercentage(this.green.win, this.green.total)
|
||||
});
|
||||
|
||||
this.colors = this.colors.sort((a, b) => (a.win > b.win ? -1 : 1));
|
||||
},
|
||||
methods: {
|
||||
translate(color){
|
||||
switch(color) {
|
||||
case "blue":
|
||||
return "Blå"
|
||||
break;
|
||||
case "red":
|
||||
return "Rød"
|
||||
break;
|
||||
case "green":
|
||||
return "Grønn"
|
||||
break;
|
||||
case "yellow":
|
||||
return "Gul"
|
||||
break;
|
||||
break;
|
||||
}
|
||||
},
|
||||
getPercentage: function(win, total) {
|
||||
return this.round(win == 0 ? 0 : (win / total) * 100);
|
||||
},
|
||||
round: function(number) {
|
||||
|
||||
//this can make the odds added together more than 100%, maybe rework?
|
||||
let actualPercentage = Math.round(number * 100) / 100;
|
||||
let rounded = actualPercentage.toFixed(0);
|
||||
return rounded;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
@import "../styles/global.scss";
|
||||
|
||||
@include mobile{
|
||||
section {
|
||||
margin-top: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
.total-raffles {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bought-container {
|
||||
margin-top: 2em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 50px;
|
||||
|
||||
.raffle-element-local {
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@include raffle;
|
||||
|
||||
.win-percentage {
|
||||
margin-left: 30px;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-left: 30px;
|
||||
&.winner-chance {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&.total-bought-color{
|
||||
font-weight: bold;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
&.amount-of-wins {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&.green-raffle {
|
||||
background-color: $light-green;
|
||||
}
|
||||
|
||||
&.blue-raffle {
|
||||
background-color: $light-blue;
|
||||
}
|
||||
|
||||
&.yellow-raffle {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
|
||||
&.red-raffle {
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<div class="update-toast" :class="showClass">
|
||||
<span>{{text}}</span>
|
||||
<div class="button-container">
|
||||
<button v-if="refreshButton" @click="refresh">Refresh</button>
|
||||
<button @click="closeToast">Lukk</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: { type: String, required: true },
|
||||
refreshButton: { type: Boolean, required: false }
|
||||
},
|
||||
data() {
|
||||
return { showClass: null };
|
||||
},
|
||||
created() {
|
||||
this.showClass = "show";
|
||||
},
|
||||
mounted() {
|
||||
if (this.refreshButton) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$emit("closeToast");
|
||||
}, 5000);
|
||||
},
|
||||
methods: {
|
||||
refresh: function() {
|
||||
location.reload();
|
||||
},
|
||||
closeToast: function() {
|
||||
this.$emit("closeToast");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.update-toast {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
background: #2d2d2d;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 50vw;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.show {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
-moz-transition: opacity 0.5s ease-in-out;
|
||||
-ms-transition: opacity 0.5s ease-in-out;
|
||||
-o-transition: opacity 0.5s ease-in-out;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
@include mobile {
|
||||
width: 85vw;
|
||||
bottom: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
& span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
& .button-container {
|
||||
& button {
|
||||
color: #2d2d2d;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 0 3px;
|
||||
font-size: 0.8rem;
|
||||
|
||||
&:active {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
212
src/ui/Vipps.vue
212
src/ui/Vipps.vue
@@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="vipps-container"
|
||||
:class="isMobile ? 'clickable' : null"
|
||||
@click="openVipps"
|
||||
>
|
||||
<img
|
||||
src="/public/assets/images/vipps-logo.svg"
|
||||
class="vipps-logo"
|
||||
alt="vipps logo"
|
||||
/>
|
||||
<span v-if="amount * price > price">
|
||||
kr.
|
||||
<span class="big-money">{{ amount * price }},-</span>
|
||||
({{ price }},- pr. lodd)
|
||||
</span>
|
||||
<span v-if="amount * price == price">
|
||||
kr.
|
||||
<span class="big-money">{{ amount * price }},-</span>
|
||||
pr. lodd
|
||||
</span>
|
||||
<ing
|
||||
src="/public/assets/images/vipps-qr.png"
|
||||
class="qr-logo"
|
||||
v-if="qrFailed"
|
||||
/>
|
||||
<canvas v-if="!qrFailed" ref="canvas" class="qr-logo"></canvas>
|
||||
<span class="phone-number">{{ phone }}</span>
|
||||
<span class="name">{{ name }}</span>
|
||||
<span class="mark-with">Merk med: {{ message }}</span>
|
||||
</div>
|
||||
<p class="click-to-open-text" v-if="isMobile">
|
||||
<i>Du kan også klikke på QR-koden for å åpne i Vipps</i>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QRCode from "qrcode";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
amount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
qrImage: null,
|
||||
qrFailed: false,
|
||||
phone: __PHONE__,
|
||||
name: __NAME__,
|
||||
price: __PRICE__,
|
||||
message: __MESSAGE__
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
amount: function(price) {
|
||||
this.calculateQr();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.calculateQr();
|
||||
},
|
||||
computed: {
|
||||
isMobile: function() {
|
||||
return this.isMobileFunction();
|
||||
},
|
||||
priceToPay: function() {
|
||||
return this.amount * (this.price * 100);
|
||||
},
|
||||
vippsUrlBasedOnUserAgent: function() {
|
||||
if (navigator.userAgent.includes("iPhone")) {
|
||||
return (
|
||||
"https://qr.vipps.no/28/2/01/031/47" +
|
||||
this.phone.replace(/ /g, "") +
|
||||
"?v=1&m=" +
|
||||
this.message +
|
||||
"&a=" +
|
||||
this.priceToPay
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
"https://qr.vipps.no/28/2/01/031/47" +
|
||||
this.phone.replace(/ /g, "") +
|
||||
"?v=1&m=" +
|
||||
this.message
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateQr: function() {
|
||||
let canvas = this.$refs["canvas"];
|
||||
QRCode.toCanvas(
|
||||
canvas,
|
||||
this.vippsUrlBasedOnUserAgent,
|
||||
{ errorCorrectionLevel: "Q" },
|
||||
(err, url) => {
|
||||
if (err != null) {
|
||||
this.qrFailed = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.drawLogoOverCanvas(canvas);
|
||||
},
|
||||
drawLogoOverCanvas(canvas) {
|
||||
const context = canvas.getContext("2d");
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2;
|
||||
context.font = "30px Arial";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.arc(centerX, centerY, 25, 0, 2 * Math.PI, false);
|
||||
context.fillStyle = "white";
|
||||
context.fill();
|
||||
context.lineWidth = 3;
|
||||
context.strokeStyle = "#fe5b23";
|
||||
context.stroke();
|
||||
context.fillText("🍾", centerX, centerY);
|
||||
},
|
||||
openVipps: function() {
|
||||
if (!this.isMobileFunction()) {
|
||||
return;
|
||||
}
|
||||
window.location.assign(this.vippsUrlBasedOnUserAgent);
|
||||
},
|
||||
isMobileFunction: function() {
|
||||
if (
|
||||
navigator.userAgent.match(/Android/i) ||
|
||||
navigator.userAgent.match(/webOS/i) ||
|
||||
navigator.userAgent.match(/iPhone/i) ||
|
||||
navigator.userAgent.match(/iPad/i) ||
|
||||
navigator.userAgent.match(/iPod/i) ||
|
||||
navigator.userAgent.match(/BlackBerry/i) ||
|
||||
navigator.userAgent.match(/Windows Phone/i)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
.vipps-container {
|
||||
font-family: Arial;
|
||||
border-radius: 10px;
|
||||
background-color: #ff5b23;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 25px;
|
||||
width: 250px;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big-money {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vipps-logo {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.phone-number {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.qr-logo {
|
||||
margin-top: 15px;
|
||||
border-radius: 10px;
|
||||
width: 220px;
|
||||
margin: 15px auto auto auto;
|
||||
}
|
||||
|
||||
.name,
|
||||
.mark-with {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.vipps-container {
|
||||
margin-left: 0px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.click-to-open-text {
|
||||
width: 65%;
|
||||
padding-top: 10px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<div class="chart">
|
||||
<canvas ref="win-chart"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { chartWinsByColor } from "@/api";
|
||||
|
||||
export default {
|
||||
async mounted() {
|
||||
let canvas = this.$refs["win-chart"].getContext("2d");
|
||||
|
||||
let response = await chartWinsByColor();
|
||||
let labels = ["Vunnet"];
|
||||
let blue = {
|
||||
label: "Blå",
|
||||
borderColor: "#57d2fb",
|
||||
backgroundColor: "#d4f2fe",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let yellow = {
|
||||
label: "Gul",
|
||||
borderColor: "#ffde5d",
|
||||
backgroundColor: "#fff6d6",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let red = {
|
||||
label: "Rød",
|
||||
borderColor: "#ef5878",
|
||||
backgroundColor: "#fbd7de",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
let green = {
|
||||
label: "Grønn",
|
||||
borderColor: "#10e783",
|
||||
backgroundColor: "#c8f9df",
|
||||
borderWidth: 2,
|
||||
data: []
|
||||
};
|
||||
|
||||
blue.data.push(response.blue.win);
|
||||
yellow.data.push(response.yellow.win);
|
||||
red.data.push(response.red.win);
|
||||
green.data.push(response.green.win);
|
||||
let highestNumber = 0;
|
||||
if (response.blue.win > highestNumber) {
|
||||
highestNumber = response.blue.win;
|
||||
}
|
||||
if (response.red.win > highestNumber) {
|
||||
highestNumber = response.red.win;
|
||||
}
|
||||
if (response.green.win > highestNumber) {
|
||||
highestNumber = response.green.win;
|
||||
}
|
||||
if (response.yellow.win > highestNumber) {
|
||||
highestNumber = response.yellow.win;
|
||||
}
|
||||
|
||||
let datasets = [blue, yellow, green, red];
|
||||
let chartdata = {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
let chart = new Chart(canvas, {
|
||||
type: "bar",
|
||||
data: chartdata,
|
||||
options: {
|
||||
animation: {
|
||||
duration: 0 // general animation time
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Antall vinn",
|
||||
fontSize: 20
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
padding: 12,
|
||||
boxWidth: 20,
|
||||
usePointStyle: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
suggestedMax: highestNumber + 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.chart {
|
||||
height: 40vh;
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
138
src/ui/Wine.vue
138
src/ui/Wine.vue
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<div class="wine">
|
||||
<slot name="top"></slot>
|
||||
<div class="wine-image">
|
||||
<img
|
||||
v-if="wine.image && loadImage"
|
||||
:src="wine.image"
|
||||
/>
|
||||
<img v-else class="wine-placeholder" alt="Wine image" />
|
||||
</div>
|
||||
|
||||
<div class="wine-details">
|
||||
<span v-if="wine.name" class="wine-name">{{ wine.name }}</span>
|
||||
<span v-if="wine.rating"><b>Rating:</b> {{ wine.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></slot>
|
||||
|
||||
<div class="bottom-section">
|
||||
<slot name="bottom"></slot>
|
||||
<a v-if="wine.vivinoLink" :href="wine.vivinoLink" class="link float-right">
|
||||
Les mer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
wine: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadImage: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/styles/media-queries";
|
||||
@import "./src/styles/variables";
|
||||
|
||||
.wine {
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
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 tablet {
|
||||
width: 250px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wine-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.wine-name{
|
||||
font-size: 20px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.wine-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
167
src/ui/Wines.vue
167
src/ui/Wines.vue
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<div v-if="wines.length > 0" class="wines-main-container">
|
||||
<div class="info-and-link">
|
||||
<h3>
|
||||
Topp 5 viner
|
||||
</h3>
|
||||
<router-link to="viner">
|
||||
<span class="vin-link">Se alle viner </span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="wine-container">
|
||||
<Wine v-for="wine in wines" :key="wine" :wine="wine">
|
||||
<template v-slot:top>
|
||||
<div class="flex justify-end">
|
||||
<div class="requested-count cursor-pointer">
|
||||
<span> {{ wine.occurences }} </span>
|
||||
<i class="icon icon--heart" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Wine>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { event } from "vue-analytics";
|
||||
import Wine from "@/ui/Wine";
|
||||
import { overallWineStatistics } from "@/api";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Wine
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wines: [],
|
||||
clickedWine: null,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let response = await overallWineStatistics();
|
||||
|
||||
response.sort();
|
||||
response = response
|
||||
.filter(wine => wine.name != null && wine.name != "")
|
||||
.sort(
|
||||
this.predicate(
|
||||
{
|
||||
name: "occurences",
|
||||
reverse: true
|
||||
},
|
||||
{
|
||||
name: "rating",
|
||||
reverse: true
|
||||
}
|
||||
)
|
||||
);
|
||||
this.wines = response.slice(0, 5);
|
||||
},
|
||||
methods: {
|
||||
predicate: function() {
|
||||
var fields = [],
|
||||
n_fields = arguments.length,
|
||||
field,
|
||||
name,
|
||||
cmp;
|
||||
|
||||
var default_cmp = function(a, b) {
|
||||
if (a == undefined) a = 0;
|
||||
if (b == undefined) b = 0;
|
||||
if (a === b) return 0;
|
||||
return a < b ? -1 : 1;
|
||||
},
|
||||
getCmpFunc = function(primer, reverse) {
|
||||
var dfc = default_cmp,
|
||||
// closer in scope
|
||||
cmp = default_cmp;
|
||||
if (primer) {
|
||||
cmp = function(a, b) {
|
||||
return dfc(primer(a), primer(b));
|
||||
};
|
||||
}
|
||||
if (reverse) {
|
||||
return function(a, b) {
|
||||
return -1 * cmp(a, b);
|
||||
};
|
||||
}
|
||||
return cmp;
|
||||
};
|
||||
|
||||
// preprocess sorting options
|
||||
for (var i = 0; i < n_fields; i++) {
|
||||
field = arguments[i];
|
||||
if (typeof field === "string") {
|
||||
name = field;
|
||||
cmp = default_cmp;
|
||||
} else {
|
||||
name = field.name;
|
||||
cmp = getCmpFunc(field.primer, field.reverse);
|
||||
}
|
||||
fields.push({
|
||||
name: name,
|
||||
cmp: cmp
|
||||
});
|
||||
}
|
||||
|
||||
// final comparison function
|
||||
return function(A, B) {
|
||||
var name, result;
|
||||
for (var i = 0; i < n_fields; i++) {
|
||||
result = 0;
|
||||
field = fields[i];
|
||||
name = field.name;
|
||||
|
||||
result = field.cmp(A[name], B[name]);
|
||||
if (result !== 0) break;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/styles/variables.scss";
|
||||
@import "./src/styles/global.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
.wines-main-container {
|
||||
margin-bottom: 10em;
|
||||
}
|
||||
|
||||
.info-and-link{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wine-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: 2rem;
|
||||
|
||||
.requested-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
background-color: rgb(244,244,244);
|
||||
border-radius: 1.1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
font-size: 1.25em;
|
||||
|
||||
span {
|
||||
padding-right: 0.5rem;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.icon--heart{
|
||||
font-size: 1.5rem;
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<div class="current-drawn-container">
|
||||
<div class="current-draw" v-if="drawing">
|
||||
<h2>TREKKER</h2>
|
||||
<div
|
||||
:class="currentColor + '-raffle'"
|
||||
class="raffle-element center-new-winner"
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
>
|
||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="current-draw" v-if="drawingDone">
|
||||
<h2>VINNER</h2>
|
||||
<div
|
||||
:class="currentColor + '-raffle'"
|
||||
class="raffle-element center-new-winner"
|
||||
:style="{ transform: 'rotate(' + getRotation() + 'deg)' }"
|
||||
>
|
||||
<span v-if="currentName && colorDone">{{ currentName }}</span>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import confetti from "canvas-confetti";
|
||||
export default {
|
||||
props: {
|
||||
currentWinner: {
|
||||
type: Object
|
||||
},
|
||||
currentWinnerDrawn: {
|
||||
type: Boolean
|
||||
},
|
||||
attendees: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentWinnerLocal: {},
|
||||
winnerColor: null,
|
||||
currentColor: null,
|
||||
winnerName: null,
|
||||
currentName: null,
|
||||
colorRounds: 0,
|
||||
nameRounds: 0,
|
||||
colorTimeout: null,
|
||||
nameTimeout: null,
|
||||
colorDone: false,
|
||||
drawing: false,
|
||||
drawingDone: false,
|
||||
winnerQueue: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
currentWinner: function(currentWinner) {
|
||||
if (currentWinner == null) {
|
||||
this.drawingDone = false;
|
||||
return;
|
||||
}
|
||||
if (this.drawing) {
|
||||
this.winnerQueue.push(currentWinner);
|
||||
return;
|
||||
}
|
||||
this.drawing = true;
|
||||
this.currentName = null;
|
||||
this.currentColor = null;
|
||||
this.nameRounds = 0;
|
||||
this.colorRounds = 0;
|
||||
this.colorDone = false;
|
||||
this.currentWinnerLocal = currentWinner;
|
||||
this.drawColor(this.currentWinnerLocal.color);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
drawName: function(winnerName) {
|
||||
if (this.nameRounds == 100) {
|
||||
clearTimeout(this.nameTimeout);
|
||||
this.currentName = winnerName;
|
||||
if (this.winnerQueue.length > 0) {
|
||||
this.currentWinnerLocal = this.winnerQueue.shift();
|
||||
this.drawing = true;
|
||||
this.nameRounds = 0;
|
||||
this.colorRounds = 0;
|
||||
this.colorDone = false;
|
||||
this.drawColor(this.currentWinnerLocal.color);
|
||||
return;
|
||||
}
|
||||
this.drawing = false;
|
||||
this.drawingDone = true;
|
||||
this.startConfetti(this.currentName);
|
||||
return;
|
||||
}
|
||||
this.currentName = this.attendees[
|
||||
this.nameRounds % this.attendees.length
|
||||
].name;
|
||||
this.nameRounds += 1;
|
||||
clearTimeout(this.nameTimeout);
|
||||
this.nameTimeout = setTimeout(() => {
|
||||
this.drawName(winnerName);
|
||||
}, 50);
|
||||
},
|
||||
drawColor: function(winnerColor) {
|
||||
this.drawingDone = false;
|
||||
if (this.colorRounds == 100) {
|
||||
this.currentColor = winnerColor;
|
||||
this.colorDone = true;
|
||||
this.drawName(this.currentWinnerLocal.name);
|
||||
|
||||
clearTimeout(this.colorTimeout);
|
||||
return;
|
||||
}
|
||||
this.currentColor = this.getColor(this.colorRounds % 4);
|
||||
this.colorRounds += 1;
|
||||
|
||||
clearTimeout(this.colorTimeout);
|
||||
this.colorTimeout = setTimeout(() => {
|
||||
this.drawColor(winnerColor);
|
||||
}, 50);
|
||||
},
|
||||
getRotation: function() {
|
||||
if (this.colorDone) {
|
||||
return 0;
|
||||
}
|
||||
let num = Math.floor(Math.random() * 15);
|
||||
let neg = Math.floor(Math.random() * 2);
|
||||
return neg == 0 ? -num : num;
|
||||
},
|
||||
getColor: function(number) {
|
||||
switch (number) {
|
||||
case 0:
|
||||
return "red";
|
||||
case 1:
|
||||
return "blue";
|
||||
case 2:
|
||||
return "green";
|
||||
case 3:
|
||||
return "yellow";
|
||||
}
|
||||
},
|
||||
startConfetti(currentName){
|
||||
//duration is computed as x * 1000 miliseconds, in this case 7*1000 = 7000 miliseconds ==> 7 seconds.
|
||||
var duration = 7 * 1000;
|
||||
var animationEnd = Date.now() + duration;
|
||||
var defaults = { startVelocity: 50, spread: 160, ticks: 50, zIndex: 0, particleCount: 20};
|
||||
var uberDefaults = { startVelocity: 65, spread: 75, zIndex: 0, particleCount: 35}
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
var interval = setInterval(function() {
|
||||
var timeLeft = animationEnd - Date.now();
|
||||
if (timeLeft <= 0) {
|
||||
return clearInterval(interval);
|
||||
}
|
||||
if(currentName == "Amund Brandsrud"){
|
||||
runCannon(uberDefaults, {x: 1, y: 1 }, {angle: 135});
|
||||
runCannon(uberDefaults, {x: 0, y: 1 }, {angle: 45});
|
||||
runCannon(uberDefaults, {y: 1 }, {angle: 90});
|
||||
runCannon(uberDefaults, {x: 0 }, {angle: 45});
|
||||
runCannon(uberDefaults, {x: 1 }, {angle: 135});
|
||||
}else{
|
||||
runCannon(defaults, {x: 0 }, {angle: 45});
|
||||
runCannon(defaults, {x: 1 }, {angle: 135});
|
||||
runCannon(defaults, {y: 1 }, {angle: 90});
|
||||
|
||||
}
|
||||
}, 250);
|
||||
|
||||
function runCannon(confettiDefaultValues, originPoint, launchAngle){
|
||||
confetti(Object.assign({}, confettiDefaultValues, {origin: originPoint }, launchAngle))
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.current-drawn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 v-if="winners.length > 0"> {{ title ? title : 'Vinnere' }}</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div v-for="(winner, index) in winners" :key="index">
|
||||
<router-link :to="`/highscore/${ encodeURIComponent(winner.name) }`">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element">{{ winner.name }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
winners: {
|
||||
type: Array
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/global.scss";
|
||||
@import "../styles/variables.scss";
|
||||
@import "../styles/media-queries.scss";
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.winners {
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.raffle-element {
|
||||
font-size: 1rem;
|
||||
width: 145px;
|
||||
height: 145px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
27
src/utils.js
27
src/utils.js
@@ -1,27 +0,0 @@
|
||||
|
||||
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 `${ye}-${mo}-${da}`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import { routes } from "@/router.js";
|
||||
import Vinlottis from "@/Vinlottis";
|
||||
import VueAnalytics from "vue-analytics";
|
||||
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { Vue as VueIntegration } from "@sentry/integrations";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(VueAnalytics, {
|
||||
id: "UA-156846886-1"
|
||||
});
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://7debc951f0074fb68d7a76a1e3ace6fa@o364834.ingest.sentry.io/4905091",
|
||||
integrations: [
|
||||
new VueIntegration({
|
||||
Vue,
|
||||
tracing: true
|
||||
}),
|
||||
new Integrations.BrowserTracing(),
|
||||
]
|
||||
})
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: routes
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
router,
|
||||
components: { Vinlottis },
|
||||
template: "<Vinlottis/>",
|
||||
render: h => h(Vinlottis)
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user