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

@@ -7,6 +7,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/index.tsx"></script>
<script type="module" src="/src/options/page-inaccessible-index.tsx"></script>
</body>
</html>
</html>

View File

@@ -0,0 +1,22 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twenty</title>
<style>
/* Reset margin and padding */
html, body {
margin: 0;
padding: 0;
height: 100%; /* Ensure body takes full viewport height */
overflow: hidden; /* Prevents scrollbars from appearing */
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/index.tsx"></script>
</body>
</html>

View File

@@ -1,40 +1,55 @@
import Crypto from 'crypto-js';
import { openOptionsPage } from '~/background/utils/openOptionsPage';
import { exchangeAuthorizationCode } from '~/db/auth.db';
import { isDefined } from '~/utils/isDefined';
// Open options page programmatically in a new tab.
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
openOptionsPage();
}
});
// chrome.runtime.onInstalled.addListener((details) => {
// if (details.reason === 'install') {
// openOptionsPage();
// }
// });
// Open options page when extension icon is clicked.
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.sendMessage(tab.id ?? 0, { action: 'TOGGLE' });
});
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
// This listens for an event from other parts of the extension, such as the content script, and performs the required tasks.
// The cases themselves are labelled such that their operations are reflected by their names.
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
switch (message.action) {
case 'getActiveTab': // e.g. "https://linkedin.com/company/twenty/"
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (isDefined(tabs) && isDefined(tabs[0])) {
sendResponse({ tab: tabs[0] });
case 'getActiveTab': {
// e.g. "https://linkedin.com/company/twenty/"
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
sendResponse({ tab });
}
});
break;
case 'openOptionsPage':
openOptionsPage();
break;
case 'CONNECT':
}
case 'launchOAuth': {
launchOAuth(({ status, message }) => {
sendResponse({ status, message });
});
break;
}
case 'openSidepanel': {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.sidePanel.open({ tabId: tab.id });
}
});
break;
}
case 'changeSidepanelUrl': {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.tabs.sendMessage(tab.id, {
action: 'changeSidepanelUrl',
message,
});
}
});
break;
}
default:
break;
}
@@ -101,13 +116,16 @@ const launchOAuth = (
callback({ status: true, message: '' });
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (isDefined(tabs) && isDefined(tabs[0])) {
chrome.tabs.sendMessage(tabs[0].id ?? 0, {
action: 'AUTHENTICATED',
});
}
});
chrome.tabs.query(
{ active: true, currentWindow: true },
([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.tabs.sendMessage(tab.id, {
action: 'executeContentScript',
});
}
},
);
}
});
}
@@ -117,14 +135,22 @@ const launchOAuth = (
});
};
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
const isDesiredRoute =
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/);
if (changeInfo.status === 'complete' && tab.active) {
if (tab.active === true) {
if (isDefined(isDesiredRoute)) {
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
}
}
await chrome.sidePanel.setOptions({
tabId,
path: tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com/)
? 'sidepanel.html'
: 'page-inaccessible.html',
enabled: true,
});
});

View File

@@ -1,5 +0,0 @@
const openOptionsPage = () => {
chrome.runtime.openOptionsPage();
};
export { openOptionsPage };

View File

@@ -1,13 +1,16 @@
import { isDefined } from '~/utils/isDefined';
interface CustomDiv extends HTMLDivElement {
onClickHandler: (newHandler: () => void) => void;
}
export const createDefaultButton = (
buttonId: string,
onClickHandler?: () => void,
buttonText = '',
) => {
const btn = document.getElementById(buttonId);
): CustomDiv => {
const btn = document.getElementById(buttonId) as CustomDiv;
if (isDefined(btn)) return btn;
const div = document.createElement('div');
const div = document.createElement('div') as CustomDiv;
const img = document.createElement('img');
const span = document.createElement('span');
@@ -52,19 +55,18 @@ export const createDefaultButton = (
Object.assign(div.style, divStyles);
});
// Handle the click event.
div.addEventListener('click', async (e) => {
e.preventDefault();
const store = await chrome.storage.local.get();
div.onClickHandler = (newHandler) => {
div.onclick = async () => {
const store = await chrome.storage.local.get();
// If an api key is not set, the options page opens up to allow the user to configure an api key.
if (!store.accessToken) {
chrome.runtime.sendMessage({ action: 'openOptionsPage' });
return;
}
onClickHandler?.();
});
// If an api key is not set, the options page opens up to allow the user to configure an api key.
if (!store.accessToken) {
chrome.runtime.sendMessage({ action: 'openSidepanel' });
return;
}
newHandler();
};
};
div.id = buttonId;

