mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 20:27:55 +00:00 
			
		
		
		
	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:
		| @@ -7,6 +7,6 @@ | |||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <div id="app"></div> |     <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> |   </body> | ||||||
| </html> | </html> | ||||||
							
								
								
									
										22
									
								
								packages/twenty-chrome-extension/sidepanel.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/twenty-chrome-extension/sidepanel.html
									
									
									
									
									
										Normal 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> | ||||||
| @@ -1,40 +1,55 @@ | |||||||
| import Crypto from 'crypto-js'; | import Crypto from 'crypto-js'; | ||||||
|  |  | ||||||
| import { openOptionsPage } from '~/background/utils/openOptionsPage'; |  | ||||||
| import { exchangeAuthorizationCode } from '~/db/auth.db'; | import { exchangeAuthorizationCode } from '~/db/auth.db'; | ||||||
| import { isDefined } from '~/utils/isDefined'; | import { isDefined } from '~/utils/isDefined'; | ||||||
|  |  | ||||||
| // Open options page programmatically in a new tab. | // Open options page programmatically in a new tab. | ||||||
| chrome.runtime.onInstalled.addListener((details) => { | // chrome.runtime.onInstalled.addListener((details) => { | ||||||
|   if (details.reason === 'install') { | //   if (details.reason === 'install') { | ||||||
|     openOptionsPage(); | //     openOptionsPage(); | ||||||
|   } | //   } | ||||||
| }); | // }); | ||||||
|  |  | ||||||
| // Open options page when extension icon is clicked. | chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); | ||||||
| chrome.action.onClicked.addListener((tab) => { |  | ||||||
|   chrome.tabs.sendMessage(tab.id ?? 0, { action: 'TOGGLE' }); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| // This listens for an event from other parts of the extension, such as the content script, and performs the required tasks. | // 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. | // The cases themselves are labelled such that their operations are reflected by their names. | ||||||
| chrome.runtime.onMessage.addListener((message, _, sendResponse) => { | chrome.runtime.onMessage.addListener((message, _, sendResponse) => { | ||||||
|   switch (message.action) { |   switch (message.action) { | ||||||
|     case 'getActiveTab': // e.g. "https://linkedin.com/company/twenty/" |     case 'getActiveTab': { | ||||||
|       chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { |       // e.g. "https://linkedin.com/company/twenty/" | ||||||
|         if (isDefined(tabs) && isDefined(tabs[0])) { |       chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { | ||||||
|           sendResponse({ tab: tabs[0] }); |         if (isDefined(tab) && isDefined(tab.id)) { | ||||||
|  |           sendResponse({ tab }); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|       break; |       break; | ||||||
|     case 'openOptionsPage': |     } | ||||||
|       openOptionsPage(); |     case 'launchOAuth': { | ||||||
|       break; |  | ||||||
|     case 'CONNECT': |  | ||||||
|       launchOAuth(({ status, message }) => { |       launchOAuth(({ status, message }) => { | ||||||
|         sendResponse({ status, message }); |         sendResponse({ status, message }); | ||||||
|       }); |       }); | ||||||
|       break; |       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: |     default: | ||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
| @@ -101,13 +116,16 @@ const launchOAuth = ( | |||||||
|  |  | ||||||
|             callback({ status: true, message: '' }); |             callback({ status: true, message: '' }); | ||||||
|  |  | ||||||
|             chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { |             chrome.tabs.query( | ||||||
|               if (isDefined(tabs) && isDefined(tabs[0])) { |               { active: true, currentWindow: true }, | ||||||
|                 chrome.tabs.sendMessage(tabs[0].id ?? 0, { |               ([tab]) => { | ||||||
|                   action: 'AUTHENTICATED', |                 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 = |   const isDesiredRoute = | ||||||
|     tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) || |     tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) || | ||||||
|     tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/); |     tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/); | ||||||
|  |  | ||||||
|   if (changeInfo.status === 'complete' && tab.active) { |   if (tab.active === true) { | ||||||
|     if (isDefined(isDesiredRoute)) { |     if (isDefined(isDesiredRoute)) { | ||||||
|       chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' }); |       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, | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,5 +0,0 @@ | |||||||
| const openOptionsPage = () => { |  | ||||||
|   chrome.runtime.openOptionsPage(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export { openOptionsPage }; |  | ||||||
| @@ -1,13 +1,16 @@ | |||||||
| import { isDefined } from '~/utils/isDefined'; | import { isDefined } from '~/utils/isDefined'; | ||||||
|  |  | ||||||
|  | interface CustomDiv extends HTMLDivElement { | ||||||
|  |   onClickHandler: (newHandler: () => void) => void; | ||||||
|  | } | ||||||
|  |  | ||||||
| export const createDefaultButton = ( | export const createDefaultButton = ( | ||||||
|   buttonId: string, |   buttonId: string, | ||||||
|   onClickHandler?: () => void, |  | ||||||
|   buttonText = '', |   buttonText = '', | ||||||
| ) => { | ): CustomDiv => { | ||||||
|   const btn = document.getElementById(buttonId); |   const btn = document.getElementById(buttonId) as CustomDiv; | ||||||
|   if (isDefined(btn)) return btn; |   if (isDefined(btn)) return btn; | ||||||
|   const div = document.createElement('div'); |   const div = document.createElement('div') as CustomDiv; | ||||||
|   const img = document.createElement('img'); |   const img = document.createElement('img'); | ||||||
|   const span = document.createElement('span'); |   const span = document.createElement('span'); | ||||||
|  |  | ||||||
| @@ -52,19 +55,18 @@ export const createDefaultButton = ( | |||||||
|     Object.assign(div.style, divStyles); |     Object.assign(div.style, divStyles); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   // Handle the click event. |   div.onClickHandler = (newHandler) => { | ||||||
|   div.addEventListener('click', async (e) => { |     div.onclick = async () => { | ||||||
|     e.preventDefault(); |  | ||||||
|       const store = await chrome.storage.local.get(); |       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 an api key is not set, the options page opens up to allow the user to configure an api key. | ||||||
|       if (!store.accessToken) { |       if (!store.accessToken) { | ||||||
|       chrome.runtime.sendMessage({ action: 'openOptionsPage' }); |         chrome.runtime.sendMessage({ action: 'openSidepanel' }); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |       newHandler(); | ||||||
|     onClickHandler?.(); |     }; | ||||||
|   }); |   }; | ||||||
|  |  | ||||||
|   div.id = buttonId; |   div.id = buttonId; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { createDefaultButton } from '~/contentScript/createButton'; | import { createDefaultButton } from '~/contentScript/createButton'; | ||||||
|  | import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl'; | ||||||
| import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink'; | import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink'; | ||||||
| import extractDomain from '~/contentScript/utils/extractDomain'; | import extractDomain from '~/contentScript/utils/extractDomain'; | ||||||
| import { createCompany, fetchCompany } from '~/db/company.db'; | import { createCompany, fetchCompany } from '~/db/company.db'; | ||||||
| @@ -71,27 +72,19 @@ export const addCompany = async () => { | |||||||
|   const companyURL = extractCompanyLinkedinLink(activeTab.url); |   const companyURL = extractCompanyLinkedinLink(activeTab.url); | ||||||
|   companyInputData.linkedinLink = { url: companyURL, label: companyURL }; |   companyInputData.linkedinLink = { url: companyURL, label: companyURL }; | ||||||
|  |  | ||||||
|   const company = await createCompany(companyInputData); |   const companyId = await createCompany(companyInputData); | ||||||
|   return company; |  | ||||||
|  |   if (isDefined(companyId)) { | ||||||
|  |     await changeSidePanelUrl( | ||||||
|  |       `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return companyId; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const insertButtonForCompany = async () => { | export const insertButtonForCompany = async () => { | ||||||
|   const companyButtonDiv = createDefaultButton( |   const companyButtonDiv = createDefaultButton('twenty-company-btn'); | ||||||
|     '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 parentDiv: HTMLDivElement | null = document.querySelector( |   const parentDiv: HTMLDivElement | null = document.querySelector( | ||||||
|     '.org-top-card-primary-actions__inner', |     '.org-top-card-primary-actions__inner', | ||||||
| @@ -105,13 +98,35 @@ export const insertButtonForCompany = async () => { | |||||||
|     parentDiv.prepend(companyButtonDiv); |     parentDiv.prepend(companyButtonDiv); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0]; |   const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0]; | ||||||
|   const company = await checkIfCompanyExists(); |   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)) { |   if (isDefined(company)) { | ||||||
|     companyBtnSpan.textContent = 'Saved'; |     await changeSidePanelUrl( | ||||||
|     Object.assign(companyButtonDiv.style, { pointerEvents: 'none' }); |       `${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`, | ||||||
|  |     ); | ||||||
|  |     if (isDefined(company.id)) openCompanyOnSidePanel(company.id); | ||||||
|   } else { |   } 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'; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { createDefaultButton } from '~/contentScript/createButton'; | import { createDefaultButton } from '~/contentScript/createButton'; | ||||||
|  | import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl'; | ||||||
| import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName'; | import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName'; | ||||||
| import { createPerson, fetchPerson } from '~/db/person.db'; | import { createPerson, fetchPerson } from '~/db/person.db'; | ||||||
| import { PersonInput } from '~/db/types/person.types'; | import { PersonInput } from '~/db/types/person.types'; | ||||||
| @@ -82,44 +83,58 @@ export const addPerson = async () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl }; |   personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl }; | ||||||
|   const person = await createPerson(personData); |   const personId = await createPerson(personData); | ||||||
|   return person; |  | ||||||
|  |   if (isDefined(personId)) { | ||||||
|  |     await changeSidePanelUrl( | ||||||
|  |       `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return personId; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const insertButtonForPerson = async () => { | export const insertButtonForPerson = async () => { | ||||||
|   const personButtonDiv = createDefaultButton('twenty-person-btn', async () => { |   const personButtonDiv = createDefaultButton('twenty-person-btn'); | ||||||
|     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'; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   if (isDefined(personButtonDiv)) { |   if (isDefined(personButtonDiv)) { | ||||||
|     const parentDiv: HTMLDivElement | null = document.querySelector( |     const addedProfileDiv: HTMLDivElement | null = document.querySelector( | ||||||
|       '.pv-top-card-v2-ctas', |       '.pv-top-card-v2-ctas__custom', | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (isDefined(parentDiv)) { |     if (isDefined(addedProfileDiv)) { | ||||||
|       Object.assign(personButtonDiv.style, { |       Object.assign(personButtonDiv.style, { | ||||||
|         marginRight: '.8rem', |         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 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)) { |     if (isDefined(person)) { | ||||||
|       personBtnSpan.textContent = 'Saved'; |       await changeSidePanelUrl( | ||||||
|       Object.assign(personButtonDiv.style, { pointerEvents: 'none' }); |         `${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`, | ||||||
|  |       ); | ||||||
|  |       if (isDefined(person.id)) openPersonOnSidePanel(person.id); | ||||||
|     } else { |     } 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'; | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile'; | import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile'; | ||||||
| import { insertButtonForPerson } from '~/contentScript/extractPersonProfile'; | import { insertButtonForPerson } from '~/contentScript/extractPersonProfile'; | ||||||
| import { isDefined } from '~/utils/isDefined'; |  | ||||||
|  |  | ||||||
| // Inject buttons into the DOM when SPA is reloaded on the resource url. | // 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/ | // 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(); |     await insertButtonForPerson(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (message.action === 'TOGGLE') { |  | ||||||
|     await toggle(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (message.action === 'AUTHENTICATED') { |  | ||||||
|     await authenticated(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   sendResponse('Executing!'); |   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); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|   | |||||||
| @@ -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; | ||||||
| @@ -5,6 +5,7 @@ export const FIND_COMPANY = gql` | |||||||
|     companies(filter: $filter) { |     companies(filter: $filter) { | ||||||
|       edges { |       edges { | ||||||
|         node { |         node { | ||||||
|  |           id | ||||||
|           name |           name | ||||||
|           linkedinLink { |           linkedinLink { | ||||||
|             url |             url | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ export const FIND_PERSON = gql` | |||||||
|     people(filter: $filter) { |     people(filter: $filter) { | ||||||
|       edges { |       edges { | ||||||
|         node { |         node { | ||||||
|  |           id | ||||||
|           name { |           name { | ||||||
|             firstName |             firstName | ||||||
|             lastName |             lastName | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ export default defineManifest({ | |||||||
|   action: {}, |   action: {}, | ||||||
|  |  | ||||||
|   //TODO: change this to a documenation page |   //TODO: change this to a documenation page | ||||||
|   options_page: 'options.html', |   options_page: 'sidepanel.html', | ||||||
|  |  | ||||||
|   background: { |   background: { | ||||||
|     service_worker: 'src/background/index.ts', |     service_worker: 'src/background/index.ts', | ||||||
| @@ -43,12 +43,12 @@ export default defineManifest({ | |||||||
|  |  | ||||||
|   web_accessible_resources: [ |   web_accessible_resources: [ | ||||||
|     { |     { | ||||||
|       resources: ['options.html'], |       resources: ['sidepanel.html', 'page-inaccessible.html'], | ||||||
|       matches: ['https://www.linkedin.com/*'], |       matches: ['https://www.linkedin.com/*'], | ||||||
|     }, |     }, | ||||||
|   ], |   ], | ||||||
|  |  | ||||||
|   permissions: ['activeTab', 'storage', 'identity'], |   permissions: ['activeTab', 'storage', 'identity', 'sidePanel'], | ||||||
|  |  | ||||||
|   host_permissions: host_permissions, |   host_permissions: host_permissions, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; | ||||||
| import { ThemeType } from '@/ui/theme/constants/ThemeLight'; | import { ThemeType } from '@/ui/theme/constants/ThemeLight'; | ||||||
| import Options from '~/options/Options'; | import Sidepanel from '~/options/Sidepanel'; | ||||||
|  |  | ||||||
| import '~/index.css'; | import '~/index.css'; | ||||||
|  |  | ||||||
| ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( | ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( | ||||||
|   <AppThemeProvider> |   <AppThemeProvider> | ||||||
|     <React.StrictMode> |     <React.StrictMode> | ||||||
|       <Options /> |       <Sidepanel /> | ||||||
|     </React.StrictMode> |     </React.StrictMode> | ||||||
|   </AppThemeProvider>, |   </AppThemeProvider>, | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ const StyledButton = styled.button< | |||||||
|  |  | ||||||
|     switch (variant) { |     switch (variant) { | ||||||
|       case 'primary': |       case 'primary': | ||||||
|         return theme.background.radialGradient; |         return theme.background.primaryInverted; | ||||||
|       case 'secondary': |       case 'secondary': | ||||||
|         return theme.background.primary; |         return theme.background.primary; | ||||||
|       default: |       default: | ||||||
| @@ -37,7 +37,7 @@ const StyledButton = styled.button< | |||||||
|  |  | ||||||
|     switch (variant) { |     switch (variant) { | ||||||
|       case 'primary': |       case 'primary': | ||||||
|         return theme.background.transparent.light; |         return theme.background.transparent.strong; | ||||||
|       case 'secondary': |       case 'secondary': | ||||||
|         return theme.border.color.medium; |         return theme.border.color.medium; | ||||||
|       default: |       default: | ||||||
| @@ -59,7 +59,7 @@ const StyledButton = styled.button< | |||||||
|  |  | ||||||
|     switch (variant) { |     switch (variant) { | ||||||
|       case 'primary': |       case 'primary': | ||||||
|         return theme.grayScale.gray0; |         return theme.font.color.inverted; | ||||||
|       case 'secondary': |       case 'secondary': | ||||||
|         return theme.font.color.primary; |         return theme.font.color.primary; | ||||||
|       default: |       default: | ||||||
| @@ -88,7 +88,7 @@ const StyledButton = styled.button< | |||||||
|       default: |       default: | ||||||
|         return ` |         return ` | ||||||
|           &:hover { |           &:hover { | ||||||
|             background: ${theme.background.radialGradientHover}}; |             background: ${theme.background.primaryInvertedHover}}; | ||||||
|           } |           } | ||||||
|         `; |         `; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -23,4 +23,6 @@ export const BACKGROUND_DARK = { | |||||||
|   overlay: RGBA(GRAY_SCALE.gray80, 0.8), |   overlay: RGBA(GRAY_SCALE.gray80, 0.8), | ||||||
|   radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, |   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%)`, |   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), |   overlay: RGBA(GRAY_SCALE.gray80, 0.8), | ||||||
|   radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, |   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%)`, |   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 {} | ||||||
|  | } | ||||||
| @@ -38,6 +38,8 @@ export default defineConfig(() => { | |||||||
|       hmr: { port: 3002 }, |       hmr: { port: 3002 }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     cacheDir: './node_modules/.vite', | ||||||
|  |  | ||||||
|     plugins: [viteManifestHack, crx({ manifest }), react(), tsconfigPaths()], |     plugins: [viteManifestHack, crx({ manifest }), react(), tsconfigPaths()], | ||||||
|   }; |   }; | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Aditya Pimpalkar
					Aditya Pimpalkar