twitch-bot/src/raffle.vue
Knut Ahlers 6455b409ce
[raffle] Add functionality to reset a raffle
in order to re-use the same raffle by wiping the entrants, resetting
time-fields to their default value and resetting status to draft

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2023-12-09 16:22:00 +01:00

1185 lines
36 KiB
Vue

<template>
<div>
<b-row>
<b-col>
<b-table
:busy="!raffleTableItems"
:fields="raffleFields"
hover
:items="raffleTableItems"
striped
primary-key="id"
>
<template #cell(_actions)="data">
<b-button-group size="sm">
<b-button
v-if="data.item.status === 'planned'"
variant="success"
title="Start Raffle"
@click="startRaffle(data.item.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'play']"
/>
</b-button>
<b-button
v-else-if="data.item.status === 'active'"
variant="warning"
title="Close Raffle"
@click="closeRaffle(data.item.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'stop']"
/>
</b-button>
<b-button
v-else-if="data.item.status === 'ended'"
variant="warning"
title="Re-Open Raffle"
@click="reopenRaffle(data.item.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'play']"
/>
</b-button>
<b-button
variant="info"
:disabled="data.item.status === 'planned'"
title="Manage Entries"
@click="showEntryDialog(data.item.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'people-group']"
/>
</b-button>
<b-button
variant="primary"
title="Edit Raffle"
@click="editRaffle(data.item)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'pen']"
/>
</b-button>
<b-button
title="Duplicate Raffle"
@click="cloneRaffle(data.item.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'clone']"
/>
</b-button>
<b-button
variant="warning"
:disabled="data.item.status !== 'ended'"
title="Reset Raffle"
@click="resetRaffle(data.item.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'recycle']"
/>
</b-button>
<b-button
variant="danger"
title="Delete Raffle"
@click="deleteRaffle(data.item.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'minus']"
/>
</b-button>
</b-button-group>
</template>
<template #cell(_status)="data">
<font-awesome-icon
v-if="data.item.status == 'planned'"
fixed-width
:icon="['fas', 'pen-ruler']"
title="Planned"
/>
<span v-else-if="data.item.status == 'active'">
<font-awesome-icon
fixed-width
:icon="['fas', 'spinner']"
spin-pulse
title="In Progress"
class="mr-1"
/>
<small class="text-muted">{{ raffleTimer(data.item) }}</small>
</span>
<font-awesome-icon
v-else-if="data.item.status == 'ended'"
fixed-width
:icon="['fas', 'stop']"
title="Ended"
/>
<font-awesome-icon
v-else
fixed-width
:icon="['fas', 'question']"
/>
</template>
<template #head(_actions)="">
<b-button-group size="sm">
<b-button
variant="success"
title="Create New Raffle"
@click="newRaffle"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'plus']"
/>
</b-button>
<b-button
variant="secondary"
title="Refresh Raffle"
@click="fetchRaffles"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'rotate']"
/>
</b-button>
</b-button-group>
</template>
</b-table>
</b-col>
</b-row>
<!-- Entries Modal -->
<b-modal
v-if="showRaffleEntriesModal"
scrollable
size="lg"
:visible="showRaffleEntriesModal"
:title="`Entries: ${openedRaffle.title}`"
hide-footer
@hidden="closeEntriesModal"
>
<b-list-group
v-if="raffleEntries.length > 0"
flush
>
<b-list-group-item class="d-flex justify-content-between align-items-center">
<strong>{{ raffleEntries.length }} entrant(s)</strong>
<b-button-group size="sm">
<b-button
variant="success"
:disabled="openedRaffle.status !== 'ended'"
@click="pickWinner"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'crown']"
/>
Pick Winner
</b-button>
<b-button
variant="secondary"
title="Refresh Entries"
@click="refreshOpenendRaffle"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'rotate']"
:spin="openedRaffleReloading"
/>
</b-button>
</b-button-group>
</b-list-group-item>
<b-list-group-item
v-for="entry in raffleEntries"
:key="entry.id"
>
<div class="d-flex align-items-center">
<font-awesome-layers
v-if="entry.wasPicked"
class="mr-1"
:title="entry.wasRedrawn ? 'Was Redrawn' : 'Has Won'"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'crown']"
:class="entry.wasRedrawn ? 'text-muted' : 'text-warning'"
/>
<font-awesome-icon
v-if="entry.wasRedrawn"
fixed-width
:icon="['fas', 'slash']"
class="text-danger"
/>
</font-awesome-layers>
<font-awesome-icon
v-if="entry.enteredAs === 'everyone'"
fixed-width
:icon="['fas', 'user']"
title="Does not Follow"
/>
<font-awesome-icon
v-else-if="entry.enteredAs === 'follower'"
fixed-width
:icon="['fas', 'heart']"
title="Follower"
/>
<font-awesome-icon
v-else-if="entry.enteredAs === 'reward'"
fixed-width
:icon="['fas', 'coins']"
title="Subscriber"
/>
<font-awesome-icon
v-else-if="entry.enteredAs === 'subscriber'"
fixed-width
:icon="['fas', 'star']"
title="Subscriber"
/>
<font-awesome-icon
v-else-if="entry.enteredAs === 'vip'"
fixed-width
:icon="['fas', 'gem']"
title="VIP"
/>
<font-awesome-icon
v-else
fixed-width
:icon="['fas', 'question']"
/>
<span class="ml-1">{{ entry.userDisplayName }}</span>
<b-badge
variant="secondary"
class="ml-auto"
>
{{ new Date(entry.enteredAt).toLocaleString() }}
</b-badge>
</div>
<b-row>
<b-col cols="11">
<pre
v-if="entry.drawResponse"
class="mb-0 mt-2"
><code>{{ entry.drawResponse }}</code></pre>
</b-col>
<b-col cols="1">
<b-button
v-if="entry.wasPicked && !entry.wasRedrawn"
variant="danger"
size="sm"
title="Re-Draw Winner"
@click="repickWinner(entry.id)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'recycle']"
/>
</b-button>
</b-col>
</b-row>
</b-list-group-item>
</b-list-group>
<p
v-else
class="mb-0"
>
No one entered into this raffle.
</p>
</b-modal>
<!-- Raffle Editor -->
<b-modal
v-if="showRaffleEditModal"
hide-header-close
:ok-disabled="!validateRaffle()"
ok-title="Save"
scrollable
size="xl"
:visible="showRaffleEditModal"
title="Edit Raffle"
@hidden="showRaffleEditModal=false"
@ok="saveRaffle"
>
<b-row>
<!-- Keyword / Title -->
<b-col cols="3">
<b-form-group
description="Where to do the raffle?"
label="Channel"
label-for="formRaffleChannel"
>
<b-input-group prepend="#">
<b-form-input
id="formRaffleChannel"
v-model="models.raffle.channel"
type="text"
:state="validateRaffleChannel()"
:disabled="models.raffle.status !== 'planned'"
/>
</b-input-group>
</b-form-group>
</b-col>
<b-col cols="3">
<b-form-group
description="Keyword to use in chat to enter"
label="Keyword"
label-for="formRaffleKeyword"
>
<b-form-input
id="formRaffleKeyword"
v-model="models.raffle.keyword"
type="text"
:state="validateRaffleNonEmpty(models.raffle.keyword)"
:disabled="models.raffle.status !== 'planned'"
/>
</b-form-group>
</b-col>
<b-col cols="6">
<b-form-group
description="Title of the raffle (displayed in overview and available in chat)"
label="Title"
label-for="formRaffleTitle"
>
<b-form-input
id="formRaffleTitle"
v-model="models.raffle.title"
type="text"
:state="validateRaffleNonEmpty(models.raffle.title)"
/>
</b-form-group>
</b-col>
</b-row>
<b-row class="mt-3">
<!-- Allow / Multiplier / Times -->
<b-col>
<b-card
header="Allowed Entries"
no-body
>
<b-list-group flush>
<b-list-group-item>
<b-form-checkbox
v-model="models.raffle.allowEveryone"
switch
:state="validateRaffleHasAllowedEntries()"
:disabled="models.raffle.status !== 'planned'"
>
Everyone
</b-form-checkbox>
</b-list-group-item>
<b-list-group-item>
<b-form
inline
class="d-flex justify-content-between"
>
<b-form-checkbox
v-model="models.raffle.allowFollower"
switch
:state="validateRaffleHasAllowedEntries()"
:disabled="models.raffle.status !== 'planned'"
>
Followers, since
</b-form-checkbox>
<b-input-group
size="sm"
append="min"
class="col-5 px-0"
>
<b-form-input
v-model="models.raffle.minFollowAge"
class="text-right"
type="number"
placeholder="min. Age"
min="0"
:state="validateRaffleIsNumber(models.raffle.minFollowAge)"
:disabled="models.raffle.status !== 'planned'"
/>
</b-input-group>
</b-form>
</b-list-group-item>
<b-list-group-item>
<b-form-checkbox
v-model="models.raffle.allowSubscriber"
switch
:state="validateRaffleHasAllowedEntries()"
:disabled="models.raffle.status !== 'planned'"
>
Subscribers
</b-form-checkbox>
</b-list-group-item>
<b-list-group-item>
<b-form-checkbox
v-model="models.raffle.allowVIP"
switch
:state="validateRaffleHasAllowedEntries()"
:disabled="models.raffle.status !== 'planned'"
>
VIPs
</b-form-checkbox>
</b-list-group-item>
</b-list-group>
</b-card>
</b-col>
<b-col>
<b-card header="Luck Modifiers">
<b-row>
<b-col>
<b-form-group
label="Followers"
label-for="formRaffleMultiFollower"
>
<b-form-input
id="formRaffleMultiFollower"
v-model="models.raffle.multiFollower"
type="number"
min="0"
step="0.1"
size="sm"
:state="validateRaffleIsNumber(models.raffle.multiFollower)"
:disabled="models.raffle.status !== 'planned'"
/>
</b-form-group>
</b-col>
<b-col>
<b-form-group
label="Subs"
label-for="formRaffleMultiSubs"
>
<b-form-input
id="formRaffleMultiSubs"
v-model="models.raffle.multiSubscriber"
type="number"
min="0"
step="0.1"
size="sm"
:state="validateRaffleIsNumber(models.raffle.multiSubscriber)"
:disabled="models.raffle.status !== 'planned'"
/>
</b-form-group>
</b-col>
<b-col>
<b-form-group
label="VIPs"
label-for="formRaffleMultiVIP"
>
<b-form-input
id="formRaffleMultiVIP"
v-model="models.raffle.multiVIP"
type="number"
min="0"
step="0.1"
size="sm"
:state="validateRaffleIsNumber(models.raffle.multiVIP)"
:disabled="models.raffle.status !== 'planned'"
/>
</b-form-group>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-text>
The base amount of tickets for <strong>Everyone</strong> is always <code>1.0</code>. You can
increase chances for certain user groups. Groups are checked from right to left: If the user
is VIP, Sub and Follower they are assigned <strong>VIPkeyword</strong> multiplier.
</b-form-text>
</b-col>
</b-row>
<!-- -->
</b-card>
</b-col>
<b-col>
<b-card
header="Times"
no-body
>
<b-list-group flush>
<b-list-group-item class="d-flex justify-content-between align-items-center">
<span>Auto-Start*</span>
<b-form-input
v-model="models.raffle.autoStartAt"
class="col-7"
type="datetime-local"
size="sm"
:min="transformISOToDateTimeLocal(new Date())"
:disabled="models.raffle.status !== 'planned'"
/>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
<span>Duration</span>
<b-input-group
size="sm"
append="min"
class="col-5 px-0"
>
<b-form-input
v-model="models.raffle.closeAfter"
type="number"
step="1"
min="0"
class="text-right"
:state="validateRaffleIsNumber(models.raffle.closeAfter)"
/>
</b-input-group>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
<span>Close At*</span>
<b-form-input
v-model="models.raffle.closeAt"
class="col-7"
type="datetime-local"
size="sm"
:min="transformISOToDateTimeLocal(new Date())"
/>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
<span>Respond in</span>
<b-input-group
size="sm"
append="sec"
class="col-5 px-0"
>
<b-form-input
v-model="models.raffle.waitForResponse"
type="number"
step="1"
min="0"
class="text-right"
:state="validateRaffleIsNumber(models.raffle.waitForResponse)"
/>
</b-input-group>
</b-list-group-item>
<b-list-group-item>
<b-form-text>
* Optional, when <strong>Close At</strong> is specified, <strong>Duration</strong> is ignored,
when no <strong>Auto-Start</strong> is specified the raffle must be started manually. If you
set <strong>Respond In</strong> to 0, no chat responses from that user are recorded after
picking them as winner.
</b-form-text>
</b-list-group-item>
</b-list-group>
</b-card>
</b-col>
</b-row>
<b-row class="mt-3">
<!-- Texts / Entries -->
<b-col>
<b-card
header="Texts"
no-body
>
<b-list-group flush>
<b-list-group-item class="">
<div class="d-flex align-items-center mb-1">
<b-form-checkbox
v-model="models.raffle.textEntryPost"
switch
class="mr-n2"
>
Message on successful entry
</b-form-checkbox>
</div>
<template-editor
v-model="models.raffle.textEntry"
/>
</b-list-group-item>
<b-list-group-item class="">
<div class="d-flex align-items-center mb-1">
<b-form-checkbox
v-model="models.raffle.textEntryFailPost"
switch
class="mr-n2"
>
Message on failed entry
</b-form-checkbox>
</div>
<template-editor
v-model="models.raffle.textEntryFail"
/>
</b-list-group-item>
<b-list-group-item class="">
<div class="d-flex align-items-center mb-1">
<b-form-checkbox
v-model="models.raffle.textWinPost"
switch
class="mr-n2"
>
Message on winner draw
</b-form-checkbox>
</div>
<template-editor
v-model="models.raffle.textWin"
/>
</b-list-group-item>
<b-list-group-item class="">
<div class="d-flex justify-content-between align-items-center mb-1">
<b-form-checkbox
v-model="models.raffle.textReminderPost"
switch
class="mr-n2"
>
Periodic reminder
</b-form-checkbox>
<b-input-group
prepend="every"
append="min"
size="sm"
class="col-2 px-0"
>
<b-form-input
v-model="models.raffle.textReminderInterval"
type="number"
step="1"
min="0"
class="text-right"
:state="validateRaffleIsNumber(models.raffle.textReminderInterval)"
/>
</b-input-group>
</div>
<template-editor
v-model="models.raffle.textReminder"
/>
</b-list-group-item>
<b-list-group-item class="">
<div class="d-flex align-items-center mb-1">
<b-form-checkbox
v-model="models.raffle.textClosePost"
switch
class="mr-n2"
>
Message on raffle close
</b-form-checkbox>
</div>
<template-editor
v-model="models.raffle.textClose"
/>
</b-list-group-item>
<b-list-group-item>
<b-form-text>
Available variables are <code>.user</code> and <code>.raffle</code> with
access to all of these configurations most notably <code>.raffle.Title</code>
and <code>.raffle.Keyword</code>.
</b-form-text>
</b-list-group-item>
</b-list-group>
</b-card>
</b-col>
</b-row>
</b-modal>
</div>
</template>
<script>
import * as constants from './const.js'
import axios from 'axios'
import TemplateEditor from './tplEditor.vue'
const ONE_MINUTE = 60000000000 // nanoseconds
const ONE_SECOND = 1000000000 // nanoseconds
export default {
components: { TemplateEditor },
computed: {
raffleEntries() {
const entries = [...this.openedRaffle.entries || []]
entries.sort((a, b) => {
const isWinner = e => e.wasPicked && !e.wasRedrawn
const wasWinnner = e => e.wasPicked && e.wasRedrawn
if (isWinner(a) && !isWinner(b)) {
return -1
} else if (isWinner(b) && !isWinner(a)) {
return 1
} else if (wasWinnner(a) && !wasWinnner(b)) {
return -1
} else if (wasWinnner(b) && !wasWinnner(a)) {
return 1
}
// Everything else: Order by ID DESC
return b.id - a.id
})
return entries
},
raffleTableItems() {
const raffles = [...this.raffles]
raffles.sort((a, b) => {
const scores = { active: 0, ended: 2, planned: 1 }
if (scores[a.status] !== scores[b.status]) {
return scores[a.status] - scores[b.status]
}
return b.id - a.id
})
return raffles
},
},
data() {
return {
models: { raffle: {} },
now: new Date(),
openedRaffle: {},
openedRaffleReloading: false,
raffleFields: [
{
class: 'col-1',
key: '_status',
label: 'Status',
tdClass: 'text-center align-middle',
thClass: 'align-middle text-center',
},
{
class: 'col-1',
key: 'channel',
label: 'Channel',
tdClass: 'align-middle',
thClass: 'align-middle',
},
{
class: 'col-1',
key: 'keyword',
label: 'Keyword',
tdClass: 'align-middle',
thClass: 'align-middle',
},
{
class: 'col-7',
key: 'title',
label: 'Title',
tdClass: 'align-middle',
thClass: 'align-middle',
},
{
class: 'col-2 text-right',
key: '_actions',
label: '',
tdClass: 'align-middle',
thClass: 'align-middle',
},
],
raffles: [],
reopenRaffleDuration: 60,
showRaffleEditModal: false,
showRaffleEntriesModal: false,
}
},
methods: {
cloneRaffle(id) {
return axios.put(`raffle/${id}/clone`, {}, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle cloned'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
closeEntriesModal() {
this.openedRaffle = {}
this.showRaffleEntriesModal = false
},
closeRaffle(id) {
this.$bvModal.msgBoxConfirm('Do you really want to close entries for this raffle?', {
buttonSize: 'sm',
cancelTitle: 'NO',
centered: true,
okTitle: 'YES',
okVariant: 'danger',
size: 'sm',
title: 'Please Confirm',
})
.then(val => {
if (!val) {
return
}
return axios.put(`raffle/${id}/close`, {}, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle closed'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
})
},
deleteRaffle(id) {
this.$bvModal.msgBoxConfirm('Do you really want to delete this raffle and all its entries?', {
buttonSize: 'sm',
cancelTitle: 'NO',
centered: true,
okTitle: 'YES',
okVariant: 'danger',
size: 'sm',
title: 'Please Confirm',
})
.then(val => {
if (!val) {
return
}
return axios.delete(`raffle/${id}`, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle deleted'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
})
},
editRaffle(raffle) {
this.$set(this.models, 'raffle', raffle)
this.showRaffleEditModal = true
},
fetchRaffles() {
return axios.get('raffle/', this.$root.axiosOptions)
.then(resp => {
this.raffles = resp.data.map(raffle => this.transformRaffleFromDB(raffle))
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
newRaffle() {
this.$set(this.models, 'raffle', {
/* eslint-disable sort-keys */
channel: '',
keyword: '!enter',
status: 'planned',
title: '',
allowEveryone: false,
allowFollower: true,
allowSubscriber: true,
allowVIP: true,
minFollowAge: 0,
multiFollower: 1.0,
multiSubscriber: 1.0,
multiVIP: 1.0,
autoStartAt: null,
closeAfter: 0,
closeAt: null,
waitForResponse: 30,
textClose: 'The raffle "{{ .raffle.Title }}" is closed now, you can no longer enter!',
textClosePost: false,
textEntry: '{{ mention .user }} you are registered, good luck!',
textEntryPost: true,
textEntryFail: '{{ mention .user }} couldn\'t register you, sorry.',
textEntryFailPost: false,
textWin: '{{ mention .user }} you won! Please speak up in chat to claim your price!',
textWinPost: false,
textReminder: 'We are currently doing a raffle until {{ dateInZone "15:04" .raffle.CloseAt "Europe/Berlin" }}: "{{ .raffle.Title }}" - type "{{ .raffle.Keyword }}" to enter!',
textReminderInterval: 15,
textReminderPost: false,
/* eslint-enable sort-keys */
})
this.showRaffleEditModal = true
},
pickWinner() {
this.openedRaffleReloading = true
return axios.put(`raffle/${this.openedRaffle.id}/pick`, {}, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Winner picked!'))
.catch(() => this.$root.toastError('Could not pick winner!'))
},
raffleTimer(raffle) {
const parts = []
let tte = new Date(raffle.closeAt) - this.now
for (const d of [3600000, 60000, 1000]) {
const pt = Math.floor(tte / d)
parts.push(String(pt).padStart(2, '0'))
tte -= pt * d
}
return parts.join(':')
},
refreshOpenendRaffle() {
this.openedRaffleReloading = true
return axios.get(`raffle/${this.openedRaffle.id}`, this.$root.axiosOptions)
.then(resp => {
this.openedRaffle = resp.data
this.openedRaffleReloading = false
})
},
reopenRaffle(raffleId) {
let duration = 10
const h = this.$createElement
const content = h('div', {}, [
h('b-input-group', { props: { append: 'min' } }, [
h('b-form-input', {
class: 'text-right',
on: {
input(value) {
duration = Number(value)
},
},
props: {
min: '0',
step: '1',
type: 'number',
value: duration,
},
}),
]),
h('b-form-text', { domProps: { innerHTML: 'The raffle will be re-opened and the "Close At" attribute will be set to the given duration from now.' } }),
h('b-form-text', { domProps: { innerHTML: 'This will <strong>NOT</strong> clear the entrants, so don\'t use this for another raffle, use the "Duplicate Raffle" functionality for that.' } }),
])
this.$bvModal.msgBoxConfirm([content], {
buttonSize: 'sm',
centered: true,
size: 'sm',
title: 'Re-Open the Raffle?',
})
.then(val => {
if (!val) {
return
}
return axios.put(`raffle/${raffleId}/reopen?duration=${duration * 60}`, {}, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle re-opened'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
})
},
repickWinner(winnerId) {
this.openedRaffleReloading = true
return axios.put(`raffle/${this.openedRaffle.id}/repick/${winnerId}`, {}, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Winner re-picked!'))
.catch(() => this.$root.toastError('Could not re-pick winner!'))
},
resetRaffle(id) {
this.$bvModal.msgBoxConfirm('Do you really want to reset this raffle?', {
buttonSize: 'sm',
cancelTitle: 'NO',
centered: true,
okTitle: 'YES',
okVariant: 'danger',
size: 'sm',
title: 'Please Confirm',
})
.then(val => {
if (!val) {
return
}
return axios.put(`raffle/${id}/reset`, {}, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle reset'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
})
},
saveRaffle() {
if (this.models.raffle.id) {
return axios.put(`raffle/${this.models.raffle.id}`, this.transformRaffleToDB(this.models.raffle), this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle updated'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
}
return axios.post('raffle/', this.transformRaffleToDB(this.models.raffle), this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle created'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
showEntryDialog(raffleId) {
this.openedRaffle = { id: raffleId }
return this.refreshOpenendRaffle()
.then(() => {
this.showRaffleEntriesModal = true
})
},
startRaffle(id) {
return axios.put(`raffle/${id}/start`, {}, this.$root.axiosOptions)
.then(() => this.$root.toastSuccess('Raffle started'))
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
transformISOToDateTimeLocal(t) {
const d = new Date(t)
d.setMinutes(d.getMinutes() - d.getTimezoneOffset())
return d.toISOString().slice(0, 16)
},
transformRaffleFromDB(raffle) {
return {
/* eslint-disable sort-keys */
...raffle,
channel: raffle.channel.replace(/^#/, ''),
// Transform durations
closeAfter: raffle.closeAfter / ONE_MINUTE,
minFollowAge: raffle.minFollowAge / ONE_MINUTE,
textReminderInterval: raffle.textReminderInterval / ONE_MINUTE,
waitForResponse: raffle.waitForResponse / ONE_SECOND,
// Transform date values
autoStartAt: raffle.autoStartAt ? this.transformISOToDateTimeLocal(raffle.autoStartAt) : '',
closeAt: raffle.closeAt ? this.transformISOToDateTimeLocal(raffle.closeAt) : '',
/* eslint-enable sort-keys */
}
},
transformRaffleToDB(raffle) {
return {
/* eslint-disable sort-keys */
...raffle,
channel: `#${raffle.channel.replace(/^#/, '')}`,
// Transform durations
closeAfter: Number(raffle.closeAfter) * ONE_MINUTE,
minFollowAge: Number(raffle.minFollowAge) * ONE_MINUTE,
textReminderInterval: Number(raffle.textReminderInterval) * ONE_MINUTE,
waitForResponse: Number(raffle.waitForResponse) * ONE_SECOND,
// Transform date values
autoStartAt: !raffle.autoStartAt ? null : new Date(raffle.autoStartAt).toISOString(),
closeAt: !raffle.closeAt ? null : new Date(raffle.closeAt).toISOString(),
// Transform numeric values
multiFollower: Number(raffle.multiFollower),
multiSubscriber: Number(raffle.multiSubscriber),
multiVIP: Number(raffle.multiVIP),
/* eslint-enable sort-keys */
}
},
validateRaffle() {
if (this.models.raffle.status === 'ended') {
// You must not modify running raffles
return false
}
for (const nf of [
'closeAfter', 'minFollowAge', 'textReminderInterval', 'waitForResponse',
'multiFollower', 'multiSubscriber', 'multiVIP',
]) {
// You must not put text in numeric fields
if (this.validateRaffleIsNumber(this.models.raffle[nf]) === false) {
return false
}
}
if (this.validateRaffleChannel() === false) {
return false
}
for (const nef of ['keyword', 'title']) {
if (this.validateRaffleNonEmpty(this.models.raffle[nef]) === false) {
// You must fill certain fields
return false
}
}
if (!this.models.raffle.closeAfter && !this.models.raffle.closeAt) {
return false
}
if (this.validateRaffleHasAllowedEntries() === false) {
// You must allow someone to enter
return false
}
return true
},
validateRaffleChannel() {
if (!/^[a-zA-Z0-9]{4,25}$/.test(this.models.raffle.channel)) {
return false
}
return null
},
validateRaffleHasAllowedEntries() {
if (this.models.raffle.allowEveryone || this.models.raffle.allowFollower || this.models.raffle.allowSubscriber || this.models.raffle.allowVIP) {
return null
}
return false
},
validateRaffleIsNumber(n) {
if (isNaN(Number(n))) {
return false
}
return null
},
validateRaffleNonEmpty(str) {
if (!str || str.trim().length === 0) {
return false
}
return null
},
},
mounted() {
this.fetchRaffles()
this.$bus.$on('raffleChanged', () => {
this.fetchRaffles()
if (this.openedRaffle?.id) {
this.refreshOpenendRaffle()
}
})
this.$bus.$on('raffleEntryChanged', () => {
if (!this.openedRaffle?.id) {
// We ignore this when there is no opened raffle
return
}
this.refreshOpenendRaffle()
})
window.setInterval(() => {
this.now = new Date()
}, 1000)
},
name: 'TwitchBotEditorAppRaffle',
}
</script>