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 @@