diff --git a/src/lib/components/events/event-item.svelte b/src/lib/components/events/event-item.svelte
index 4771985f6..4001b96ad 100644
--- a/src/lib/components/events/event-item.svelte
+++ b/src/lib/components/events/event-item.svelte
@@ -1,6 +1,8 @@
-
+
@@ -53,20 +72,23 @@
rel="noopener noreferrer">Join
- {info.description}
+
+ {@html info.description}
+
+
+
+
diff --git a/src/lib/ical/common.ts b/src/lib/ical/common.ts
index bb8252a54..71fd04c88 100644
--- a/src/lib/ical/common.ts
+++ b/src/lib/ical/common.ts
@@ -122,9 +122,9 @@ export function computeIcalDatetime(event: IcalOutput): Date {
return parseRawIcalDatetime(rawDatetime as string);
}
-export function slugifyEvent(summary: string, month: string, day: number): string {
+export function slugifyEvent(summary: string, year: string, month: string, day: number): string {
const cleanedSummary = summary.replace(/[^\w\s_]/g, '').replace(/(\s|-|_)+/g, '-');
- const slug = [cleanedSummary, month, day].join('-').toLowerCase();
+ const slug = [cleanedSummary, year, month, day].join('-').toLowerCase();
return slug;
}
@@ -145,21 +145,30 @@ export interface AcmEventDescription {
variables: Map
;
}
-export function parseDescription(content: string): AcmEventDescription {
- const resultingLines = [];
+export function parseDescription(content: string, varPrefix = 'ACM_'): AcmEventDescription {
const variables = new Map();
- for (const line of content.split(/\\n/)) {
- if (line.includes('=')) {
- const [key, ...value] = line.split('=');
- variables.set(key.trim(), value.join('=').trim());
- } else {
- // Add line to unescaped description.
- resultingLines.push(line.replace(/\\/g, ''));
- }
+ let description = content.replace(/\\n/g, '
').replace(/\\/g, '');
+
+ // Extract variables from the description until there are no more.
+ while (description.includes(varPrefix)) {
+ const start = description.indexOf(varPrefix);
+ const nextTag = description.indexOf('<', start);
+ const end =
+ nextTag > -1
+ ? nextTag // Stop at next HTML tag (e.g. '
')
+ : description.length; // Or stop at end of string
+
+ const variable = description.substring(start, end);
+
+ const splitAt = variable.indexOf('=');
+ const key = variable.substring(0, splitAt).trim();
+ const value = variable.substring(splitAt + 1);
+
+ variables.set(key, value);
+ description = description.substring(0, start) + description.substring(end);
}
- const description = resultingLines.join('\n');
return { description, variables };
}
diff --git a/src/lib/ical/parse.ts b/src/lib/ical/parse.ts
index b344443e1..61b36147a 100644
--- a/src/lib/ical/parse.ts
+++ b/src/lib/ical/parse.ts
@@ -52,11 +52,11 @@ export function parse(icalData: string): AcmEvent[] {
: '/discord';
const date = computeIcalDatetime(event);
+ const year = date.toLocaleString(ACM_LOCALE, { year: 'numeric' });
const month = date.toLocaleString(ACM_LOCALE, { month: 'long' });
const day = date.getDate();
const time = date.toLocaleTimeString(ACM_LOCALE, { hour: 'numeric', minute: 'numeric' });
-
- const slug = slugifyEvent(summary, month, day);
+ const slug = slugifyEvent(summary, year, month, day);
const recurring = checkForRecurrence(String(event['RRULE']));
@@ -72,7 +72,7 @@ export function parse(icalData: string): AcmEvent[] {
? acmDev
: acmGeneral;
- collection.push({
+ const item = {
month,
day,
time,
@@ -84,7 +84,9 @@ export function parse(icalData: string): AcmEvent[] {
slug,
recurring,
acmPath,
- });
+ };
+
+ collection.push(item);
return collection;
}, [])
diff --git a/src/lib/stores/toasts.ts b/src/lib/stores/toasts.ts
new file mode 100644
index 000000000..293c2bcb8
--- /dev/null
+++ b/src/lib/stores/toasts.ts
@@ -0,0 +1,66 @@
+import { acmGeneral } from '$lib/constants';
+import { writable } from 'svelte/store';
+
+const MAX_TOASTS = 4;
+const TOAST_TIMEOUT = 2e3;
+
+const numToasts = writable(0);
+
+export enum ToastType {
+ Success = 'success',
+ Error = 'error',
+ Info = 'info',
+}
+
+export interface Toast {
+ id: number;
+ content: string;
+ type?: ToastType;
+ dismissible?: boolean;
+ timeout?: number;
+ path?: string;
+}
+
+export const toasts = writable([]);
+
+function makeToast(
+ id: number,
+ { content, type, dismissible, timeout, path }: Omit
+): Required {
+ return {
+ id,
+ content: content,
+ type: type ?? ToastType.Info,
+ dismissible: dismissible ?? true,
+ timeout: timeout ?? TOAST_TIMEOUT,
+ path: path ?? acmGeneral.slug,
+ };
+}
+
+export function toast(props: Omit): void {
+ numToasts.update((value: number) => {
+ const nextToast = makeToast(value + 1, props);
+
+ toasts.update((allToasts) => {
+ postponeDismissal(nextToast.id, nextToast.timeout);
+
+ while (allToasts.length > MAX_TOASTS - 1) {
+ const currId = allToasts[0].id;
+ dismissToast(currId);
+ allToasts.shift();
+ }
+
+ return [...allToasts, nextToast];
+ });
+
+ return nextToast.id;
+ });
+}
+
+export function postponeDismissal(id: number, timeout: number): void {
+ setTimeout(() => dismissToast(id), timeout);
+}
+
+export function dismissToast(id: number): void {
+ toasts.update((all) => all.filter((t) => t.id !== id));
+}
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index bc7a87149..92c69d840 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -1,11 +1,13 @@
+
diff --git a/src/routes/events.json.ts b/src/routes/events.json.ts
index a2411e5b8..f04cb55ff 100644
--- a/src/routes/events.json.ts
+++ b/src/routes/events.json.ts
@@ -2,11 +2,13 @@ import { AcmEvent, parse } from '$lib/ical/parse';
import type { EndpointOutput } from '@sveltejs/kit';
import type { DefaultBody } from '@sveltejs/kit/types/endpoint';
+// Constants
+const caching = false; // Make this false to disable server-side caching in development.
+const expirationTimeout = 1e3 * 60 * 1; // Fetch updates every 1 minute.
const ICAL_TARGET_URL =
'https://calendar.google.com/calendar/ical/738lnit63cr2lhp7jtduvj0c9g%40group.calendar.google.com/public/basic.ics';
-const caching = false; // Make this false to disable server-side caching in development.
-const expirationTimeout = 1e3 * 60 * 3; // Fetch updates every 2 minutes.
+// Globals
let eventExpirationTimestamp = 0;
let events: AcmEvent[] = [];
diff --git a/static/global.css b/static/global.css
index 6d641916e..62da48784 100644
--- a/static/global.css
+++ b/static/global.css
@@ -34,6 +34,11 @@ body {
--acm-dark: #101315;
--acm-light: #f8f8f8;
+ --acm-general-rgb: 44, 145, 198;
+ --acm-algo-rgb: 157, 53, 231;
+ --acm-create-rgb: 255, 67, 101;
+ --acm-dev-rgb: 30, 108, 255;
+
--desktop-breakpoint: 768px;
--navbar-height: 82px;