View File

@@ -1,4 +1,5 @@
import { createDefaultButton } from '~/contentScript/createButton';
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
import extractDomain from '~/contentScript/utils/extractDomain';
import { createCompany, fetchCompany } from '~/db/company.db';
@@ -71,27 +72,19 @@ export const addCompany = async () => {
const companyURL = extractCompanyLinkedinLink(activeTab.url);
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
const company = await createCompany(companyInputData);
return company;
const companyId = await createCompany(companyInputData);
if (isDefined(companyId)) {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
);
}
return companyId;
};
export const insertButtonForCompany = async () => {
const companyButtonDiv = createDefaultButton(
'twenty-company-btn',
async () => {
if (isDefined(companyButtonDiv)) {
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
companyBtnSpan.textContent = 'Saving...';
const company = await addCompany();
if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
} else {
companyBtnSpan.textContent = 'Try again';
}
}
},
);
const companyButtonDiv = createDefaultButton('twenty-company-btn');
const parentDiv: HTMLDivElement | null = document.querySelector(
'.org-top-card-primary-actions__inner',
@@ -105,13 +98,35 @@ export const insertButtonForCompany = async () => {
parentDiv.prepend(companyButtonDiv);
}
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
const company = await checkIfCompanyExists();
const openCompanyOnSidePanel = (companyId: string) => {
companyButtonSpan.textContent = 'View in Twenty';
companyButtonDiv.onClickHandler(async () => {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
);
chrome.runtime.sendMessage({ action: 'openSidepanel' });
});
};
if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`,
);
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
} else {
companyBtnSpan.textContent = 'Add to Twenty';
companyButtonSpan.textContent = 'Add to Twenty';
companyButtonDiv.onClickHandler(async () => {
companyButtonSpan.textContent = 'Saving...';
const companyId = await addCompany();
if (isDefined(companyId)) {
openCompanyOnSidePanel(companyId);
} else {
companyButtonSpan.textContent = 'Try again';
}
});
}
};

View File

@@ -1,4 +1,5 @@
import { createDefaultButton } from '~/contentScript/createButton';
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
import { createPerson, fetchPerson } from '~/db/person.db';
import { PersonInput } from '~/db/types/person.types';
@@ -82,44 +83,58 @@ export const addPerson = async () => {
}
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
const person = await createPerson(personData);
return person;
const personId = await createPerson(personData);
if (isDefined(personId)) {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
);
}
return personId;
};
export const insertButtonForPerson = async () => {
const personButtonDiv = createDefaultButton('twenty-person-btn', async () => {
if (isDefined(personButtonDiv)) {
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
personBtnSpan.textContent = 'Saving...';
const person = await addPerson();
if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
} else {
personBtnSpan.textContent = 'Try again';
}
}
});
const personButtonDiv = createDefaultButton('twenty-person-btn');
if (isDefined(personButtonDiv)) {
const parentDiv: HTMLDivElement | null = document.querySelector(
'.pv-top-card-v2-ctas',
const addedProfileDiv: HTMLDivElement | null = document.querySelector(
'.pv-top-card-v2-ctas__custom',
);
if (isDefined(parentDiv)) {
if (isDefined(addedProfileDiv)) {
Object.assign(personButtonDiv.style, {
marginRight: '.8rem',
});
parentDiv.prepend(personButtonDiv);
addedProfileDiv.prepend(personButtonDiv);
}
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
const person = await checkIfPersonExists();
const openPersonOnSidePanel = (personId: string) => {
personButtonSpan.textContent = 'View in Twenty';
personButtonDiv.onClickHandler(async () => {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
);
chrome.runtime.sendMessage({ action: 'openSidepanel' });
});
};
if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`,
);
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
} else {
personBtnSpan.textContent = 'Add to Twenty';
personButtonSpan.textContent = 'Add to Twenty';
personButtonDiv.onClickHandler(async () => {
personButtonSpan.textContent = 'Saving...';
const personId = await addPerson();
if (isDefined(personId)) openPersonOnSidePanel(personId);
else personButtonSpan.textContent = 'Try again';
});
}
}
};

View File

