Skip to content

Commit

Permalink
Merge pull request #160 from EthanThatOneKid/fix/143
Browse files Browse the repository at this point in the history
Refactored iCal parser to parse RRules + Adding recurring label for recurring event
  • Loading branch information
anhduy1202 committed Oct 29, 2021
2 parents 9f0d827 + 4ca4d16 commit eab5d5f
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 149 deletions.
50 changes: 49 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
},
"dependencies": {
"rfs": "^9.0.6"
"rfs": "^9.0.6",
"rrule": "^2.6.8"
},
"devDependencies": {
"@sveltejs/adapter-static": "next",
Expand Down
7 changes: 3 additions & 4 deletions src/lib/components/events/event-carousel.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import EventItem from './event-item.svelte';
import type { AcmEvent } from '$lib/parse-ical-data';
import type { AcmEvent } from '$lib/ical/parse';
export let events: AcmEvent[] = [];
Expand All @@ -13,7 +13,7 @@
const scrollTheCarousel = (movementScalar: number, isSmooth: boolean = false) =>
carouselRef.scrollBy({
left: -movementScalar,
behavior: isSmooth ? 'smooth' : 'auto'
behavior: isSmooth ? 'smooth' : 'auto',
});
const scrollOnMouseMove = (event: MouseEvent) => isGrabbing && scrollTheCarousel(event.movementX);
const startGrabbing = () => (isGrabbing = true);
Expand All @@ -39,8 +39,7 @@
on:mousedown={startGrabbing}
on:mouseup={endGrabbing}
on:mouseleave={endGrabbing}
on:wheel={scrollHorizontally}
>
on:wheel={scrollHorizontally}>
<div class="event-item-buffer" />
{#each events as eventInfo}
<EventItem info={eventInfo} />
Expand Down
22 changes: 15 additions & 7 deletions src/lib/components/events/event-item.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { AcmEvent } from '$lib/parse-ical-data';
import { links } from '$lib/constants/links';
import type { AcmEvent } from '$lib/ical/parse';
import AcmButton from '$lib/components/utils/acm-button.svelte';
export let info: AcmEvent;
let isActive = false;
let isSuccessfullyCopied = false;
let isActive: boolean = false;
let isRecurring: boolean = info.recurring;
let isSuccessfullyCopied: boolean = false;
let anchor: HTMLDivElement;
const copyEventLink = (slug: string) => {
Expand All @@ -25,7 +25,7 @@
anchor.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'center'
inline: 'center',
});
}
});
Expand All @@ -39,12 +39,14 @@
{info.month}
{info.day}
</span>
{#if isRecurring}
<p class="event-recurring">RECURRING</p>
{/if}
</p>
<h3
class="headers"
class:copied={isSuccessfullyCopied}
on:click={() => copyEventLink(info.slug)}
>
on:click={() => copyEventLink(info.slug)}>
{info.summary}
</h3>
<div>
Expand Down Expand Up @@ -92,6 +94,12 @@
border-radius: 30px;
}
.event-recurring {
font-weight: 800;
color: var(--acm-blue);
font-size: 16px;
}
.event-card h3 {
position: relative;
width: 186px;
Expand Down
9 changes: 9 additions & 0 deletions src/lib/constants/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum Time {
Millisecond = 1,
Second = 1e3 * Millisecond,
Minute = 60 * Second,
Hour = 60 * Minute,
Day = 24 * Hour,
}

export const ACM_LOCALE = 'en-US';
132 changes: 132 additions & 0 deletions src/lib/ical/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import RRule from 'rrule';

interface IcalOutput {
[key: string]: string | string[] | IcalOutput[];
}

const cleanIcalKey = (key: string): string => {
if (key.startsWith('DTSTART')) return 'DTSTART';
if (key.startsWith('DTEND')) return 'DTEND';
return key;
};

/**
* The code in this function is derived from
* https://github.com/adrianlee44/ical2json.
* @param source The raw calendar data in ICAL format.
* @returns The parsed ICAL data.
*/
export const parseRawIcal = (source: string): IcalOutput => {
const output: IcalOutput = {};
const lines = source.split(/\r\n|\n|\r/);
const parents: IcalOutput[] = [];
let parent: IcalOutput = {};
let current: IcalOutput = output;
let currentKey = '';

for (const line of lines) {
let currentValue = '';
if (line.charAt(0) === ' ') {
current[currentKey] += line.substr(1);
} else {
const splitAt = line.indexOf(':');
if (splitAt < 0) {
continue;
}
currentKey = cleanIcalKey(line.substr(0, splitAt));
currentValue = line.substr(splitAt + 1);
switch (currentKey) {
case 'BEGIN':
parents.push(parent);
parent = current;
if (parent[currentValue] == null) {
parent[currentValue] = [];
}
// Create a new object, store the reference for future uses.
current = {};
(parent[currentValue] as IcalOutput[]).push(current);
break;
case 'END':
current = parent;
parent = parents.pop() as IcalOutput;
break;
default:
if (current[currentKey]) {
if (!Array.isArray(current[currentKey])) {
current[currentKey] = [current[currentKey]] as string[];
}
(current[currentKey] as string[]).push(currentValue);
} else {
(current[currentKey] as string) = currentValue;
}
}
}
}

return output;
};

/**
* This function parses the raw ICAL datetime string into a Date object.
* @param datetime Example: October 31st, 2021 = "20211031T000000"
* @returns The parsed Date object.
*/
const parseRawIcalDatetime = (datetime: string): Date => {
const fullYear = datetime.slice(0, 4);
const month = datetime.slice(4, 6);
const day = datetime.slice(6, 8);
const hours = datetime.slice(9, 11);
const minutes = datetime.slice(11, 13);
const seconds = datetime.slice(13, 15);
const date = new Date();
date.setFullYear(Number(fullYear), Number(month) - 1, Number(day));
date.setHours(Number(hours) - 7, Number(minutes), Number(seconds));
return date;
};

export const computeIcalDatetime = (event: any) => {
const rawDatetime = event['DTSTART'];
const rawRrule = event['RRULE'];
if (rawRrule !== undefined) {
try {
const ruleSrc = `DTSTART:${rawDatetime}Z\nRRULE:${rawRrule}`;
const recurrence = RRule.fromString(ruleSrc);
const date = recurrence.after(new Date());
date.setHours(date.getHours());
return date;
} catch {}
}
return parseRawIcalDatetime(rawDatetime);
};

export const slugifyEvent = (summary: string, month: string, day: number): string => {
const cleanedSummary = summary.replace(/[^\w\s\_]/g, '').replace(/(\s|\-|\_)+/g, '-');
const slug = [cleanedSummary, month, day].join('-').toLowerCase();
return slug;
};

export const checkForRecurrence = (raw?: string): boolean => {
if (raw === undefined) return false;
try {
const recurrence = RRule.fromString(raw);
return recurrence.isFullyConvertibleToText();
} catch {}
return false;
};

export const parseDescription = (content: string): Record<string, string> => {
const result = {};
for (const line of content.split('\n')) {
const [key, value] = line.split('=');
result[key.trim()] = value;
}
return result;
};

export const sortByDate = () => {
return ({ date: date1 }, { date: date2 }) => (date1.valueOf() > date2.valueOf() ? 1 : -1);
};

export const filterIfPassed = (now: number, offset: number = 0) => {
return ({ date }: { date: Date }) => date.valueOf() + offset > now;
};
Loading

1 comment on commit eab5d5f

@vercel
Copy link

@vercel vercel bot commented on eab5d5f Oct 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.