Skip to content

Commit

Permalink
Merge pull request #929 from sgfost/feat/launch-date-signups
Browse files Browse the repository at this point in the history
feat: track + notify user signups
  • Loading branch information
alee authored Jan 31, 2024
2 parents 834bc5c + 798a986 commit 81557d7
Show file tree
Hide file tree
Showing 22 changed files with 727 additions and 69 deletions.
30 changes: 29 additions & 1 deletion client/src/api/tournament/request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { url } from "@port-of-mars/client/util";
import { TournamentRoundInviteStatus, TournamentStatus } from "@port-of-mars/shared/types";
import {
TournamentRoundInviteStatus,
TournamentRoundScheduleDate,
TournamentStatus,
} from "@port-of-mars/shared/types";
import { TStore } from "@port-of-mars/client/plugins/tstore";
import { AjaxRequest } from "@port-of-mars/client/plugins/ajax";

Expand All @@ -16,6 +20,16 @@ export class TournamentAPI {
}
}

async getTournamentRoundSchedule(): Promise<Array<TournamentRoundScheduleDate>> {
try {
return this.ajax.get(url("/tournament/schedule"), ({ data }) => data);
} catch (e) {
console.log("Unable to get tournament round schedule");
console.log(e);
throw e;
}
}

async getInviteStatus(): Promise<TournamentRoundInviteStatus> {
try {
return this.ajax.get(url("/tournament/invite-status"), ({ data }) => data);
Expand All @@ -25,4 +39,18 @@ export class TournamentAPI {
throw e;
}
}

async setSignup(signup: boolean, tournamentRoundDateId: number, inviteId: number) {
const action = signup ? "add" : "remove";
try {
const params = `?tournamentRoundDateId=${tournamentRoundDateId}&inviteId=${inviteId}`;
return this.ajax.post(url(`/tournament/signup/${action}${params}`), ({ data }) => {
this.store.commit("SET_TOURNAMENT_ROUND_SCHEDULE", data);
});
} catch (e) {
console.log(`Unable to ${action} signup`);
console.log(e);
throw e;
}
}
}
155 changes: 110 additions & 45 deletions client/src/components/global/Schedule.vue
Original file line number Diff line number Diff line change
@@ -1,73 +1,129 @@
<template>
<div>
<b-list-group class="p-1">
<template v-for="(times, date) in launchTimes">
<template v-for="(launchTimes, date) in groupedLaunchTimes">
<b-list-group-item class="text-center bg-primary border-0 my-1" :key="date">
<b>{{ date }}</b>
</b-list-group-item>
<b-list-group-item
class="p-3 text-center bg-dark border-0 my-1 d-flex justify-content-between"
v-for="game in times"
:key="game.date.getTime()"
class="p-0 bg-dark border-0 my-1 d-flex flex-column"
v-for="launchTime in launchTimes"
:key="launchTime.date.getTime()"
>
<div class="launch-date">
<b>{{ formatTime(game.date) }}</b>
<div class="p-3 text-center d-flex justify-content-between align-items-center">
<div class="launch-date">
<b>{{ formatTime(launchTime.date) }}</b>
</div>
<div class="d-flex align-items-between">
<div v-if="!invite?.hasParticipated" class="d-flex align-items-center mr-3">
<label class="mb-0 mr-2" :for="`signup-toggle-${launchTime.tournamentRoundDateId}`">
<small v-if="launchTime.isSignedUp" class="text-success">Signed up</small>
<small v-else>Sign up to get notified *</small>
</label>
<Toggle
:id="`signup-toggle-${launchTime.tournamentRoundDateId}`"
v-model="launchTime.isSignedUp"
@click="toggleSignup(launchTime)"
/>
</div>
<b-button-group>
<a
id="add-to-gcal"
class="btn btn-dark py-0"
:href="launchTime.googleInviteURL"
title="add to Google Calendar"
target="_blank"
>
<b-icon-google scale=".8"></b-icon-google>
</a>
<b-popover target="add-to-gcal" placement="bottom" triggers="hover focus">
<template #title></template>
add to Google Calendar
</b-popover>
<a
id="download-ics"
class="btn btn-dark py-0"
:href="launchTime.icsInviteURL"
title="download as ics"
target="_blank"
>
<b-icon-calendar-plus-fill scale=".8"></b-icon-calendar-plus-fill>
</a>
<b-popover target="download-ics" placement="bottom" triggers="hover focus">
<template #title></template>
download as .ics
</b-popover>
</b-button-group>
</div>
</div>
<div>
<b-progress
id="interest-bar"
:value="launchTime.signupCount + 0.5"
:max="signupsPopularityThreshold"
class="bg-dark"
height="0.5rem"
:variant="getInterestBarVariant(launchTime.signupCount)"
title="interest level"
style="opacity: 0.5"
></b-progress>
</div>
<b-button-group>
<a
class="btn btn-secondary py-0"
:href="game.googleInviteURL"
title="add to Google Calendar"
target="_blank"
>
<b-icon-google scale=".8"></b-icon-google>
</a>
<a
class="btn btn-primary py-0"
:href="game.icsInviteURL"
title="download as ics"
target="_blank"
>
<b-icon-calendar-plus-fill scale=".8"></b-icon-calendar-plus-fill>
</a>
</b-button-group>
</b-list-group-item>
</template>
</b-list-group>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Component, Inject, Prop, Vue } from "vue-property-decorator";
import { google, ics } from "calendar-link";
import {
TournamentRoundInviteStatus,
TournamentRoundScheduleDate,
} from "@port-of-mars/shared/types";
import { TournamentAPI } from "@port-of-mars/client/api/tournament/request";
import Toggle from "@port-of-mars/client/components/global/Toggle.vue";
interface LaunchTime extends TournamentRoundScheduleDate {
date: Date;
googleInviteURL: string;
icsInviteURL: string;
}
interface LaunchTimes {
[date: string]: {
date: Date;
googleInviteURL: string;
icsInviteURL: string;
}[];
interface GroupedLaunchTimes {
[date: string]: Array<LaunchTime>;
}
@Component({})
@Component({
components: {
Toggle,
},
})
export default class Schedule extends Vue {
@Prop({ default: "schedule-header" })
scheduleId!: string;
@Prop({ default: "schedule-header" }) scheduleId!: string;
@Prop() schedule!: Array<TournamentRoundScheduleDate>;
@Prop() invite!: TournamentRoundInviteStatus | null;
@Prop()
schedule!: Array<number>;
@Inject() readonly api!: TournamentAPI;
static readonly SITE_URL = "https://portofmars.asu.edu";
get launchTimes() {
get groupedLaunchTimes() {
return this.groupLaunchTimesByDate(this.schedule);
}
groupLaunchTimesByDate(launchTimes: number[]) {
// returns an object with date strings mapped to individual launch times and invite links
// could use a Map<string, object> also
const grouped: LaunchTimes = {};
for (const time of launchTimes) {
const launchDate = new Date(time);
get signupsPopularityThreshold() {
return this.$store.getters.tournamentStatus.signupsPopularityThreshold;
}
getInterestBarVariant(signupCount: number) {
return signupCount < this.signupsPopularityThreshold / 2 ? "warning" : "success";
}
groupLaunchTimesByDate(schedule: Array<TournamentRoundScheduleDate>): GroupedLaunchTimes {
// returns an object with date strings mapped to individual scheduled dates with invite links
const grouped: GroupedLaunchTimes = {};
for (const scheduleDate of schedule) {
const launchDate = new Date(scheduleDate.timestamp);
const dateStr = launchDate.toLocaleDateString([], {
weekday: "long",
month: "long",
Expand All @@ -80,14 +136,23 @@ export default class Schedule extends Vue {
const googleInviteURL = google(calendarEvent);
const icsInviteURL = ics(calendarEvent);
grouped[dateStr].push({
date: new Date(time),
...scheduleDate,
date: launchDate,
googleInviteURL,
icsInviteURL,
});
}
return grouped;
}
async toggleSignup(launchTime: LaunchTime) {
const tournamentRoundDateId = launchTime.tournamentRoundDateId;
if (!this.invite) return;
// toggle signup - if launchTime.isSignedUp is true they were already registered for this launchTime
const shouldSignUp = !launchTime.isSignedUp;
await this.api.setSignup(shouldSignUp, tournamentRoundDateId, this.invite.id);
}
get calendarEventDescription() {
return (
`Register and complete all Port of Mars onboarding tasks at ${Schedule.SITE_URL} ASAP. \n\n` +
Expand All @@ -102,7 +167,7 @@ export default class Schedule extends Vue {
location: Schedule.SITE_URL,
start,
description: this.calendarEventDescription,
duration: [1, "hour"],
duration: [1, "hour"] as any,
};
}
Expand Down
58 changes: 58 additions & 0 deletions client/src/components/global/Toggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<input v-bind="$attrs" v-on="$listeners" type="checkbox" class="toggle-input" />
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component({
inheritAttrs: false,
})
export default class Toggle extends Vue {}
</script>

<style lang="scss" scoped>
.switch.square label .lever {
width: 54px;
height: 34px;
border-radius: 0px;
}
.switch.square label .lever:after {
width: 26px;
height: 26px;
border-radius: 0px;
left: 4px;
top: 4px;
}
.toggle-input {
position: relative;
appearance: none;
width: 45px; /* Adjust the width as needed */
height: 25px; /* Adjust the height as needed */
background-color: #a49ca6; /* Background color when the toggle is off */
border-radius: 2px; /* Square corners */
cursor: pointer;
outline: none;
}
.toggle-input:checked {
background-color: rgb(95, 141, 75); /* Background color when the toggle is on */
}
.toggle-input::before {
content: "";
position: absolute;
width: 20px; /* Width of the toggle button */
height: 20px; /* Height of the toggle button */
background-color: #fff; /* Color of the toggle button */
border-radius: 5%; /* Make it a rounded square*/
top: 3px; /* Adjust the vertical position */
left: 3px; /* Adjust the horizontal position */
transition: 0.3s; /* Transition for smooth animation */
}
.toggle-input:checked::before {
transform: translateX(20px); /* Move the toggle button to the right when checked */
}
</style>
3 changes: 3 additions & 0 deletions client/src/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export default {
if (data.tournamentStatus) {
context.commit("SET_TOURNAMENT_STATUS", data.tournamentStatus);
}
if (data.tournamentRoundSchedule) {
context.commit("SET_TOURNAMENT_ROUND_SCHEDULE", data.tournamentRoundSchedule);
}
context.commit("SET_FREE_PLAY_ENABLED", data.isFreePlayEnabled);
context.commit("SET_TOURNAMENT_ENABLED", data.isTournamentEnabled);
context.commit("SET_ANNOUNCEMENT_BANNER_TEXT", data.announcementBannerText);
Expand Down
12 changes: 7 additions & 5 deletions client/src/store/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,24 +207,26 @@ export default {
},

tournamentRoundHasUpcomingLaunch(state: State) {
return !!state.tournamentStatus?.currentRound.schedule.length;
return !!state.tournamentRoundSchedule?.length;
},

tournamentSchedule(state: State) {
return state.tournamentStatus?.currentRound.schedule;
return state.tournamentRoundSchedule;
},

nextLaunchTime(state: State) {
return state.tournamentStatus?.currentRound.schedule[0];
if (state.tournamentRoundSchedule?.length) {
return state.tournamentRoundSchedule[0].timestamp;
}
},

isTournamentLobbyOpen(state: State) {
if (!state.tournamentStatus) {
if (!state.tournamentStatus || !state.tournamentRoundSchedule?.length) {
return false;
}
const beforeOffset = state.tournamentStatus.lobbyOpenBeforeOffset;
const afterOffset = state.tournamentStatus.lobbyOpenAfterOffset;
const nextLaunchTime = state.tournamentStatus.currentRound.schedule[0];
const nextLaunchTime = state.tournamentRoundSchedule[0].timestamp;
const timeNow = new Date().getTime();
return timeNow >= nextLaunchTime - beforeOffset && timeNow <= nextLaunchTime + afterOffset;
},
Expand Down
5 changes: 5 additions & 0 deletions client/src/store/mutations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DashboardMessage,
Role,
SystemHealthChangesData,
TournamentRoundScheduleDate,
TournamentStatus,
} from "@port-of-mars/shared/types";
import _ from "lodash";
Expand All @@ -41,6 +42,10 @@ export default {
state.tournamentStatus = status;
},

SET_TOURNAMENT_ROUND_SCHEDULE(state: State, schedule: Array<TournamentRoundScheduleDate>) {
state.tournamentRoundSchedule = schedule;
},

SET_ANNOUNCEMENT_BANNER_TEXT(state: State, text: string) {
state.announcementBannerText = text;
},
Expand Down
Loading

0 comments on commit 81557d7

Please sign in to comment.