Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand iCal to include teams & add public UI #197

Merged
merged 5 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Loading