@@ -1,6 +1,5 @@
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
import { isDefined } from '~/utils/isDefined';
// Inject buttons into the DOM when SPA is reloaded on the resource url.
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
@@ -20,85 +19,5 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
await insertButtonForPerson();
}
if (message.action === 'TOGGLE') {
await toggle();
}
if (message.action === 'AUTHENTICATED') {
await authenticated();
}
sendResponse('Executing!');
});
const IFRAME_WIDTH = '400px';
const createIframe = () => {
const iframe = document.createElement('iframe');
iframe.style.background = 'lightgrey';
iframe.style.height = '100vh';
iframe.style.width = IFRAME_WIDTH;
iframe.style.position = 'fixed';
iframe.style.top = '0px';
iframe.style.right = `-${IFRAME_WIDTH}`;
iframe.style.zIndex = '9000000000000000000';
iframe.style.transition = 'ease-in-out 0.3s';
return iframe;
};
const handleContentIframeLoadComplete = () => {
//If the pop-out window is already open then we replace loading iframe with our content iframe
if (optionsIframe.style.right === '0px') contentIframe.style.right = '0px';
optionsIframe.style.display = 'none';
contentIframe.style.display = 'block';
};
//Creating one iframe where we are loading our front end in the background
const contentIframe = createIframe();
contentIframe.style.display = 'none';
chrome.storage.local.get().then((store) => {
if (isDefined(store.loginToken)) {
contentIframe.src = `${import.meta.env.VITE_FRONT_BASE_URL}`;
contentIframe.onload = handleContentIframeLoadComplete;
}
});
const optionsIframe = createIframe();
optionsIframe.src = chrome.runtime.getURL('options.html');
document.body.appendChild(contentIframe);
document.body.appendChild(optionsIframe);
const toggleIframe = (iframe: HTMLIFrameElement) => {
if (
iframe.style.right === `-${IFRAME_WIDTH}` &&
iframe.style.display !== 'none'
) {
iframe.style.right = '0px';
} else if (iframe.style.right === '0px' && iframe.style.display !== 'none') {
iframe.style.right = `-${IFRAME_WIDTH}`;
}
};
const toggle = async () => {
const store = await chrome.storage.local.get();
if (isDefined(store.accessToken)) {
toggleIframe(contentIframe);
} else {
toggleIframe(optionsIframe);
}
};
const authenticated = async () => {
const store = await chrome.storage.local.get();
if (isDefined(store.loginToken)) {
contentIframe.src = `${
import.meta.env.VITE_FRONT_BASE_URL
}/verify?loginToken=${store.loginToken.token}`;
contentIframe.onload = handleContentIframeLoadComplete;
toggleIframe(contentIframe);
} else {
toggleIframe(optionsIframe);
}
};

View File

@@ -0,0 +1,16 @@
import { isDefined } from '~/utils/isDefined';
const changeSidePanelUrl = async (url: string) => {
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
if (isDefined(activeTab) && isDefined(url)) {
chrome.storage.local.set({ [`sidepanelUrl_${activeTab.id}`]: url });
chrome.runtime.sendMessage({
action: 'changeSidepanelUrl',
message: { url },
});
}
};
export default changeSidePanelUrl;

View File

@@ -5,6 +5,7 @@ export const FIND_COMPANY = gql`
companies(filter: $filter) {
edges {
node {
id
name
linkedinLink {
url

View File

@@ -5,6 +5,7 @@ export const FIND_PERSON = gql`
people(filter: $filter) {
edges {
node {
id
name {
firstName
lastName

View File

@@ -26,7 +26,7 @@ export default defineManifest({
action: {},
//TODO: change this to a documenation page
options_page: 'options.html',
options_page: 'sidepanel.html',
background: {
service_worker: 'src/background/index.ts',
@@ -43,12 +43,12 @@ export default defineManifest({
web_accessible_resources: [
{
resources: ['options.html'],
resources: ['sidepanel.html', 'page-inaccessible.html'],
matches: ['https://www.linkedin.com/*'],
},
],
permissions: ['activeTab', 'storage', 'identity'],
permissions: ['activeTab', 'storage', 'identity', 'sidePanel'],
host_permissions: host_permissions,

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 {}
}

View File

@@ -38,6 +38,8 @@ export default defineConfig(() => {
hmr: { port: 3002 },
},
cacheDir: './node_modules/.vite',
plugins: [viteManifestHack, crx({ manifest }), react(), tsconfigPaths()],
};
});