mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-21 04:11:16 +00:00
Knut Ahlers
6455b409ce
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>
1185 lines
36 KiB
Vue
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>
|