Remove chrome extension (#13729)

We'll bring it back later
This commit is contained in:
Félix Malfait
2025-08-07 16:32:34 +02:00
committed by GitHub
parent 3e1c9295d9
commit a27dc5ddf1
110 changed files with 0 additions and 11284 deletions
@@ -1,48 +0,0 @@
name: CI Chrome Extension
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-chrome-extension/**
chrome-extension-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 15
runs-on: ubuntu-latest
env:
VITE_SERVER_BASE_URL: http://localhost:3000
VITE_FRONT_BASE_URL: http://localhost:3001
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Chrome Extension / Run build
run: npx nx build twenty-chrome-extension
ci-chrome-extension-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, chrome-extension-build]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
-4
View File
@@ -4,10 +4,6 @@
"name": "ROOT",
"path": "../"
},
{
"name": "packages/twenty-chrome-extension",
"path": "../packages/twenty-chrome-extension"
},
{
"name": "packages/twenty-docker",
"path": "../packages/twenty-docker"
-1
View File
@@ -89,7 +89,6 @@ packages/
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-chrome-extension/ # Chrome extension
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
-1
View File
@@ -349,7 +349,6 @@
},
"workspaces": {
"packages": [
"packages/twenty-chrome-extension",
"packages/twenty-front",
"packages/twenty-server",
"packages/twenty-emails",
@@ -1,6 +0,0 @@
VITE_SERVER_BASE_URL=https://api.twenty.com
VITE_FRONT_BASE_URL=https://app.twenty.com
VITE_MODE=production
# Used to generate packages/twenty-chrome-extension/src/generated/graphql.tsx
AUTH_TOKEN=<YOUR-TOKEN-HERE>
@@ -1,6 +0,0 @@
module.exports = {
extends: ['./.eslintrc.cjs'],
rules: {
'no-console': 'error',
},
};
@@ -1,20 +0,0 @@
module.exports = {
root: true,
extends: ['../../.eslintrc.global.cjs', '../../.eslintrc.react.cjs'],
ignorePatterns: [
'node_modules',
'dist',
'**/generated/*',
],
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
parserOptions: {
project: ['packages/twenty-chrome-extension/tsconfig.*.json'],
},
rules: {
'@nx/workspace-explicit-boolean-predicates-in-if': 'warn',
},
},
],
};
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -1 +0,0 @@
src/generated
@@ -1,60 +0,0 @@
# Twenty Chrome Extension.
This extension allows you to save `company` and `people` information to your twenty workspace directly from LinkedIn.
To install the extension in development mode with hmr (hot module reload), follow these steps.
- STEP 1: Clone the repository and run `yarn install` in the root directory.
- STEP 2: Once the dependencies installation succeeds, create a file with env variables by executing the following command in the root directory.
```
cp ./packages/twenty-chrome-extension/.env.example ./packages/twenty-chrome-extension/.env
```
- STEP 3 (optional): Update values of the environment variables to match those of your instance for `twenty-front` and `twenty-server`. If you want to work on your local machine with the default setup from `Twenty Docs`, replace everything in the .env file with the following.
```
VITE_SERVER_BASE_URL=http://localhost:3000
VITE_FRONT_BASE_URL=http://localhost:3001
```
- STEP 4: Now, execute the following command in the root directory to start up the development server on Port 3002. This will create a `dist` folder in `twenty-chrome-extension`.
```
npx nx start twenty-chrome-extension
```
- STEP 5: Open Google Chrome and head to the extensions page by typing `chrome://extensions` in the address bar.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/01-img-one.png" width="600" />
</p>
- STEP 6: Turn on the `Developer mode` from the top-right corner and click `Load unpacked`.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/02-img-two.png" width="600" />
</p>
- STEP 7: Select the `dist` folder from `twenty-chrome-extension`.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/03-img-three.png" width="600" />
</p>
- STEP 8: This opens up the `options` page, where you must enter your API key.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/04-img-four.png" width="600" />
</p>
- STEP 9: Reload any LinkedIn page that you opened before installing the extension for seamless experience.
- STEP 10: Visit any individual or company profile on LinkedIn and click the `Add to Twenty` button to test.
<p align="center">
<img src="../twenty-chrome-extension/public/readme-images/05-img-five.png" width="600" />
</p>
To install the extension in production mode without hmr (hot module reload), replace the command in STEP FOUR with `npx nx build twenty-chrome-extension`. You may or may not want to execute STEP THREE based on your requirements.
@@ -1,34 +0,0 @@
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: [
{
[`${import.meta.env.VITE_SERVER_BASE_URL}/graphql`]: {
// some of the mutations and queries require authorization (people or companies)
// so to regenerate the schema with types we need to pass an auth token
headers: {
Authorization: `Bearer ${import.meta.env.AUTH_TOKEN}`,
},
},
},
],
overwrite: true,
documents: ['./src/**/*.ts', '!src/generated/**/*.*'],
generates: {
'./src/generated/graphql.tsx': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: {
skipTypename: true,
withHooks: true,
withHOC: false,
withComponent: false,
},
},
},
};
export default config;
@@ -1,12 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twenty</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/loading-index.tsx"></script>
</body>
</html>
@@ -1,13 +0,0 @@
{
"name": "twenty-chrome-extension",
"description": "",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "npx vite build"
},
"dependencies": {
"twenty-shared": "workspace:*"
}
}
@@ -1,12 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twenty</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/page-inaccessible-index.tsx"></script>
</body>
</html>
@@ -1,71 +0,0 @@
{
"name": "twenty-chrome-extension",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"tags": ["scope:frontend"],
"targets": {
"build": {
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "{projectRoot}/dist"
},
"dependsOn": ["^build"]
},
"start": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"cwd": "packages/twenty-chrome-extension",
"command": "VITE_MODE=development vite"
}
},
"preview": {
"executor": "@nx/vite:preview-server",
"options": {
"buildTarget": "twenty-chrome-extension:build",
"port": 3002,
"open": true
}
},
"reset:env": {
"executor": "nx:run-commands",
"inputs": ["{projectRoot}/.env.example"],
"outputs": ["{projectRoot}/.env"],
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "cp .env.example .env"
}
},
"typecheck": {},
"lint": {
"options": {
"lintFilePatterns": [
"{projectRoot}/src/**/*.{ts,tsx,json}",
"{projectRoot}/package.json"
],
"maxWarnings": 0,
"reportUnusedDisableDirectives": "error"
},
"configurations": {
"ci": { "eslintConfig": "{projectRoot}/.eslintrc-ci.cjs" },
"fix": {}
}
},
"fmt": {
"options": {
"files": "src"
},
"configurations": {
"fix": {}
}
},
"graphql:generate": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "graphql-codegen"
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 B

