From f780ea63f26d7bbb4ffb3876d98b00eb1a4f037b Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sat, 4 Feb 2023 13:22:31 -0500 Subject: [PATCH 1/2] Export to ICS --- package-lock.json | 90 +++++++++++++++++ package.json | 1 + src/components/Footers.tsx | 3 + src/lib/gapi.ts | 201 +++++++++++++++++++++++++------------ 4 files changed, 229 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index a1222ee..b0f3d60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@types/react": "^17.0.48", "@types/react-dom": "^17.0.17", "html-entities": "^2.3.3", + "ics": "^2.41.0", "msgpack-lite": "^0.1.26", "nanoid": "^3.3.4", "react": "^18.2.0", @@ -9609,6 +9610,15 @@ "node": ">=0.10.0" } }, + "node_modules/ics": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/ics/-/ics-2.41.0.tgz", + "integrity": "sha512-6oleMfOpdBIrZGMNrTutwW7eFwua8lOkymDNxMXlsVF00HghqH+I3S6frt3a2rfjXTlkI0qiY2rnsKP2JQ9vJA==", + "dependencies": { + "nanoid": "^3.1.23", + "yup": "^0.32.9" + } + }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -12383,6 +12393,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -12703,6 +12718,11 @@ "multicast-dns": "cli.js" } }, + "node_modules/nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "node_modules/nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -14634,6 +14654,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -16615,6 +16640,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -17920,6 +17950,23 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + }, + "engines": { + "node": ">=10" + } } }, "dependencies": { @@ -24816,6 +24863,15 @@ "safer-buffer": ">= 2.1.2 < 3.0.0" } }, + "ics": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/ics/-/ics-2.41.0.tgz", + "integrity": "sha512-6oleMfOpdBIrZGMNrTutwW7eFwua8lOkymDNxMXlsVF00HghqH+I3S6frt3a2rfjXTlkI0qiY2rnsKP2JQ9vJA==", + "requires": { + "nanoid": "^3.1.23", + "yup": "^0.32.9" + } + }, "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -26805,6 +26861,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -27049,6 +27110,11 @@ "thunky": "^1.0.2" } }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -28262,6 +28328,11 @@ "react-is": "^16.13.1" } }, + "property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -29709,6 +29780,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -30692,6 +30768,20 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } } } } diff --git a/package.json b/package.json index 5073f28..27d3a83 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/react": "^17.0.48", "@types/react-dom": "^17.0.17", "html-entities": "^2.3.3", + "ics": "^2.41.0", "msgpack-lite": "^0.1.26", "nanoid": "^3.3.4", "react": "^18.2.0", diff --git a/src/components/Footers.tsx b/src/components/Footers.tsx index 2934ee3..438425a 100644 --- a/src/components/Footers.tsx +++ b/src/components/Footers.tsx @@ -176,6 +176,9 @@ export function LeftFooter(props: { Sign in with Google )} + Last updated: {state.lastUpdated}. diff --git a/src/lib/gapi.ts b/src/lib/gapi.ts index b67608b..3f8b5d2 100644 --- a/src/lib/gapi.ts +++ b/src/lib/gapi.ts @@ -1,7 +1,8 @@ -import { useGoogleLogin } from "@react-oauth/google"; +// import { useGoogleLogin } from "@react-oauth/google"; +import { EventAttributes, DateArray, createEvents } from "ics"; import { Activity } from "./activity"; -import { CALENDAR_COLOR } from "./colors"; +// import { CALENDAR_COLOR } from "./colors"; import { Term } from "./dates"; import { State } from "./state"; @@ -26,6 +27,31 @@ function toISOString(date: Date): string { ].join(""); } +/** Returns a date as a UTC date array */ +function toDateArray(date: Date): DateArray { + return [ + date.getUTCFullYear(), + date.getUTCMonth() + 1, + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + ]; +} + +/** Downloads a file with the given text data */ +function download(filename: string, text: string) { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + /** Returns a date as an RRULE string without a timezone. */ function toRRuleString(date: Date): string { return Array.from(toISOString(date)) @@ -34,10 +60,33 @@ function toRRuleString(date: Date): string { } /** Return a list of events for an activity that happen on a given term. */ -function toEvents( - activity: Activity, - term: Term -): Array { +// function toGAPIEvents( +// activity: Activity, +// term: Term +// ): Array { +// return activity.events.flatMap((event) => +// event.slots.map((slot) => { +// const startDate = term.startDateFor(slot.startSlot); +// const startDateEnd = term.startDateFor(slot.endSlot); +// const endDate = term.endDateFor(slot.startSlot); +// const exDates = term.exDatesFor(slot.startSlot); +// const rDate = term.rDateFor(slot.startSlot); +// return { +// summary: event.name, +// location: event.room, +// start: { dateTime: toISOString(startDate), timeZone: TIMEZONE }, +// end: { dateTime: toISOString(startDateEnd), timeZone: TIMEZONE }, +// recurrence: [ +// // for some reason, gcal wants UNTIL to be a date, not time +// `RRULE:FREQ=WEEKLY;UNTIL=${toRRuleString(endDate).split("T")[0]}`, +// `EXDATE;TZID=${TIMEZONE}:${exDates.map(toRRuleString).join(",")}`, +// rDate && `RDATE;TZID=${TIMEZONE}:${toRRuleString(rDate)}`, +// ].filter((t): t is string => t !== undefined), +// }; +// }) +// ); +// } +function toICSEvents(activity: Activity, term: Term): Array { return activity.events.flatMap((event) => event.slots.map((slot) => { const startDate = term.startDateFor(slot.startSlot); @@ -45,17 +94,20 @@ function toEvents( const endDate = term.endDateFor(slot.startSlot); const exDates = term.exDatesFor(slot.startSlot); const rDate = term.rDateFor(slot.startSlot); + console.log(event.name, startDate); return { - summary: event.name, + title: event.name, location: event.room, - start: { dateTime: toISOString(startDate), timeZone: TIMEZONE }, - end: { dateTime: toISOString(startDateEnd), timeZone: TIMEZONE }, - recurrence: [ + start: toDateArray(startDate), + startInputType: "utc", + end: toDateArray(startDateEnd), + endInputType: "utc", + recurrenceRule: [ // for some reason, gcal wants UNTIL to be a date, not time - `RRULE:FREQ=WEEKLY;UNTIL=${toRRuleString(endDate).split("T")[0]}`, + `FREQ=WEEKLY;UNTIL=${toRRuleString(endDate).split("T")[0]}`, `EXDATE;TZID=${TIMEZONE}:${exDates.map(toRRuleString).join(",")}`, rDate && `RDATE;TZID=${TIMEZONE}:${toRRuleString(rDate)}`, - ].filter((t): t is string => t !== undefined), + ].filter((t): t is string => t !== undefined)[0], }; }) ); @@ -67,64 +119,81 @@ export function useCalendarExport( onSuccess?: () => void, onError?: () => void ): () => void { - /** Insert a new calendar for this semester. */ - const insertCalendar = async (): Promise => { - const calendarName = `Hydrant: ${state.term.niceName}`; - const resp = await gapi.client.calendar.calendars.insert( - {}, - { summary: calendarName } - ); - return resp.result.id!; - }; + // /** Insert a new calendar for this semester. */ + // const insertCalendar = async (): Promise => { + // const calendarName = `Hydrant: ${state.term.niceName}`; + // const resp = await gapi.client.calendar.calendars.insert( + // {}, + // { summary: calendarName } + // ); + // return resp.result.id!; + // }; - /** Set the background of the calendar to the State color. */ - const setCalendarBackground = async (calendarId: string) => { - const resp = await gapi.client.calendar.calendarList.get({ calendarId }); - const calendar = resp.result; - calendar.backgroundColor = CALENDAR_COLOR; - await gapi.client.calendar.calendarList.update({ - calendarId: calendar.id!, - colorRgbFormat: true, - resource: calendar, - }); - }; + // /** Set the background of the calendar to the State color. */ + // const setCalendarBackground = async (calendarId: string) => { + // const resp = await gapi.client.calendar.calendarList.get({ calendarId }); + // const calendar = resp.result; + // calendar.backgroundColor = CALENDAR_COLOR; + // await gapi.client.calendar.calendarList.update({ + // calendarId: calendar.id!, + // colorRgbFormat: true, + // resource: calendar, + // }); + // }; /** Add the classes / non-classes to the calendar. */ - const addCalendarEvents = async (calendarId: string) => { - const batch = gapi.client.newBatch(); - state.selectedActivities - .flatMap((activity) => toEvents(activity, state.term)) - .forEach((resource) => - batch.add( - gapi.client.calendar.events.insert({ - calendarId, - resource, - }) - ) - ); - await batch.then(); + // const addCalendarEvents = async (calendarId: string) => { + // const batch = gapi.client.newBatch(); + // state.selectedActivities + // .flatMap((activity) => toEvents(activity, state.term)) + // .forEach((resource) => + // batch.add( + // gapi.client.calendar.events.insert({ + // calendarId, + // resource, + // }) + // ) + // ); + // await batch.then(); + // }; + const addCalendarEvents = async () => { + const events = state.selectedActivities.flatMap((activity) => + toICSEvents(activity, state.term) + ); + const calendarName = `Hydrant: ${state.term.niceName}`; + events.forEach((event) => { + event.calName = calendarName; + }); + console.log(events); + createEvents(events, (error, value) => { + if (error) onError?.(); + download(`${state.term.urlName}.ics`, value); + onSuccess?.(); + }); }; - /** Create a new calendar and populate it. */ - const exportCalendar = async () => { - const calendarId = await insertCalendar(); - await setCalendarBackground(calendarId); - await addCalendarEvents(calendarId); - onSuccess?.(); - window.open("https://calendar.google.com", "_blank"); - }; + // /** Create a new calendar and populate it. */ + // const exportCalendar = async () => { + // const calendarId = await insertCalendar(); + // await setCalendarBackground(calendarId); + // await addCalendarEvents(calendarId); + // onSuccess?.(); + // window.open("https://calendar.google.com", "_blank"); + // }; + + // /** Request permission and create calendar. */ + // const onCalendarExport = useGoogleLogin({ + // scope: "https://www.googleapis.com/auth/calendar", + // onSuccess: (tokenResponse) => { + // if (tokenResponse?.access_token) { + // gapi.client.setApiKey(process.env.REACT_APP_API_KEY!); + // gapi.client.load("calendar", "v3", exportCalendar); + // } + // }, + // onError, + // }); + + // return onCalendarExport; - /** Request permission and create calendar. */ - const onCalendarExport = useGoogleLogin({ - scope: "https://www.googleapis.com/auth/calendar", - onSuccess: (tokenResponse) => { - if (tokenResponse?.access_token) { - gapi.client.setApiKey(process.env.REACT_APP_API_KEY!); - gapi.client.load("calendar", "v3", exportCalendar); - } - }, - onError, - }); - - return onCalendarExport; + return addCalendarEvents; } From 668c744ea226bde236c7a3bb2eb2d47b6d1d543c Mon Sep 17 00:00:00 2001 From: Andi Qu Date: Sun, 5 Feb 2023 10:01:27 -0500 Subject: [PATCH 2/2] Split GCal and ICS export into different hooks --- src/components/Footers.tsx | 7 +- src/lib/gapi.ts | 197 +++++++++++++++++++------------------ 2 files changed, 106 insertions(+), 98 deletions(-) diff --git a/src/components/Footers.tsx b/src/components/Footers.tsx index 438425a..e42652e 100644 --- a/src/components/Footers.tsx +++ b/src/components/Footers.tsx @@ -18,7 +18,7 @@ import { useRef, useState } from "react"; import { COLOR_SCHEME_PRESETS } from "../lib/colors"; import { State } from "../lib/state"; -import { useCalendarExport } from "../lib/gapi"; +import { useICSExport } from "../lib/gapi"; import { DEFAULT_PREFERENCES, Preferences } from "../lib/schema"; function PreferencesModal(props: { @@ -145,8 +145,7 @@ export function LeftFooter(props: { const [isExporting, setIsExporting] = useState(false); // TODO: fix gcal export - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const onCalendarExport = useCalendarExport( + const onICSExport = useICSExport( state, () => setIsExporting(false), () => setIsExporting(false) @@ -176,7 +175,7 @@ export function LeftFooter(props: { Sign in with Google )} - diff --git a/src/lib/gapi.ts b/src/lib/gapi.ts index 3f8b5d2..6b17a3b 100644 --- a/src/lib/gapi.ts +++ b/src/lib/gapi.ts @@ -1,8 +1,8 @@ -// import { useGoogleLogin } from "@react-oauth/google"; +import { useGoogleLogin } from "@react-oauth/google"; import { EventAttributes, DateArray, createEvents } from "ics"; import { Activity } from "./activity"; -// import { CALENDAR_COLOR } from "./colors"; +import { CALENDAR_COLOR } from "./colors"; import { Term } from "./dates"; import { State } from "./state"; @@ -40,11 +40,14 @@ function toDateArray(date: Date): DateArray { /** Downloads a file with the given text data */ function download(filename: string, text: string) { - var element = document.createElement('a'); - element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); - element.setAttribute('download', filename); + var element = document.createElement("a"); + element.setAttribute( + "href", + "data:text/plain;charset=utf-8," + encodeURIComponent(text) + ); + element.setAttribute("download", filename); - element.style.display = 'none'; + element.style.display = "none"; document.body.appendChild(element); element.click(); @@ -60,32 +63,33 @@ function toRRuleString(date: Date): string { } /** Return a list of events for an activity that happen on a given term. */ -// function toGAPIEvents( -// activity: Activity, -// term: Term -// ): Array { -// return activity.events.flatMap((event) => -// event.slots.map((slot) => { -// const startDate = term.startDateFor(slot.startSlot); -// const startDateEnd = term.startDateFor(slot.endSlot); -// const endDate = term.endDateFor(slot.startSlot); -// const exDates = term.exDatesFor(slot.startSlot); -// const rDate = term.rDateFor(slot.startSlot); -// return { -// summary: event.name, -// location: event.room, -// start: { dateTime: toISOString(startDate), timeZone: TIMEZONE }, -// end: { dateTime: toISOString(startDateEnd), timeZone: TIMEZONE }, -// recurrence: [ -// // for some reason, gcal wants UNTIL to be a date, not time -// `RRULE:FREQ=WEEKLY;UNTIL=${toRRuleString(endDate).split("T")[0]}`, -// `EXDATE;TZID=${TIMEZONE}:${exDates.map(toRRuleString).join(",")}`, -// rDate && `RDATE;TZID=${TIMEZONE}:${toRRuleString(rDate)}`, -// ].filter((t): t is string => t !== undefined), -// }; -// }) -// ); -// } +function toGoogleCalendarEvents( + activity: Activity, + term: Term +): Array { + return activity.events.flatMap((event) => + event.slots.map((slot) => { + const startDate = term.startDateFor(slot.startSlot); + const startDateEnd = term.startDateFor(slot.endSlot); + const endDate = term.endDateFor(slot.startSlot); + const exDates = term.exDatesFor(slot.startSlot); + const rDate = term.rDateFor(slot.startSlot); + return { + summary: event.name, + location: event.room, + start: { dateTime: toISOString(startDate), timeZone: TIMEZONE }, + end: { dateTime: toISOString(startDateEnd), timeZone: TIMEZONE }, + recurrence: [ + // for some reason, gcal wants UNTIL to be a date, not time + `RRULE:FREQ=WEEKLY;UNTIL=${toRRuleString(endDate).split("T")[0]}`, + `EXDATE;TZID=${TIMEZONE}:${exDates.map(toRRuleString).join(",")}`, + rDate && `RDATE;TZID=${TIMEZONE}:${toRRuleString(rDate)}`, + ].filter((t): t is string => t !== undefined), + }; + }) + ); +} + function toICSEvents(activity: Activity, term: Term): Array { return activity.events.flatMap((event) => event.slots.map((slot) => { @@ -114,49 +118,79 @@ function toICSEvents(activity: Activity, term: Term): Array { } /** Hook that returns an export calendar function. */ -export function useCalendarExport( +export function useGoogleCalendarExport( state: State, onSuccess?: () => void, onError?: () => void ): () => void { - // /** Insert a new calendar for this semester. */ - // const insertCalendar = async (): Promise => { - // const calendarName = `Hydrant: ${state.term.niceName}`; - // const resp = await gapi.client.calendar.calendars.insert( - // {}, - // { summary: calendarName } - // ); - // return resp.result.id!; - // }; - - // /** Set the background of the calendar to the State color. */ - // const setCalendarBackground = async (calendarId: string) => { - // const resp = await gapi.client.calendar.calendarList.get({ calendarId }); - // const calendar = resp.result; - // calendar.backgroundColor = CALENDAR_COLOR; - // await gapi.client.calendar.calendarList.update({ - // calendarId: calendar.id!, - // colorRgbFormat: true, - // resource: calendar, - // }); - // }; + /** Insert a new calendar for this semester. */ + const insertCalendar = async (): Promise => { + const calendarName = `Hydrant: ${state.term.niceName}`; + const resp = await gapi.client.calendar.calendars.insert( + {}, + { summary: calendarName } + ); + return resp.result.id!; + }; + + /** Set the background of the calendar to the State color. */ + const setCalendarBackground = async (calendarId: string) => { + const resp = await gapi.client.calendar.calendarList.get({ calendarId }); + const calendar = resp.result; + calendar.backgroundColor = CALENDAR_COLOR; + await gapi.client.calendar.calendarList.update({ + calendarId: calendar.id!, + colorRgbFormat: true, + resource: calendar, + }); + }; /** Add the classes / non-classes to the calendar. */ - // const addCalendarEvents = async (calendarId: string) => { - // const batch = gapi.client.newBatch(); - // state.selectedActivities - // .flatMap((activity) => toEvents(activity, state.term)) - // .forEach((resource) => - // batch.add( - // gapi.client.calendar.events.insert({ - // calendarId, - // resource, - // }) - // ) - // ); - // await batch.then(); - // }; - const addCalendarEvents = async () => { + const addCalendarEvents = async (calendarId: string) => { + const batch = gapi.client.newBatch(); + state.selectedActivities + .flatMap((activity) => toGoogleCalendarEvents(activity, state.term)) + .forEach((resource) => + batch.add( + gapi.client.calendar.events.insert({ + calendarId, + resource, + }) + ) + ); + await batch.then(); + }; + + /** Create a new calendar and populate it. */ + const exportCalendar = async () => { + const calendarId = await insertCalendar(); + await setCalendarBackground(calendarId); + await addCalendarEvents(calendarId); + onSuccess?.(); + window.open("https://calendar.google.com", "_blank"); + }; + + /** Request permission and create calendar. */ + const onCalendarExport = useGoogleLogin({ + scope: "https://www.googleapis.com/auth/calendar", + onSuccess: (tokenResponse) => { + if (tokenResponse?.access_token) { + gapi.client.setApiKey(process.env.REACT_APP_API_KEY!); + gapi.client.load("calendar", "v3", exportCalendar); + } + }, + onError, + }); + + return onCalendarExport; +} + +export function useICSExport( + state: State, + onSuccess?: () => void, + onError?: () => void +): () => void { + return async () => { const events = state.selectedActivities.flatMap((activity) => toICSEvents(activity, state.term) ); @@ -171,29 +205,4 @@ export function useCalendarExport( onSuccess?.(); }); }; - - // /** Create a new calendar and populate it. */ - // const exportCalendar = async () => { - // const calendarId = await insertCalendar(); - // await setCalendarBackground(calendarId); - // await addCalendarEvents(calendarId); - // onSuccess?.(); - // window.open("https://calendar.google.com", "_blank"); - // }; - - // /** Request permission and create calendar. */ - // const onCalendarExport = useGoogleLogin({ - // scope: "https://www.googleapis.com/auth/calendar", - // onSuccess: (tokenResponse) => { - // if (tokenResponse?.access_token) { - // gapi.client.setApiKey(process.env.REACT_APP_API_KEY!); - // gapi.client.load("calendar", "v3", exportCalendar); - // } - // }, - // onError, - // }); - - // return onCalendarExport; - - return addCalendarEvents; }