plugin updates
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export const OTHER_USER_CALENDAR_MISSING = 'OTHER_USER_CALENDAR_MISSING';
|
||||
export const CURRENT_USER_CALENDAR_MISSING = 'CURRENT_USER_CALENDAR_MISSING';
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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')};
|
||||
`;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
export default styled.div`
|
||||
height: 30px;
|
||||
`;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -1,6 +0,0 @@
|
||||
const ConnectionStatus = {
|
||||
Connected: 'Connected',
|
||||
NotConnected: 'NotConnected',
|
||||
} as const;
|
||||
|
||||
export default ConnectionStatus;
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface IForm {
|
||||
guid: string;
|
||||
name: string;
|
||||
}
|
||||
Reference in New Issue
Block a user