Skip to content

Commit

Permalink
Merge pull request #182 from slmnio/manual-guests
Browse files Browse the repository at this point in the history
Add manual guests & dashboard editor
  • Loading branch information
slmnio authored May 16, 2023
2 parents 7873366 + 341bec9 commit f871755
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 13 deletions.
9 changes: 6 additions & 3 deletions server/src/actions/update-broadcast.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ const { safeInput } = require("../action-utils/action-utils");
module.exports = {
key: "update-broadcast",
auth: ["client"],
optionalParams: ["match", "advertise", "playerCams", "mapAttack", "title"],
optionalParams: ["match", "advertise", "playerCams", "mapAttack", "title", "manualGuests"],
/***
* @param {AnyAirtableID} match
* @param {ClientData} client
* @returns {Promise<void>}
*/
// eslint-disable-next-line no-empty-pattern
async handler({ match: matchID, advertise, playerCams, mapAttack, title }, { client }) {
async handler({ match: matchID, advertise, playerCams, mapAttack, title, manualGuests }, { client }) {
let broadcast = await this.helpers.get(client?.broadcast?.[0]);
if (!broadcast) throw ("No broadcast associated");

console.log({ matchID, advertise, playerCams, mapAttack, title });
console.log({ matchID, advertise, playerCams, mapAttack, title, manualGuests });
let validatedData = {};

if (matchID !== undefined) {
Expand All @@ -40,6 +40,9 @@ module.exports = {
if (title !== undefined) {
validatedData["Title"] = safeInput(title);
}
if (manualGuests !== undefined) {
validatedData["Manual Guests"] = safeInput(manualGuests);
}

console.log(validatedData);

Expand Down
9 changes: 5 additions & 4 deletions website/src/assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ a:not(.link-text), a:not(.link-text):hover {
background-color: var(--theme);
}

.btn-primary {
background-color: var(--brand-theme);
border-color: var(--brand-theme);
}
/*.btn:hover:not(:disabled), .btn.hover:not(:disabled) {*/
/* background-color: #00a367 !important;*/
/* border-color: #00a367 !important;*/
Expand Down Expand Up @@ -279,3 +275,8 @@ input.no-arrows[type=number] {
width: 500px;
height: 500px;
}

.table.border-no-top tr:first-child td,
.table.border-no-top tr:first-child th {
border-top: none;
}
8 changes: 6 additions & 2 deletions website/src/components/broadcast/desk/Caster.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<div class="caster-name flex-center">
<div class="c-name industry-align">{{ name }}</div>
<div class="c-twitter industry-align" v-if="twitter">{{ twitter }}</div>
<div class="c-pronouns industry-align" v-if="player && player.pronouns && showPronouns && !pronounsOnNewline">{{ player.pronouns }}</div>
<div class="c-pronouns industry-align" v-if="player && player.pronouns && showPronouns && pronounsOnNewline" v-html="breakUp(player.pronouns)"></div>
<div class="c-pronouns industry-align" v-if="pronouns && showPronouns && !pronounsOnNewline">{{ pronouns }}</div>
<div class="c-pronouns industry-align" v-if="pronouns && showPronouns && pronounsOnNewline" v-html="breakUp(pronouns)"></div>
</div>
</div>
</transition>
Expand Down Expand Up @@ -47,7 +47,11 @@ export default {
player() {
return this.caster || this.guest.player;
},
pronouns() {
return this.player?.pronouns || this.guest?.pronouns;
},
twitter() {
if (this.guest?.manual && this.guest?.twitter) return this.guest?.twitter;
if (!this.player?.socials) return "";
const twitter = this.player.socials.find(s => s.type === "Twitter");
if (!twitter) return "";
Expand Down
3 changes: 3 additions & 0 deletions website/src/components/broadcast/desk/CasterCam.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ export default {
return this.manualCamera ? true : this.apiVisible;
},
useCam() {
if (this.guest?.webcam?.includes("view=")) return true;
if (this.disableVideo) return null;
return this.guest?.use_cam || false;
},
streamID() {
if (this.guest?.webcam) return this.guest.webcam;
if (this.relayPrefix) return this.relayPrefix;
return this.guest?.cam_code || "";
},
Expand Down Expand Up @@ -175,6 +177,7 @@ export default {
border-radius: 50%;
box-shadow: 0 0 8px 0 black;
background-size: cover;
background-position: center;
transform: translate(0, -10%);
transition: all .4s ease;
}
Expand Down
17 changes: 14 additions & 3 deletions website/src/components/broadcast/desk/DeskOverlay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TourneyBar :left="broadcast.event && broadcast.event.short" :right="broadcast.subtitle" :event="broadcast.event" />
</div>
<transition-group class="casters flex-center" name="anim-talent">
<Caster v-for="(caster, i) in casters" :key="caster.id" :guest="caster" :color="getColor(i)"
<Caster v-for="(caster, i) in casters" :key="caster.manual ? caster.name : caster.id" :guest="caster" :color="getColor(i)"
:event="event" :disable-video="shouldDisableCasterVideo" :class="{'wide-feed': caster.wide_feed}"
:show-pronouns="showPronouns" :pronouns-on-newline="pronounsOnNewline" />
</transition-group>
Expand All @@ -22,6 +22,7 @@ import TourneyBar from "@/components/broadcast/TourneyBar";
import Caster from "@/components/broadcast/desk/Caster";
import DeskMatch from "@/components/broadcast/desk/DeskMatch";
import { themeBackground1 } from "@/utils/theme-styles";
import { createGuestObject } from "@/utils/content-utils";
export default {
name: "DeskOverlay",
Expand Down Expand Up @@ -59,9 +60,14 @@ export default {
})
});
},
manualGuests() {
if (!this.broadcast?.manual_guests) return [];
const manualGuests = this.broadcast.manual_guests.split("\n").filter(Boolean).map(guestString => createGuestObject(guestString));
console.log(manualGuests);
return manualGuests;
},
guests: function() {
if (!this.broadcast?.guests) return [];
return ReactiveArray("guests", {
const guests = (!this.broadcast?.guests) ? [] : ReactiveArray("guests", {
player: ReactiveThing("player", {
socials: ReactiveArray("socials")
}),
Expand All @@ -70,6 +76,11 @@ export default {
theme: ReactiveThing("theme")
})
})(this.broadcast);
return [
...guests,
...this.manualGuests
];
},
casters() {
if (!this.guests.length) {
Expand Down
9 changes: 9 additions & 0 deletions website/src/components/website/dashboard/DashboardModule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ export default {
border: none;
}
.module-content >>> .table tr th:last-child,
.module-content >>> .table tr td:last-child {
border-right: none;
}
.module-content >>> .table tr th:first-child,
.module-content >>> .table tr td:first-child {
border-left: none;
}
.clip-swipe-down-enter-active,
.clip-swipe-down-leave-active {
transition: clip-path 200ms ease, max-height 200ms ease;
Expand Down
164 changes: 164 additions & 0 deletions website/src/components/website/dashboard/DeskEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<template>
<div class="desk-editor">
<table class="table table-bordered table-sm table-dark mb-0 opacity-changes border-no-top" :class="{'low-opacity': processing }">
<tr v-if="manualGuests?.length">
<th>Name</th>
<th>Avatar</th>
<th>Webcam link</th>
<th>Twitter handle</th>
<th>Pronouns</th>
<th></th>
</tr>
<tr class="guest-row" v-for="(guest, i) in manualGuests" :key="i">
<td>
<b-form-input v-model="manualGuests[i].name" type="text" placeholder="Guest name" :state="!manualGuests[i]?.name ? false : checkInputKeyMatches(manualGuests[i], 'name')" />
<b-form-invalid-feedback>{{ manualGuests[i]?.name ? 'Name must be distinguishable from other inputs' : 'Name is required' }}</b-form-invalid-feedback>
</td>
<td>
<div class="d-flex">
<div class="avatar-preview rounded-circle mr-1 bg-center flex-shrink-0" :style="{'backgroundImage': manualGuests[i]?.avatar && `url(${manualGuests[i]?.avatar})`}"></div>
<div>
<b-form-input v-model="manualGuests[i].avatar" type="text" placeholder="https://i.imgur.com/HHvEClX.png" :state="checkInputKeyMatches(manualGuests[i], 'avatar')" />
<b-form-invalid-feedback>Avatars must be valid links</b-form-invalid-feedback>
</div>
</div>
</td>
<td>
<b-form-input v-model="manualGuests[i].webcam" type="text" placeholder="https://vdo.ninja/?view=SLMN" :state="checkInputKeyMatches(manualGuests[i], 'webcam')" />
<b-form-invalid-feedback>Webcams must be valid VDO.Ninja view links</b-form-invalid-feedback>
</td>
<td>
<b-form-input v-model="manualGuests[i].twitter" type="text" placeholder="@slmnio" :state="checkInputKeyMatches(manualGuests[i], 'twitter')" />
<b-form-invalid-feedback>Twitter handles must start with @</b-form-invalid-feedback>
</td>
<td>
<b-form-input v-model="manualGuests[i].pronouns" type="text" placeholder="he/him" :state="checkInputKeyMatches(manualGuests[i], 'pronouns')" />
<b-form-invalid-feedback>Pronouns must contain "/"</b-form-invalid-feedback>
</td>
<td>
<b-button variant="danger" @click="confirmRemoveGuest(i)"><i class="fas fa-trash"></i></b-button>
</td>
</tr>
<tr v-if="!manualGuests?.length">
<td class="text-center" colspan="5">
<i>No manual guests listed</i>
</td>
</tr>
</table>
<div class="p-2 w-100 d-flex">
<b-button variant="success" @click="addGuest">
<i class="fas fa-fw fa-plus"></i> Add guest
</b-button>
<div class="spacer flex-grow-1"></div>
<b-button :disabled="processing" class="ml-2" :variant="saveData === lastSavedData ? 'secondary' : 'primary'" @click="saveGuests">
<i class="fas fa-fw" :class="{'fa-save': !processing, 'fa-pulse fa-spinner': processing}"></i> Save guests
</b-button>
</div>
</div>
</template>

<script>
import { ReactiveArray, ReactiveThing } from "@/utils/reactive";
import { createGuestObject, getGuestString } from "@/utils/content-utils";
import { BButton, BFormInput, BFormInvalidFeedback } from "bootstrap-vue";
import { updateBroadcastData } from "@/utils/dashboard";
export default {
name: "DeskEditor",
components: { BFormInvalidFeedback, BButton, BFormInput },
props: {
broadcast: {}
},
data: () => ({
processing: false,
manualGuests: [],
lastLoadedManualGuests: [],
lastSavedData: null
}),
computed: {
guests() {
if (!this.broadcast?.guests) return [];
return ReactiveArray("guests", {
player: ReactiveThing("player", {
socials: ReactiveArray("socials")
}),
theme: ReactiveThing("theme"),
prediction_team: ReactiveThing("prediction_team", {
theme: ReactiveThing("theme")
})
})(this.broadcast);
},
saveData() {
return this.manualGuests.map(guest => getGuestString(guest)).join("\n");
}
},
methods: {
addGuest() {
this.manualGuests.push({});
},
checkInputKeyMatches(guest, key) {
if (!guest?.[key]) return null;
const tempGuest = createGuestObject(getGuestString(guest));
console.log(guest[key], tempGuest[key], tempGuest[key] === guest[key]);
return tempGuest[key]?.trim() === guest[key]?.trim();
},
confirmRemoveGuest(i) {
const okay = window.confirm(`Remove guest "${this.manualGuests?.[i]?.name}"?`) === true;
if (!okay) return;
this.manualGuests.splice(i, 1);
},
async saveGuests() {
this.processing = true;
this.lastSavedData = this.saveData;
try {
const response = await updateBroadcastData(this.$root.auth, {
manualGuests: this.saveData
});
if (!response.error) {
this.$notyf.success("Updated guests");
}
} finally {
this.processing = false;
}
}
},
watch: {
broadcast: {
deep: true,
immediate: true,
handler(newData) {
if (newData?.manual_guests && JSON.stringify(newData?.manual_guests) !== JSON.stringify(this.lastLoadedManualGuests)) {
this.manualGuests = newData.manual_guests.split("\n").map(e => createGuestObject(e));
this.lastLoadedManualGuests = newData.manual_guests.split("\n").map(e => createGuestObject(e));
this.lastSavedData = newData.manual_guests.replaceAll(",", "|");
}
}
}
}
};
</script>
<style scoped>
.guest-row >>> .invalid-feedback {
color: #ff6473;
}
::placeholder {
color: rgba(0,0,0,0.3);
}
.avatar-preview {
width: 2.375em;
height: 2.375em;
background-color: rgba(255,255,255,0.2);
}
.opacity-changes {
opacity: 1;
transition: opacity .3s ease;
}
.low-opacity {
opacity: 0.5;
pointer-events: none;
user-select: none;
cursor: wait;
}
</style>
30 changes: 30 additions & 0 deletions website/src/utils/content-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,33 @@ export function unescapeText(text) {
.replaceAll("&quot;", "\"")
.replaceAll("&#039;", "'");
}


export function createGuestObject(str) {
const guest = {
manual: true
};

str.split(/[,|]/).forEach(part => {
if (!part) return;
part = part.trim();

if (part.startsWith("@")) {
guest.twitter = part;
} else if (part.includes("view=")) {
guest.webcam = part;
} else if (part.startsWith("http")) {
guest.avatar = part;
} else if (part.includes("/")) {
guest.pronouns = part;
} else {
guest.name = part;
}
});
return guest;
}

export function getGuestString(guest) {
delete guest.manual;
return Object.values(guest).join("|");
}
17 changes: 16 additions & 1 deletion website/src/views/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<DashboardModule title="Match Editor" icon-class="fas fa-pennant" class="broadcast-match-editor mb-2" v-if="liveMatch" start-opened>
<MatchEditor :hide-match-extras="true" :match="liveMatch"></MatchEditor>
</DashboardModule>
<DashboardModule title="Desk Guests" icon-class="fas fa-users" class="desk-editor mb-2" start-opened>
<template v-slot:header v-if="deskGuestSource">Desk guests pulled from: {{ deskGuestSource }}</template>
<DeskEditor :broadcast="broadcast" />
</DashboardModule>
<DashboardModule title="Bracket Implications" icon-class="fas fa-sitemap" class="broadcast-bracket-editor mb-2" v-if="bracketCount">
<BracketImplications :match="liveMatch" link-to-detailed-match show-resolve-button />
</DashboardModule>
Expand Down Expand Up @@ -74,10 +78,11 @@ import DashboardModule from "@/components/website/dashboard/DashboardModule.vue"
import BracketImplications from "@/components/website/dashboard/BracketImplications.vue";
import PreviewProgramDisplay from "@/components/website/dashboard/PreviewProgramDisplay.vue";
import Bracket from "@/components/website/bracket/Bracket.vue";
import DeskEditor from "@/components/website/dashboard/DeskEditor.vue";
export default {
name: "Dashboard",
components: { Bracket, PreviewProgramDisplay, BracketImplications, DashboardModule, DashboardClock, ScheduleEditor, BroadcastEditor, CommsControl, Commercials, Predictions, MatchEditor, MatchThumbnail, BroadcastSwitcher, BButton },
components: { DeskEditor, Bracket, PreviewProgramDisplay, BracketImplications, DashboardModule, DashboardClock, ScheduleEditor, BroadcastEditor, CommsControl, Commercials, Predictions, MatchEditor, MatchThumbnail, BroadcastSwitcher, BButton },
computed: {
user() {
if (!this.$root.auth.user?.airtableID) return {};
Expand Down Expand Up @@ -140,6 +145,16 @@ export default {
})
})
})(this.liveMatch);
},
deskGuestSource() {
if (this.broadcast?.guests) {
return "Broadcast › Guests";
} else if (this.liveMatch?.casters) {
return "Broadcast › Live Match › Casters";
} else if (this.broadcast?.manual_guests) {
return "Broadcast › Manual Guests";
}
return null;
}
},
methods: {
Expand Down

0 comments on commit f871755

Please sign in to comment.