commit
1d570fb06c
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,7 +52,24 @@ const AvailabilityEditor = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSpecificDates && (
|
||||
<StyledMain>
|
||||
<GoogleCalendar
|
||||
timeMin={dayjs(times[0], 'HHmm-DDMMYYYY').toISOString()}
|
||||
timeMax={dayjs(times[times.length-1], 'HHmm-DDMMYYYY').add(15, 'm').toISOString()}
|
||||
timeZone={timezone}
|
||||
onImport={busyArray => onChange(
|
||||
times.filter(time => !busyArray.some(busy =>
|
||||
dayjs(time, 'HHmm-DDMMYYYY').isBetween(busy.start, busy.end, null, '[)')
|
||||
))
|
||||
)}
|
||||
/>
|
||||
</StyledMain>
|
||||
)}
|
||||
|
||||
<Wrapper>
|
||||
<ScrollWrapper>
|
||||
<Container>
|
||||
<TimeLabels>
|
||||
{!!timeLabels.length && timeLabels.map((label, i) =>
|
||||
|
|
@ -120,7 +144,9 @@ const AvailabilityEditor = ({
|
|||
);
|
||||
})}
|
||||
</Container>
|
||||
</ScrollWrapper>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import { Wrapper, Top, Bottom } from './buttonStyle';
|
|||
const Button = ({
|
||||
buttonHeight,
|
||||
buttonWidth,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
...props
|
||||
}) => (
|
||||
<Wrapper buttonHeight={buttonHeight} buttonWidth={buttonWidth}>
|
||||
<Top {...props} />
|
||||
<Bottom />
|
||||
<Top primaryColor={primaryColor} secondaryColor={secondaryColor} {...props} />
|
||||
<Bottom secondaryColor={secondaryColor} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<Center>
|
||||
<Button
|
||||
onClick={() => signIn()}
|
||||
isLoading={signedIn === undefined}
|
||||
buttonWidth="270px"
|
||||
primaryColor="#4286F5"
|
||||
secondaryColor="#3367BD">
|
||||
<LoginButton>
|
||||
<img src={googleLogo} alt="" />
|
||||
<span>Sync with Google Calendar</span>
|
||||
</LoginButton>
|
||||
</Button>
|
||||
</Center>
|
||||
) : (
|
||||
<CalendarList>
|
||||
<p>
|
||||
{/* eslint-disable-next-line */}
|
||||
<strong>Sync with Google Calendar</strong> (<a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
signOut();
|
||||
}}>log out</a>)
|
||||
</p>
|
||||
<Options>
|
||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||
/* eslint-disable-next-line */
|
||||
<a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
||||
}}>Select all</a>
|
||||
)}
|
||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||
/* eslint-disable-next-line */
|
||||
<a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
||||
}}>Select none</a>
|
||||
)}
|
||||
</Options>
|
||||
{calendars !== undefined ? calendars.map(calendar => (
|
||||
<div key={calendar.id}>
|
||||
<CheckboxInput
|
||||
type="checkbox"
|
||||
role="checkbox"
|
||||
id={calendar.id}
|
||||
color={calendar.color}
|
||||
checked={calendar.checked}
|
||||
onChange={e => setCalendars(calendars.map(c => c.id === calendar.id ? {...c, checked: !c.checked} : c))}
|
||||
/>
|
||||
<CheckboxLabel htmlFor={calendar.id} color={calendar.color} />
|
||||
<CalendarLabel htmlFor={calendar.id}>{calendar.name}</CalendarLabel>
|
||||
</div>
|
||||
)) : (
|
||||
<Loader />
|
||||
)}
|
||||
{calendars !== undefined && (
|
||||
<>
|
||||
<Info>Importing will overwrite your currently inputted availability</Info>
|
||||
<Button
|
||||
buttonWidth="170px"
|
||||
buttonHeight="35px"
|
||||
isLoading={freeBusyLoading}
|
||||
onClick={() => importAvailability()}
|
||||
>Import availability</Button>
|
||||
</>
|
||||
)}
|
||||
</CalendarList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleCalendar;
|
||||
|
|
@ -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;
|
||||
`;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ const Event = (props) => {
|
|||
onChange={event => setTimezone(event.currentTarget.value)}
|
||||
options={timezones}
|
||||
/>
|
||||
{/* eslint-disable-next-line */}
|
||||
{event?.timezone && event.timezone !== timezone && <p>This event was created in the timezone <strong>{event.timezone}</strong>. <a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimezone(event.timezone);
|
||||
|
|
@ -381,6 +382,7 @@ const Event = (props) => {
|
|||
event?.timezone === undefined
|
||||
&& Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
|
||||
)) && (
|
||||
/* eslint-disable-next-line */
|
||||
<p>Your local timezone is detected to be <strong>{Intl.DateTimeFormat().resolvedOptions().timeZone}</strong>. <a href="#" onClick={e => {
|
||||
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 => {
|
||||
|
|
|
|||
9
crabfit-frontend/src/res/google.svg
Normal file
9
crabfit-frontend/src/res/google.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2443 2500" style="enable-background:new 0 0 2443 2500;" xml:space="preserve">
|
||||
<path style="fill:#FFFFFF" d="M1245.8,1018.7V1481h686.5c-13.8,114.9-88.6,287.9-254.7,404.2v0c-105.2,73.4-246.4,124.6-431.8,124.6
|
||||
c-329.4,0-609-217.3-708.7-517.7h0l0,0v0c-26.3-77.5-41.5-160.6-41.5-246.4c0-85.8,15.2-168.9,40.1-246.4v0
|
||||
c101-300.4,380.6-517.7,710.1-517.7v0c233.9,0,391.7,101,481.7,185.5l351.6-343.3C1863.2,123.2,1582.2,0,1245.8,0
|
||||
C758.6,0,337.8,279.6,133,686.5h0h0C48.6,855.4,0.1,1045,0.1,1245.7S48.6,1636,133,1804.9h0l0,0
|
||||
c204.8,406.9,625.6,686.5,1112.8,686.5c336.3,0,618.7-110.7,824.9-301.7l0,0h0c235.3-217.3,370.9-537,370.9-916.3
|
||||
c0-102.4-8.3-177.2-26.3-254.7H1245.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 854 B |
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue