feat: replace iframe with chrome sidepanel (#5197)

fixes - #5201


https://github.com/twentyhq/twenty/assets/13139771/871019c6-6456-46b4-95dd-07ffb33eb4fd

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Aditya Pimpalkar
2024-05-21 09:39:43 +01:00
committed by GitHub
parent 4907ae5a74
commit eb78be6c61
21 changed files with 456 additions and 309 deletions

View File

@ -1,124 +0,0 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { Loader } from '@/ui/display/loader/components/Loader';
import { MainButton } from '@/ui/input/button/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
const StyledWrapper = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.noisy};
display: flex;
height: 100vh;
justify-content: center;
`;
const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.primary};
width: 400px;
height: 350px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledActionContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
width: 300px;
`;
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-transform: uppercase;
`;
const Options = () => {
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [error, setError] = useState('');
const [serverBaseUrl, setServerBaseUrl] = useState(
import.meta.env.VITE_SERVER_BASE_URL,
);
const authenticate = () => {
setIsAuthenticating(true);
setError('');
chrome.runtime.sendMessage({ action: 'CONNECT' }, ({ status, message }) => {
if (status === true) {
setIsAuthenticated(true);
setIsAuthenticating(false);
chrome.storage.local.set({ isAuthenticated: true });
} else {
setError(message);
setIsAuthenticating(false);
}
});
};
useEffect(() => {
const getState = async () => {
const store = await chrome.storage.local.get();
if (store.serverBaseUrl !== '') {
setServerBaseUrl(store.serverBaseUrl);
} else {
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
}
if (store.isAuthenticated === true) setIsAuthenticated(true);
};
void getState();
}, []);
const handleBaseUrlChange = (value: string) => {
setServerBaseUrl(value);
setError('');
chrome.storage.local.set({ serverBaseUrl: value });
};
return (
<StyledWrapper>
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={64} width={64} />
<StyledActionContainer>
<TextInput
label="Server URL"
value={serverBaseUrl}
onChange={handleBaseUrlChange}
placeholder="My base server URL"
error={error}
fullWidth
/>
{isAuthenticating ? (
<Loader />
) : isAuthenticated ? (
<StyledLabel>Connected!</StyledLabel>
) : (
<>
<MainButton
title="Connect your account"
onClick={() => authenticate()}
fullWidth
/>
<MainButton
title="Sign up"
variant="secondary"
onClick={() => window.open(`${serverBaseUrl}`, '_blank')}
fullWidth
/>
</>
)}
</StyledActionContainer>
</StyledContainer>
</StyledWrapper>
);
};
export default Options;

View File

@ -0,0 +1,64 @@
import styled from '@emotion/styled';
import { MainButton } from '@/ui/input/button/MainButton';
const StyledWrapper = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.primary};
display: flex;
height: 100vh;
justify-content: center;
`;
const StyledContainer = styled.div`
width: 400px;
height: 350px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: ${({ theme }) => theme.spacing(8)};
`;
const StyledTextContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
`;
const StyledLargeText = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;
const StyledMediumText = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.md};
`;
const PageInaccessible = () => {
return (
<StyledWrapper>
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
<StyledTextContainer>
<StyledLargeText>
Extension not available on the website
</StyledLargeText>
<StyledMediumText>
Open LinkedIn to use the extension
</StyledMediumText>
</StyledTextContainer>
<MainButton
title="Go to LinkedIn"
onClick={() => window.open('https://www.linkedin.com/')}
/>
</StyledContainer>
</StyledWrapper>
);
};
export default PageInaccessible;

View File

