plugin updates

This commit is contained in:
Tony Volpe
2024-06-17 15:33:26 -04:00
parent 3751a5a1a6
commit e4e274a9a7
2674 changed files with 0 additions and 507851 deletions

View File

@@ -1,311 +0,0 @@
import React, { useRef, useState, useEffect } from 'react';
import { styled } from '@linaria/react';
import {
CALYPSO,
CALYPSO_LIGHT,
CALYPSO_MEDIUM,
OBSIDIAN,
} from '../UIComponents/colors';
import UISpinner from '../UIComponents/UISpinner';
import LoadState, { LoadStateType } from '../enums/loadState';
const Container = styled.div`
color: ${OBSIDIAN};
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-size: 14px;
position: relative;
`;
interface IControlContainerProps {
focused: boolean;
}
const ControlContainer = styled.div<IControlContainerProps>`
align-items: center;
background-color: hsl(0, 0%, 100%);
border-color: hsl(0, 0%, 80%);
border-radius: 4px;
border-style: solid;
border-width: ${props => (props.focused ? '0' : '1px')};
cursor: default;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
min-height: 38px;
outline: 0 !important;
position: relative;
transition: all 100ms;
box-sizing: border-box;
box-shadow: ${props =>
props.focused ? `0 0 0 2px ${CALYPSO_MEDIUM}` : 'none'};
&:hover {
border-color: hsl(0, 0%, 70%);
}
`;
const ValueContainer = styled.div`
align-items: center;
display: flex;
flex: 1;
flex-wrap: wrap;
padding: 2px 8px;
position: relative;
overflow: hidden;
box-sizing: border-box;
`;
const Placeholder = styled.div`
color: hsl(0, 0%, 50%);
margin-left: 2px;
margin-right: 2px;
position: absolute;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
font-size: 16px;
`;
const SingleValue = styled.div`
color: hsl(0, 0%, 20%);
margin-left: 2px;
margin-right: 2px;
max-width: calc(100% - 8px);
overflow: hidden;
position: absolute;
text-overflow: ellipsis;
white-space: nowrap;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
`;
const IndicatorContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
flex-shrink: 0;
box-sizing: border-box;
`;
const DropdownIndicator = styled.div`
border-top: 8px solid ${CALYPSO};
border-left: 6px solid transparent;
border-right: 6px solid transparent;
width: 0px;
height: 0px;
margin: 10px;
`;
const InputContainer = styled.div`
margin: 2px;
padding-bottom: 2px;
padding-top: 2px;
visibility: visible;
color: hsl(0, 0%, 20%);
box-sizing: border-box;
`;
const Input = styled.input`
box-sizing: content-box;
background: rgba(0, 0, 0, 0) none repeat scroll 0px center;
border: 0px none;
font-size: inherit;
opacity: 1;
outline: currentcolor none 0px;
padding: 0px;
color: inherit;
font-family: inherit;
`;
const InputShadow = styled.div`
position: absolute;
opacity: 0;
font-size: inherit;
`;
const MenuContainer = styled.div`
position: absolute;
top: 100%;
background-color: #fff;
border-radius: 4px;
margin-bottom: 8px;
margin-top: 8px;
z-index: 9999;
box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1);
width: 100%;
`;
const MenuList = styled.div`
max-height: 300px;
overflow-y: auto;
padding-bottom: 4px;
padding-top: 4px;
position: relative;
`;
const MenuGroup = styled.div`
padding-bottom: 8px;
padding-top: 8px;
`;
const MenuGroupHeader = styled.div`
color: #999;
cursor: default;
font-size: 75%;
font-weight: 500;
margin-bottom: 0.25em;
text-transform: uppercase;
padding-left: 12px;
padding-left: 12px;
`;
interface IMenuItemProps {
selected: boolean;
}
const MenuItem = styled.div<IMenuItemProps>`
display: block;
background-color: ${props =>
props.selected ? CALYPSO_MEDIUM : 'transparent'};
color: ${props => (props.selected ? '#fff' : 'inherit')};
cursor: default;
font-size: inherit;
width: 100%;
padding: 8px 12px;
&:hover {
background-color: ${props =>
props.selected ? CALYPSO_MEDIUM : CALYPSO_LIGHT};
}
`;
interface IAsyncSelectProps {
placeholder: string;
value: any;
loadOptions?: Function;
defaultOptions?: any[];
onChange: Function;
}
export default function AsyncSelect({
placeholder,
value,
loadOptions,
onChange,
defaultOptions,
}: IAsyncSelectProps) {
const inputEl = useRef<HTMLInputElement>(null);
const inputShadowEl = useRef<HTMLDivElement>(null);
const [isFocused, setFocus] = useState(false);
const [loadState, setLoadState] = useState<LoadStateType>(
LoadState.NotLoaded
);
const [localValue, setLocalValue] = useState('');
const [options, setOptions] = useState(defaultOptions);
const inputSize = `${
inputShadowEl.current ? inputShadowEl.current.clientWidth + 10 : 2
}px`;
useEffect(() => {
if (loadOptions && loadState === LoadState.NotLoaded) {
loadOptions('', (result: any) => {
setOptions(result);
setLoadState(LoadState.Idle);
});
}
}, [loadOptions, loadState]);
const renderItems = (items: any[] = [], parentKey?: number) => {
return items.map((item, index) => {
if (item.options) {
return (
<MenuGroup key={`async-select-item-${index}`}>
<MenuGroupHeader id={`${index}-heading`}>
{item.label}
</MenuGroupHeader>
<div>{renderItems(item.options, index)}</div>
</MenuGroup>
);
} else {
const key = `async-select-item-${
parentKey !== undefined ? `${parentKey}-${index}` : index
}`;
return (
<MenuItem
key={key}
id={key}
selected={value && item.value === value.value}
onClick={() => {
onChange(item);
setFocus(false);
}}
>
{item.label}
</MenuItem>
);
}
});
};
return (
<Container>
<ControlContainer
id="leadin-async-selector"
focused={isFocused}
onClick={() => {
if (isFocused) {
if (inputEl.current) {
inputEl.current.blur();
}
setFocus(false);
setLocalValue('');
} else {
if (inputEl.current) {
inputEl.current.focus();
}
setFocus(true);
}
}}
>
<ValueContainer>
{localValue === '' &&
(!value ? (
<Placeholder>{placeholder}</Placeholder>
) : (
<SingleValue>{value.label}</SingleValue>
))}
<InputContainer>
<Input
ref={inputEl}
onFocus={() => {
setFocus(true);
}}
onChange={e => {
setLocalValue(e.target.value);
setLoadState(LoadState.Loading);
loadOptions &&
loadOptions(e.target.value, (result: any) => {
setOptions(result);
setLoadState(LoadState.Idle);
});
}}
value={localValue}
width={inputSize}
id="asycn-select-input"
/>
<InputShadow ref={inputShadowEl}>{localValue}</InputShadow>
</InputContainer>
</ValueContainer>
<IndicatorContainer>
{loadState === LoadState.Loading && <UISpinner />}
<DropdownIndicator />
</IndicatorContainer>
</ControlContainer>
{isFocused && (
<MenuContainer>
<MenuList>{renderItems(options)}</MenuList>
</MenuContainer>
)}
</Container>
);
}

View File

@@ -1,55 +0,0 @@
import React from 'react';
import UIButton from '../UIComponents/UIButton';
import UIContainer from '../UIComponents/UIContainer';
import HubspotWrapper from './HubspotWrapper';
import { adminUrl, redirectNonce } from '../../constants/leadinConfig';
import { pluginPath } from '../../constants/leadinConfig';
import { __ } from '@wordpress/i18n';
interface IErrorHandlerProps {
status: number;
resetErrorState?: React.MouseEventHandler<HTMLButtonElement>;
errorInfo?: {
header: string;
message: string;
action: string;
};
}
function redirectToPlugin() {
window.location.href = `${adminUrl}admin.php?page=leadin&leadin_expired=${redirectNonce}`;
}
export default function ErrorHandler({
status,
resetErrorState,
errorInfo = { header: '', message: '', action: '' },
}: IErrorHandlerProps) {
const isUnauthorized = status === 401 || status === 403;
const errorHeader = isUnauthorized
? __("Your plugin isn't authorized", 'leadin')
: errorInfo.header;
const errorMessage = isUnauthorized
? __('Reauthorize your plugin to access your free HubSpot tools', 'leadin')
: errorInfo.message;
return (
<HubspotWrapper pluginPath={pluginPath}>
<UIContainer textAlign="center">
<h4>{errorHeader}</h4>
<p>
<b>{errorMessage}</b>
</p>
{isUnauthorized ? (
<UIButton data-test-id="authorize-button" onClick={redirectToPlugin}>
{__('Go to plugin', 'leadin')}
</UIButton>
) : (
<UIButton data-test-id="retry-button" onClick={resetErrorState}>
{errorInfo.action}
</UIButton>
)}
</UIContainer>
</HubspotWrapper>
);
}

View File

@@ -1,26 +0,0 @@
import { styled } from '@linaria/react';
interface IHubspotWrapperProps {
pluginPath: string;
padding?: string;
}
export default styled.div<IHubspotWrapperProps>`
background-image: ${props =>
`url(${props.pluginPath}/public/assets/images/hubspot.svg)`};
background-color: #f5f8fa;
background-repeat: no-repeat;
background-position: center 25px;
background-size: 120px;
color: #33475b;
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-size: 14px;
padding: ${(props: any) => props.padding || '90px 20% 25px'};
p {
font-size: inherit !important;
line-height: 24px;
margin: 4px 0;
}
`;

View File

@@ -1,12 +0,0 @@
import React from 'react';
import HubspotWrapper from './HubspotWrapper';
import UISpinner from '../UIComponents/UISpinner';
import { pluginPath } from '../../constants/leadinConfig';
export default function LoadingBlock() {
return (
<HubspotWrapper pluginPath={pluginPath}>
<UISpinner size={50} />
</HubspotWrapper>
);
}

View File

@@ -1,81 +0,0 @@
import React, { Fragment, useEffect } from 'react';
import { portalId, refreshToken } from '../../constants/leadinConfig';
import UISpacer from '../UIComponents/UISpacer';
import PreviewForm from './PreviewForm';
import FormSelect from './FormSelect';
import { IFormBlockProps } from '../../gutenberg/FormBlock/registerFormBlock';
import {
usePostBackgroundMessage,
BackgroudAppContext,
useBackgroundAppContext,
} from '../../iframe/useBackgroundApp';
import { ProxyMessages } from '../../iframe/integratedMessages';
import LoadingBlock from '../Common/LoadingBlock';
import { getOrCreateBackgroundApp } from '../../utils/backgroundAppUtils';
interface IFormEditProps extends IFormBlockProps {
preview: boolean;
origin: 'gutenberg' | 'elementor';
}
function FormEdit({
attributes,
isSelected,
setAttributes,
preview = true,
origin = 'gutenberg',
}: IFormEditProps) {
const { formId, formName } = attributes;
const formSelected = portalId && formId;
const isBackgroundAppReady = useBackgroundAppContext();
const monitorFormPreviewRender = usePostBackgroundMessage();
const handleChange = (selectedForm: { value: string; label: string }) => {
setAttributes({
portalId,
formId: selectedForm.value,
formName: selectedForm.label,
});
};
useEffect(() => {
monitorFormPreviewRender({
key: ProxyMessages.TrackFormPreviewRender,
payload: {
origin,
},
});
}, [origin]);
return !isBackgroundAppReady ? (
<LoadingBlock />
) : (
<Fragment>
{(isSelected || !formSelected) && (
<FormSelect
formId={formId}
formName={formName}
handleChange={handleChange}
origin={origin}
/>
)}
{formSelected && (
<Fragment>
{isSelected && <UISpacer />}
{preview && <PreviewForm portalId={portalId} formId={formId} />}
</Fragment>
)}
</Fragment>
);
}
export default function FormEditContainer(props: IFormEditProps) {
return (
<BackgroudAppContext.Provider
value={refreshToken && getOrCreateBackgroundApp(refreshToken)}
>
<FormEdit {...props} />
</BackgroudAppContext.Provider>
);
}

View File

@@ -1,80 +0,0 @@
import React from 'react';
import FormSelector from './FormSelector';
import LoadingBlock from '../Common/LoadingBlock';
import { __ } from '@wordpress/i18n';
import useForms from './hooks/useForms';
import useCreateFormFromTemplate from './hooks/useCreateFormFromTemplate';
import { FormType, isDefaultForm } from '../../constants/defaultFormOptions';
import ErrorHandler from '../Common/ErrorHandler';
interface IFormSelectProps {
formId: string;
formName: string;
handleChange: Function;
origin: 'gutenberg' | 'elementor';
}
export default function FormSelect({
formId,
formName,
handleChange,
origin = 'gutenberg',
}: IFormSelectProps) {
const { search, formApiError, reset } = useForms();
const {
createFormByTemplate,
reset: createReset,
isCreating,
hasError,
formApiError: createApiError,
} = useCreateFormFromTemplate(origin);
const value =
formId && formName
? {
label: formName,
value: formId,
}
: null;
const handleLocalChange = (option: { value: FormType }) => {
if (isDefaultForm(option.value)) {
createFormByTemplate(option.value).then(({ guid, name }) => {
handleChange({
value: guid,
label: name,
});
});
} else {
handleChange(option);
}
};
return isCreating ? (
<LoadingBlock />
) : formApiError || createApiError ? (
<ErrorHandler
status={formApiError ? formApiError.status : createApiError.status}
resetErrorState={() => {
if (hasError) {
createReset();
} else {
reset();
}
}}
errorInfo={{
header: __('There was a problem retrieving your forms', 'leadin'),
message: __(
'Please refresh your forms or try again in a few minutes',
'leadin'
),
action: __('Refresh forms', 'leadin'),
}}
/>
) : (
<FormSelector
loadOptions={search}
onChange={(option: { value: FormType }) => handleLocalChange(option)}
value={value}
/>
);
}

View File

@@ -1,36 +0,0 @@
import React from 'react';
import HubspotWrapper from '../Common/HubspotWrapper';
import { pluginPath } from '../../constants/leadinConfig';
import AsyncSelect from '../Common/AsyncSelect';
import { __ } from '@wordpress/i18n';
interface IFormSelectorProps {
loadOptions: Function;
onChange: Function;
value: any;
}
export default function FormSelector({
loadOptions,
onChange,
value,
}: IFormSelectorProps) {
return (
<HubspotWrapper pluginPath={pluginPath}>
<p data-test-id="leadin-form-select">
<b>
{__(
'Select an existing form or create a new one from a template',
'leadin'
)}
</b>
</p>
<AsyncSelect
placeholder={__('Search for a form', 'leadin')}
value={value}
loadOptions={loadOptions}
onChange={onChange}
/>
</HubspotWrapper>
);
}

View File

@@ -1,29 +0,0 @@
import React, { useEffect, useRef } from 'react';
import UIOverlay from '../UIComponents/UIOverlay';
import { formsScriptPayload, hublet } from '../../constants/leadinConfig';
import useFormScript from './hooks/useFormsScript';
export default function PreviewForm({
portalId,
formId,
}: {
portalId: number;
formId: string;
}) {
const inputEl = useRef<HTMLDivElement>(null);
const ready = useFormScript();
useEffect(() => {
if (!ready) {
return;
}
if (inputEl.current) {
inputEl.current.innerHTML = '';
const embedScript = document.createElement('script');
embedScript.innerHTML = `hbspt.forms.create({ portalId: '${portalId}', formId: '${formId}', region: '${hublet}', ${formsScriptPayload} });`;
inputEl.current.appendChild(embedScript);
}
}, [formId, portalId, ready, inputEl]);
return <UIOverlay ref={inputEl} />;
}

View File

@@ -1,53 +0,0 @@
import { useState } from 'react';
import {
usePostAsyncBackgroundMessage,
usePostBackgroundMessage,
} from '../../../iframe/useBackgroundApp';
import { FormType } from '../../../constants/defaultFormOptions';
import LoadState, { LoadStateType } from '../../enums/loadState';
import { ProxyMessages } from '../../../iframe/integratedMessages';
export default function useCreateFormFromTemplate(origin = 'gutenberg') {
const proxy = usePostAsyncBackgroundMessage();
const track = usePostBackgroundMessage();
const [loadState, setLoadState] = useState<LoadStateType>(LoadState.Idle);
const [formApiError, setFormApiError] = useState<any>(null);
const createFormByTemplate = (type: FormType) => {
setLoadState(LoadState.Loading);
track({
key: ProxyMessages.TrackFormCreatedFromTemplate,
payload: {
type,
origin,
},
});
return proxy({
key: ProxyMessages.CreateFormFromTemplate,
payload: type,
})
.then(form => {
setLoadState(LoadState.Idle);
return form;
})
.catch(err => {
setFormApiError(err);
track({
key: ProxyMessages.TrackFormCreationFailed,
payload: {
origin,
},
});
setLoadState(LoadState.Failed);
});
};
return {
isCreating: loadState === LoadState.Loading,
hasError: loadState === LoadState.Failed,
formApiError,
createFormByTemplate,
reset: () => setLoadState(LoadState.Idle),
};
}

View File

@@ -1,42 +0,0 @@
import { useState } from 'react';
import debounce from 'lodash/debounce';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import { DEFAULT_OPTIONS } from '../../../constants/defaultFormOptions';
import { ProxyMessages } from '../../../iframe/integratedMessages';
import { IForm } from '../../types';
export default function useForms() {
const proxy = usePostAsyncBackgroundMessage();
const [formApiError, setFormApiError] = useState<any>(null);
const search = debounce(
(search: string, callback: Function) => {
return proxy({
key: ProxyMessages.FetchForms,
payload: {
search,
},
})
.then(forms => {
callback([
...forms.map((form: IForm) => ({
label: form.name,
value: form.guid,
})),
DEFAULT_OPTIONS,
]);
})
.catch(error => {
setFormApiError(error);
});
},
300,
{ trailing: true }
);
return {
search,
formApiError,
reset: () => setFormApiError(null),
};
}

View File

@@ -1,30 +0,0 @@
import $ from 'jquery';
import { useEffect, useState } from 'react';
import { formsScript } from '../../../constants/leadinConfig';
import Raven from '../../../lib/Raven';
let promise: Promise<string | undefined>;
function loadFormsScript() {
if (!promise) {
promise = new Promise((resolve, reject) =>
$.getScript(formsScript)
.done(resolve)
.fail(reject)
);
}
return promise;
}
export default function useFormScript() {
const [ready, setReady] = useState(false);
useEffect(() => {
loadFormsScript()
.then(() => setReady(true))
.catch(error => Raven.captureException(error));
}, []);
return ready;
}

View File

@@ -1,95 +0,0 @@
import React, { Fragment, useEffect } from 'react';
import LoadingBlock from '../Common/LoadingBlock';
import MeetingSelector from './MeetingSelector';
import MeetingWarning from './MeetingWarning';
import useMeetings, {
useSelectedMeeting,
useSelectedMeetingCalendar,
} from './hooks/useMeetings';
import HubspotWrapper from '../Common/HubspotWrapper';
import ErrorHandler from '../Common/ErrorHandler';
import { pluginPath } from '../../constants/leadinConfig';
import { __ } from '@wordpress/i18n';
import Raven from 'raven-js';
interface IMeetingControllerProps {
url: string;
handleChange: Function;
}
export default function MeetingController({
handleChange,
url,
}: IMeetingControllerProps) {
const {
mappedMeetings: meetings,
loading,
error,
reload,
connectCalendar,
} = useMeetings();
const selectedMeetingOption = useSelectedMeeting(url);
const selectedMeetingCalendar = useSelectedMeetingCalendar(url);
useEffect(() => {
if (!url && meetings.length > 0) {
handleChange(meetings[0].value);
}
}, [meetings, url, handleChange]);
const handleLocalChange = (option: { value: string }) => {
handleChange(option.value);
};
const handleConnectCalendar = () => {
return connectCalendar()
.then(() => {
reload();
})
.catch(error => {
Raven.captureMessage('Unable to connect calendar', {
extra: { error },
});
});
};
return (
<Fragment>
{loading ? (
<LoadingBlock />
) : error ? (
<ErrorHandler
status={(error && error.status) || error}
resetErrorState={() => reload()}
errorInfo={{
header: __(
'There was a problem retrieving your meetings',
'leadin'
),
message: __(
'Please refresh your meetings or try again in a few minutes',
'leadin'
),
action: __('Refresh meetings', 'leadin'),
}}
/>
) : (
<HubspotWrapper padding="90px 32px 24px" pluginPath={pluginPath}>
{selectedMeetingCalendar && (
<MeetingWarning
status={selectedMeetingCalendar}
onConnectCalendar={handleConnectCalendar}
/>
)}
{meetings.length > 1 && (
<MeetingSelector
onChange={handleLocalChange}
options={meetings}
value={selectedMeetingOption}
/>
)}
</HubspotWrapper>
)}
</Fragment>
);
}

View File

@@ -1,65 +0,0 @@
import React, { Fragment, useEffect } from 'react';
import { IMeetingBlockProps } from '../../gutenberg/MeetingsBlock/registerMeetingBlock';
import MeetingController from './MeetingController';
import PreviewMeeting from './PreviewMeeting';
import {
BackgroudAppContext,
useBackgroundAppContext,
usePostBackgroundMessage,
} from '../../iframe/useBackgroundApp';
import { refreshToken } from '../../constants/leadinConfig';
import { ProxyMessages } from '../../iframe/integratedMessages';
import LoadingBlock from '../Common/LoadingBlock';
import { getOrCreateBackgroundApp } from '../../utils/backgroundAppUtils';
interface IMeetingEditProps extends IMeetingBlockProps {
preview?: boolean;
origin?: 'gutenberg' | 'elementor';
}
function MeetingEdit({
attributes: { url },
isSelected,
setAttributes,
preview = true,
origin = 'gutenberg',
}: IMeetingEditProps) {
const isBackgroundAppReady = useBackgroundAppContext();
const monitorFormPreviewRender = usePostBackgroundMessage();
const handleChange = (newUrl: string) => {
setAttributes({
url: newUrl,
});
};
useEffect(() => {
monitorFormPreviewRender({
key: ProxyMessages.TrackMeetingPreviewRender,
payload: {
origin,
},
});
}, [origin]);
return !isBackgroundAppReady ? (
<LoadingBlock />
) : (
<Fragment>
{(isSelected || !url) && (
<MeetingController url={url} handleChange={handleChange} />
)}
{preview && url && <PreviewMeeting url={url} />}
</Fragment>
);
}
export default function MeetingsEditContainer(props: IMeetingEditProps) {
return (
<BackgroudAppContext.Provider
value={refreshToken && getOrCreateBackgroundApp(refreshToken)}
>
<MeetingEdit {...props} />
</BackgroudAppContext.Provider>
);
}

View File

@@ -1,38 +0,0 @@
import React, { Fragment } from 'react';
import AsyncSelect from '../Common/AsyncSelect';
import UISpacer from '../UIComponents/UISpacer';
import { __ } from '@wordpress/i18n';
interface IMeetingSelectorProps {
options: any[];
onChange: Function;
value: any;
}
export default function MeetingSelector({
options,
onChange,
value,
}: IMeetingSelectorProps) {
const optionsWrapper = [
{
label: __('Meeting name', 'leadin'),
options,
},
];
return (
<Fragment>
<UISpacer />
<p data-test-id="leadin-meeting-select">
<b>{__('Select a meeting scheduling page', 'leadin')}</b>
</p>
<AsyncSelect
defaultOptions={optionsWrapper}
onChange={onChange}
placeholder={__('Select a meeting', 'leadin')}
value={value}
/>
</Fragment>
);
}

View File

@@ -1,42 +0,0 @@
import React from 'react';
import UIAlert from '../UIComponents/UIAlert';
import UIButton from '../UIComponents/UIButton';
import { CURRENT_USER_CALENDAR_MISSING } from './constants';
import { __ } from '@wordpress/i18n';
interface IMeetingWarningProps {
status: string;
onConnectCalendar: React.MouseEventHandler<HTMLButtonElement>;
}
export default function MeetingWarning({
status,
onConnectCalendar,
}: IMeetingWarningProps) {
const isMeetingOwner = status === CURRENT_USER_CALENDAR_MISSING;
const titleText = isMeetingOwner
? __('Your calendar is not connected', 'leadin')
: __('Calendar is not connected', 'leadin');
const titleMessage = isMeetingOwner
? __(
'Please connect your calendar to activate your scheduling pages',
'leadin'
)
: __(
'Make sure that everybody in this meeting has connected their calendar from the Meetings page in HubSpot',
'leadin'
);
return (
<UIAlert titleText={titleText} titleMessage={titleMessage}>
{isMeetingOwner && (
<UIButton
use="tertiary"
id="meetings-connect-calendar"
onClick={onConnectCalendar}
>
{__('Connect calendar', 'leadin')}
</UIButton>
)}
</UIAlert>
);
}

View File

@@ -1,106 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import useCurrentUserFetch from './hooks/useCurrentUserFetch';
import useMeetingsFetch from './hooks/useMeetingsFetch';
import LoadState from '../enums/loadState';
interface IMeetingsContextWrapperState {
loading: boolean;
error: any;
meetings: any[];
currentUser: any;
meetingUsers: any;
selectedMeeting: string;
}
interface IMeetingsContext extends IMeetingsContextWrapperState {
reload: Function;
}
interface IMeetingsContextWrapperProps {
url: string;
}
export const MeetingsContext = React.createContext<IMeetingsContext>({
loading: true,
error: null,
meetings: [],
currentUser: null,
meetingUsers: {},
selectedMeeting: '',
reload: () => {},
});
export default function MeetingsContextWrapper({
url,
children,
}: React.PropsWithChildren<IMeetingsContextWrapperProps>) {
const [state, setState] = useState<IMeetingsContextWrapperState>({
loading: true,
error: null,
meetings: [],
currentUser: null,
meetingUsers: {},
selectedMeeting: url,
});
const {
meetings,
meetingUsers,
loadMeetingsState,
error: errorMeeting,
reload: reloadMeetings,
} = useMeetingsFetch();
const {
user: currentUser,
loadUserState,
error: errorUser,
reload: reloadUser,
} = useCurrentUserFetch();
const reload = useCallback(() => {
reloadUser();
reloadMeetings();
}, [reloadUser, reloadMeetings]);
useEffect(() => {
if (
!state.loading &&
!state.error &&
state.currentUser &&
state.meetings.length === 0
) {
reloadMeetings();
}
}, [state, reloadMeetings]);
useEffect(() => {
setState(previous => ({
...previous,
loading:
loadUserState === LoadState.Loading ||
loadMeetingsState === LoadState.Loading,
currentUser,
meetings,
meetingUsers: meetingUsers.reduce((p, c) => ({ ...p, [c.id]: c }), {}),
error: errorMeeting || errorUser,
selectedMeeting: url,
}));
}, [
loadUserState,
loadMeetingsState,
currentUser,
meetings,
meetingUsers,
errorMeeting,
errorUser,
url,
setState,
]);
return (
<MeetingsContext.Provider value={{ ...state, reload }}>
{children}
</MeetingsContext.Provider>
);
}

View File

@@ -1,31 +0,0 @@
import React, { Fragment, useEffect, useRef } from 'react';
import UIOverlay from '../UIComponents/UIOverlay';
import useMeetingsScript from './hooks/useMeetingsScript';
interface IPreviewForm {
url: string;
}
export default function PreviewForm({ url }: IPreviewForm) {
const ready = useMeetingsScript();
const inputEl = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ready) {
return;
}
if (inputEl.current) {
inputEl.current.innerHTML = '';
const container = document.createElement('div');
container.dataset.src = `${url}?embed=true`;
container.classList.add('meetings-iframe-container');
inputEl.current.appendChild(container);
const embedScript = document.createElement('script');
embedScript.innerHTML =
'hbspt.meetings.create(".meetings-iframe-container");';
inputEl.current.appendChild(embedScript);
}
}, [url, ready, inputEl]);
return <Fragment>{url && <UIOverlay ref={inputEl}></UIOverlay>}</Fragment>;
}

View File

@@ -1,2 +0,0 @@
export const OTHER_USER_CALENDAR_MISSING = 'OTHER_USER_CALENDAR_MISSING';
export const CURRENT_USER_CALENDAR_MISSING = 'CURRENT_USER_CALENDAR_MISSING';

View File

@@ -1,45 +0,0 @@
import { useEffect, useState } from 'react';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import LoadState, { LoadStateType } from '../../enums/loadState';
import { ProxyMessages } from '../../../iframe/integratedMessages';
let user: any = null;
export default function useCurrentUserFetch() {
const proxy = usePostAsyncBackgroundMessage();
const [loadState, setLoadState] = useState<LoadStateType>(
LoadState.NotLoaded
);
const [error, setError] = useState<null | Error>(null);
const createUser = () => {
if (!user) {
setLoadState(LoadState.NotLoaded);
}
};
const reload = () => {
user = null;
setLoadState(LoadState.NotLoaded);
setError(null);
};
useEffect(() => {
if (loadState === LoadState.NotLoaded && !user) {
setLoadState(LoadState.Loading);
proxy({
key: ProxyMessages.FetchOrCreateMeetingUser,
})
.then(data => {
user = data;
setLoadState(LoadState.Idle);
})
.catch(err => {
setError(err);
setLoadState(LoadState.Failed);
});
}
}, [loadState]);
return { user, loadUserState: loadState, error, createUser, reload };
}

View File

@@ -1,121 +0,0 @@
import { useCallback } from 'react';
import { __ } from '@wordpress/i18n';
import {
CURRENT_USER_CALENDAR_MISSING,
OTHER_USER_CALENDAR_MISSING,
} from '../constants';
import useMeetingsFetch, { MeetingUser } from './useMeetingsFetch';
import useCurrentUserFetch from './useCurrentUserFetch';
import LoadState from '../../enums/loadState';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import { ProxyMessages } from '../../../iframe/integratedMessages';
function getDefaultMeetingName(
meeting: any,
currentUser: any,
meetingUsers: any
) {
const [meetingOwnerId] = meeting.meetingsUserIds;
let result = __('Default', 'leadin');
if (
currentUser &&
meetingOwnerId !== currentUser.id &&
meetingUsers[meetingOwnerId]
) {
const user = meetingUsers[meetingOwnerId];
result += ` (${user.userProfile.fullName})`;
}
return result;
}
function hasCalendarObject(user: any) {
return (
user &&
user.meetingsUserBlob &&
user.meetingsUserBlob.calendarSettings &&
user.meetingsUserBlob.calendarSettings.email
);
}
export default function useMeetings() {
const proxy = usePostAsyncBackgroundMessage();
const {
meetings,
meetingUsers,
error: meetingsError,
loadMeetingsState,
reload: reloadMeetings,
} = useMeetingsFetch();
const {
user: currentUser,
error: userError,
loadUserState,
reload: reloadUser,
} = useCurrentUserFetch();
const reload = useCallback(() => {
reloadUser();
reloadMeetings();
}, [reloadUser, reloadMeetings]);
const connectCalendar = () => {
return proxy({
key: ProxyMessages.ConnectMeetingsCalendar,
});
};
return {
mappedMeetings: meetings.map(meet => ({
label:
meet.name || getDefaultMeetingName(meet, currentUser, meetingUsers),
value: meet.link,
})),
meetings,
meetingUsers,
currentUser,
error: meetingsError || (userError as any),
loading:
loadMeetingsState == LoadState.Loading ||
loadUserState === LoadState.Loading,
reload,
connectCalendar,
};
}
export function useSelectedMeeting(url: string) {
const { mappedMeetings: meetings } = useMeetings();
const option = meetings.find(({ value }) => value === url);
return option;
}
export function useSelectedMeetingCalendar(url: string) {
const { meetings, meetingUsers, currentUser } = useMeetings();
const meeting = meetings.find(meet => meet.link === url);
const mappedMeetingUsersId: {
[key: number]: MeetingUser;
} = meetingUsers.reduce((p, c) => ({ ...p, [c.id]: c }), {});
if (!meeting) {
return null;
} else {
const { meetingsUserIds } = meeting;
if (
currentUser &&
meetingsUserIds.includes(currentUser.id) &&
!hasCalendarObject(currentUser)
) {
return CURRENT_USER_CALENDAR_MISSING;
} else if (
meetingsUserIds
.map(id => mappedMeetingUsersId[id])
.some((user: any) => !hasCalendarObject(user))
) {
return OTHER_USER_CALENDAR_MISSING;
} else {
return null;
}
}
}

View File

@@ -1,58 +0,0 @@
import { useEffect, useState } from 'react';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import LoadState, { LoadStateType } from '../../enums/loadState';
import { ProxyMessages } from '../../../iframe/integratedMessages';
export interface Meeting {
meetingsUserIds: number[];
name: string;
link: string;
}
export interface MeetingUser {
id: string;
}
let meetings: Meeting[] = [];
let meetingUsers: MeetingUser[] = [];
export default function useMeetingsFetch() {
const proxy = usePostAsyncBackgroundMessage();
const [loadState, setLoadState] = useState<LoadStateType>(
LoadState.NotLoaded
);
const [error, setError] = useState(null);
const reload = () => {
meetings = [];
setError(null);
setLoadState(LoadState.NotLoaded);
};
useEffect(() => {
if (loadState === LoadState.NotLoaded && meetings.length === 0) {
setLoadState(LoadState.Loading);
proxy({
key: ProxyMessages.FetchMeetingsAndUsers,
})
.then(data => {
setLoadState(LoadState.Loaded);
meetings = data && data.meetingLinks;
meetingUsers = data && data.meetingUsers;
})
.catch(e => {
setError(e);
setLoadState(LoadState.Failed);
});
}
}, [loadState]);
return {
meetings,
meetingUsers,
loadMeetingsState: loadState,
error,
reload,
};
}

View File

@@ -1,30 +0,0 @@
import $ from 'jquery';
import { useState, useEffect } from 'react';
import { meetingsScript } from '../../../constants/leadinConfig';
import Raven from '../../../lib/Raven';
let promise: Promise<any>;
function loadMeetingsScript() {
if (!promise) {
promise = new Promise((resolve, reject) =>
$.getScript(meetingsScript)
.done(resolve)
.fail(reject)
);
}
return promise;
}
export default function useMeetingsScript() {
const [ready, setReady] = useState(false);
useEffect(() => {
loadMeetingsScript()
.then(() => setReady(true))
.catch(error => Raven.captureException(error));
}, []);
return ready;
}

View File

@@ -1,69 +0,0 @@
import React from 'react';
import { styled } from '@linaria/react';
import { MARIGOLD_LIGHT, MARIGOLD_MEDIUM, OBSIDIAN } from './colors';
const AlertContainer = styled.div`
background-color: ${MARIGOLD_LIGHT};
border-color: ${MARIGOLD_MEDIUM};
color: ${OBSIDIAN};
font-size: 14px;
align-items: center;
justify-content: space-between;
display: flex;
border-style: solid;
border-top-style: solid;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-width: 1px;
min-height: 60px;
padding: 8px 20px;
position: relative;
text-align: left;
`;
const Title = styled.p`
font-family: 'Lexend Deca';
font-style: normal;
font-weight: 700;
font-size: 16px;
line-height: 19px;
color: ${OBSIDIAN};
margin: 0;
padding: 0;
`;
const Message = styled.p`
font-family: 'Lexend Deca';
font-style: normal;
font-weight: 400;
font-size: 14px;
margin: 0;
padding: 0;
`;
const MessageContainer = styled.div`
display: flex;
flex-direction: column;
`;
interface IUIAlertProps {
titleText: string;
titleMessage: string;
}
export default function UIAlert({
titleText,
titleMessage,
children,
}: React.PropsWithChildren<IUIAlertProps>) {
return (
<AlertContainer>
<MessageContainer>
<Title>{titleText}</Title>
<Message>{titleMessage}</Message>
</MessageContainer>
{children}
</AlertContainer>
);
}

View File

@@ -1,19 +0,0 @@
import { styled } from '@linaria/react';
import { HEFFALUMP, LORAX, OLAF } from './colors';
interface IButtonProps {
use?: string;
}
export default styled.button<IButtonProps>`
background-color:${props => (props.use === 'tertiary' ? HEFFALUMP : LORAX)};
border: 3px solid ${props => (props.use === 'tertiary' ? HEFFALUMP : LORAX)};
color: ${OLAF}
border-radius: 3px;
font-size: 14px;
line-height: 14px;
padding: 12px 24px;
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-weight: 500;
white-space: nowrap;
`;

View File

@@ -1,9 +0,0 @@
import { styled } from '@linaria/react';
interface IUIContainerProps {
textAlign?: string;
}
export default styled.div<IUIContainerProps>`
text-align: ${props => (props.textAlign ? props.textAlign : 'inherit')};
`;

View File

@@ -1,14 +0,0 @@
import { styled } from '@linaria/react';
export default styled.div`
position: relative;
&:after {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
`;

View File

@@ -1,5 +0,0 @@
import { styled } from '@linaria/react';
export default styled.div`
height: 30px;
`;

View File

@@ -1,78 +0,0 @@
import React from 'react';
import { styled } from '@linaria/react';
import { CALYPSO_MEDIUM, CALYPSO } from './colors';
const SpinnerOuter = styled.div`
align-items: center;
color: #00a4bd;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
margin: '2px';
`;
const SpinnerInner = styled.div`
align-items: center;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
`;
interface IColorProp {
color: string;
}
const Circle = styled.circle<IColorProp>`
fill: none;
stroke: ${props => props.color};
stroke-width: 5;
stroke-linecap: round;
transform-origin: center;
`;
const AnimatedCircle = styled.circle<IColorProp>`
fill: none;
stroke: ${props => props.color};
stroke-width: 5;
stroke-linecap: round;
transform-origin: center;
animation: dashAnimation 2s ease-in-out infinite,
spinAnimation 2s linear infinite;
@keyframes dashAnimation {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -50;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -140;
}
}
@keyframes spinAnimation {
transform: rotate(360deg);
}
`;
export default function UISpinner({ size = 20 }) {
return (
<SpinnerOuter>
<SpinnerInner>
<svg height={size} width={size} viewBox="0 0 50 50">
<Circle color={CALYPSO_MEDIUM} cx="25" cy="25" r="22.5" />
<AnimatedCircle color={CALYPSO} cx="25" cy="25" r="22.5" />
</svg>
</SpinnerInner>
</SpinnerOuter>
);
}

View File

@@ -1,9 +0,0 @@
export const CALYPSO = '#00a4bd';
export const CALYPSO_MEDIUM = '#7fd1de';
export const CALYPSO_LIGHT = '#e5f5f8';
export const LORAX = '#ff7a59';
export const OLAF = '#ffffff';
export const HEFFALUMP = '#425b76';
export const MARIGOLD_LIGHT = '#fef8f0';
export const MARIGOLD_MEDIUM = '#fae0b5';
export const OBSIDIAN = '#33475b';

View File

@@ -1,6 +0,0 @@
const ConnectionStatus = {
Connected: 'Connected',
NotConnected: 'NotConnected',
} as const;
export default ConnectionStatus;

View File

@@ -1,11 +0,0 @@
const LoadState = {
NotLoaded: 'NotLoaded',
Loading: 'Loading',
Loaded: 'Loaded',
Idle: 'Idle',
Failed: 'Failed',
} as const;
export type LoadStateType = typeof LoadState[keyof typeof LoadState];
export default LoadState;

View File

@@ -1,4 +0,0 @@
export interface IForm {
guid: string;
name: string;
}