Renamed /src to /frontend.
This commit is contained in:
82
frontend/ui/Attendees.vue
Normal file
82
frontend/ui/Attendees.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<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>
|
||||
133
frontend/ui/Banner.vue
Normal file
133
frontend/ui/Banner.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<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>
|
||||
337
frontend/ui/Chat.vue
Normal file
337
frontend/ui/Chat.vue
Normal file
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<hr />
|
||||
<h2>Chat</h2>
|
||||
<div class="history" ref="history" v-if="chatHistory.length > 0">
|
||||
<div class="opaque-skirt"></div>
|
||||
<div v-if="hasMorePages" class="fetch-older-history">
|
||||
<button @click="loadMoreHistory">Hent eldre meldinger</button>
|
||||
</div>
|
||||
|
||||
<div class="history-message"
|
||||
v-for="(history, index) in chatHistory"
|
||||
:key="`${history.username}-${history.timestamp}-${index}`"
|
||||
>
|
||||
<div>
|
||||
<span class="username">{{ history.username }}</span>
|
||||
<span class="timestamp">{{ getTime(history.timestamp) }}</span>
|
||||
</div>
|
||||
<span class="message">{{ history.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="username" class="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.."
|
||||
/>
|
||||
|
||||
<div class="validation-error" v-if="validationError">
|
||||
{{ validationError }}
|
||||
</div>
|
||||
<button @click="setUsername">Lagre brukernavn</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getChatHistory } from "@/api";
|
||||
import io from "socket.io-client";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
socket: null,
|
||||
chatHistory: [],
|
||||
hasMorePages: true,
|
||||
message: "",
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
temporaryUsername: null,
|
||||
username: null,
|
||||
validationError: undefined
|
||||
};
|
||||
},
|
||||
created() {
|
||||
getChatHistory(1, this.pageSize)
|
||||
.then(resp => {
|
||||
this.chatHistory = resp.messages;
|
||||
this.hasMorePages = resp.total != resp.messages.length;
|
||||
});
|
||||
const username = window.localStorage.getItem('username');
|
||||
if (username) {
|
||||
this.username = username;
|
||||
this.emitUsernameOnConnect = true;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chatHistory: {
|
||||
handler: function(newVal, oldVal) {
|
||||
if (oldVal.length == 0) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
else if (newVal && newVal.length == oldVal.length) {
|
||||
if (this.isScrollPositionAtBottom()) {
|
||||
this.scrollToBottomOfHistory();
|
||||
}
|
||||
} else {
|
||||
const prevOldestMessage = oldVal[0];
|
||||
this.scrollToMessageElement(prevOldestMessage);
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
},
|
||||
mounted() {
|
||||
const BASE_URL = __APIURL__ || window.location.origin;
|
||||
this.socket = io(`${BASE_URL}`);
|
||||
this.socket.on("chat", msg => {
|
||||
this.chatHistory.push(msg);
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", msg => {
|
||||
this.wasDisconnected = true;
|
||||
});
|
||||
|
||||
this.socket.on("connect", msg => {
|
||||
if (
|
||||
this.emitUsernameOnConnect ||
|
||||
(this.wasDisconnected && this.username != null)
|
||||
) {
|
||||
this.setUsername(this.username);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("accept_username", msg => {
|
||||
const { reason, success, username } = msg;
|
||||
this.usernameAccepted = success;
|
||||
|
||||
if (success !== true) {
|
||||
this.username = null;
|
||||
this.validationError = reason;
|
||||
} else {
|
||||
this.usernameAllowed = true;
|
||||
this.username = username;
|
||||
this.validationError = null;
|
||||
window.localStorage.setItem("username", username);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
loadMoreHistory() {
|
||||
let { page, pageSize } = this;
|
||||
page = page + 1;
|
||||
|
||||
getChatHistory(page, pageSize)
|
||||
.then(resp => {
|
||||
this.chatHistory = resp.messages.concat(this.chatHistory);
|
||||
this.page = page;
|
||||
this.hasMorePages = resp.total != this.chatHistory.length;
|
||||
});
|
||||
},
|
||||
pad(num) {
|
||||
if (num > 9) return num;
|
||||
return `0${num}`;
|
||||
},
|
||||
getTime(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
const timeString = `${this.pad(date.getHours())}:${this.pad(
|
||||
date.getMinutes()
|
||||
)}:${this.pad(date.getSeconds())}`;
|
||||
|
||||
if (date.getDate() == new Date().getDate()) {
|
||||
return timeString;
|
||||
}
|
||||
return `${date.toLocaleDateString()} ${timeString}`;
|
||||
},
|
||||
sendMessage() {
|
||||
const message = { message: this.message };
|
||||
this.socket.emit("chat", message);
|
||||
this.message = '';
|
||||
this.scrollToBottomOfHistory();
|
||||
},
|
||||
setUsername(username=undefined) {
|
||||
if (this.temporaryUsername) {
|
||||
username = this.temporaryUsername;
|
||||
}
|
||||
const message = { username: username };
|
||||
this.socket.emit("username", message);
|
||||
},
|
||||
removeUsername() {
|
||||
this.username = null;
|
||||
this.temporaryUsername = null;
|
||||
window.localStorage.removeItem("username");
|
||||
},
|
||||
isScrollPositionAtBottom() {
|
||||
const { history } = this.$refs;
|
||||
if (history) {
|
||||
return history.offsetHeight + history.scrollTop >= history.scrollHeight;
|
||||
}
|
||||
return false
|
||||
},
|
||||
scrollToBottomOfHistory() {
|
||||
setTimeout(() => {
|
||||
const { history } = this.$refs;
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}, 1);
|
||||
},
|
||||
scrollToMessageElement(message) {
|
||||
const elemTimestamp = this.getTime(message.timestamp);
|
||||
const self = this;
|
||||
const getTimeStamp = (elem) => elem.getElementsByClassName('timestamp')[0].innerText;
|
||||
const prevOldestMessageInNewList = (elem) => getTimeStamp(elem) == elemTimestamp;
|
||||
|
||||
setTimeout(() => {
|
||||
const { history } = self.$refs;
|
||||
const childrenElements = Array.from(history.getElementsByClassName('history-message'));
|
||||
|
||||
const elemInNewList = childrenElements.find(prevOldestMessageInNewList);
|
||||
history.scrollTop = elemInNewList.offsetTop - 70
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/media-queries.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
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;
|
||||
position: relative;
|
||||
|
||||
&-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.35rem 0;
|
||||
position: relative;
|
||||
|
||||
.username {
|
||||
font-weight: bold;
|
||||
font-size: 1.05rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.9rem;
|
||||
top: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&-message:nth-of-type(2) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
& .opaque-skirt {
|
||||
width: 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: 1rem 0;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.username-dialog {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
.validation-error {
|
||||
position: absolute;
|
||||
background-color: $light-red;
|
||||
color: $red;
|
||||
top: -3.5rem;
|
||||
left: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2.1rem;
|
||||
left: 2rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
transform: rotate(45deg);
|
||||
background-color: $light-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background: #b7debd;
|
||||
color: #333;
|
||||
padding: 10px 30px;
|
||||
border: 0;
|
||||
width: fit-content;
|
||||
font-size: 1rem;
|
||||
/* height: 1.5rem; */
|
||||
/* max-height: 1.5rem; */
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: transform 0.5s ease;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
</style>
|
||||
161
frontend/ui/Countdown.vue
Normal file
161
frontend/ui/Countdown.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<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>
|
||||
32
frontend/ui/Footer.vue
Normal file
32
frontend/ui/Footer.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
120
frontend/ui/Highscore.vue
Normal file
120
frontend/ui/Highscore.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<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
frontend/ui/Modal.vue
Normal file
101
frontend/ui/Modal.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<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>
|
||||
146
frontend/ui/PurchaseGraph.vue
Normal file
146
frontend/ui/PurchaseGraph.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<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>
|
||||
427
frontend/ui/RaffleGenerator.vue
Normal file
427
frontend/ui/RaffleGenerator.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<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()
|
||||
|
||||
window.ga('send', {
|
||||
hitType: "event",
|
||||
eventCategory: "Raffles",
|
||||
eventAction: "Generate",
|
||||
eventLabel: 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>
|
||||
124
frontend/ui/RequestedWineCard.vue
Normal file
124
frontend/ui/RequestedWineCard.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<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 "@/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>
|
||||
115
frontend/ui/ScanToVinmonopolet.vue
Normal file
115
frontend/ui/ScanToVinmonopolet.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<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 "@/styles/variables";
|
||||
@import "@/styles/global";
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
color: $red;
|
||||
}
|
||||
</style>
|
||||
81
frontend/ui/Tabs.vue
Normal file
81
frontend/ui/Tabs.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<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>
|
||||
99
frontend/ui/TextToast.vue
Normal file
99
frontend/ui/TextToast.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<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>
|
||||
216
frontend/ui/TotalBought.vue
Normal file
216
frontend/ui/TotalBought.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<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>
|
||||
100
frontend/ui/UpdateToast.vue
Normal file
100
frontend/ui/UpdateToast.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<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
frontend/ui/Vipps.vue
Normal file
212
frontend/ui/Vipps.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<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>
|
||||
112
frontend/ui/WinGraph.vue
Normal file
112
frontend/ui/WinGraph.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<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
frontend/ui/Wine.vue
Normal file
138
frontend/ui/Wine.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<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 "@/styles/media-queries";
|
||||
@import "@/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>
|
||||
166
frontend/ui/Wines.vue
Normal file
166
frontend/ui/Wines.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<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 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 "@/styles/variables.scss";
|
||||
@import "@/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>
|
||||
217
frontend/ui/WinnerDraw.vue
Normal file
217
frontend/ui/WinnerDraw.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<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>
|
||||
55
frontend/ui/Winners.vue
Normal file
55
frontend/ui/Winners.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user