Skip to content

Commit

Permalink
Expand iCal to include teams & add public UI (#197)
Browse files Browse the repository at this point in the history
* Add team support to ical endpoint, start work on interface for importing ical calendar

* Add buttons to event & team schedules, integrate with vue3 changes

---------

Co-authored-by: Solomon Cammack <[email protected]>
  • Loading branch information
zusorio and slmnio authored Jun 20, 2024
1 parent adc99f0 commit d6a833f
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 29 deletions.
39 changes: 26 additions & 13 deletions server/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,19 +247,20 @@ module.exports = ({ app, Cache, io }) => {
].map(hex => parseInt(hex, 16)).join(":");
}

async function generateCal(event) {
let theme = (event.theme ? await Cache.get(event.theme[0]) : null);
let matches = await Promise.all((event.matches || []).map(id => Cache.get(id)));
async function generateCal({ team, event }) {
const matchContainer = team || event;
let theme = (matchContainer.theme ? await Cache.get(matchContainer.theme[0]) : null);
let matches = await Promise.all((matchContainer.matches || []).map(id => Cache.get(id)));
matches = matches.filter(m => m?.start).map(m => getMatchCal(m, event));
if (!matches.length) return null;
let cal = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//2022//SLMN.GG//EN",
"PRODID:-//2023//SLMN.GG//EN",
"METHOD:PUBLISH",
"CALSCALE:GREGORIAN",
`NAME:${event.name} (SLMN.GG)`,
`X-WR-CALNAME:${event.name} (SLMN.GG)`,
`NAME:${matchContainer.name} (SLMN.GG)`,
`X-WR-CALNAME:${matchContainer.name} (SLMN.GG)`,
`COLOR:${theme ? getCalCol(theme.color_theme) : "64:64:64"}`,
"REFRESH-INTERVAL;VALUE=DURATION:PT10M",
"X-PUBLISHED-TTL:PT10M"
Expand All @@ -276,14 +277,26 @@ module.exports = ({ app, Cache, io }) => {

app.get("/ical", async (req, res) => {
try {
if (!req.query.event) return res.status(400).send("The 'event' query is required");
let event = await Cache.get(req.query.event);
if (!event || event.__tableName !== "Events") return res.status(400).send("Unknown event");

let ical = await generateCal(event);
if (!ical) return res.status(400).send("No matches scheduled");
if (!req.query.event && !req.query.team) return res.status(400).send("You must specify a 'team' or 'event'");
if (req.query.event && req.query.team) return res.status(400).send("You must specify only one of 'team' or 'event'");
if (req.query.team) {
let team = await Cache.get(req.query.team);
if (!team || team.__tableName !== "Teams") return res.status(400).send("Unknown team");

let event = await Cache.get(team.event?.[0]);
if (!event || event.__tableName !== "Events") return res.status(400).send("Did not find an event for this team");

let ical = await generateCal({ team, event });
if (!ical) return res.status(400).send("No matches scheduled");
return res.header("Content-Type", "text/calendar").send(ical);
} else {
let event = await Cache.get(req.query.event);
if (!event || event.__tableName !== "Events") return res.status(400).send("Unknown event");

return res.header("Content-Type", "text/calendar").send(ical);
let ical = await generateCal({ event });
if (!ical) return res.status(400).send("No matches scheduled");
return res.header("Content-Type", "text/calendar").send(ical);
}
} catch (e) {
console.error(e);
return res.status(500).send(e.message);
Expand Down
73 changes: 73 additions & 0 deletions website/src/components/website/AddToCalendar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<template>
<div>
<b-modal id="add-to-calendar-modal" ref="add-to-calendar-modal" title="Add to calendar" hide-footer>
<p>Automatically sync all {{ target.name }} matches to your calendar.</p>

<b-button-group>
<b-button variant="primary" class="text-white no-link-style" :href="googleCalendarURL" target="_blank">
<i class="fab fa-google mr-2"></i>Google
Calendar
</b-button>
<b-button variant="primary" class="text-white no-link-style" :href="outlookURL" target="_blank">
<i class="fab fa-windows mr-2"></i>Outlook
</b-button>
<b-button variant="primary" class="text-white no-link-style" :href="webcalURL" target="_blank"><i class="fab fa-apple mr-2"></i>Apple</b-button>
</b-button-group>

<p class="mt-3">Or copy this link to your clipboard and add it to your calendar manually:</p>
<pre><copy-text-button>{{ calendarURL }}</copy-text-button></pre>
</b-modal>

<b-button v-b-modal.add-to-calendar-modal size="sm">
<i class="fas fa-calendar-plus" :class="{'mr-2': !small}"></i> <span v-if="!small">Sync calendar</span>
</b-button>
</div>
</template>

<script>
import { getDataServerAddress } from "@/utils/fetch";
import CopyTextButton from "@/components/website/CopyTextButton.vue";
export default {
name: "AddToCalendar",
components: {
CopyTextButton,
},
props: {
event: Object,
team: Object,
small: Boolean
},
computed: {
target() {
return this.team || this.event;
},
calendarURL() {
try {
const url = new URL(getDataServerAddress() + "/ical");
if (this.team) {
url.searchParams.set("team", this.team.id);
} else if (this.event) {
url.searchParams.set("event", this.event.id);
}
return url.toString();
} catch (e) {
console.error(e);
return null;
}
},
webcalURL() {
if (!this.calendarURL) return null;
const url = new URL(this.calendarURL);
url.protocol = "webcal";
return url.toString();
},
googleCalendarURL() {
return `https://calendar.google.com/calendar/r?cid=${encodeURIComponent(this.webcalURL)}`;
},
outlookURL() {
return `https://outlook.live.com/calendar/0/addfromweb/?url=${encodeURIComponent(this.webcalURL)}&name=${encodeURIComponent(this.target.name + " (SLMN.GG)")}`;
}
}
};
</script>
4 changes: 4 additions & 0 deletions website/src/components/website/schedule/TimezoneSwapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,8 @@ export default {
.timezone-swapper:not(.align-left) option {
direction: rtl;
}
.timezone-swapper:not(.align-left) .form-group {
margin-bottom: 0;
}
</style>
2 changes: 1 addition & 1 deletion website/src/utils/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function getDataServerAddress() {
if (import.meta.env.VITE_DATA_SERVER) return import.meta.env.VITE_DATA_SERVER;

if (import.meta.env.VITE_DEPLOY_MODE === "local") {
return `//${window.location.hostname}:8901`;
return `${window.location.protocol}//${window.location.hostname}:8901`;
}
return "https://data.slmn.gg";
}
Expand Down
53 changes: 40 additions & 13 deletions website/src/views/sub-views/event/EventSchedule.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
<template>
<div class="event-schedule container">
<div class="d-sm-flex w-100 timezone-swapper-holder flex-column align-items-end gap-1 d-none">
<TimezoneSwapper :inline="true" />
<b-form-group v-if="showBroadcastSettings" label-cols="auto" label-size="sm" label="Broadcast">
<b-form-select
v-if="eventBroadcasts?.length"
v-model="selectedBroadcastID"
:options="eventBroadcasts"
size="sm"
class="w-auto" />
</b-form-group>
<div class="schedule-title">
<h2 class="text-center">Schedule</h2>

<div class="d-flex w-100 flex-column align-items-end gap-1 top-right-settings">
<b-dropdown auto-close="outside">
<template #button-content>
<i class="fas fa-cog fa-fw mr-1"></i>
<span class="d-none d-lg-inline-block" style="line-height:1">Settings & Sync</span>
</template>
<div class="dropdown-content d-flex flex-column align-items-end gap-3 p-3" style="min-width: min(100vw, 300px)">
<TimezoneSwapper align="left" :inline="true" />
<b-form-group v-if="showBroadcastSettings" label-cols="auto" label-size="sm" label="Broadcast">
<b-form-select
v-if="eventBroadcasts?.length"
v-model="selectedBroadcastID"
:options="eventBroadcasts"
size="sm"
class="w-auto" />
</b-form-group>
<AddToCalendar :event="event" />
</div>
</b-dropdown>
</div>
</div>

<div class="schedule-top mb-2">
<h2 class="text-center">Schedule</h2>
<ul v-if="pagedMatches.length > 1" class="schedule-group-holder nav justify-content-center">
<li
v-for="(pm) in pagedMatches"
Expand Down Expand Up @@ -62,17 +74,19 @@ import TimezoneSwapper from "@/components/website/schedule/TimezoneSwapper";
import { canEditMatch, isEventStaffOrHasRole } from "@/utils/client-action-permissions";
import { useAuthStore } from "@/stores/authStore";
import { useRouteQuery } from "@vueuse/router";
import AddToCalendar from "@/components/website/AddToCalendar.vue";
export default {
name: "EventSchedule",
components: { TimezoneSwapper, ScheduleMatch },
components: { AddToCalendar, TimezoneSwapper, ScheduleMatch },
props: ["event"],
data: () => ({
activeScheduleNum: useRouteQuery("page", undefined, { transform: val => val === "all" ? val : parseInt(val), mode: "replace" }),
hideCompleted: false,
hideNoVods: false,
selectedBroadcastID: null
selectedBroadcastID: null,
showSettings: false
}),
computed: {
showAll() {
Expand Down Expand Up @@ -274,4 +288,17 @@ export default {
margin-top: 1.5em;
}
}
.top-right-settings {
position: absolute;
top: 0;
right: 0;
}
.top-right-settings .group-content {
z-index: 100;
box-shadow: 0 0 4px 2px #202020;
}
.schedule-title {
position: relative;
}
</style>
35 changes: 33 additions & 2 deletions website/src/views/sub-views/team/TeamSchedule.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
<template>
<div class="container">
<h2 class="text-center mb-3">Team matches</h2>
<div class="schedule-title">
<h2 class="text-center">Schedule</h2>

<div class="d-flex w-100 flex-column align-items-end gap-1 top-right-settings">
<b-dropdown auto-close="outside">
<template #button-content>
<i class="fas fa-cog fa-fw mr-1"></i>
<span class="d-none d-lg-inline-block" style="line-height:1">Settings & Sync</span>
</template>
<div class="dropdown-content d-flex flex-column align-items-end gap-3 p-3" style="min-width: min(100vw, 300px)">
<TimezoneSwapper align="left" :inline="true" />
<AddToCalendar :team="team" />
</div>
</b-dropdown>
</div>
</div>


<div class="w-100">
<ScheduleMatch
v-for="match in matches"
Expand All @@ -18,10 +35,14 @@ import { ReactiveArray, ReactiveRoot, ReactiveThing } from "@/utils/reactive";
import { sortMatches } from "@/utils/sorts";
import { canEditMatch } from "@/utils/client-action-permissions";
import { useAuthStore } from "@/stores/authStore";
import TimezoneSwapper from "@/components/website/schedule/TimezoneSwapper.vue";
import AddToCalendar from "@/components/website/AddToCalendar.vue";
export default {
name: "TeamMatches",
components: {
AddToCalendar,
TimezoneSwapper,
ScheduleMatch
},
props: ["team"],
Expand Down Expand Up @@ -50,5 +71,15 @@ export default {
</script>
<style scoped>
.top-right-settings {
position: absolute;
top: 0;
right: 0;
}
.schedule-title {
position: relative;
}
.dropdown-content {
margin: -.5em 0;
}
</style>

0 comments on commit d6a833f

Please sign in to comment.