@@ -1,12 +0,0 @@
<svg width="32" height="32" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2343_96406)">
<path d="M136 2.28882e-05H0L0.000144482 136H136V2.28882e-05ZM27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27ZM107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="black"/>
<path d="M27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27Z" fill="white"/>
<path d="M107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2343_96406">
<rect width="136" height="136" rx="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 830 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,22 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="images/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,96 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
// Open options page programmatically in a new tab.
// chrome.runtime.onInstalled.addListener((details) => {
// if (details.reason === 'install') {
// openOptionsPage();
// }
// });
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 }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
sendResponse({ tab });
}
});
break;
}
case 'openSidepanel': {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.sidePanel.open({ tabId: tab.id });
}
});
break;
}
default:
break;
}
return true;
});
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 (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,
});
});
const setTokenStateFromCookie = (cookie: string) => {
const decodedValue = decodeURIComponent(cookie);
const tokenPair = JSON.parse(decodedValue);
if (isDefined(tokenPair)) {
chrome.storage.local.set({
isAuthenticated: true,
accessToken: tokenPair.accessOrWorkspaceAgnosticToken,
refreshToken: tokenPair.refreshToken,
});
}
};
chrome.cookies.onChanged.addListener(async ({ cookie }) => {
if (cookie.name === 'tokenPair') {
const store = await chrome.storage.local.get(['clientUrl']);
const clientUrl = isDefined(store.clientUrl)
? store.clientUrl
: import.meta.env.VITE_FRONT_BASE_URL;
chrome.cookies.get({ name: 'tokenPair', url: `${clientUrl}` }, (cookie) => {
if (isDefined(cookie)) {
setTokenStateFromCookie(cookie.value);
}
});
}
});
// This will only run the very first time the extension loads, after we have stored the
// cookiesRead variable to true, this will not allow to change the token state everytime background script runs
chrome.cookies.get(
{ name: 'tokenPair', url: `${import.meta.env.VITE_FRONT_BASE_URL}` },
async (cookie) => {
const store = await chrome.storage.local.get(['cookiesRead']);
if (isDefined(cookie) && !isDefined(store.cookiesRead)) {
setTokenStateFromCookie(cookie.value);
chrome.storage.local.set({ cookiesRead: true });
}
},
);
@@ -1,77 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
interface CustomDiv extends HTMLDivElement {
onClickHandler: (newHandler: () => void) => void;
}
export const createDefaultButton = (
buttonId: string,
buttonText = '',
): CustomDiv => {
const btn = document.getElementById(buttonId) as CustomDiv;
if (isDefined(btn)) return btn;
const div = document.createElement('div') as CustomDiv;
const img = document.createElement('img');
const span = document.createElement('span');
span.textContent = buttonText;
img.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
img.height = 16;
img.width = 16;
img.alt = 'Twenty logo';
// Write universal styles for the button
const divStyles = {
border: '1px solid black',
borderRadius: '20px',
backgroundColor: 'black',
color: 'white',
fontWeight: '600',
fontSize: '1.5rem',
display: 'flex',
alignItems: 'center',
gap: '5px',
justifyContent: 'center',
padding: '0 1rem',
cursor: 'pointer',
height: '32px',
width: 'max-content',
};
Object.assign(div.style, divStyles);
// Apply common styles to specifc states of a button.
div.addEventListener('mouseenter', () => {
const hoverStyles = {
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
backgroundColor: '#5e5e5e',
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
borderColor: '#5e5e5e',
};
Object.assign(div.style, hoverStyles);
});
div.addEventListener('mouseleave', () => {
Object.assign(div.style, divStyles);
});
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: 'openSidepanel' });
return;
}
newHandler();
};
};
div.id = buttonId;
div.appendChild(img);
div.appendChild(span);
return div;
};
@@ -1,126 +0,0 @@
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';
import { CompanyInput } from '~/db/types/company.types';
import { isDefined } from 'twenty-shared/utils';
export const checkIfCompanyExists = async () => {
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
const companyURL = extractCompanyLinkedinLink(activeTab.url);
return await fetchCompany({
linkedinLink: {
url: { eq: companyURL },
label: { eq: companyURL },
},
});
};
export const addCompany = async () => {
// Extract company-specific data from the DOM
const companyNameElement = document.querySelector(
'.org-top-card-summary__title',
);
const domainNameElement = document.querySelector(
'.org-top-card-primary-actions__inner a',
);
const addressElement = document.querySelectorAll(
'.org-top-card-summary-info-list__info-item',
)[1];
const employeesNumberElement = document.querySelectorAll(
'.org-top-card-summary-info-list__info-item',
)[3];
// Get the text content or other necessary data from the DOM elements
const companyName = companyNameElement
? companyNameElement.getAttribute('title')
: '';
const domainName = extractDomain(
domainNameElement && domainNameElement.getAttribute('href'),
);
const address = addressElement
? addressElement.textContent?.trim().replace(/\s+/g, ' ')
: '';
const employees = employeesNumberElement
? Number(
employeesNumberElement.textContent
?.trim()
.replace(/\s+/g, ' ')
.split('-')[0],
)
: 0;
// Prepare company data to send to the backend
const companyInputData: CompanyInput = {
name: companyName ?? '',
domainName: domainName,
address: address ?? '',
employees: employees,
};
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
// Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty
const companyURL = extractCompanyLinkedinLink(activeTab.url);
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
const companyId = await createCompany(companyInputData);
if (isDefined(companyId)) {
await changeSidePanelUrl(`/object/company/${companyId}`);
}
return companyId;
};
export const insertButtonForCompany = async () => {
const companyButtonDiv = createDefaultButton('twenty-company-btn');
const companyDiv: HTMLDivElement | null = document.querySelector(
'.org-top-card__primary-content',
);
if (isDefined(companyDiv)) {
Object.assign(companyButtonDiv.style, {
marginTop: '.8rem',
});
companyDiv.parentElement?.append(companyButtonDiv);
}
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
const company = await checkIfCompanyExists();
const openCompanyOnSidePanel = (companyId: string) => {
companyButtonSpan.textContent = 'View in Twenty';
companyButtonDiv.onClickHandler(async () => {
await changeSidePanelUrl(`/object/company/${companyId}`);
chrome.runtime.sendMessage({ action: 'openSidepanel' });
});
};
if (isDefined(company)) {
await changeSidePanelUrl(`/object/company/${company.id}`);
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
} else {
await changeSidePanelUrl(`/objects/companies`);
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,133 +0,0 @@
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';
import { isDefined } from 'twenty-shared/utils';
export const checkIfPersonExists = async () => {
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
let activeTabUrl = '';
if (isDefined(activeTab.url.endsWith('/'))) {
activeTabUrl = activeTab.url.slice(0, -1);
}
const personNameElement = document.querySelector('.text-heading-xlarge');
const personName = personNameElement ? personNameElement.textContent : '';
const { firstName, lastName } = extractFirstAndLastName(String(personName));
const person = await fetchPerson({
name: {
firstName: { eq: firstName },
lastName: { eq: lastName },
},
linkedinLink: { url: { eq: activeTabUrl }, label: { eq: activeTabUrl } },
});
return person;
};
export const addPerson = async () => {
const personNameElement = document.querySelector('.text-heading-xlarge');
const separatorElement = document.querySelector(
'.pv-text-details__separator',
);
const personCityElement = separatorElement?.previousElementSibling;
const profilePictureElement = document.querySelector(
'.pv-top-card-profile-picture__image',
);
const firstListItem = document.querySelector(
'div[data-view-name="profile-component-entity"]',
);
const secondDivElement = firstListItem?.querySelector('div:nth-child(2)');
const ariaHiddenSpan = secondDivElement?.querySelector(
'span[aria-hidden="true"]',
);
// Get the text content or other necessary data from the DOM elements.
const personName = personNameElement ? personNameElement.textContent : '';
const personCity = personCityElement
? personCityElement.textContent?.trim().replace(/\s+/g, ' ').split(',')[0]
: '';
const profilePicture = profilePictureElement
? profilePictureElement?.getAttribute('src')
: '';
const jobTitle = ariaHiddenSpan ? ariaHiddenSpan.textContent?.trim() : '';
const { firstName, lastName } = extractFirstAndLastName(String(personName));
// Prepare person data to send to the backend.
const personData: PersonInput = {
name: { firstName, lastName },
city: personCity ?? '',
avatarUrl: profilePicture ?? '',
jobTitle: jobTitle ?? '',
linkedinLink: { url: '', label: '' },
};
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
let activeTabUrl = '';
// Remove last slash from the URL for consistency when saving usernames.
if (isDefined(activeTab.url.endsWith('/'))) {
activeTabUrl = activeTab.url.slice(0, -1);
}
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
const personId = await createPerson(personData);
if (isDefined(personId)) {
await changeSidePanelUrl(`/object/person/${personId}`);
}
return personId;
};
export const insertButtonForPerson = async () => {
const personButtonDiv = createDefaultButton('twenty-person-btn');
if (isDefined(personButtonDiv)) {
const addedProfileDiv = document.querySelector('.artdeco-card > .ph5');
if (isDefined(addedProfileDiv)) {
Object.assign(personButtonDiv.style, {
marginTop: '.8rem',
});
addedProfileDiv.append(personButtonDiv);
}
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
const person = await checkIfPersonExists();
const openPersonOnSidePanel = (personId: string) => {
personButtonSpan.textContent = 'View in Twenty';
personButtonDiv.onClickHandler(async () => {
await changeSidePanelUrl(`/object/person/${personId}`);
chrome.runtime.sendMessage({ action: 'openSidepanel' });
});
};
if (isDefined(person)) {
await changeSidePanelUrl(`/object/person/${person.id}`);
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
} else {
await changeSidePanelUrl(`/objects/people`);
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,44 +0,0 @@
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
import { isDefined } from 'twenty-shared/utils';
// 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/
// await insertButtonForCompany();
const companyRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/;
const personRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/;
const executeScript = async () => {
const loc = window.location.href;
switch (true) {
case companyRoute.test(loc):
await insertButtonForCompany();
break;
case personRoute.test(loc):
await insertButtonForPerson();
break;
default:
break;
}
};
// The content script gets executed upon load, so the the content script is executed when a user visits https://www.linkedin.com/feed/.
// However, there would never be another reload in a single page application unless triggered manually.
// Therefore, if the user navigates to a person or a company page, we must manually re-execute the content script to create the "Add to Twenty" button.
// e.g. create "Add to Twenty" button when a user navigates to https://www.linkedin.com/in/mabdullahabaid/ from https://www.linkedin.com/feed/
chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
if (message.action === 'executeContentScript') {
await executeScript();
}
sendResponse('Executing!');
});
chrome.storage.local.onChanged.addListener(async (store) => {
if (isDefined(store.accessToken)) {
if (isDefined(store.accessToken.newValue)) {
await executeScript();
}
}
});
@@ -1,58 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
const btn = document.getElementById('twenty-settings-btn');
if (!isDefined(btn)) {
const div = document.createElement('div');
const img = document.createElement('img');
img.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
img.height = 20;
img.width = 20;
img.alt = 'Twenty logo';
// Write universal styles for the button
const divStyles = {
border: '1px solid black',
borderRadius: '50%',
backgroundColor: 'black',
color: 'white',
fontWeight: '600',
fontSize: '1.5rem',
display: 'flex',
alignItems: 'center',
gap: '5px',
justifyContent: 'center',
padding: '0 1rem',
cursor: 'pointer',
height: '50px',
width: '50px',
position: 'fixed',
bottom: '80px',
right: '20px',
zIndex: '9999999999999999999999999',
};
div.addEventListener('mouseenter', () => {
const hoverStyles = {
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
backgroundColor: '#5e5e5e',
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
borderColor: '#5e5e5e',
};
Object.assign(div.style, hoverStyles);
});
div.addEventListener('mouseleave', () => {
Object.assign(div.style, divStyles);
});
div.onclick = async () => {
chrome.runtime.sendMessage({ action: 'openSidepanel' });
chrome.storage.local.set({ navigateSidepanel: 'settings' });
};
div.appendChild(img);
Object.assign(div.style, divStyles);
document.body.appendChild(div);
}
@@ -1,12 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
const changeSidePanelUrl = async (url: string) => {
if (isDefined(url)) {
chrome.storage.local.set({ navigateSidepanel: 'sidepanel' });
// we first clear the sidepanelUrl to trigger the onchange listener on sidepanel
// which will pass the post meessage to handle internal navigation of iframe
chrome.storage.local.set({ sidepanelUrl: '' });
chrome.storage.local.set({ sidepanelUrl: url });
}
};
export default changeSidePanelUrl;
@@ -1,19 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
// "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/".
const extractCompanyLinkedinLink = (activeTabUrl: string) => {
// Regular expression to match the company ID
const regex = /\/company\/([^/]*)/;
// Extract the company ID using the regex
const match = activeTabUrl.match(regex);
if (isDefined(match) && isDefined(match[1])) {
const companyID = match[1];
const cleanCompanyURL = `https://www.linkedin.com/company/${companyID}`;
return cleanCompanyURL;
}
return '';
};
export default extractCompanyLinkedinLink;
@@ -1,15 +0,0 @@
const extractDomain = (url: string | null) => {
if (!url) return '';
const hostname = new URL(url).hostname;
let domain = hostname.replace('www.', '');
const parts = domain.split('.');
if (parts.length > 2) {
domain = parts.slice(1).join('.');
}
return domain;
};
export default extractDomain;
@@ -1,9 +0,0 @@
// Separate first name and last name from a full name.
const extractFirstAndLastName = (fullName: string) => {
const spaceIndex = fullName.lastIndexOf(' ');
const firstName = fullName.substring(0, spaceIndex);
const lastName = fullName.substring(spaceIndex + 1);
return { firstName, lastName };
};
export default extractFirstAndLastName;
@@ -1,20 +0,0 @@
import {
ExchangeAuthCodeInput,
ExchangeAuthCodeResponse,
Tokens,
} from '~/db/types/auth.types';
import { EXCHANGE_AUTHORIZATION_CODE } from '~/graphql/auth/mutations';
import { callMutation } from '~/utils/requestDb';
import { isDefined } from 'twenty-shared/utils';
export const exchangeAuthorizationCode = async (
exchangeAuthCodeInput: ExchangeAuthCodeInput,
): Promise<Tokens | null> => {
const data = await callMutation<ExchangeAuthCodeResponse>(
EXCHANGE_AUTHORIZATION_CODE,
exchangeAuthCodeInput,
);
if (isDefined(data?.exchangeAuthorizationCode))
return data.exchangeAuthorizationCode;
else return null;
};
@@ -1,41 +0,0 @@
import {
CompanyInput,
CreateCompanyResponse,
FindCompanyResponse,
} from '~/db/types/company.types';
import { Company, CompanyFilterInput } from '~/generated/graphql';
import { CREATE_COMPANY } from '~/graphql/company/mutations';
import { FIND_COMPANY } from '~/graphql/company/queries';
import { callMutation, callQuery } from '../utils/requestDb';
import { isDefined } from 'twenty-shared/utils';
export const fetchCompany = async (
companyfilerInput: CompanyFilterInput,
): Promise<Company | null> => {
const data = await callQuery<FindCompanyResponse>(FIND_COMPANY, {
filter: {
...companyfilerInput,
},
});
if (isDefined(data?.companies.edges)) {
return data.companies.edges.length > 0
? isDefined(data.companies.edges[0].node)
? data.companies.edges[0].node
: null
: null;
}
return null;
};
export const createCompany = async (
company: CompanyInput,
): Promise<string | null> => {
const data = await callMutation<CreateCompanyResponse>(CREATE_COMPANY, {
input: company,
});
if (isDefined(data)) {
return data.createCompany.id;
}
return null;
};
@@ -1,41 +0,0 @@
import {
CreatePersonResponse,
FindPersonResponse,
PersonInput,
} from '~/db/types/person.types';
import { Person, PersonFilterInput } from '~/generated/graphql';
import { CREATE_PERSON } from '~/graphql/person/mutations';
import { FIND_PERSON } from '~/graphql/person/queries';
import { callMutation, callQuery } from '../utils/requestDb';
import { isDefined } from 'twenty-shared/utils';
export const fetchPerson = async (
personFilterData: PersonFilterInput,
): Promise<Person | null> => {
const data = await callQuery<FindPersonResponse>(FIND_PERSON, {
filter: {
...personFilterData,
},
});
if (isDefined(data?.people.edges)) {
return data.people.edges.length > 0
? isDefined(data.people.edges[0].node)
? data.people.edges[0].node
: null
: null;
}
return null;
};
export const createPerson = async (
person: PersonInput,
): Promise<string | null> => {
const data = await callMutation<CreatePersonResponse>(CREATE_PERSON, {
input: person,
});
if (isDefined(data?.createPerson)) {
return data.createPerson.id;
}
return null;
};
@@ -1,35 +0,0 @@
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { Tokens } from '~/db/types/auth.types';
import { RENEW_TOKEN } from '~/graphql/auth/mutations';
import { isDefined } from 'twenty-shared/utils';
export const renewToken = async (
appToken: string,
): Promise<{ renewToken: { tokens: Tokens } } | null> => {
const store = await chrome.storage.local.get();
const serverUrl = `${
isDefined(store.serverBaseUrl)
? store.serverBaseUrl
: import.meta.env.VITE_SERVER_BASE_URL
}/graphql`;
// Create new client to call refresh token graphql mutation
const client = new ApolloClient({
uri: serverUrl,
cache: new InMemoryCache({}),
});
const { data } = await client.mutate({
mutation: RENEW_TOKEN,
variables: {
appToken,
},
fetchPolicy: 'network-only',
});
if (isDefined(data)) {
return data;
} else {
return null;
}
};
@@ -1,20 +0,0 @@
export type AuthToken = {
token: string;
expiresAt: Date;
};
export type ExchangeAuthCodeInput = {
authorizationCode: string;
codeVerifier?: string;
clientSecret?: string;
};
export type Tokens = {
loginToken: AuthToken;
accessOrWorkspaceAgnosticToken: AuthToken;
refreshToken: AuthToken;
};
export type ExchangeAuthCodeResponse = {
exchangeAuthorizationCode: Tokens;
};
@@ -1,10 +0,0 @@
import { Company, CompanyConnection } from '~/generated/graphql';
export type CompanyInput = Pick<
Company,
'name' | 'domainName' | 'address' | 'employees' | 'linkedinLink'
>;
export type FindCompanyResponse = {
companies: Pick<CompanyConnection, 'edges'>;
};
export type CreateCompanyResponse = { createCompany: { id: string } };
@@ -1,8 +0,0 @@
import { Person, PersonConnection } from '~/generated/graphql';
export type PersonInput = Pick<
Person,
'name' | 'city' | 'avatarUrl' | 'jobTitle' | 'linkedinLink'
>;
export type FindPersonResponse = { people: Pick<PersonConnection, 'edges'> };
export type CreatePersonResponse = { createPerson: { id: string } };
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string;
@@ -1,45 +0,0 @@
import { gql } from '@apollo/client';
export const EXCHANGE_AUTHORIZATION_CODE = gql`
mutation ExchangeAuthorizationCode(
$authorizationCode: String!
$codeVerifier: String
$clientSecret: String
) {
exchangeAuthorizationCode(
authorizationCode: $authorizationCode
codeVerifier: $codeVerifier
clientSecret: $clientSecret
) {
loginToken {
token
expiresAt
}
accessOrWorkspaceAgnosticToken {
token
expiresAt
}
refreshToken {
token
expiresAt
}
}
}
`;
export const RENEW_TOKEN = gql`
mutation RenewToken($appToken: String!) {
renewToken(appToken: $appToken) {
tokens {
accessOrWorkspaceAgnosticToken {
token
expiresAt
}
refreshToken {
token
expiresAt
}
}
}
}
`;
@@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const CREATE_COMPANY = gql`
mutation CreateOneCompany($input: CompanyCreateInput!) {
createCompany(data: $input) {
id
}
}
`;
@@ -1,18 +0,0 @@
import { gql } from '@apollo/client';
export const FIND_COMPANY = gql`
query FindCompany($filter: CompanyFilterInput!) {
companies(filter: $filter) {
edges {
node {
id
name
linkedinLink {
url
label
}
}
}
}
}
`;
@@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const CREATE_PERSON = gql`
mutation CreateOnePerson($input: PersonCreateInput!) {
createPerson(data: $input) {
id
}
}
`;
@@ -1,21 +0,0 @@
import { gql } from '@apollo/client';
export const FIND_PERSON = gql`
query FindPerson($filter: PersonFilterInput!) {
people(filter: $filter) {
edges {
node {
id
name {
firstName
lastName
}
linkedinLink {
url
label
}
}
}
}
}
`;
@@ -1,11 +0,0 @@
* {
margin: 0;
box-sizing: border-box;
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
font-size: 13px;
}
@@ -1,60 +0,0 @@
import { defineManifest } from '@crxjs/vite-plugin';
import packageData from '../package.json';
const external_sites =
process.env.VITE_MODE === 'development'
? [`https://app.twenty.com/*`, `http://localhost:3001/*`]
: [`https://app.twenty.com/*`];
export default defineManifest({
manifest_version: 3,
name: 'Twenty',
description: packageData.description,
version: packageData.version,
icons: {
16: 'logo/32-32.png',
32: 'logo/32-32.png',
48: 'logo/32-32.png',
},
action: {},
//TODO: change this to a documenation page
options_page: 'sidepanel.html',
background: {
service_worker: 'src/background/index.ts',
type: 'module',
},
content_scripts: [
{
matches: ['https://www.linkedin.com/*'],
js: [
'src/contentScript/index.ts',
'src/contentScript/insertSettingsButton.ts',
],
run_at: 'document_end',
},
],
web_accessible_resources: [
{
resources: ['sidepanel.html', 'page-inaccessible.html'],
matches: ['https://www.linkedin.com/*'],
},
],
permissions: ['activeTab', 'storage', 'identity', 'sidePanel', 'cookies'],
// setting host permissions to all http connections will allow
// for people who host on their custom domain to get access to
// extension instead of white listing individual urls
host_permissions: ['https://*/*', 'http://*/*'],
externally_connectable: {
matches: external_sites,
},
});
@@ -1,41 +0,0 @@
import { useEffect, useState } from 'react';
import Settings from '~/options/Settings';
import Sidepanel from '~/options/Sidepanel';
import { isDefined } from 'twenty-shared/utils';
const App = () => {
const [currentScreen, setCurrentScreen] = useState('');
useEffect(() => {
const setCurrentScreenState = async () => {
const store = await chrome.storage.local.get(['navigateSidepanel']);
if (isDefined(store.navigateSidepanel)) {
setCurrentScreen(store.navigateSidepanel);
}
};
setCurrentScreenState();
}, []);
useEffect(() => {
chrome.storage.local.onChanged.addListener((updatedStore) => {
if (
isDefined(updatedStore.navigateSidepanel) &&
isDefined(updatedStore.navigateSidepanel.newValue)
) {
setCurrentScreen(updatedStore.navigateSidepanel.newValue);
}
});
}, [setCurrentScreen]);
switch (currentScreen) {
case 'sidepanel':
return <Sidepanel />;
case 'settings':
return <Settings />;
default:
return <Settings />;
}
};
export default App;
@@ -1,23 +0,0 @@
import { Loader } from '@/ui/display/loader/components/Loader';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.noisy};
display: flex;
flex-direction: column;
height: 100vh;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
`;
const Loading = () => {
return (
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={64} width={64} />
<Loader />
</StyledContainer>
);
};
export default Loading;
@@ -1,64 +0,0 @@
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;
@@ -1,123 +0,0 @@
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { MainButton } from '@/ui/input/button/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { clearStore } from '~/utils/apolloClient';
import { isDefined } from 'twenty-shared/utils';
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 Settings = () => {
const [serverBaseUrl, setServerBaseUrl] = useState('');
const [clientUrl, setClientUrl] = useState('');
const [currentClientUrl, setCurrentClientUrl] = useState('');
const [currentServerUrl, setCurrentServerUrl] = useState('');
useEffect(() => {
const getState = async () => {
const store = await chrome.storage.local.get([
'serverBaseUrl',
'clientUrl',
]);
if (isDefined(store.serverBaseUrl)) {
setServerBaseUrl(store.serverBaseUrl);
setCurrentServerUrl(store.serverBaseUrl);
} else {
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
setCurrentServerUrl(import.meta.env.VITE_SERVER_BASE_URL);
}
if (isDefined(store.clientUrl)) {
setClientUrl(store.clientUrl);
setCurrentClientUrl(store.clientUrl);
} else {
setClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
setCurrentClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
}
};
void getState();
}, []);
const handleSettingsChange = () => {
chrome.storage.local.set({
serverBaseUrl,
clientUrl,
navigateSidepanel: 'sidepanel',
});
clearStore();
};
const handleCloseSettings = () => {
chrome.storage.local.set({
navigateSidepanel: 'sidepanel',
});
};
return (
<StyledWrapper>
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
<StyledActionContainer>
<TextInput
label="Client URL"
value={clientUrl}
onChange={setClientUrl}
placeholder="My client URL"
fullWidth
/>
<TextInput
label="Server URL"
value={serverBaseUrl}
onChange={setServerBaseUrl}
placeholder="My server URL"
fullWidth
/>
<MainButton
title="Done"
disabled={
currentClientUrl === clientUrl &&
currentServerUrl === serverBaseUrl
}
variant="primary"
onClick={handleSettingsChange}
fullWidth
/>
<MainButton
title="Close"
variant="secondary"
onClick={handleCloseSettings}
fullWidth
/>
</StyledActionContainer>
</StyledContainer>
</StyledWrapper>
);
};
export default Settings;
@@ -1,173 +0,0 @@
import styled from '@emotion/styled';
import { useCallback, useEffect, useRef, useState } from 'react';
import { MainButton } from '@/ui/input/button/MainButton';
import { isDefined } from 'twenty-shared/utils';
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 [isAuthenticated, setIsAuthenticated] = useState(false);
const [clientUrl, setClientUrl] = useState(
import.meta.env.VITE_FRONT_BASE_URL,
);
const iframeRef = useRef<HTMLIFrameElement>(null);
const setIframeState = useCallback(async () => {
const store = await chrome.storage.local.get([
'isAuthenticated',
'sidepanelUrl',
'clientUrl',
'accessToken',
'refreshToken',
]);
if (
store.isAuthenticated === true &&
isDefined(store.accessToken) &&
isDefined(store.refreshToken) &&
new Date(store.accessToken.expiresAt).getTime() >= Date.now()
) {
setIsAuthenticated(true);
if (isDefined(store.sidepanelUrl)) {
if (isDefined(store.clientUrl)) {
setClientUrl(`${store.clientUrl}${store.sidepanelUrl}`);
} else {
setClientUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}${store.sidepanelUrl}`,
);
}
}
} else {
chrome.storage.local.set({ isAuthenticated: false });
if (isDefined(store.clientUrl)) {
setClientUrl(store.clientUrl);
}
}
}, [setClientUrl]);
useEffect(() => {
void setIframeState();
}, [setIframeState]);
useEffect(() => {
window.addEventListener('message', async (event) => {
const store = await chrome.storage.local.get([
'clientUrl',
'accessToken',
'refreshToken',
]);
const clientUrl = isDefined(store.clientUrl)
? store.clientUrl
: import.meta.env.VITE_FRONT_BASE_URL;
if (
isDefined(store.accessToken) &&
isDefined(store.refreshToken) &&
event.origin === clientUrl &&
event.data === 'loaded'
) {
event.source?.postMessage(
{
type: 'tokens',
value: {
accessToken: {
token: store.accessToken.token,
expiresAt: store.accessToken.expiresAt,
},
refreshToken: {
token: store.refreshToken.token,
expiresAt: store.refreshToken.expiresAt,
},
},
},
clientUrl,
);
}
});
}, []);
useEffect(() => {
chrome.storage.local.onChanged.addListener(async (updatedStore) => {
if (isDefined(updatedStore.isAuthenticated)) {
if (updatedStore.isAuthenticated.newValue === true) {
setIframeState();
}
}
if (isDefined(updatedStore.sidepanelUrl)) {
if (isDefined(updatedStore.sidepanelUrl.newValue)) {
const store = await chrome.storage.local.get(['clientUrl']);
const clientUrl = isDefined(store.clientUrl)
? store.clientUrl
: import.meta.env.VITE_FRONT_BASE_URL;
iframeRef.current?.contentWindow?.postMessage(
{
type: 'navigate',
value: updatedStore.sidepanelUrl.newValue,
},
clientUrl,
);
}
}
});
}, [setIframeState]);
return isAuthenticated ? (
<StyledIframe
ref={iframeRef}
title="twenty-website"
src={clientUrl}
></StyledIframe>
) : (
<StyledWrapper>
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
<StyledActionContainer>
<MainButton
title="Connect your account"
fullWidth
onClick={() => {
window.open(clientUrl, '_blank');
}}
/>
</StyledActionContainer>
</StyledContainer>
</StyledWrapper>
);
};
export default Sidepanel;
@@ -1,20 +0,0 @@
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 App from '~/options/App';
import '~/index.css';
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
<AppThemeProvider>
<React.StrictMode>
<App />
</React.StrictMode>
</AppThemeProvider>,
);
declare module '@emotion/react' {
export interface Theme extends ThemeType {}
}
@@ -1,20 +0,0 @@
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 Loading from '~/options/Loading';
import '~/index.css';
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
<AppThemeProvider>
<React.StrictMode>
<Loading />
</React.StrictMode>
</AppThemeProvider>,
);
declare module '@emotion/react' {
export interface Theme extends ThemeType {}
}
@@ -1,38 +0,0 @@
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
const StyledLoaderContainer = styled.div`
justify-content: center;
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
width: ${({ theme }) => theme.spacing(6)};
height: ${({ theme }) => theme.spacing(3)};
border-radius: ${({ theme }) => theme.border.radius.pill};
border: 1px solid ${({ theme }) => theme.font.color.tertiary};
overflow: hidden;
`;
const StyledLoader = styled(motion.div)`
background-color: ${({ theme }) => theme.font.color.tertiary};
border-radius: ${({ theme }) => theme.border.radius.pill};
height: 8px;
width: 8px;
`;
export const Loader = () => (
<StyledLoaderContainer>
<StyledLoader
animate={{
x: [-16, 0, 16],
width: [8, 12, 8],
height: [8, 2, 8],
}}
transition={{
duration: 0.8,
times: [0, 0.15, 0.3],
repeat: Infinity,
}}
/>
</StyledLoaderContainer>
);
@@ -1,44 +0,0 @@
import styled from '@emotion/styled';
type H2TitleProps = {
title: string;
description?: string;
adornment?: React.ReactNode;
};
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledTitleContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
`;
const StyledTitle = styled.h2`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: 0;
`;
const StyledDescription = styled.h3`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin: 0;
margin-top: ${({ theme }) => theme.spacing(3)};
`;
export const H2Title = ({ title, description, adornment }: H2TitleProps) => (
<StyledContainer>
<StyledTitleContainer>
<StyledTitle>{title}</StyledTitle>
{adornment}
</StyledTitleContainer>
{description && <StyledDescription>{description}</StyledDescription>}
</StyledContainer>
);
@@ -1,85 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
export type ButtonSize = 'medium' | 'small';
export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type ButtonAccent = 'default' | 'blue' | 'danger';
export type ButtonProps = {
className?: string;
Icon?: React.ReactNode;
title?: string;
fullWidth?: boolean;
variant?: ButtonVariant;
size?: ButtonSize;
position?: ButtonPosition;
accent?: ButtonAccent;
soon?: boolean;
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
const StyledButton = styled.button<ButtonProps>`
border: 1px solid transparent;
border-radius: ${({ position, theme }) => {
switch (position) {
case 'left':
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
case 'right':
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
case 'middle':
return '0px';
case 'standalone':
return theme.border.radius.sm;
default:
return theme.border.radius.sm;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: inline-flex;
align-items: center;
font-family: ${({ theme }) => theme.font.family};
font-weight: 500;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
padding: 0 ${({ theme }) => theme.spacing(2)};
white-space: nowrap;
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
&:hover {
border-color: transparent;
filter: brightness(0.9);
}
&:focus {
outline: none;
}
`;
export const Button = ({
className,
Icon,
title,
fullWidth = false,
variant = 'primary',
size = 'medium',
position = 'standalone',
soon = false,
disabled = false,
onClick,
}: ButtonProps) => (
<StyledButton
fullWidth={fullWidth}
variant={variant}
size={size}
position={position}
disabled={soon || disabled}
className={className}
onClick={onClick}
>
{Icon && Icon}
{title}
{soon && 'Soon'}
</StyledButton>
);
@@ -1,116 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
type Variant = 'primary' | 'secondary';
type MainButtonProps = {
title: string;
fullWidth?: boolean;
width?: number;
variant?: Variant;
soon?: boolean;
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<
Pick<MainButtonProps, 'fullWidth' | 'width' | 'variant'>
>`
align-items: center;
background: ${({ theme, variant, disabled }) => {
if (disabled === true) {
return theme.background.secondary;
}
switch (variant) {
case 'primary':
return theme.background.primaryInverted;
case 'secondary':
return theme.background.primary;
default:
return theme.background.primary;
}
}};
border: 1px solid;
border-color: ${({ theme, disabled, variant }) => {
if (disabled === true) {
return theme.background.transparent.lighter;
}
switch (variant) {
case 'primary':
return theme.background.transparent.strong;
case 'secondary':
return theme.border.color.medium;
default:
return theme.background.primary;
}
}};
border-radius: ${({ theme }) => theme.border.radius.md};
${({ theme, disabled }) => {
if (disabled === true) {
return '';
}
return `box-shadow: ${theme.boxShadow.light};`;
}}
color: ${({ theme, variant, disabled }) => {
if (disabled === true) {
return theme.font.color.light;
}
switch (variant) {
case 'primary':
return theme.font.color.inverted;
case 'secondary':
return theme.font.color.primary;
default:
return theme.font.color.primary;
}
}};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
flex-direction: row;
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
outline: none;
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
width: ${({ fullWidth, width }) =>
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
${({ theme, variant }) => {
switch (variant) {
case 'secondary':
return `
&:hover {
background: ${theme.background.tertiary};
}
`;
default:
return `
&:hover {
background: ${theme.background.primaryInvertedHover}};
}
`;
}
}};
`;
export const MainButton = ({
title,
width,
fullWidth = false,
variant = 'primary',
type,
onClick,
disabled,
className,
}: MainButtonProps) => {
return (
<StyledButton
className={className}
{...{ disabled, fullWidth, width, onClick, type, variant }}
>
{title}
</StyledButton>
);
};
@@ -1,87 +0,0 @@
import styled from '@emotion/styled';
import React, { useId } from 'react';
interface TextInputProps {
label?: string;
value: string;
onChange: (value: string) => void;
fullWidth?: boolean;
error?: string;
placeholder?: string;
icon?: React.ReactNode;
}
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
display: flex;
flex-direction: column;
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledLabel = styled.label`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-transform: uppercase;
`;
const StyledInputContainer = styled.div`
display: flex;
align-items: center;
border: 1px solid ${({ theme }) => theme.color.gray30};
border-radius: 4px;
padding: 8px;
`;
const StyledIcon = styled.span`
margin-right: 8px;
`;
const StyledInput = styled.input`
flex: 1;
border: none;
outline: none;
font-size: 14px;
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
}
`;
const StyledErrorHelper = styled.div`
color: ${({ theme }) => theme.color.red};
font-size: 12px;
padding: 5px 0;
`;
const TextInput: React.FC<TextInputProps> = ({
label,
value,
onChange,
fullWidth,
error,
placeholder,
icon,
}) => {
const instanceId = useId();
return (
<StyledContainer fullWidth={fullWidth}>
{label && <StyledLabel htmlFor={instanceId}>{label}</StyledLabel>}
<StyledInputContainer>
{icon && <StyledIcon>{icon}</StyledIcon>}
<StyledInput
id={instanceId}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
/>
</StyledInputContainer>
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
</StyledContainer>
);
};
export { TextInput };
@@ -1,84 +0,0 @@
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
export type ToggleSize = 'small' | 'medium';
type ContainerProps = {
isOn: boolean;
color?: string;
toggleSize: ToggleSize;
};
const StyledContainer = styled.div<ContainerProps>`
align-items: center;
background-color: ${({ theme, isOn, color }) =>
isOn ? (color ?? theme.color.blue) : theme.background.quaternary};
border-radius: 10px;
cursor: pointer;
display: flex;
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
transition: background-color 0.3s ease;
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
`;
const StyledCircle = styled(motion.div)<{
toggleSize: ToggleSize;
}>`
background-color: ${({ theme }) => theme.background.primary};
border-radius: 50%;
height: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
width: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
`;
export type ToggleProps = {
value?: boolean;
onChange?: (value: boolean) => void;
color?: string;
toggleSize?: ToggleSize;
};
export const Toggle = ({
value,
onChange,
color,
toggleSize = 'medium',
}: ToggleProps) => {
const [isOn, setIsOn] = useState(value ?? false);
const circleVariants = {
on: { x: toggleSize === 'small' ? 10 : 14 },
off: { x: 2 },
};
const handleChange = () => {
setIsOn(!isOn);
if (isDefined(onChange)) {
onChange(!isOn);
}
};
useEffect(() => {
if (value !== isOn) {
setIsOn(value ?? false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
<StyledContainer
onClick={handleChange}
isOn={isOn}
color={color}
toggleSize={toggleSize}
>
<StyledCircle
animate={isOn ? 'on' : 'off'}
variants={circleVariants}
toggleSize={toggleSize}
/>
</StyledContainer>
);
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

@@ -1,15 +0,0 @@
import { ThemeProvider } from '@emotion/react';
import { THEME_LIGHT } from '@/ui/theme/constants/ThemeLight';
type AppThemeProviderProps = {
children: JSX.Element;
};
const AppThemeProvider: React.FC<AppThemeProviderProps> = ({ children }) => {
const theme = THEME_LIGHT;
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
};
export { AppThemeProvider };
@@ -1,10 +0,0 @@
import { COLOR } from '@/ui/theme/constants/Colors';
export const ACCENT_DARK = {
primary: COLOR.blueAccent75,
secondary: COLOR.blueAccent80,
tertiary: COLOR.blueAccent85,
quaternary: COLOR.blueAccent90,
accent3570: COLOR.blueAccent70,
accent4060: COLOR.blueAccent60,
};
@@ -1,10 +0,0 @@
import { COLOR } from '@/ui/theme/constants/Colors';
export const ACCENT_LIGHT = {
primary: COLOR.blueAccent25,
secondary: COLOR.blueAccent20,
tertiary: COLOR.blueAccent15,
quaternary: COLOR.blueAccent10,
accent3570: COLOR.blueAccent35,
accent4060: COLOR.blueAccent40,
};
@@ -1,9 +0,0 @@
export const ANIMATION = {
duration: {
instant: 0.075,
fast: 0.15,
normal: 0.3,
},
};
export type AnimationDuration = 'instant' | 'fast' | 'normal';
@@ -1,28 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
import DarkNoise from '@/ui/theme/assets/dark-noise.jpg';
import { COLOR } from '@/ui/theme/constants/Colors';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
import { RGBA } from '@/ui/theme/constants/Rgba';
export const BACKGROUND_DARK = {
noisy: `url(${DarkNoise.toString()});`,
primary: GRAY_SCALE.gray85,
secondary: GRAY_SCALE.gray80,
tertiary: GRAY_SCALE.gray75,
quaternary: GRAY_SCALE.gray70,
danger: COLOR.red80,
transparent: {
primary: RGBA(GRAY_SCALE.gray85, 0.5),
secondary: RGBA(GRAY_SCALE.gray80, 0.5),
strong: RGBA(GRAY_SCALE.gray0, 0.14),
medium: RGBA(GRAY_SCALE.gray0, 0.1),
light: RGBA(GRAY_SCALE.gray0, 0.06),
lighter: RGBA(GRAY_SCALE.gray0, 0.03),
danger: RGBA(COLOR.red, 0.08),
},
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,
};
@@ -1,28 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
import LightNoise from '@/ui/theme/assets/light-noise.png';
import { COLOR } from '@/ui/theme/constants/Colors';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
import { RGBA } from '@/ui/theme/constants/Rgba';
export const BACKGROUND_LIGHT = {
noisy: `url(${LightNoise.toString()});`,
primary: GRAY_SCALE.gray0,
secondary: GRAY_SCALE.gray10,
tertiary: GRAY_SCALE.gray15,
quaternary: GRAY_SCALE.gray20,
danger: COLOR.red10,
transparent: {
primary: RGBA(GRAY_SCALE.gray0, 0.5),
secondary: RGBA(GRAY_SCALE.gray10, 0.5),
strong: RGBA(GRAY_SCALE.gray100, 0.16),
medium: RGBA(GRAY_SCALE.gray100, 0.08),
light: RGBA(GRAY_SCALE.gray100, 0.04),
lighter: RGBA(GRAY_SCALE.gray100, 0.02),
danger: RGBA(COLOR.red, 0.08),
},
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,
};
@@ -1,4 +0,0 @@
export const BLUR = {
light: 'blur(6px)',
strong: 'blur(20px)',
};
@@ -1,10 +0,0 @@
export const BORDER_COMMON = {
radius: {
xs: '2px',
sm: '4px',
md: '8px',
xl: '20px',
pill: '999px',
rounded: '100%',
},
};
@@ -1,15 +0,0 @@
import { BORDER_COMMON } from '@/ui/theme/constants/BorderCommon';
import { COLOR } from '@/ui/theme/constants/Colors';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
export const BORDER_DARK = {
color: {
strong: GRAY_SCALE.gray55,
medium: GRAY_SCALE.gray65,
light: GRAY_SCALE.gray70,
secondaryInverted: GRAY_SCALE.gray35,
inverted: GRAY_SCALE.gray20,
danger: COLOR.red70,
},
...BORDER_COMMON,
};
@@ -1,15 +0,0 @@
import { BORDER_COMMON } from '@/ui/theme/constants/BorderCommon';
import { COLOR } from '@/ui/theme/constants/Colors';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
export const BORDER_LIGHT = {
color: {
strong: GRAY_SCALE.gray25,
medium: GRAY_SCALE.gray20,
light: GRAY_SCALE.gray15,
secondaryInverted: GRAY_SCALE.gray50,
inverted: GRAY_SCALE.gray60,
danger: COLOR.red20,
},
...BORDER_COMMON,
};
@@ -1,18 +0,0 @@
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
import { RGBA } from '@/ui/theme/constants/Rgba';
export const BOX_SHADOW_DARK = {
light: `0px 2px 4px 0px ${RGBA(
GRAY_SCALE.gray100,
0.04,
)}, 0px 0px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
strong: `2px 4px 16px 0px ${RGBA(
GRAY_SCALE.gray100,
0.16,
)}, 0px 2px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
underline: `0px 1px 0px 0px ${RGBA(GRAY_SCALE.gray100, 0.32)}`,
superHeavy: `2px 4px 16px 0px ${RGBA(
GRAY_SCALE.gray100,
0.12,
)}, 0px 2px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.04)}`,
};
@@ -1,21 +0,0 @@
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
import { RGBA } from '@/ui/theme/constants/Rgba';
export const BOX_SHADOW_LIGHT = {
light: `0px 2px 4px 0px ${RGBA(
GRAY_SCALE.gray100,
0.04,
)}, 0px 0px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
strong: `2px 4px 16px 0px ${RGBA(
GRAY_SCALE.gray100,
0.12,
)}, 0px 2px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.04)}`,
underline: `0px 1px 0px 0px ${RGBA(GRAY_SCALE.gray100, 0.32)}`,
superHeavy: `0px 0px 8px 0px ${RGBA(
GRAY_SCALE.gray100,
0.16,
)}, 0px 8px 64px -16px ${RGBA(
GRAY_SCALE.gray100,
0.48,
)}, 0px 24px 56px -16px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
};
@@ -1,7 +0,0 @@
import { MAIN_COLORS } from '@/ui/theme/constants/MainColors';
import { SECONDARY_COLORS } from '@/ui/theme/constants/SecondaryColors';
export const COLOR = {
...MAIN_COLORS,
...SECONDARY_COLORS,
};
@@ -1,17 +0,0 @@
export const FONT_COMMON = {
size: {
xxs: '0.625rem',
xs: '0.85rem',
sm: '0.92rem',
md: '1rem',
lg: '1.23rem',
xl: '1.54rem',
xxl: '1.85rem',
},
weight: {
regular: 400,
medium: 500,
semiBold: 600,
},
family: 'Inter, sans-serif',
};
@@ -1,16 +0,0 @@
import { COLOR } from '@/ui/theme/constants/Colors';
import { FONT_COMMON } from '@/ui/theme/constants/FontCommon';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
export const FONT_DARK = {
color: {
primary: GRAY_SCALE.gray20,
secondary: GRAY_SCALE.gray35,
tertiary: GRAY_SCALE.gray45,
light: GRAY_SCALE.gray50,
extraLight: GRAY_SCALE.gray55,
inverted: GRAY_SCALE.gray100,
danger: COLOR.red,
},
...FONT_COMMON,
};
@@ -1,16 +0,0 @@
import { COLOR } from '@/ui/theme/constants/Colors';
import { FONT_COMMON } from '@/ui/theme/constants/FontCommon';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
export const FONT_LIGHT = {
color: {
primary: GRAY_SCALE.gray60,
secondary: GRAY_SCALE.gray50,
tertiary: GRAY_SCALE.gray40,
light: GRAY_SCALE.gray35,
extraLight: GRAY_SCALE.gray30,
inverted: GRAY_SCALE.gray0,
danger: COLOR.red,
},
...FONT_COMMON,
};
@@ -1,22 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
export const GRAY_SCALE = {
gray100: '#000000',
gray90: '#141414',
gray85: '#171717',
gray80: '#1b1b1b',
gray75: '#1d1d1d',
gray70: '#222222',
gray65: '#292929',
gray60: '#333333',
gray55: '#4c4c4c',
gray50: '#666666',
gray45: '#818181',
gray40: '#999999',
gray35: '#b3b3b3',
gray30: '#cccccc',
gray25: '#d6d6d6',
gray20: '#ebebeb',
gray15: '#f1f1f1',
gray10: '#fcfcfc',
gray0: '#ffffff',
};
@@ -1,8 +0,0 @@
import { css } from '@emotion/react';
export const HOVER_BACKGROUND = (props: any) => css`
transition: background 0.1s ease;
&:hover {
background: ${props.theme.background.transparent.light};
}
`;
@@ -1,13 +0,0 @@
export const ICON = {
size: {
sm: 14,
md: 16,
lg: 20,
xl: 40,
},
stroke: {
sm: 1.6,
md: 2,
lg: 2.5,
},
};
@@ -1,5 +0,0 @@
import { MAIN_COLORS } from '@/ui/theme/constants/MainColors';
export const MAIN_COLOR_NAMES = Object.keys(MAIN_COLORS) as ThemeColor[];
export type ThemeColor = keyof typeof MAIN_COLORS;
@@ -1,15 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
export const MAIN_COLORS = {
green: '#55ef3c',
turquoise: '#15de8f',
sky: '#00e0ff',
blue: '#1961ed',
purple: '#915ffd',
pink: '#f54bd0',
red: '#f83e3e',
orange: '#ff7222',
yellow: '#ffd338',
gray: GRAY_SCALE.gray30,
};
@@ -1 +0,0 @@
export const MOBILE_VIEWPORT = 768;
@@ -1,7 +0,0 @@
export const MODAL = {
size: {
sm: '300px',
md: '400px',
lg: '53%',
},
};
@@ -1,9 +0,0 @@
import { css } from '@emotion/react';
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
export const OVERLAY_BACKGROUND = (props: { theme: ThemeType }) => css`
backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%);
background: ${props.theme.background.transparent.secondary};
box-shadow: ${props.theme.boxShadow.strong};
`;
@@ -1,8 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
import hexRgb from 'hex-rgb';
export const RGBA = (hex: string, alpha: number) => {
return `rgba(${hexRgb(hex, { format: 'array' })
.slice(0, -1)
.join(',')},${alpha})`;
};
@@ -1,106 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
export const SECONDARY_COLORS = {
yellow80: '#2e2a1a',
yellow70: '#453d1e',
yellow60: '#746224',
yellow50: '#b99b2e',
yellow40: '#ffe074',
yellow30: '#ffedaf',
yellow20: '#fff6d7',
yellow10: '#fffbeb',
green80: '#1d2d1b',
green70: '#23421e',
green60: '#2a5822',
green50: '#42ae31',
green40: '#88f477',
green30: '#ccfac5',
green20: '#ddfcd8',
green10: '#eefdec',
turquoise80: '#172b23',
turquoise70: '#173f2f',
turquoise60: '#166747',
turquoise50: '#16a26b',
turquoise40: '#5be8b1',
turquoise30: '#a1f2d2',
turquoise20: '#d0f8e9',
turquoise10: '#e8fcf4',
sky80: '#152b2e',
sky70: '#123f45',
sky60: '#0e6874',
sky50: '#07a4b9',
sky40: '#4de9ff',
sky30: '#99f3ff',
sky20: '#ccf9ff',
sky10: '#e5fcff',
blue80: '#171e2c',
blue70: '#172642',
blue60: '#18356d',
blue50: '#184bad',
blue40: '#5e90f2',
blue30: '#a3c0f8',
blue20: '#d1dffb',
blue10: '#e8effd',
purple80: '#231e2e',
purple70: '#2f2545',
purple60: '#483473',
purple50: '#6c49b8',
purple40: '#b28ffe',
purple30: '#d3bffe',
purple20: '#e9dfff',
purple10: '#f4efff',
pink80: '#2d1c29',
pink70: '#43213c',
pink60: '#702c61',
pink50: '#b23b98',
pink40: '#f881de',
pink30: '#fbb7ec',
pink20: '#fddbf6',
pink10: '#feedfa',
red80: '#2d1b1b',
red70: '#441f1f',
red60: '#712727',
red50: '#b43232',
red40: '#fa7878',
red30: '#fcb2b2',
red20: '#fed8d8',
red10: '#feecec',
orange80: '#2e2018',
orange70: '#452919',
orange60: '#743b1b',
orange50: '#b9571f',
orange40: '#ff9c64',
orange30: '#ffc7a7',
orange20: '#ffe3d3',
orange10: '#fff1e9',
gray80: GRAY_SCALE.gray70,
gray70: GRAY_SCALE.gray65,
gray60: GRAY_SCALE.gray55,
gray50: GRAY_SCALE.gray40,
gray40: GRAY_SCALE.gray25,
gray30: GRAY_SCALE.gray20,
gray20: GRAY_SCALE.gray15,
gray10: GRAY_SCALE.gray10,
blueAccent90: '#141a25',
blueAccent85: '#151d2e',
blueAccent80: '#152037',
blueAccent75: '#16233f',
blueAccent70: '#17294a',
blueAccent60: '#18356d',
blueAccent40: '#a3c0f8',
blueAccent35: '#c8d9fb',
blueAccent25: '#dae6fc',
blueAccent20: '#e2ecfd',
blueAccent15: '#edf2fe',
blueAccent10: '#f5f9fd',
};
@@ -1,28 +0,0 @@
import { COLOR } from '@/ui/theme/constants/Colors';
export const TAG_DARK = {
text: {
green: COLOR.green10,
turquoise: COLOR.turquoise10,
sky: COLOR.sky10,
blue: COLOR.blue10,
purple: COLOR.purple10,
pink: COLOR.pink10,
red: COLOR.red10,
orange: COLOR.orange10,
yellow: COLOR.yellow10,
gray: COLOR.gray10,
},
background: {
green: COLOR.green60,
turquoise: COLOR.turquoise60,
sky: COLOR.sky60,
blue: COLOR.blue60,
purple: COLOR.purple60,
pink: COLOR.pink60,
red: COLOR.red60,
orange: COLOR.orange60,
yellow: COLOR.yellow60,
gray: COLOR.gray60,
},
};
@@ -1,28 +0,0 @@
import { COLOR } from '@/ui/theme/constants/Colors';
export const TAG_LIGHT = {
text: {
green: COLOR.green60,
turquoise: COLOR.turquoise60,
sky: COLOR.sky60,
blue: COLOR.blue60,
purple: COLOR.purple60,
pink: COLOR.pink60,
red: COLOR.red60,
orange: COLOR.orange60,
yellow: COLOR.yellow60,
gray: COLOR.gray60,
},
background: {
green: COLOR.green20,
turquoise: COLOR.turquoise20,
sky: COLOR.sky20,
blue: COLOR.blue20,
purple: COLOR.purple20,
pink: COLOR.pink20,
red: COLOR.red20,
orange: COLOR.orange20,
yellow: COLOR.yellow20,
gray: COLOR.gray20,
},
};
@@ -1,13 +0,0 @@
export const TEXT = {
lineHeight: {
lg: 1.5,
md: 1.2,
},
iconSizeMedium: 16,
iconSizeSmall: 14,
iconStrikeLight: 1.6,
iconStrikeMedium: 2,
iconStrikeBold: 2.5,
};
@@ -1,21 +0,0 @@
import { css } from '@emotion/react';
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
export const TEXT_INPUT_STYLE = (props: { theme: ThemeType }) => css`
background-color: transparent;
border: none;
color: ${props.theme.font.color.primary};
font-family: ${props.theme.font.family};
font-size: inherit;
font-weight: inherit;
outline: none;
padding: ${props.theme.spacing(0)} ${props.theme.spacing(2)};
&::placeholder,
&::-webkit-input-placeholder {
color: ${props.theme.font.color.light};
font-family: ${props.theme.font.family};
font-weight: ${props.theme.font.weight.medium};
}
`;
@@ -1,44 +0,0 @@
/* eslint-disable @nx/workspace-no-hardcoded-colors */
import { ANIMATION } from '@/ui/theme/constants/Animation';
import { BLUR } from '@/ui/theme/constants/Blur';
import { COLOR } from '@/ui/theme/constants/Colors';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
import { ICON } from '@/ui/theme/constants/Icon';
import { MODAL } from '@/ui/theme/constants/Modal';
import { TEXT } from '@/ui/theme/constants/Text';
export const THEME_COMMON = {
color: COLOR,
grayScale: GRAY_SCALE,
icon: ICON,
modal: MODAL,
text: TEXT,
blur: BLUR,
animation: ANIMATION,
snackBar: {
success: {
background: '#16A26B',
color: '#D0F8E9',
},
error: {
background: '#B43232',
color: '#FED8D8',
},
info: {
background: COLOR.gray80,
color: GRAY_SCALE.gray0,
},
},
spacingMultiplicator: 4,
spacing: (...args: number[]) =>
args.map((multiplicator) => `${multiplicator * 4}px`).join(' '),
betweenSiblingsGap: `2px`,
table: {
horizontalCellMargin: '8px',
checkboxColumnWidth: '32px',
horizontalCellPadding: '8px',
},
rightDrawerWidth: '500px',
clickableElementBackgroundTransition: 'background 0.1s ease',
lastLayerZIndex: 2147483647,
};
@@ -1,21 +0,0 @@
import { ACCENT_DARK } from '@/ui/theme/constants/AccentDark';
import { BACKGROUND_DARK } from '@/ui/theme/constants/BackgroundDark';
import { BORDER_DARK } from '@/ui/theme/constants/BorderDark';
import { BOX_SHADOW_DARK } from '@/ui/theme/constants/BoxShadowDark';
import { FONT_DARK } from '@/ui/theme/constants/FontDark';
import { TAG_DARK } from '@/ui/theme/constants/TagDark';
import { THEME_COMMON } from '@/ui/theme/constants/ThemeCommon';
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
export const THEME_DARK: ThemeType = {
...THEME_COMMON,
...{
accent: ACCENT_DARK,
background: BACKGROUND_DARK,
border: BORDER_DARK,
tag: TAG_DARK,
boxShadow: BOX_SHADOW_DARK,
font: FONT_DARK,
name: 'dark',
},
};
@@ -1,22 +0,0 @@
import { ACCENT_LIGHT } from '@/ui/theme/constants/AccentLight';
import { BACKGROUND_LIGHT } from '@/ui/theme/constants/BackgroundLight';
import { BORDER_LIGHT } from '@/ui/theme/constants/BorderLight';
import { BOX_SHADOW_LIGHT } from '@/ui/theme/constants/BoxShadowLight';
import { FONT_LIGHT } from '@/ui/theme/constants/FontLight';
import { TAG_LIGHT } from '@/ui/theme/constants/TagLight';
import { THEME_COMMON } from '@/ui/theme/constants/ThemeCommon';
export const THEME_LIGHT = {
...THEME_COMMON,
...{
accent: ACCENT_LIGHT,
background: BACKGROUND_LIGHT,
border: BORDER_LIGHT,
tag: TAG_LIGHT,
boxShadow: BOX_SHADOW_LIGHT,
font: FONT_LIGHT,
name: 'light',
},
};
export type ThemeType = typeof THEME_LIGHT;
@@ -1,20 +0,0 @@
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 {}
}

Some files were not shown because too many files have changed in this diff Show More