From ab4ed44c745288b06504f0804d06530053bd2d96 Mon Sep 17 00:00:00 2001
From: Ben Grant
Date: Tue, 1 Jun 2021 20:07:30 +1000
Subject: [PATCH 1/3] Fix button margin in Safari
---
.../src/components/CalendarField/calendarFieldStyle.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
index e351b19..7aeafaa 100644
--- a/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
+++ b/crabfit-frontend/src/components/CalendarField/calendarFieldStyle.ts
@@ -70,6 +70,7 @@ export const Date = styled.button`
color: inherit;
background: none;
border: 0;
+ margin: 0;
appearance: none;
background-color: ${props => props.theme.primaryBackground};
From 2d9f98eda5892859f7163eebf4ab96161b9d86d0 Mon Sep 17 00:00:00 2001
From: Ben Grant
Date: Tue, 1 Jun 2021 20:42:45 +1000
Subject: [PATCH 2/3] MSAL for microsoft outlook calendar sync
---
crabfit-frontend/package.json | 1 +
.../microsoft-identity-association.json | 7 +++++++
crabfit-frontend/yarn.lock | 20 +++++++++++++++++--
3 files changed, 26 insertions(+), 2 deletions(-)
create mode 100644 crabfit-frontend/public/.well-known/microsoft-identity-association.json
diff --git a/crabfit-frontend/package.json b/crabfit-frontend/package.json
index 32303b8..e0baff5 100644
--- a/crabfit-frontend/package.json
+++ b/crabfit-frontend/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
+ "@microsoft/microsoft-graph-client": "^2.2.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
diff --git a/crabfit-frontend/public/.well-known/microsoft-identity-association.json b/crabfit-frontend/public/.well-known/microsoft-identity-association.json
new file mode 100644
index 0000000..4a95022
--- /dev/null
+++ b/crabfit-frontend/public/.well-known/microsoft-identity-association.json
@@ -0,0 +1,7 @@
+{
+ "associatedApplications": [
+ {
+ "applicationId": "78739601-9834-4d41-a281-74ca2a50b2e6"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/crabfit-frontend/yarn.lock b/crabfit-frontend/yarn.lock
index 140d018..1db0795 100644
--- a/crabfit-frontend/yarn.lock
+++ b/crabfit-frontend/yarn.lock
@@ -1105,7 +1105,7 @@
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.6":
+"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.6", "@babel/runtime@^7.4.4":
version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==
@@ -1504,6 +1504,15 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
+"@microsoft/microsoft-graph-client@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@microsoft/microsoft-graph-client/-/microsoft-graph-client-2.2.1.tgz#0ef045e1210551f234466a234bb0c60ea2ad8334"
+ integrity sha512-fbDN3UJ+jtSP9llAejqmslMcv498YuIrS3OS/Luivb8OSjdUESZKdP0gcUunnuNIayePVT0/bGYSJTzAIptJQQ==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ msal "^1.4.4"
+ tslib "^1.9.3"
+
"@nodelib/fs.scandir@2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69"
@@ -7485,6 +7494,13 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+msal@^1.4.4:
+ version "1.4.11"
+ resolved "https://registry.yarnpkg.com/msal/-/msal-1.4.11.tgz#255e74e200ee5d603dca30e4e48e47fb57441370"
+ integrity sha512-8vW5/+irlcQQk87r8Qp3/kQEc552hr7FQLJ6GF5LLkqnwJDDxrswz6RYPiQhmiampymIs0PbHVZrNf8m+6DmgQ==
+ dependencies:
+ tslib "^1.9.3"
+
multicast-dns-service-types@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
@@ -10834,7 +10850,7 @@ tsconfig-paths@^3.9.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
-tslib@^1.8.1, tslib@^1.9.0:
+tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
From e03ddf6814b5a555966617eb3a2e9f9200e2369f Mon Sep 17 00:00:00 2001
From: Ben Grant
Date: Tue, 1 Jun 2021 23:22:42 +1000
Subject: [PATCH 3/3] Outlook calendar sync
---
crabfit-frontend/package.json | 1 +
crabfit-frontend/public/i18n/en/event.json | 3 +-
.../AvailabilityEditor/AvailabilityEditor.tsx | 38 ++-
.../GoogleCalendar/GoogleCalendar.tsx | 10 +-
.../GoogleCalendar/googleCalendarStyle.ts | 16 ++
.../OutlookCalendar/OutlookCalendar.tsx | 240 ++++++++++++++++++
crabfit-frontend/src/components/index.ts | 1 +
crabfit-frontend/src/res/outlook.svg | 54 ++++
crabfit-frontend/yarn.lock | 14 +
9 files changed, 362 insertions(+), 15 deletions(-)
create mode 100644 crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx
create mode 100644 crabfit-frontend/src/res/outlook.svg
diff --git a/crabfit-frontend/package.json b/crabfit-frontend/package.json
index e0baff5..0ce5dd3 100644
--- a/crabfit-frontend/package.json
+++ b/crabfit-frontend/package.json
@@ -3,6 +3,7 @@
"version": "1.0.0",
"private": true,
"dependencies": {
+ "@azure/msal-browser": "^2.14.2",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
"@microsoft/microsoft-graph-client": "^2.2.1",
diff --git a/crabfit-frontend/public/i18n/en/event.json b/crabfit-frontend/public/i18n/en/event.json
index 569723a..2c2b63e 100644
--- a/crabfit-frontend/public/i18n/en/event.json
+++ b/crabfit-frontend/public/i18n/en/event.json
@@ -58,6 +58,7 @@
"select_none": "Select none",
"info": "Importing will overwrite your current availability",
"button": "Import availability"
- }
+ },
+ "outlook_cal": "Sync with Outlook Calendar"
}
}
diff --git a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
index 444cf7e..8488cab 100644
--- a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
+++ b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
@@ -5,6 +5,8 @@ import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isBetween from 'dayjs/plugin/isBetween';
+import dayjs_timezone from 'dayjs/plugin/timezone';
+import utc from 'dayjs/plugin/utc';
import {
Wrapper,
@@ -22,11 +24,13 @@ import {
} from 'components/AvailabilityViewer/availabilityViewerStyle';
import { Time } from './availabilityEditorStyle';
-import { GoogleCalendar, Center } from 'components';
+import { GoogleCalendar, OutlookCalendar, Center } from 'components';
dayjs.extend(localeData);
dayjs.extend(customParseFormat);
dayjs.extend(isBetween);
+dayjs.extend(utc);
+dayjs.extend(dayjs_timezone);
const AvailabilityEditor = ({
times,
@@ -63,16 +67,28 @@ const AvailabilityEditor = ({
{isSpecificDates && (
- onChange(
- times.filter(time => !busyArray.some(busy =>
- dayjs(time, 'HHmm-DDMMYYYY').isBetween(busy.start, busy.end, null, '[)')
- ))
- )}
- />
+
+ onChange(
+ times.filter(time => !busyArray.some(busy =>
+ dayjs(time, 'HHmm-DDMMYYYY').isBetween(busy.start, busy.end, null, '[)')
+ ))
+ )}
+ />
+ onChange(
+ times.filter(time => !busyArray.some(busy =>
+ dayjs(time, 'HHmm-DDMMYYYY').isBetween(dayjs.tz(busy.start.dateTime, busy.start.timeZone), dayjs.tz(busy.end.dateTime, busy.end.timeZone), null, '[)')
+ ))
+ )}
+ />
+
)}
diff --git a/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
index 62f6235..b1517a0 100644
--- a/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
+++ b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
@@ -12,6 +12,8 @@ import {
CalendarLabel,
Info,
Options,
+ Title,
+ Icon,
} from './googleCalendarStyle';
import googleLogo from 'res/google.svg';
@@ -109,13 +111,15 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
) : (
-
+
+
{/* eslint-disable-next-line */}
- {t('event:you.google_cal.login')} ( {
+ {t('event:you.google_cal.login')}
+ ( {
e.preventDefault();
signOut();
}}>{t('event:you.google_cal.logout')})
-
+
{calendars !== undefined && !calendars.every(c => c.checked) && (
/* eslint-disable-next-line */
diff --git a/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts b/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts
index dcb1cae..fe31510 100644
--- a/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts
+++ b/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts
@@ -12,6 +12,7 @@ export const LoginButton = styled.div`
`;
export const CalendarList = styled.div`
+ width: 100%;
& > div {
display: flex;
margin: 2px 0;
@@ -110,3 +111,18 @@ export const Options = styled.div`
font-size: 14px;
padding: 0 0 5px;
`;
+
+export const Title = styled.p`
+ display: flex;
+ align-items: center;
+
+ & strong {
+ margin-right: 1ex;
+ }
+`;
+
+export const Icon = styled.img`
+ height: 24px;
+ width: 24px;
+ margin-right: 12px;
+`;
diff --git a/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx b/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx
new file mode 100644
index 0000000..cae2983
--- /dev/null
+++ b/crabfit-frontend/src/components/OutlookCalendar/OutlookCalendar.tsx
@@ -0,0 +1,240 @@
+import { useState, useEffect } from 'react';
+import { PublicClientApplication } from "@azure/msal-browser";
+import { Client } from "@microsoft/microsoft-graph-client";
+import { useTranslation } from 'react-i18next';
+
+import { Button, Center } from 'components';
+import { Loader } from '../Loading/loadingStyle';
+import {
+ LoginButton,
+ CalendarList,
+ CheckboxInput,
+ CheckboxLabel,
+ CalendarLabel,
+ Info,
+ Options,
+ Title,
+ Icon,
+} from '../GoogleCalendar/googleCalendarStyle';
+
+import outlookLogo from 'res/outlook.svg';
+
+// Initialise the MSAL object
+const publicClientApplication = new PublicClientApplication({
+ auth: {
+ clientId: '78739601-9834-4d41-a281-74ca2a50b2e6',
+ redirectUri: process.env.NODE_ENV === 'production' ? 'https://crab.fit' : 'http://localhost:3000',
+ },
+ cache: {
+ cacheLocation: 'sessionStorage',
+ storeAuthStateInCookie: true,
+ },
+});
+
+const getAuthenticatedClient = accessToken => {
+ const client = Client.init({
+ authProvider: done => done(null, accessToken),
+ });
+ return client;
+};
+
+const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
+ const [client, setClient] = useState(undefined);
+ const [calendars, setCalendars] = useState(undefined);
+ const [freeBusyLoading, setFreeBusyLoading] = useState(false);
+ const { t } = useTranslation('event');
+
+ const checkLogin = async () => {
+ const accounts = publicClientApplication.getAllAccounts();
+ if (accounts && accounts.length > 0) {
+ try {
+ const accessToken = await getAccessToken();
+ setClient(getAuthenticatedClient(accessToken));
+ } catch (e) {
+ console.error(e);
+ signOut();
+ }
+ } else {
+ setClient(null);
+ }
+ };
+
+ const signIn = async () => {
+ try {
+ await publicClientApplication.loginPopup({
+ scopes: ['Calendars.Read', 'Calendars.Read.Shared'],
+ });
+ } catch (e) {
+ console.error(e);
+ } finally {
+ checkLogin();
+ }
+ };
+
+ const signOut = async () => {
+ try {
+ await publicClientApplication.logoutRedirect({
+ onRedirectNavigate: () => false,
+ });
+ } catch (e) {
+ console.error(e);
+ } finally {
+ checkLogin();
+ }
+ };
+
+ const getAccessToken = async () => {
+ try {
+ const accounts = publicClientApplication.getAllAccounts();
+ if (accounts.length <= 0) throw new Error('login_required');
+
+ // Try to get silently
+ const result = await publicClientApplication.acquireTokenSilent({
+ scopes: ['Calendars.Read', 'Calendars.Read.Shared'],
+ account: accounts[0],
+ });
+ return result.accessToken;
+ } catch (e) {
+ if ([
+ 'consent_required',
+ 'interaction_required',
+ 'login_required',
+ 'no_account_in_silent_request'
+ ].includes(e.message)) {
+ // Try to get with popup
+ const result = await publicClientApplication.acquireTokenPopup({
+ scopes: ['Calendars.Read', 'Calendars.Read.Shared'],
+ });
+ return result.accessToken;
+ } else {
+ throw e;
+ }
+ }
+ };
+
+ const importAvailability = () => {
+ setFreeBusyLoading(true);
+ gtag('event', 'outlook_cal_sync', {
+ 'event_category': 'event',
+ });
+ client.api('/me/calendar/getSchedule').post({
+ schedules: calendars.filter(c => c.checked).map(c => c.id),
+ startTime: {
+ dateTime: timeMin,
+ timeZone,
+ },
+ endTime: {
+ dateTime: timeMax,
+ timeZone,
+ },
+ availabilityViewInterval: 30,
+ })
+ .then(response => {
+ onImport(response.value.reduce((busy, c) => c.hasOwnProperty('error') ? busy : [...busy, ...c.scheduleItems.filter(item => item.status === 'busy' || item.status === 'tentative')], []));
+ })
+ .catch(e => {
+ console.error(e);
+ signOut();
+ })
+ .finally(() => setFreeBusyLoading(false));
+ };
+
+ useEffect(() => checkLogin(), []);
+
+ useEffect(() => {
+ if (client) {
+ client.api('/me/calendars').get()
+ .then(response => {
+ setCalendars(response.value.map(item => ({
+ 'name': item.name,
+ 'description': item.owner.name,
+ 'id': item.owner.address,
+ 'color': item.hexColor,
+ 'checked': item.isDefaultCalendar === true,
+ })));
+ })
+ .catch(e => {
+ console.error(e);
+ signOut();
+ });
+ }
+ }, [client]);
+
+ return (
+ <>
+ {!client ? (
+
+
+
+ ) : (
+
+
+
+ {/* eslint-disable-next-line */}
+ {t('event:you.outlook_cal')}
+ ( {
+ e.preventDefault();
+ signOut();
+ }}>{t('event:you.google_cal.logout')})
+
+
+ {calendars !== undefined && !calendars.every(c => c.checked) && (
+ /* eslint-disable-next-line */
+ {
+ e.preventDefault();
+ setCalendars(calendars.map(c => ({...c, checked: true})));
+ }}>{t('event:you.google_cal.select_all')}
+ )}
+ {calendars !== undefined && calendars.every(c => c.checked) && (
+ /* eslint-disable-next-line */
+ {
+ e.preventDefault();
+ setCalendars(calendars.map(c => ({...c, checked: false})));
+ }}>{t('event:you.google_cal.select_none')}
+ )}
+
+ {calendars !== undefined ? calendars.map(calendar => (
+
+ setCalendars(calendars.map(c => c.id === calendar.id ? {...c, checked: !c.checked} : c))}
+ />
+
+ {calendar.name}
+
+ )) : (
+
+ )}
+ {calendars !== undefined && (
+ <>
+ {t('event:you.google_cal.info')}
+
+ >
+ )}
+
+ )}
+ >
+ );
+};
+
+export default OutlookCalendar;
diff --git a/crabfit-frontend/src/components/index.ts b/crabfit-frontend/src/components/index.ts
index c0f472f..2838c00 100644
--- a/crabfit-frontend/src/components/index.ts
+++ b/crabfit-frontend/src/components/index.ts
@@ -11,6 +11,7 @@ export { default as AvailabilityEditor } from './AvailabilityEditor/Availability
export { default as Error } from './Error/Error';
export { default as Loading } from './Loading/Loading';
export { default as GoogleCalendar } from './GoogleCalendar/GoogleCalendar';
+export { default as OutlookCalendar } from './OutlookCalendar/OutlookCalendar';
export { default as Center } from './Center/Center';
export { default as Donate } from './Donate/Donate';
diff --git a/crabfit-frontend/src/res/outlook.svg b/crabfit-frontend/src/res/outlook.svg
new file mode 100644
index 0000000..702befb
--- /dev/null
+++ b/crabfit-frontend/src/res/outlook.svg
@@ -0,0 +1,54 @@
+
+
+
+
diff --git a/crabfit-frontend/yarn.lock b/crabfit-frontend/yarn.lock
index 1db0795..e7c363a 100644
--- a/crabfit-frontend/yarn.lock
+++ b/crabfit-frontend/yarn.lock
@@ -2,6 +2,20 @@
# yarn lockfile v1
+"@azure/msal-browser@^2.14.2":
+ version "2.14.2"
+ resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-2.14.2.tgz#4efa031ad16d5a3a527eddb6222e15dd6e2ea3e8"
+ integrity sha512-JKHE9Rer41CI8tweiyE91M8ZbGvQV9P+jOPB4ZtPxyxCi2f7ED3jNfdzyUJ1eGB+hCRnvO56M1Xc61T1R+JfYg==
+ dependencies:
+ "@azure/msal-common" "^4.3.0"
+
+"@azure/msal-common@^4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-4.3.0.tgz#b540e92748656724088bf77192e59943a93135bc"
+ integrity sha512-jFqUWe83wVb6O8cNGGBFg2QlKvqM1ezUgJTEV7kIsAPX0RXhGFE4B1DLNt6hCnkTXDbw+KGW0zgxOEr4MJQwLw==
+ dependencies:
+ debug "^4.1.1"
+
"@babel/code-frame@7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"