@ -0,0 +1,169 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { Loader } from '@/ui/display/loader/components/Loader';
import { MainButton } from '@/ui/input/button/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { isDefined } from '~/utils/isDefined';
const StyledIframe = styled.iframe`
display: block;
width: 100%;
height: 100vh;
border: none;
`;
const StyledWrapper = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.primary};
display: flex;
height: 100vh;
justify-content: center;
`;
const StyledContainer = styled.div`
width: 400px;
height: 350px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: ${({ theme }) => theme.spacing(8)};
`;
const StyledActionContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
width: 300px;
`;
const Sidepanel = () => {
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [iframeSrc, setIframeSrc] = useState(
import.meta.env.VITE_FRONT_BASE_URL,
);
const [error, setError] = useState('');
const [serverBaseUrl, setServerBaseUrl] = useState('');
const authenticate = () => {
setIsAuthenticating(true);
setError('');
chrome.runtime.sendMessage(
{ action: 'launchOAuth' },
({ status, message }) => {
if (status === true) {
setIsAuthenticated(true);
setIsAuthenticating(false);
chrome.storage.local.set({ isAuthenticated: true });
} else {
setError(message);
setIsAuthenticating(false);
}
},
);
};
useEffect(() => {
const getState = async () => {
const store = await chrome.storage.local.get();
if (isDefined(store.serverBaseUrl)) {
setServerBaseUrl(store.serverBaseUrl);
} else {
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
}
if (store.isAuthenticated === true) setIsAuthenticated(true);
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
if (
isDefined(activeTab) &&
isDefined(store[`sidepanelUrl_${activeTab.id}`])
) {
const url = store[`sidepanelUrl_${activeTab.id}`];
setIframeSrc(url);
}
};
void getState();
}, []);
useEffect(() => {
const handleBrowserEvents = ({ action }: { action: string }) => {
if (action === 'changeSidepanelUrl') {
setIframeSrc('');
}
};
chrome.runtime.onMessage.addListener(handleBrowserEvents);
return () => {
chrome.runtime.onMessage.removeListener(handleBrowserEvents);
};
}, []);
useEffect(() => {
const getIframeState = async () => {
const store = await chrome.storage.local.get();
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
if (
isDefined(activeTab) &&
isDefined(store[`sidepanelUrl_${activeTab.id}`])
) {
const url = store[`sidepanelUrl_${activeTab.id}`];
setIframeSrc(url);
}
};
void getIframeState();
}, [iframeSrc]);
const handleBaseUrlChange = (value: string) => {
setServerBaseUrl(value);
setError('');
chrome.storage.local.set({ serverBaseUrl: value });
};
return isAuthenticated ? (
<StyledIframe title="twenty-website" src={iframeSrc}></StyledIframe>
) : (
<StyledWrapper>
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
{isAuthenticating ? (
<Loader />
) : (
<StyledActionContainer>
<TextInput
label="Server URL"
value={serverBaseUrl}
onChange={handleBaseUrlChange}
placeholder="My base server URL"
error={error}
fullWidth
/>
<MainButton
title="Connect your account"
onClick={() => authenticate()}
fullWidth
/>
<MainButton
title="Sign up"
variant="secondary"
onClick={() =>
window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`, '_blank')
}
fullWidth
/>
</StyledActionContainer>
)}
</StyledContainer>
</StyledWrapper>
);
};
export default Sidepanel;

View File

@ -3,14 +3,14 @@ import ReactDOM from 'react-dom/client';
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
import Options from '~/options/Options';
import Sidepanel from '~/options/Sidepanel';
import '~/index.css';
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
<AppThemeProvider>
<React.StrictMode>
<Options />
<Sidepanel />
</React.StrictMode>
</AppThemeProvider>,
);

View File

@ -22,7 +22,7 @@ const StyledButton = styled.button<
switch (variant) {
case 'primary':
return theme.background.radialGradient;
return theme.background.primaryInverted;
case 'secondary':
return theme.background.primary;
default:
@ -37,7 +37,7 @@ const StyledButton = styled.button<
switch (variant) {
case 'primary':
return theme.background.transparent.light;
return theme.background.transparent.strong;
case 'secondary':
return theme.border.color.medium;
default:
@ -59,7 +59,7 @@ const StyledButton = styled.button<
switch (variant) {
case 'primary':
return theme.grayScale.gray0;
return theme.font.color.inverted;
case 'secondary':
return theme.font.color.primary;
default:
@ -88,7 +88,7 @@ const StyledButton = styled.button<
default:
return `
&:hover {
background: ${theme.background.radialGradientHover}};
background: ${theme.background.primaryInvertedHover}};
}
`;
}

View File

@ -23,4 +23,6 @@ export const BACKGROUND_DARK = {
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
primaryInverted: GRAY_SCALE.gray20,
primaryInvertedHover: GRAY_SCALE.gray15,
};

View File

@ -23,4 +23,6 @@ export const BACKGROUND_LIGHT = {
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
primaryInverted: GRAY_SCALE.gray60,
primaryInvertedHover: GRAY_SCALE.gray55,
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
import PageInaccessible from '~/options/PageInaccessible';
import '~/index.css';
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
<AppThemeProvider>
<React.StrictMode>
<PageInaccessible />
</React.StrictMode>
</AppThemeProvider>,
);
declare module '@emotion/react' {
export interface Theme extends ThemeType {}
}