Did a lot. Tested SSE & started on socket.io, should prob kill s.io replace w/ ws
This commit is contained in:
@@ -10,6 +10,7 @@ import RegisterWinePage from "@/components/admin/RegisterWinePage";
|
||||
import archiveLotteryPage from "@/components/admin/archiveLotteryPage";
|
||||
import registerAttendeePage from "@/components/admin/registerAttendeePage";
|
||||
import DrawWinnerPage from "@/components/admin/DrawWinnerPage";
|
||||
import PrizeDistributionPage from "@/components/admin/PrizeDistributionPage";
|
||||
import PushPage from "@/components/admin/PushPage";
|
||||
|
||||
export default {
|
||||
@@ -37,6 +38,12 @@ export default {
|
||||
slug: "draw",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Prisutdeling",
|
||||
component: PrizeDistributionPage,
|
||||
slug: "price",
|
||||
counter: null
|
||||
},
|
||||
{
|
||||
name: "Arkiver lotteri",
|
||||
component: archiveLotteryPage,
|
||||
|
||||
@@ -32,14 +32,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prize-distribution">
|
||||
<h2>Prisutdeling</h2>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="startPrizeDistribution">Start automatisk prisutdeling med SMS</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="winners.length > 0">Vinnere</h2>
|
||||
<div class="winners" v-if="winners.length > 0">
|
||||
<div :class="winner.color + '-raffle'" class="raffle-element" v-for="(winner, index) in winners" :key="index">
|
||||
@@ -181,29 +173,7 @@ export default {
|
||||
this.drawingWinner = false;
|
||||
this.secondsLeft = this.drawTime;
|
||||
},
|
||||
startPrizeDistribution() {
|
||||
if (!window.confirm("Er du sikker på at du vil starte prisutdeling?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawingWinner = false;
|
||||
|
||||
const options = { method: "POST" };
|
||||
fetch(`/api/lottery/prize-distribution/start`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Startet prisutdeling. SMS'er sendt ut!`
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke starte prisutdeling`,
|
||||
description: response.message
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
notifyWinner(winner) {
|
||||
const options = { method: "POST" };
|
||||
|
||||
|
||||
198
frontend/components/admin/PrizeDistributionPage.vue
Normal file
198
frontend/components/admin/PrizeDistributionPage.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Prisutdeling</h1>
|
||||
|
||||
<div class="prize-distribution">
|
||||
<h2>Prisutdeling</h2>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="vin-button" @click="startPrizeDistribution">
|
||||
Start automatisk prisutdeling med SMS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sms-service-output">
|
||||
<h2>Logger</h2>
|
||||
|
||||
<div class="connection-status">
|
||||
<p>{{ logConnectionStatus }}</p>
|
||||
</div>
|
||||
|
||||
<button @click="closeConnection">Close connection</button>
|
||||
<button @click="connectToEventStream">Open connection</button>
|
||||
|
||||
<div class="terminal">
|
||||
<ul v-if="logs.length">
|
||||
<li v-for="(log, index) in reversedLogs" :class="{selected: selectedIndexs.find(el => el === index) !== undefined}">
|
||||
<!-- <span class="line">{{ secondsAgo(log) }}</span> -->
|
||||
<span class="line">{{ logLength - index }}</span>
|
||||
<LogMessage :log="log" @toggleMessage="(value) => setSelectedIndex(index, value)" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>No logs yet...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LogMessage from "@/components/admin/logMessage";
|
||||
|
||||
export default {
|
||||
components: { LogMessage },
|
||||
computed: {
|
||||
reversedLogs() {
|
||||
return [...this.logs].reverse()
|
||||
},
|
||||
logLength() {
|
||||
return this.logs.length;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
source: null,
|
||||
logConnectionStatus: 'Disconnected',
|
||||
logs: [],
|
||||
selectedIndexs: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.connectToEventStream();
|
||||
window.addEventListener('beforeunload', this.closeConnection);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.closeConnection();
|
||||
},
|
||||
methods: {
|
||||
setSelectedIndex(index, value) {
|
||||
if (value === true) {
|
||||
this.selectedIndexs.push(index);
|
||||
} else {
|
||||
this.selectedIndexs = this.selectedIndexs.filter(el => el !== index);
|
||||
}
|
||||
},
|
||||
closeConnection() {
|
||||
console.log("close event received");
|
||||
this.logConnectionStatus = 'closed';
|
||||
this.source.close();
|
||||
},
|
||||
secondsAgo(log) {
|
||||
const seconds = Math.floor((new Date() - new Date(log.timestamp)) / 1000)
|
||||
return `${seconds} s`
|
||||
},
|
||||
connectToEventStream() {
|
||||
let SOURCE_URL = '/api/logs/sms?contentTransferEncoding=base64'
|
||||
if (this.lastMessageId) {
|
||||
SOURCE_URL = `${SOURCE_URL}&lastMessageId=${this.lastMessageId}`
|
||||
}
|
||||
this.source = new EventSource(SOURCE_URL);
|
||||
|
||||
if (this.logs.length) {
|
||||
this.logs.push({
|
||||
message: '- - - new session - - -',
|
||||
timestamp: new Date(),
|
||||
type: 'INFO'
|
||||
})
|
||||
}
|
||||
|
||||
this.source.onmessage = (event) => this.handleMessage(event);
|
||||
this.source.onopen = (event) => this.handleConnectionOpened(event);
|
||||
this.source.onerror = (event) => this.handleError(error);
|
||||
},
|
||||
handleConnectionOpened(event) {
|
||||
this.logConnectionStatus = 'Connected';
|
||||
},
|
||||
handleMessage(event) {
|
||||
this.lastMessageId = event.lastEventId;
|
||||
let message = JSON.parse(atob(event.data));
|
||||
if (message.type === 'close') {
|
||||
this.closeConnection();
|
||||
}
|
||||
|
||||
this.logs.push(message);
|
||||
},
|
||||
handleError(event) {
|
||||
if (event.eventPhase == EventSource.CLOSED) {
|
||||
console.log("Server closed connection, clean up client side");
|
||||
this.source.close();
|
||||
}
|
||||
|
||||
if (event.target.readyState == EventSource.CLOSED)
|
||||
this.logConnectionStatus = "Disconnected"
|
||||
else if (event.target.readyState == EventSource.CONNECTING)
|
||||
this.logConnectionStatus = "Connecting..."
|
||||
else
|
||||
console.warning('Unknown SSE event state');
|
||||
},
|
||||
startPrizeDistribution() {
|
||||
if (!window.confirm("Er du sikker på at du vil starte prisutdeling?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { method: "POST" };
|
||||
fetch(`/api/lottery/prize-distribution/start`, options)
|
||||
.then(resp => resp.json())
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
this.$toast.info({
|
||||
title: `Startet prisutdeling. SMS'er sendt ut!`,
|
||||
});
|
||||
} else {
|
||||
this.$toast.error({
|
||||
title: `Klarte ikke starte prisutdeling`,
|
||||
description: response.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.terminal {
|
||||
padding: 1.5rem 1rem 1rem;
|
||||
background-color: #333333;
|
||||
color: white;
|
||||
font-family: "Roboto Mono", Monaco, "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
white-space: pre;
|
||||
max-width: 90vw;
|
||||
overflow-x: auto;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
text-decoration: none;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.selected {
|
||||
background-color: lightgrey;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
padding-right: 0.5rem;
|
||||
border-right: 2px solid grey;
|
||||
min-width: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
146
frontend/components/admin/logMessage.vue
Normal file
146
frontend/components/admin/logMessage.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="flex column">
|
||||
<div @click="toggleMessage" :class="{error: log.type === 'error', open: messageOpen}" class="flex log" :data-details="`${log.type} @ ${log.timestamp}`">
|
||||
<Chevron class="chevron" :rotate="messageOpen" />
|
||||
<span>{{ log.message }}</span>
|
||||
|
||||
</div>
|
||||
<transition name="slide">
|
||||
<div v-if="messageOpen" class="details">{{ otherData(log) }}</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chevron from "@/ui/Chevron";
|
||||
|
||||
export default {
|
||||
components: { Chevron },
|
||||
props: {
|
||||
log: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messageOpen: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMessage(index) {
|
||||
this.messageOpen = !this.messageOpen;
|
||||
this.$emit('toggleMessage', this.messageOpen)
|
||||
setTimeout(() => {
|
||||
document.getElementsByClassName('selected')[0].classList.add('error')
|
||||
}, 1000)
|
||||
},
|
||||
otherData(data) {
|
||||
const ldata = { ...data };
|
||||
delete ldata.type;
|
||||
delete ldata.message
|
||||
if (Object.keys(ldata).length > 0) {
|
||||
return Object.keys(data).map(key => {
|
||||
if (typeof data[key] === 'object') {
|
||||
return `${key}: ${JSON.stringify(data[key], null, 1)}`
|
||||
}
|
||||
return `${key}: ${data[key]}`}
|
||||
).join('\n')
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.details {
|
||||
display: inline-flex;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
margin: 0.2rem 0.2rem 0;
|
||||
padding: 0 1rem 0.5rem;
|
||||
border-bottom: 1px solid #333333;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.slide-enter-active {
|
||||
-moz-transition-duration: 0.2s;
|
||||
-webkit-transition-duration: 0.2s;
|
||||
-o-transition-duration: 0.2s;
|
||||
transition-duration: 0.2s;
|
||||
-moz-transition-timing-function: ease;
|
||||
-webkit-transition-timing-function: ease;
|
||||
-o-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.slide-leave-active {
|
||||
-moz-transition-duration: 0.3s;
|
||||
-webkit-transition-duration: 0.3s;
|
||||
-o-transition-duration: 0.3s;
|
||||
transition-duration: 0.3s;
|
||||
-moz-transition-timing-function: ease;
|
||||
-webkit-transition-timing-function: ease;
|
||||
-o-transition-timing-function: ease;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.slide-enter-to, .slide-leave {
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-enter, .slide-leave-to {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.log {
|
||||
padding: 0 0.5rem;
|
||||
position: relative;
|
||||
// width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&.error {
|
||||
border-top-right-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
background-color: #F24647;
|
||||
color: white;
|
||||
}
|
||||
&.error.open {
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(128, 127, 127, 0.8);
|
||||
|
||||
&.error {
|
||||
background-color: #F24647;
|
||||
border-top-left-radius: 0px;
|
||||
}
|
||||
|
||||
&.error:after {
|
||||
background-color: rgba(#F24647, .8);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
pointer-events: none;
|
||||
content: attr(data-details);
|
||||
display: block;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: -22px;
|
||||
line-height: 22px;
|
||||
left: 0;
|
||||
background-color: inherit;
|
||||
padding: 0 .5rem;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user