diff --git a/crabfit-frontend/package.json b/crabfit-frontend/package.json
index 31de9ef..785e94c 100644
--- a/crabfit-frontend/package.json
+++ b/crabfit-frontend/package.json
@@ -14,6 +14,7 @@
"@types/react-dom": "^17.0.1",
"axios": "^0.21.1",
"dayjs": "^1.10.4",
+ "gapi-script": "^1.2.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-hook-form": "^6.15.4",
diff --git a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
index 927799f..f6b1085 100644
--- a/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
+++ b/crabfit-frontend/src/components/AvailabilityEditor/AvailabilityEditor.tsx
@@ -2,9 +2,11 @@ import { useState, useRef, Fragment } from 'react';
import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import customParseFormat from 'dayjs/plugin/customParseFormat';
+import isBetween from 'dayjs/plugin/isBetween';
import {
Wrapper,
+ ScrollWrapper,
Container,
Date,
Times,
@@ -14,16 +16,21 @@ import {
TimeLabels,
TimeLabel,
TimeSpace,
+ StyledMain,
} from 'components/AvailabilityViewer/availabilityViewerStyle';
import { Time } from './availabilityEditorStyle';
+import { GoogleCalendar } from 'components';
+
dayjs.extend(localeData);
dayjs.extend(customParseFormat);
+dayjs.extend(isBetween);
const AvailabilityEditor = ({
times,
timeLabels,
dates,
+ timezone,
isSpecificDates,
value = [],
onChange,
@@ -45,82 +52,101 @@ const AvailabilityEditor = ({
};
return (
-
-
-
- {!!timeLabels.length && timeLabels.map((label, i) =>
-
- {label.label?.length !== '' && {label.label}}
-
- )}
-
- {dates.map((date, x) => {
- const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
- const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
- return (
-
-
- {isSpecificDates && {parsedDate.format('MMM D')}}
- {parsedDate.format('ddd')}
+ <>
+ {isSpecificDates && (
+
+ onChange(
+ times.filter(time => !busyArray.some(busy =>
+ dayjs(time, 'HHmm-DDMMYYYY').isBetween(busy.start, busy.end, null, '[)')
+ ))
+ )}
+ />
+
+ )}
-
- {timeLabels.map((timeLabel, y) => {
- if (!timeLabel.time) return null;
- if (!times.includes(`${timeLabel.time}-${date}`)) {
- return (
-
- );
- }
- const time = `${timeLabel.time}-${date}`;
+
+
+
+
+ {!!timeLabels.length && timeLabels.map((label, i) =>
+
+ {label.label?.length !== '' && {label.label}}
+
+ )}
+
+ {dates.map((date, x) => {
+ const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
+ const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
+ return (
+
+
+ {isSpecificDates && {parsedDate.format('MMM D')}}
+ {parsedDate.format('ddd')}
- return (
-
- {last && dates.length !== x+1 && (
-
- )}
-
- );
- })}
-
-
+ return (
+
+
+ {last && dates.length !== x+1 && (
+
+ )}
+
+ );
+ })}
+
+
+
+ >
);
};
diff --git a/crabfit-frontend/src/components/Button/Button.tsx b/crabfit-frontend/src/components/Button/Button.tsx
index 5e90449..896d574 100644
--- a/crabfit-frontend/src/components/Button/Button.tsx
+++ b/crabfit-frontend/src/components/Button/Button.tsx
@@ -3,11 +3,13 @@ import { Wrapper, Top, Bottom } from './buttonStyle';
const Button = ({
buttonHeight,
buttonWidth,
+ primaryColor,
+ secondaryColor,
...props
}) => (
-
-
+
+
);
diff --git a/crabfit-frontend/src/components/Button/buttonStyle.ts b/crabfit-frontend/src/components/Button/buttonStyle.ts
index 46115c4..6bac31d 100644
--- a/crabfit-frontend/src/components/Button/buttonStyle.ts
+++ b/crabfit-frontend/src/components/Button/buttonStyle.ts
@@ -16,10 +16,10 @@ export const Top = styled.button`
cursor: pointer;
font: inherit;
box-sizing: border-box;
- background: ${props => props.theme.primary};
+ background: ${props => props.primaryColor || props.theme.primary};
color: #FFF;
font-weight: 600;
- text-shadow: 0 -1.5px .5px ${props => props.theme.primaryDark};
+ text-shadow: 0 -1.5px .5px ${props => props.secondaryColor || props.theme.primaryDark};
padding: 0;
border-radius: 3px;
height: var(--btn-height);
@@ -43,6 +43,10 @@ export const Top = styled.button`
color: transparent;
cursor: wait;
+ & img {
+ opacity: 0;
+ }
+
@keyframes load {
from {
transform: rotate(0deg);
@@ -69,7 +73,7 @@ export const Top = styled.button`
export const Bottom = styled.div`
box-sizing: border-box;
- background: ${props => props.theme.primaryDark};
+ background: ${props => props.secondaryColor || props.theme.primaryDark};
border-radius: 3px;
height: var(--btn-height);
width: var(--btn-width);
diff --git a/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
new file mode 100644
index 0000000..7031326
--- /dev/null
+++ b/crabfit-frontend/src/components/GoogleCalendar/GoogleCalendar.tsx
@@ -0,0 +1,163 @@
+import { useState, useEffect } from 'react';
+import { loadGapiInsideDOM } from 'gapi-script';
+
+import { Button, Center } from 'components';
+import { Loader } from '../Loading/loadingStyle';
+import {
+ LoginButton,
+ CalendarList,
+ CheckboxInput,
+ CheckboxLabel,
+ CalendarLabel,
+ Info,
+ Options,
+} from './googleCalendarStyle';
+
+import googleLogo from 'res/google.svg';
+
+const signIn = () => window.gapi.auth2.getAuthInstance().signIn();
+
+const signOut = () => window.gapi.auth2.getAuthInstance().signOut();
+
+const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
+ const [signedIn, setSignedIn] = useState(undefined);
+ const [calendars, setCalendars] = useState(undefined);
+ const [freeBusyLoading, setFreeBusyLoading] = useState(false);
+
+ const calendarLogin = async () => {
+ const gapi = await loadGapiInsideDOM();
+ gapi.load('client:auth2', () => {
+ window.gapi.client.init({
+ clientId: '276505195333-9kjl7e48m272dljbspkobctqrpet0n8m.apps.googleusercontent.com',
+ discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest'],
+ scope: 'https://www.googleapis.com/auth/calendar.readonly',
+ })
+ .then(() => {
+ // Listen for state changes
+ window.gapi.auth2.getAuthInstance().isSignedIn.listen(isSignedIn => setSignedIn(isSignedIn));
+
+ // Handle initial sign-in state
+ setSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get());
+ })
+ .catch(e => {
+ console.error(e);
+ setSignedIn(false);
+ });
+ });
+ };
+
+ const importAvailability = () => {
+ setFreeBusyLoading(true);
+ window.gapi.client.calendar.freebusy.query({
+ timeMin,
+ timeMax,
+ timeZone,
+ items: calendars.filter(c => c.checked).map(c => ({id: c.id})),
+ })
+ .then(response => {
+ onImport(response.result.calendars ? Object.values(response.result.calendars).reduce((busy, c) => [...busy, ...c.busy], []) : []);
+ setFreeBusyLoading(false);
+ }, e => {
+ console.error(e);
+ setFreeBusyLoading(false);
+ });
+ };
+
+ useEffect(() => calendarLogin(), []);
+
+ useEffect(() => {
+ if (signedIn) {
+ window.gapi.client.calendar.calendarList.list({
+ 'minAccessRole': 'freeBusyReader'
+ })
+ .then(response => {
+ setCalendars(response.result.items.map(item => ({
+ 'name': item.summary,
+ 'description': item.description,
+ 'id': item.id,
+ 'color': item.backgroundColor,
+ 'checked': item.hasOwnProperty('primary') && item.primary === true,
+ })));
+ })
+ .catch(e => {
+ console.error(e);
+ signOut();
+ });
+ }
+ }, [signedIn]);
+
+ return (
+ <>
+ {!signedIn ? (
+
+
+
+ ) : (
+
+
+ {/* eslint-disable-next-line */}
+ Sync with Google Calendar ( {
+ e.preventDefault();
+ signOut();
+ }}>log out)
+
+
+ {calendars !== undefined && !calendars.every(c => c.checked) && (
+ /* eslint-disable-next-line */
+ {
+ e.preventDefault();
+ setCalendars(calendars.map(c => ({...c, checked: true})));
+ }}>Select all
+ )}
+ {calendars !== undefined && calendars.every(c => c.checked) && (
+ /* eslint-disable-next-line */
+ {
+ e.preventDefault();
+ setCalendars(calendars.map(c => ({...c, checked: false})));
+ }}>Select none
+ )}
+
+ {calendars !== undefined ? calendars.map(calendar => (
+
+ setCalendars(calendars.map(c => c.id === calendar.id ? {...c, checked: !c.checked} : c))}
+ />
+
+ {calendar.name}
+
+ )) : (
+
+ )}
+ {calendars !== undefined && (
+ <>
+ Importing will overwrite your currently inputted availability
+
+ >
+ )}
+
+ )}
+ >
+ );
+};
+
+export default GoogleCalendar;
diff --git a/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts b/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts
new file mode 100644
index 0000000..dcb1cae
--- /dev/null
+++ b/crabfit-frontend/src/components/GoogleCalendar/googleCalendarStyle.ts
@@ -0,0 +1,112 @@
+import styled from '@emotion/styled';
+
+export const LoginButton = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ & img {
+ height: 1em;
+ margin-right: .8em;
+ }
+`;
+
+export const CalendarList = styled.div`
+ & > div {
+ display: flex;
+ margin: 2px 0;
+ }
+`;
+
+export const CheckboxInput = styled.input`
+ height: 0px;
+ width: 0px;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: 0;
+ font-size: 0;
+ transform: scale(0);
+ position: absolute;
+
+ &:checked + label::after {
+ opacity: 1;
+ transform: scale(1);
+ }
+ &[disabled] + label {
+ opacity: .6;
+ }
+ &[disabled] + label:after {
+ border: 2px solid ${props => props.theme.text};
+ background-color: ${props => props.theme.text};
+ }
+ &:focus + label {
+ box-shadow: 0 0 0 2px ${props => props.theme.text}44;
+ background-color: ${props => props.theme.text}44;
+ outline: none;
+ }
+ &:checked:focus + label {
+ box-shadow: 0 0 0 2px ${props => props.color || props.theme.primary}44;
+ background-color: ${props => props.color || props.theme.primary}44;
+ }
+`;
+
+export const CheckboxLabel = styled.label`
+ display: inline-block;
+ height: 24px;
+ width: 24px;
+ min-width: 24px;
+ position: relative;
+ border-radius: 3px;
+ transition: background-color 0.2s, box-shadow 0.2s;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ height: 14px;
+ width: 14px;
+ border: 2px solid ${props => props.theme.text};
+ border-radius: 2px;
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ }
+ &::after {
+ content: '';
+ display: inline-block;
+ height: 14px;
+ width: 14px;
+ border: 2px solid ${props => props.color || props.theme.primary};
+ background-color: ${props => props.color || props.theme.primary};
+ border-radius: 2px;
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMjEsN0w5LDE5TDMuNSwxMy41TDQuOTEsMTIuMDlMOSwxNi4xN0wxOS41OSw1LjU5TDIxLDdaIiAvPjwvc3ZnPg==');
+ background-size: 16px;
+ background-position: center;
+ background-repeat: no-repeat;
+ opacity: 0;
+ transform: scale(.5);
+ transition: opacity 0.15s, transform 0.15s;
+ }
+`;
+
+export const CalendarLabel = styled.label`
+ margin-left: .6em;
+ font-size: 15px;
+ font-weight: 500;
+ line-height: 24px;
+`;
+
+export const Info = styled.div`
+ font-size: 14px;
+ opacity: .6;
+ font-weight: 500;
+ padding: 14px 0 10px;
+`;
+
+export const Options = styled.div`
+ font-size: 14px;
+ padding: 0 0 5px;
+`;
diff --git a/crabfit-frontend/src/components/index.ts b/crabfit-frontend/src/components/index.ts
index 0d60028..a3fcc60 100644
--- a/crabfit-frontend/src/components/index.ts
+++ b/crabfit-frontend/src/components/index.ts
@@ -10,6 +10,7 @@ export { default as AvailabilityViewer } from './AvailabilityViewer/Availability
export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor';
export { default as Error } from './Error/Error';
export { default as Loading } from './Loading/Loading';
+export { default as GoogleCalendar } from './GoogleCalendar/GoogleCalendar';
export { default as Center } from './Center/Center';
export { default as Donate } from './Donate/Donate';
diff --git a/crabfit-frontend/src/pages/Event/Event.tsx b/crabfit-frontend/src/pages/Event/Event.tsx
index 3184ab8..7b86e85 100644
--- a/crabfit-frontend/src/pages/Event/Event.tsx
+++ b/crabfit-frontend/src/pages/Event/Event.tsx
@@ -370,6 +370,7 @@ const Event = (props) => {
onChange={event => setTimezone(event.currentTarget.value)}
options={timezones}
/>
+ {/* eslint-disable-next-line */}
{event?.timezone && event.timezone !== timezone && This event was created in the timezone {event.timezone}. {
e.preventDefault();
setTimezone(event.timezone);
@@ -381,6 +382,7 @@ const Event = (props) => {
event?.timezone === undefined
&& Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
)) && (
+ /* eslint-disable-next-line */
Your local timezone is detected to be {Intl.DateTimeFormat().resolvedOptions().timeZone}. {
e.preventDefault();
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
@@ -435,6 +437,7 @@ const Event = (props) => {
times={times}
timeLabels={timeLabels}
dates={dates}
+ timezone={timezone}
isSpecificDates={!!dates.length && dates[0].length === 8}
value={user.availability}
onChange={async availability => {
diff --git a/crabfit-frontend/src/res/google.svg b/crabfit-frontend/src/res/google.svg
new file mode 100644
index 0000000..3b71e33
--- /dev/null
+++ b/crabfit-frontend/src/res/google.svg
@@ -0,0 +1,9 @@
+
+
diff --git a/crabfit-frontend/yarn.lock b/crabfit-frontend/yarn.lock
index 8967169..2916191 100644
--- a/crabfit-frontend/yarn.lock
+++ b/crabfit-frontend/yarn.lock
@@ -5273,6 +5273,11 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+gapi-script@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/gapi-script/-/gapi-script-1.2.0.tgz#8106ad0abb36661ce4fab62ef6efada288d7169e"
+ integrity sha512-NKTVKiIwFdkO1j1EzcrWu/Pz7gsl1GmBmgh+qhuV2Ytls04W/Eg5aiBL91SCiBM9lU0PMu7p1hTVxhh1rPT5Lw==
+
gensync@^1.0.0-beta.1:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"