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:
@ -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;
|
||||
@ -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;
|
||||
169
packages/twenty-chrome-extension/src/options/Sidepanel.tsx
Normal file
169
packages/twenty-chrome-extension/src/options/Sidepanel.tsx
Normal 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;
|
||||
@ -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>,
|
||||
);
|
||||
|
||||
@ -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}};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user