Remove old frontend

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-06-13 13:38:48 -07:00
parent 2adfe50054
commit d6ba8e1072
646 changed files with 671 additions and 78623 deletions

View File

@@ -46,7 +46,7 @@ bin/probod: pkg/server/api/console/v1/schema/schema.go pkg/server/api/console/v1
.PHONY: @probo/console
@probo/console: NODE_ENV=production
@probo/console:
$(NPM) --workspace $@ run typecheck
$(NPM) --workspace $@ run check
$(NPM) --workspace $@ run build
pkg/server/api/console/v1/schema/schema.go \
@@ -60,7 +60,7 @@ help: ## Show this help
.PHONY:dev
dev: ## Start the development server
parallel -j 2 --line-buffer ::: "gow -r=false run cmd/probod/main.go" "cd apps/console2 && npm run dev"
parallel -j 2 --line-buffer ::: "gow -r=false run cmd/probod/main.go" "cd apps/console && npm run dev"
.PHONY: fmt
fmt: fmt-go ## Format Go code
@@ -90,4 +90,3 @@ stack-ps: ## List the docker stack containers
.PHONY: psql
psql: ## Open a psql shell to the postgres container
$(DOCKER_COMPOSE) exec postgres psql -U probod -d probod

View File

@@ -1,8 +0,0 @@
LIVE_RELOAD=true
POSTHOG_HOST=
POSTHOG_KEY=
FARO_PUSH_URL=
API_SERVER_HOST="http://localhost:8080"

View File

@@ -1,7 +0,0 @@
LIVE_RELOAD=
POSTHOG_HOST=
POSTHOG_KEY=
FARO_PUSH_URL=
HTTP_ENDPOINT="http://example.com"

View File

@@ -1,6 +0,0 @@
POSTHOG_HOST=
POSTHOG_KEY=
FARO_PUSH_URL=
API_SERVER_HOST=

View File

@@ -1,2 +1,25 @@
!dist/.keep
dist/
# Logs
old
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?

View File

@@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/App.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -1,32 +1,28 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
settings: {
react: {
version: "detect",
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
"react/react-in-jsx-scope": "off",
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
];
)

View File

@@ -1,47 +1,37 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Probo - Console</title>
<link rel="icon shortcut" href="/assets/favicon.ico" sizes="32x32" />
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml" />
<link
rel="icon"
href="/assets/favicon-dark.svg"
type="image/svg+xml"
media="(prefers-color-scheme: dark)"
/>
<link
rel="icon"
href="/assets/favicon-16x16.png"
type="image/png"
sizes="16x16"
/>
<link
rel="icon"
href="/assets/favicon-32x32.png"
type="image/png"
sizes="32x32"
/>
<link
rel="apple-touch-icon"
href="/assets/apple-touch-icon.png"
sizes="180x180"
/>
<head>
<meta charset="UTF-8" />
<meta name="description" content="The open-source compliance platform" />
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="/favicons/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="/favicons/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/favicons/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="/favicons/favicon-128.png" sizes="128x128" />
<meta name="application-name" content="&nbsp;" />
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
<meta name="msapplication-square70x70logo" content="/favicons/mstile-70x70.png" />
<meta name="msapplication-square150x150logo" content="/favicons/mstile-150x150.png" />
<meta name="msapplication-wide310x150logo" content="/favicons/mstile-310x150.png" />
<meta name="msapplication-square310x310logo" content="/favicons/mstile-310x310.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Probo Console</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body class="font-sans">
<div id="root"></div>
<script type="module" src="/src/App.tsx"></script>
</body>
</html>

View File

@@ -1,93 +1,61 @@
{
"name": "@probo/console",
"version": "0.0.1",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "npx node --env-file .env.development scripts/esbuild.mjs watch",
"build": "npx node --env-file .env.production scripts/esbuild.mjs",
"relay": "npx relay-compiler --output quiet-with-errors",
"relay:watch": "npx relay-compiler --output quiet-with-errors --watch",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "npx tsc --noEmit"
"dev": "vite",
"relay": "find src -type d -name \"__generated__\" -exec rm -rf {} + && npx relay-compiler ./relay.config.json",
"build": "tsc -b && vite build",
"lint": "eslint .",
"check": "tsc --noEmit -p tsconfig.app.json",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@probo/helpers": "1.0.0",
"@probo/hooks": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/react-lazy": "1.0.0",
"@probo/vendors": "^0.0.1",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.4",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1",
"@probo/ui": "1.0.0",
"@probo/vendors": "0.0.1",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.5",
"@tanstack/react-query": "^5.76.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^4.1.0",
"fuse.js": "^7.1.0",
"lucide-react": "^0.475.0",
"posthog-js": "^1.215.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet-async": "^2.0.5",
"react-markdown": "^10.1.0",
"react-relay": "^18.2.0",
"react-router": "^7.1.5",
"rehype-raw": "^7.0.0",
"relay-runtime": "^18.2.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
"minisearch": "^7.1.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-relay": "^19.0.0",
"react-router": "^7.6.0",
"relay-runtime": "^19.0.0",
"use-debounce": "^10.0.5",
"usehooks-ts": "^3.1.1",
"zod": "^3.25.17"
},
"devDependencies": {
"@babel/core": "^7.26.7",
"@babel/preset-env": "^7.26.7",
"@babel/preset-typescript": "^7.26.0",
"@eslint/js": "^9.21.0",
"@probo/esbuild-plugin-postcss": "^0.0.1",
"@probo/tsconfig": "^0.0.1",
"@tailwindcss/postcss": "^4.0.9",
"@types/babel-core": "^6.25.10",
"@types/node": "^22.13.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-relay": "^18.2.0",
"@types/relay-runtime": "^18.2.5",
"autoprefixer": "^10.4.20",
"babel-plugin-relay": "^18.2.0",
"esbuild": "^0.25.0",
"eslint": "^9.21.0",
"eslint-plugin-react": "^7.37.4",
"@eslint/js": "^9.25.0",
"@tailwindcss/vite": "^4.1.7",
"@types/node": "^22.15.21",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-relay": "^18.2.1",
"@types/relay-runtime": "^19.0.1",
"@vitejs/plugin-react": "^4.4.1",
"babel-plugin-relay": "^19.0.0",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"relay-compiler": "^18.2.0",
"tailwindcss": "^4.0.9",
"typescript": "^5.8.3",
"typescript-eslint": "^8.25.0"
},
"relay": {
"src": "src",
"language": "typescript",
"schema": "../../pkg/server/api/console/v1/schema.graphql",
"noFutureProofEnums": true,
"customScalarTypes": {
"Datetime": "string",
"CursorKey": "string"
},
"excludes": [
"**/node_modules/**",
"**/__mocks__/**",
"**/__generated__/**"
]
"graphql": "^16.11.0",
"prettier": "^3.5.3",
"relay-compiler": "^19.0.0",
"tailwindcss": "^4.1.7",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,3 +0,0 @@
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.239641 0.239641C0.559163 -0.0798804 1.0772 -0.0798804 1.39672 0.239641L4.5 3.34292L7.60326 0.239641C7.92278 -0.0798804 8.44086 -0.0798804 8.76038 0.239641C9.07985 0.559163 9.07985 1.0772 8.76038 1.39672L5.65707 4.5L8.76038 7.60326C9.07985 7.92278 9.07985 8.44086 8.76038 8.76038C8.44086 9.07985 7.92278 9.07985 7.60326 8.76038L4.5 5.65707L1.39672 8.76038C1.0772 9.07985 0.559163 9.07985 0.239641 8.76038C-0.0798804 8.44086 -0.0798804 7.92278 0.239641 7.60326L3.34292 4.5L0.239641 1.39672C-0.0798804 1.0772 -0.0798804 0.559163 0.239641 0.239641Z" fill="#6B716A"/>
</svg>

Before

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.0 MiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 625 B

After

Width:  |  Height:  |  Size: 625 B

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 945 B

After

Width:  |  Height:  |  Size: 945 B

View File

@@ -1,3 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 0C6.36819 0 6.66667 0.298477 6.66667 0.666667V6.05719L7.86193 4.86193C8.12228 4.60158 8.54439 4.60158 8.80474 4.86193C9.06509 5.12228 9.06509 5.54439 8.80474 5.80474L6.4714 8.13807C6.21106 8.39842 5.78895 8.39842 5.5286 8.13807L3.19526 5.80474C2.93491 5.54439 2.93491 5.12228 3.19526 4.86193C3.45561 4.60158 3.87772 4.60158 4.13807 4.86193L5.33333 6.05719V0.666667C5.33333 0.298477 5.63181 0 6 0ZM0.666667 7.33333C1.03486 7.33333 1.33333 7.63181 1.33333 8V9.33333C1.33333 10.0697 1.93028 10.6667 2.66667 10.6667H9.33333C10.0697 10.6667 10.6667 10.0697 10.6667 9.33333V8C10.6667 7.63181 10.9651 7.33333 11.3333 7.33333C11.7015 7.33333 12 7.63181 12 8V9.33333C12 10.8061 10.8061 12 9.33333 12H2.66667C1.19392 12 0 10.8061 0 9.33333V8C0 7.63181 0.298477 7.33333 0.666667 7.33333Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 950 B

View File

@@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.99966 1.39993C3.90702 1.39993 1.39993 3.90702 1.39993 6.99966C1.39993 10.0923 3.90701 12.5994 6.99966 12.5994C10.0923 12.5994 12.5994 10.0923 12.5994 6.99966C12.5994 3.90701 10.0923 1.39993 6.99966 1.39993ZM0 6.99966C0 3.13386 3.13386 0 6.99966 0C10.8655 0 13.9993 3.13386 13.9993 6.99966C13.9993 10.8655 10.8655 13.9993 6.99966 13.9993C3.13386 13.9993 0 10.8655 0 6.99966ZM7.69962 3.49983V6.70972L9.73947 8.74957L8.74957 9.73947L6.29969 7.28959V3.49983H7.69962Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 636 B

View File

@@ -1,5 +0,0 @@
<svg width="14" height="3" viewBox="0 0 14 3" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.59993 1.39998C5.59993 0.626783 6.22671 0 6.99991 0C7.77311 0 8.3999 0.626783 8.3999 1.39998C8.3999 2.17318 7.77311 2.79997 6.99991 2.79997C6.22671 2.79997 5.59993 2.17318 5.59993 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1999 1.39998C11.1999 0.626783 11.8266 0 12.5998 0C13.373 0 13.9998 0.626783 13.9998 1.39998C13.9998 2.17318 13.373 2.79997 12.5998 2.79997C11.8266 2.79997 11.1999 2.17318 11.1999 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1.39998C0 0.626774 0.626806 0 1.39998 0C2.17316 0 2.79997 0.626774 2.79997 1.39998C2.79997 2.17319 2.17316 2.79997 1.39998 2.79997C0.626806 2.79997 0 2.17319 0 1.39998Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 857 B

View File

@@ -1,5 +0,0 @@
<svg width="14" height="3" viewBox="0 0 14 3" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.59993 1.39998C5.59993 0.626783 6.22671 0 6.99991 0C7.77311 0 8.3999 0.626783 8.3999 1.39998C8.3999 2.17318 7.77311 2.79997 6.99991 2.79997C6.22671 2.79997 5.59993 2.17318 5.59993 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1999 1.39998C11.1999 0.626783 11.8266 0 12.5998 0C13.373 0 13.9998 0.626783 13.9998 1.39998C13.9998 2.17318 13.373 2.79997 12.5998 2.79997C11.8266 2.79997 11.1999 2.17318 11.1999 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1.39998C0 0.626774 0.626806 0 1.39998 0C2.17316 0 2.79997 0.626774 2.79997 1.39998C2.79997 2.17319 2.17316 2.79997 1.39998 2.79997C0.626806 2.79997 0 2.17319 0 1.39998Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 857 B

View File

@@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Probo - Console</title>
<link rel="icon shortcut" href="/assets/favicon.ico" sizes="32x32" />
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml" />
<link
rel="icon"
href="/assets/favicon-dark.svg"
type="image/svg+xml"
media="(prefers-color-scheme: dark)"
/>
<link
rel="icon"
href="/assets/favicon-16x16.png"
type="image/png"
sizes="16x16"
/>
<link
rel="icon"
href="/assets/favicon-32x32.png"
type="image/png"
sizes="32x32"
/>
<link
rel="apple-touch-icon"
href="/assets/apple-touch-icon.png"
sizes="180x180"
/>
<meta name="description" content="The open-source compliance platform" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/App.css" />
</head>
<body class="font-sans">
<div id="root"></div>
<script type="module" src="/App.js" defer></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,6 +1,7 @@
{
"src": "./src",
"schema": "./schema.graphql",
"schema": "../../pkg/server/api/console/v1/schema.graphql",
"language": "typescript",
"eagerEsModules": true
"eagerEsModules": true,
"noFutureProofEnums": true
}

View File

@@ -1,278 +0,0 @@
import babel from "@babel/core";
import postCssPlugin from "@probo/esbuild-plugin-postcss";
import autoprefixer from "autoprefixer";
import { execSync, spawn } from "child_process";
import * as esbuild from "esbuild";
import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import process from "node:process";
import tailwindcssPlugin from "@tailwindcss/postcss";
async function copyRecursive(src, dest) {
await fs.mkdir(dest, { recursive: true });
const files = await fs.readdir(src);
for (const file of files) {
const srcPath = path.join(src, file);
const destPath = path.join(dest, file);
const fileInfo = await fs.lstat(srcPath);
if (fileInfo.isDirectory()) {
await copyRecursive(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
}
}
}
function runRelayCompilerInWatchMode() {
const relayProcess = spawn("npm", ["run", "relay:watch"]);
relayProcess.stdout.on("data", (data) => {
console.log(`[relay] ${data}`);
});
relayProcess.stderr.on("data", (data) => {
console.error(`[relay] ${data}`);
});
relayProcess.on("error", (error) => {
console.error("Failed to start Relay compiler:", error);
});
console.log("Relay compiler started in watch mode.");
}
const envVars = Object.fromEntries(
Object.entries(process.env).map(([key, value]) => [
`process.env.${key}`,
JSON.stringify(value),
])
);
const hotReloading = {
name: "hot-reloading",
async setup(build) {
if (!process.env.LIVE_RELOAD) {
return;
}
build.initialOptions.banner = build.initialOptions.banner || {};
build.initialOptions.banner.js = `
${build.initialOptions.banner.js || ""};
if (!window.__esbuild_event_source__) {
window.__esbuild_event_source__ = true;
new EventSource('/esbuild').addEventListener('change', (e) => {
const { added, removed, updated } = JSON.parse(e.data);
if (!added.length && !removed.length && updated.length === 1) {
for (const link of document.getElementsByTagName("link")) {
const url = new URL(link.href);
if (url.host === location.host && url.pathname === updated[0]) {
const next = link.cloneNode();
next.href = updated[0] + '?' + Math.random().toString(36).slice(2);
next.onload = () => link.remove();
link.parentNode.insertBefore(next, link.nextSibling);
return;
}
}
}
location.reload();
});
}
`;
return;
},
};
const relayPlugin = {
name: "relay-compiler",
setup(build) {
build.initialOptions.loader = {
...build.initialOptions.loader,
".graphql": "text",
};
build.onLoad({ filter: /\.(ts|tsx)$/ }, async (args) => {
const source = await fs.readFile(args.path, "utf8");
const result = await babel.transformAsync(source, {
filename: args.path,
presets: [
[
"@babel/preset-env",
{
modules: false,
},
],
"@babel/preset-typescript",
],
plugins: ["relay"],
sourceMaps: true,
});
return {
contents: result.code,
loader: "default",
};
});
},
};
const htmlPlugin = {
name: 'html-plugin',
setup(build) {
build.onEnd(async (result) => {
// do not run this in watch mode
if (process.argv[2] === "watch") {
return;
}
if (result.errors.length > 0) {
return;
}
if (!result.metafile) {
console.error('Metafile not available');
return;
}
const outputs = Object.keys(result.metafile.outputs);
const jsOutputs = outputs.filter(file => file.endsWith('.js') && file.includes('/App-'));
const cssOutputs = outputs.filter(file => file.endsWith('.css') && file.includes('/App-'));
if (jsOutputs.length === 0 || cssOutputs.length === 0) {
console.error('Could not find entry point outputs');
return;
}
const jsFile = path.basename(jsOutputs[0]);
const cssFile = path.basename(cssOutputs[0]);
console.log('Found JS file:', jsFile);
console.log('Found CSS file:', cssFile);
const htmlPath = path.join(process.cwd(), 'dist', 'index.html');
let html = await fs.readFile(htmlPath, "utf8");
html = html.replace('"/App.css"', `"/${cssFile}"`);
html = html.replace('"/App.js"', `"/${jsFile}"`);
await fs.writeFile(htmlPath, html);
console.log("Successfully updated index.html with hashed filenames");
});
}
};
const defaultOptions = {
logLevel: "info",
entryPoints: ["src/App.tsx"],
bundle: true,
minify: true,
publicPath: "/",
outdir: "dist",
assetNames: "assets/[name]-[hash]",
chunkNames: "chunks/[name]-[hash]",
entryNames: "[dir]/[name]-[hash]",
splitting: true,
format: "esm",
sourcemap: "external",
metafile: true,
loader: {
".png": "file",
".jpg": "file",
".jpeg": "file",
".svg": "file",
".gif": "file",
},
plugins: [
relayPlugin,
hotReloading,
htmlPlugin,
postCssPlugin({
plugins: [tailwindcssPlugin(), autoprefixer()],
}),
],
define: {
...envVars,
},
};
await copyRecursive("public", "dist");
const [, , command] = process.argv;
if (command === "watch") {
runRelayCompilerInWatchMode();
const ctx = await esbuild.context({
...defaultOptions,
minify: false,
entryNames: "[dir]/[name]"
});
await ctx.watch();
let { host, port } = await ctx.serve({ servedir: "dist" });
http
.createServer((req, res) => {
const options = {
hostname: host,
port: port,
path: req.url,
method: req.method,
headers: req.headers,
};
const urlObj = new URL(req.url, `http://${req.headers.host}`);
const extensionPattern = /\.[^/]+$/;
const proxyReq = http.request(options, (proxyRes) => {
if (
proxyRes.statusCode === 404 &&
extensionPattern.test(urlObj.pathname)
) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 - Not Found");
return;
}
if (proxyRes.statusCode === 404) {
const indexPath = path.join(
import.meta.dirname,
"../dist",
"index.html"
);
fs.readFile(indexPath, "utf8")
.then((data) => {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(data);
})
.catch((err) => {
console.error("Error reading index.html:", err);
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("500 - Internal Server Error");
});
proxyRes.resume();
return;
}
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
});
req.pipe(proxyReq, { end: true });
})
.listen(3000, () => {
console.log(`listening on http://localhost:3000`);
});
} else {
execSync("npm run relay", { stdio: "inherit" });
await esbuild.build(defaultOptions);
}

View File

@@ -1,323 +0,0 @@
@import "tailwindcss";
@import "@radix-ui/colors/blue.css";
@import "@radix-ui/colors/blue-dark.css";
@import "@radix-ui/colors/olive.css";
@import "@radix-ui/colors/olive-alpha.css";
@import "@radix-ui/colors/olive-dark.css";
@import "@radix-ui/colors/olive-dark-alpha.css";
@import "@radix-ui/colors/lime.css";
@import "@radix-ui/colors/lime-dark.css";
@import "@radix-ui/colors/red.css";
@import "@radix-ui/colors/red-dark.css";
@import "@radix-ui/colors/red-dark-alpha.css";
@import "@radix-ui/colors/orange.css";
@import "@radix-ui/colors/orange-dark.css";
@import "./styles/document-content.css";
@plugin 'tailwindcss-animate';
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));
@theme {
/* Legacy */
/* --color-border: hsl(var(--border)); -> solid-b
--color-input: hsl(var(--input)); -> solid-b
--color-ring: hsl(var(--ring)); -> active-b
--color-background: hsl(var(--background)); -> level-0
--color-foreground: hsl(var(--foreground)); -> primary
--color-primary: hsl(var(--primary)); -> primary
--color-primary-foreground: hsl(var(--primary-foreground)); -> invert
--color-secondary: hsl(var(--secondary)); -> invert
--color-secondary-foreground: hsl(var(--secondary-foreground)); -> secondary
--color-destructive: hsl(var(--destructive)); -> danger-bg
--color-destructive-foreground: hsl(var(--destructive-foreground)); -> danger
--color-muted: hsl(var(--muted)); -> invert-bg
--color-muted-foreground: hsl(var(--muted-foreground)); -> tertiary
--color-accent: hsl(var(--accent)); -> accent-bg
--color-accent-foreground: hsl(var(--accent-foreground)); -> accent
--color-popover: hsl(var(--popover)); -> level-0
--color-popover-foreground: hsl(var(--popover-foreground)); -> primary
--color-card: hsl(var(--card)); -> level-0
--color-card-foreground: hsl(var(--card-foreground)); -> primary
--color-sidebar: hsl(var(--sidebar-background)); -> level-0
--color-sidebar-foreground: hsl(var(--sidebar-foreground)); -> secondary
--color-sidebar-primary: hsl(var(--sidebar-primary)); -> not used
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border)); -> solid-b
--color-sidebar-ring: hsl(var(--sidebar-ring)); -> active-b */
/* New */
/* Surfaces */
--color-level-0: var(--level-0);
--color-level-1: var(--level-1);
--color-level-2: var(--level-2);
--color-level-3: var(--level-3);
/* Text colors */
--color-primary: var(--primary);
--color-invert: var(--invert);
--color-secondary: var(--secondary);
--color-tertiary: var(--tertiary);
--color-quaternary: var(--quaternary);
--color-disabled: var(--disabled);
--color-accent: var(--accent);
--color-danger: var(--danger);
--color-success: var(--success);
--color-info: var(--info);
--color-warning: var(--warning);
/* Background colors */
--color-primary-bg: var(--primary-bg);
--color-invert-bg: var(--invert-bg);
--color-secondary-bg: var(--secondary-bg);
--color-tertiary-bg: var(--tertiary-bg);
--color-subtle-bg: var(--subtle-bg);
--color-highlight-bg: var(--highlight-bg);
--color-accent-bg: var(--accent-bg);
--color-active-bg: var(--active-bg);
--color-danger-bg: var(--danger-bg);
--color-danger-plain-bg: var(--danger-plain-bg);
--color-success-bg: var(--success-bg);
--color-info-bg: var(--info-bg);
--color-warning-bg: var(--warning-bg);
/* Hover */
--color-h-primary-bg: var(--h-primary-bg);
--color-h-invert-bg: var(--h-invert-bg);
--color-h-secondary-bg: var(--h-secondary-bg);
--color-h-tertiary-bg: var(--h-tertiary-bg);
--color-h-subtle-bg: var(--h-subtle-bg);
--color-h-highlight-bg: var(--h-highlight-bg);
--color-h-accent-bg: var(--h-accent-bg);
--color-h-active-bg: var(--h-active-bg);
/* Pressed */
--color-p-primary-bg: var(--p-primary-bg);
--color-p-invert-bg: var(--p-invert-bg);
--color-p-secondary-bg: var(--p-secondary-bg);
--color-p-tertiary-bg: var(--p-tertiary-bg);
--color-p-subtle-bg: var(--p-subtle-bg);
--color-p-highlight-bg: var(--p-highlight-bg);
--color-p-accent-bg: var(--p-accent-bg);
--color-p-active-bg: var(--p-active-bg);
/* Borders */
--color-solid-b: var(--solid-b);
--color-low-b: var(--low-b);
--color-mid-b: var(--mid-b);
--color-strong-b: var(--strong-b);
--color-active-b: var(--active-b);
--color-accent-b: var(--accent-b);
--color-danger-b: var(--danger-b);
--color-success-b: var(--success-b);
--color-warning-b: var(--warning-b);
--color-info-b: var(--info-b);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-mid-b, currentColor);
}
}
@layer base {
/* Legacy */
/* :root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--popover: 224 71% 4%;
--popover-foreground: 215 20.2% 65.1%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
} */
/* New */
:root {
/* Surfaces */
--level-0: #FAFAF5;
--level-1: #FFFFFF;
--level-2: #FFFFFF;
--level-3: var(--olive-12);
/* Text colors */
--primary: var(--olive-12);
--invert: var(--olive-1);
--secondary: var(--olive-11);
--tertiary: var(--olive-10);
--quaternary: var(--olive-8);
--disabled: var(--olive-7);
--accent: var(--lime-10);
--danger: var(--red-11);
--success: var(--lime-11);
--info: var(--blue-11);
--warning: var(--orange-11);
/* Background colors */
--primary-bg: var(--olive-12);
--invert-bg: #ffffff;
--secondary-bg: var(--olive-1);
--tertiary-bg: transparent;
--subtle-bg: var(--olive-a2);
--highlight-bg: var(--olive-a3);
--accent-bg: var(--olive-12);
--active-bg: var(--lime-4);
--danger-bg: var(--red-3);
--danger-plain-bg: var(--red-10);
--success-bg: var(--lime-3);
--info-bg: var(--blue-3);
--warning-bg: var(--orange-3);
/* Hover */
--h-primary-bg: #273025;
--h-invert-bg: var(--olive-2);
--h-secondary-bg: var(--olive-2);
--h-tertiary-bg: var(--olive-a2);
--h-subtle-bg: var(--olive-a3);
--h-highlight-bg: var(--olive-a4);
--h-accent-bg: #1E2C1C;
--h-active-bg: var(--lime-5);
/* Pressed */
--p-primary-bg: #30392E;
--p-invert-bg: var(--olive-3);
--p-secondary-bg: var(--olive-3);
--p-tertiary-bg: var(--olive-a3);
--p-subtle-bg: var(--olive-a4);
--p-highlight-bg: var(--olive-a5);
--p-accent-bg: #293A25;
--p-active-bg: var(--lime-6);
/* Borders */
--solid-b: var(--olive-4);
--low-b: var(--olive-a4);
--mid-b: var(--olive-a7);
--strong-b: var(--olive-a8);
--active-b: var(--olive-12);
--accent-b: var(--lime-11);
--danger-b: var(--red-10);
--success-b: var(--lime-10);
--warning-b: var(--orange-10);
--info-b: var(--blue-10);
}
.dark {
/* Surfaces */
--level-0: var(--olive-1);
--level-1: var(--olive-2);
--level-2: var(--olive-3);
--level-3: var(--olive-7);
/* Text colors */
--success: var(--lime-a11);
/* Background colors */
--primary-bg: var(--lime-10);
--invert-bg: var(--olive-1);
--secondary-bg: var(--olive-a3);
--active-bg: var(--olive-a4);
--accent-bg: var(--lime-10);
--danger-bg: var(--red-a3);
--danger-plain-bg: var(--red-a10);
--success-bg: var(--lime-a3);
--info-bg: var(--blue-a3);
--warning-bg: var(--orange-a3);
/* Hover */
--h-primary-bg: #C4F042E1;
--h-secondary-bg: var(--olive-a3);
--h-highlight-bg: var(--olive-a5);
--h-accent-bg: var(--lime-a9);
--h-active-bg: var(--olive-a5);
/* Pressed */
--p-primary-bg: #C4F042D7;
--p-secondary-bg: var(--olive-a4);
--p-highlight-bg: var(--olive-a6);
--p-accent-bg: var(--lime-a10);
--p-active-bg: var(--olive-a6);
/* Borders */
--solid-b: var(--olive-5);
--mid-b: var(--olive-a5);
--active-b: var(--olive-a12);
--warning-b: var(--orange-a10);
}
}
@layer base {
* {
@apply border-solid-b;
}
body {
@apply font-sans antialiased bg-level-0 text-primary;
}
}

View File

@@ -1,91 +1,12 @@
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { lazy } from "@probo/react-lazy";
import { StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { HelmetProvider } from "react-helmet-async";
import { RelayEnvironmentProvider } from "react-relay";
import { BrowserRouter, Route, Routes, useLocation } from "react-router";
import "./App.css";
import "./styles/document-content.css";
import ErrorBoundary from "./components/ErrorBoundary";
import AuthLayout from "./layouts/AuthLayout";
import { RelayEnvironment } from "./RelayEnvironment";
import { AuthenticationRoutes } from "./pages/authentication/Routes";
import { OrganizationsRoutes } from "./pages/organizations/Routes";
import SigningRequestsPage from "./pages/SigningRequestsPage";
import { RouterProvider } from "react-router";
import { router } from "./routes";
import { Toasts } from "@probo/ui";
posthog.init(process.env.POSTHOG_KEY!, {
api_host: process.env.POSTHOG_HOST,
session_recording: {
maskAllInputs: true,
},
loaded: (posthog) => {
if (!process.env.POSTHOG_KEY) posthog.debug();
},
});
const OrganizationSelectionPage = lazy(
() => import("./pages/OrganizationSelectionPage"),
);
function App() {
export function App() {
return (
<StrictMode>
<PostHogProvider client={posthog}>
<RelayEnvironmentProvider environment={RelayEnvironment}>
<HelmetProvider>
<BrowserRouter>
<Routes>
<Route path="/*">
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<OrganizationSelectionPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="organizations/*"
element={<OrganizationsRoutes />}
/>
<Route
path="documents/signing-requests"
element={<SigningRequestsPage />}
/>
<Route
path="*"
element={
<AuthLayout>
<AuthenticationRoutes />
</AuthLayout>
}
/>
</Route>
</Routes>
</BrowserRouter>
</HelmetProvider>
</RelayEnvironmentProvider>
</PostHogProvider>
</StrictMode>
<>
<RouterProvider router={router} />
<Toasts />
</>
);
}
function ErrorBoundaryWithLocation({
children,
}: {
children: React.ReactNode;
}) {
const location = useLocation();
return <ErrorBoundary key={location.pathname}>{children}</ErrorBoundary>;
}
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(<App />);

View File

@@ -1,123 +0,0 @@
import {
Environment,
FetchFunction,
Network,
RecordSource,
Store,
} from "relay-runtime";
import { buildEndpoint } from "./utils";
import { GraphQLError } from "graphql";
export class UnAuthenticatedError extends Error {
constructor() {
super("UNAUTHENTICATED");
this.name = "UnAuthenticatedError";
}
}
export class InternalServerError extends Error {
constructor() {
super("INTERNAL_SERVER_ERROR");
this.name = "InternalServerError";
}
}
const hasUnauthenticatedError = (error: GraphQLError) =>
error.extensions?.code == "UNAUTHENTICATED";
const fetchRelay: FetchFunction = async (
request,
variables,
_,
uploadables
) => {
const requestInit: RequestInit = {
method: "POST",
credentials: "include",
headers: {},
};
if (uploadables) {
const formData = new FormData();
formData.append(
"operations",
JSON.stringify({
operationName: request.name,
query: request.text,
variables: variables,
})
);
const uploadableMap: {
[key: string]: string[];
} = {};
Object.keys(uploadables).forEach((key, index) => {
uploadableMap[index] = [`variables.${key}`];
});
formData.append("map", JSON.stringify(uploadableMap));
Object.keys(uploadables).forEach((key, index) => {
formData.append(index.toString(), uploadables[key]);
});
requestInit.body = formData;
} else {
requestInit.headers = {
Accept:
"application/graphql-response+json; charset=utf-8, application/json; charset=utf-8",
"Content-Type": "application/json",
};
requestInit.body = JSON.stringify({
operationName: request.name,
query: request.text,
variables,
});
}
const response = await fetch(
buildEndpoint("/api/console/v1/query"),
requestInit
);
if (response.status === 500) {
throw new InternalServerError();
}
const json = await response.json();
if (json.errors) {
const errors = json.errors as GraphQLError[];
if (errors.find(hasUnauthenticatedError)) {
throw new UnAuthenticatedError();
}
throw new Error(
`Error fetching GraphQL query '${
request.name
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
json.errors
)}`
);
}
return json;
};
function createRelayEnvironment() {
const source = new RecordSource();
const store = new Store(source, {
queryCacheExpirationTime: 1 * 60 * 1000,
gcReleaseBufferSize: 20,
});
return new Environment({
network: Network.create(fetchRelay),
store,
});
}
export const RelayEnvironment: Environment = createRelayEnvironment();

View File

@@ -1,65 +0,0 @@
import * as React from "react";
import { Suspense, useEffect } from "react";
import {
graphql,
PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
} from "react-relay";
import { NavUser } from "@/components/NavUser";
import { Sidebar } from "@/components/ui/sidebar";
import type { AppSidebarQuery as AppSidebarQueryType } from "./__generated__/AppSidebarQuery.graphql";
import { OrganizationSwitcher } from "@/components/OrganizationSwitcher";
import { AppSidebarShell } from "./AppSidebarShell";
import { AppSidebarSkeleton } from "./AppSidebarSkeleton";
import { NavMain } from "./NavMain";
const AppSidebarQuery = graphql`
query AppSidebarQuery {
viewer {
id
...OrganizationSwitcher_organizations
...NavUser_viewer
}
}
`;
function AppSidebarContent({
queryRef,
...props
}: React.ComponentProps<typeof Sidebar> & {
queryRef: PreloadedQuery<AppSidebarQueryType>;
}) {
const data = usePreloadedQuery(AppSidebarQuery, queryRef);
return (
<AppSidebarShell
organizationSwitcher={
<OrganizationSwitcher organizations={data.viewer} />
}
navMain={<NavMain />}
navUser={<NavUser viewer={data.viewer} />}
{...props}
/>
);
}
export function AppSidebar(props: React.ComponentProps<typeof Sidebar>) {
const [queryRef, loadQuery] =
useQueryLoader<AppSidebarQueryType>(AppSidebarQuery);
useEffect(() => {
loadQuery({});
}, [loadQuery]);
if (!queryRef) {
return <AppSidebarSkeleton {...props} />;
}
return (
<Suspense fallback={<AppSidebarSkeleton {...props} />}>
<AppSidebarContent queryRef={queryRef} {...props} />
</Suspense>
);
}

View File

@@ -1,32 +0,0 @@
import { NavSecondary } from "./NavSecondary";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
} from "./ui/sidebar";
import { ReactNode } from "react";
interface AppSidebarShellProps extends React.ComponentProps<typeof Sidebar> {
navUser: ReactNode;
navMain: ReactNode;
organizationSwitcher: ReactNode;
}
export function AppSidebarShell({
navUser,
navMain,
organizationSwitcher,
...props
}: AppSidebarShellProps) {
return (
<Sidebar variant="sidebar" {...props}>
<SidebarHeader>{organizationSwitcher}</SidebarHeader>
<SidebarContent>
{navMain}
<NavSecondary className="mt-auto" />
</SidebarContent>
<SidebarFooter>{navUser}</SidebarFooter>
</Sidebar>
);
}

View File

@@ -1,18 +0,0 @@
import { AppSidebarShell } from "./AppSidebarShell";
import { NavMainSkeleton } from "./NavMainSkeleton";
import { NavUserSkeleton } from "./NavUserSkeleton";
import { OrganizationSwitcherSkeleton } from "./OrganizationSwitcherSkeleton";
import { Sidebar } from "./ui/sidebar";
export function AppSidebarSkeleton(
props: React.ComponentProps<typeof Sidebar>,
) {
return (
<AppSidebarShell
organizationSwitcher={<OrganizationSwitcherSkeleton />}
navMain={<NavMainSkeleton />}
navUser={<NavUserSkeleton />}
{...props}
/>
);
}

View File

@@ -1,284 +0,0 @@
.document-editor-container {
border-radius: 0.5rem;
position: relative;
line-height: 1.5;
font-weight: 400;
margin-top: 0.5rem;
border: 1px solid var(--solid-b);
display: block;
width: 100%;
}
.document-editor {
background: var(--bg-invert-bg);
position: relative;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
width: 100%;
}
.document-editor-inner {
background: #fff;
position: relative;
border-radius: 0 0 0.5rem 0.5rem;
flex: 1;
min-height: 250px;
}
.document-editor-input {
min-height: 250px;
max-height: 500px;
resize: none;
font-size: 15px;
position: relative;
tab-size: 1;
outline: 0;
padding: 15px 10px;
overflow-y: auto;
width: 100%;
display: block;
color: #000;
}
.document-editor-placeholder {
color: #999;
overflow: hidden;
position: absolute;
text-overflow: ellipsis;
top: 15px;
left: 10px;
font-size: 15px;
user-select: none;
display: inline-block;
pointer-events: none;
}
.document-editor-paragraph {
margin: 0 0 15px 0;
position: relative;
}
.document-editor .toolbar {
display: flex;
flex-wrap: wrap;
padding: 8px;
border-bottom: 1px solid hsl(var(--solid-b));
background-color: hsl(var(--invert-bg));
border-radius: 0.5rem 0.5rem 0 0;
}
.document-editor .toolbar button {
border: 0;
display: flex;
background: none;
border-radius: 4px;
padding: 8px;
cursor: pointer;
vertical-align: middle;
align-items: center;
justify-content: center;
}
.document-editor .toolbar button:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.document-editor .toolbar button.active {
background-color: rgba(0, 0, 0, 0.1);
}
.document-editor .toolbar .divider {
width: 1px;
background-color: hsl(var(--solid-b));
margin: 0 8px;
height: 24px;
}
/* Typography */
.editor-heading-h1 {
font-size: 24px;
font-weight: 700;
margin: 0 0 12px 0;
}
.editor-heading-h2 {
font-size: 20px;
font-weight: 700;
margin: 10px 0;
}
.editor-heading-h3 {
font-size: 18px;
font-weight: 600;
margin: 8px 0;
}
.editor-heading-h4 {
font-size: 16px;
font-weight: 600;
margin: 8px 0;
}
.editor-heading-h5 {
font-size: 14px;
font-weight: 600;
margin: 8px 0;
}
.editor-quote {
margin: 0;
margin-left: 20px;
padding-left: 16px;
border-left: 4px solid hsl(var(--solid-b));
color: hsl(var(--tertiary));
}
.editor-list-ol {
padding: 0;
margin: 0 0 0 16px;
list-style-type: decimal;
display: block;
counter-reset: li;
}
.editor-list-ul {
padding: 0;
margin: 0 0 0 16px;
list-style-type: disc;
display: block;
}
.editor-listitem {
margin: 8px 0 0 0;
display: list-item;
position: relative;
padding-left: 8px;
}
/* Ensure list items display consistently across browsers */
li.editor-listitem::marker {
font-weight: normal;
color: inherit;
}
/* Specifically for ordered lists */
ol.editor-list-ol > .editor-listitem {
list-style-type: decimal;
list-style-position: outside;
}
/* For nested ordered lists */
ol.editor-list-ol ol.editor-list-ol > .editor-listitem {
list-style-type: lower-alpha;
}
.editor-listitem--checked,
.editor-listitem--unchecked {
position: relative;
margin-left: 0;
padding-left: 24px;
list-style-type: none;
}
.editor-listitem--checked:before,
.editor-listitem--unchecked:before {
content: '';
width: 16px;
height: 16px;
position: absolute;
left: 0;
top: 2px;
background-size: 16px;
background-position: center;
background-repeat: no-repeat;
}
.editor-listitem--checked:before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>');
}
.editor-listitem--unchecked:before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>');
}
.editor-nested-listitem {
list-style-type: none;
}
/* Text formatting */
.editor-text-bold {
font-weight: bold;
}
.editor-text-italic {
font-style: italic;
}
.editor-text-underline {
text-decoration: underline;
}
.editor-text-strikethrough {
text-decoration: line-through;
}
.editor-text-underlineStrikethrough {
text-decoration: underline line-through;
}
.editor-text-code {
background-color: hsl(var(--invert-bg));
padding: 1px 4px;
border-radius: 4px;
font-family: monospace;
}
.editor-link {
color: hsl(var(--primary));
text-decoration: underline;
}
.editor-code {
background-color: hsl(var(--invert-bg));
font-family: monospace;
display: block;
padding: 8px 16px;
line-height: 1.6;
font-size: 13px;
border-radius: 0.5rem;
margin: 8px 0;
}
/* Code highlight */
.editor-tokenComment {
color: slategray;
}
.editor-tokenPunctuation {
color: #999;
}
.editor-tokenProperty {
color: #905;
}
.editor-tokenSelector {
color: #690;
}
.editor-tokenOperator {
color: #9a6e3a;
}
.editor-tokenAttr {
color: #07a;
}
.editor-tokenVariable {
color: #e90;
}
.editor-tokenFunction {
color: #dd4a68;
}

View File

@@ -1,66 +0,0 @@
import { HelpCircle } from "lucide-react";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { Input } from "./ui/input";
import { cn } from "@/lib/utils";
export function EditableField({
label,
value,
onChange,
type = "text",
helpText,
required,
multiline = false,
}: {
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
helpText?: string;
required?: boolean;
multiline?: boolean;
}) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor={label} className="text-sm font-medium">
{label}
{required && <span className="text-red-500">*</span>}
</Label>
{helpText && (
<div className="relative flex items-center">
<HelpCircle className="h-4 w-4 text-tertiary" />
<span className="sr-only">{helpText}</span>
</div>
)}
</div>
{multiline ? (
<Textarea
id={label}
value={value}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange(e.target.value)
}
className={cn(
"w-full resize-none",
required && !value && "border-red-500",
)}
placeholder={`Enter ${label.toLowerCase()}`}
rows={4}
/>
) : (
<Input
id={label}
type={type}
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(e.target.value)
}
className={cn("w-full", required && !value && "border-red-500")}
placeholder={`Enter ${label.toLowerCase()}`}
/>
)}
</div>
);
}

View File

@@ -1,51 +0,0 @@
import { Component, ErrorInfo, ReactNode } from "react";
import { ErrorPage } from "@/pages/ErrorPage";
import { UnAuthenticatedError } from "@/RelayEnvironment";
import { Navigate } from "react-router";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
isUnAuthenticated: boolean;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, isUnAuthenticated: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
if (error instanceof UnAuthenticatedError) {
return { hasError: true, error, isUnAuthenticated: true };
}
return { hasError: true, error, isUnAuthenticated: false };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.error("ErrorBoundary caught an error:", error);
console.error("Error info:", info.componentStack);
}
render(): ReactNode {
if (this.state.isUnAuthenticated) {
return <Navigate to="/login" replace state={{ authRequired: true }} />;
}
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return <ErrorPage error={this.state.error} />;
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -1,210 +0,0 @@
"use client";
import {
ChevronRight,
SquareCheck,
Inbox,
Store,
type LucideIcon,
Flame,
BookOpen,
FileText,
Settings,
Users,
Box,
Database
} from "lucide-react";
import { Link, useLocation, useParams } from "react-router";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
SidebarGroup,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
interface NavItem {
title: string;
url?: string;
icon: LucideIcon;
isActive?: boolean;
items?: {
title: string;
url: string;
icon: LucideIcon;
}[];
}
export function NavMain() {
const location = useLocation();
const { organizationId } = useParams();
const items: NavItem[] = getNavItems(organizationId);
if (!organizationId) {
return null;
}
const isItemActive = (item: { url?: string; items?: { url: string }[] }) => {
if (item.url && location.pathname.startsWith(item.url)) {
return true;
}
if (item.items?.length) {
return item.items.some((subItem) =>
location.pathname.startsWith(subItem.url),
);
}
return false;
};
return (
<SidebarGroup>
<SidebarMenu>
{items.map((item) => {
const active = isItemActive(item) || item.isActive;
return (
<Collapsible key={item.title} asChild defaultOpen={active}>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip={item.title}
data-active={active ? "true" : undefined}
>
<Link to={item.url ?? "#"}>
<item.icon
className={active ? "text-lime-9" : "text-lime-6"}
/>
<span className="font-medium">{item.title}</span>
</Link>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<ChevronRight />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => {
const subItemActive = location.pathname.startsWith(
subItem.url,
);
return (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
data-active={subItemActive ? "true" : undefined}
>
<Link to={subItem.url}>
<subItem.icon
className={
subItemActive
? "text-lime-9"
: "text-lime-6"
}
/>
<span className="font-medium">
{subItem.title}
</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
);
})}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
);
})}
</SidebarMenu>
</SidebarGroup>
);
}
function getNavItems(organizationId?: string): NavItem[] {
return [
{
title: "Tasks",
icon: Inbox,
url: organizationId
? `/organizations/${organizationId}/tasks`
: undefined,
},
{
title: "Measures",
icon: SquareCheck,
url: organizationId
? `/organizations/${organizationId}/measures`
: undefined,
},
{
title: "Risks",
icon: Flame,
url: organizationId
? `/organizations/${organizationId}/risks`
: undefined,
},
{
title: "Frameworks",
url: organizationId
? `/organizations/${organizationId}/frameworks`
: undefined,
icon: BookOpen,
},
{
title: "People",
url: `/organizations/${organizationId}/people`,
icon: Users,
},
{
title: "Vendors",
url: `/organizations/${organizationId}/vendors`,
icon: Store,
},
{
title: "Documents",
url: organizationId
? `/organizations/${organizationId}/documents`
: undefined,
icon: FileText,
},
{
title: "Assets",
url: organizationId
? `/organizations/${organizationId}/assets`
: undefined,
icon: Box,
},
{
title: "Data",
url: organizationId
? `/organizations/${organizationId}/data`
: undefined,
icon: Database,
},
{
title: "Settings",
url: organizationId
? `/organizations/${organizationId}/settings`
: undefined,
icon: Settings,
},
];
}

View File

@@ -1,55 +0,0 @@
"use client";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
export function NavMainSkeleton() {
return (
<SidebarGroup>
<SidebarGroupLabel className="pl-3">
<div className="py-0.5">
<div className="h-3 w-24 rounded-sm bg-subtle-bg animate-pulse" />
</div>
</SidebarGroupLabel>
<SidebarMenu className="space-y-1.5">
<SidebarMenuItem>
<SidebarMenuButton className="animate-pulse">
<div className="h-4 w-4 rounded-md bg-lime-5" />
<div className="py-[3px]">
<div className="h-3.5 w-32 rounded-md bg-subtle-bg" />
</div>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton className="animate-pulse">
<div className="h-4 w-4 rounded-md bg-lime-6" />
<div className="py-[3px]">
<div className="h-3.5 w-32 rounded-md bg-subtle-bg" />
</div>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton className="animate-pulse">
<div className="h-4 w-4 rounded-md bg-lime-6" />
<div className="py-[3px]">
<div className="h-3.5 w-32 rounded-md bg-subtle-bg" />
</div>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton className="animate-pulse">
<div className="h-4 w-4 rounded-md bg-lime-6" />
<div className="py-[3px]">
<div className="h-3.5 w-32 rounded-md bg-subtle-bg" />
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

View File

@@ -1,60 +0,0 @@
import * as React from "react";
import { LifeBuoy, Send } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
const navItems = [
{
title: "Support",
url: "mailto:support@getprobo.com",
icon: LifeBuoy,
},
{
title: "Feedback",
url: "#",
icon: Send,
},
];
export function NavSecondary(
props: React.ComponentPropsWithoutRef<typeof SidebarGroup>,
) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{navItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild={item.url !== "#"}
size="sm"
tooltip={item.title}
className={
item.url === "#" ? "opacity-50 cursor-not-allowed" : ""
}
>
{item.url !== "#" ? (
<a href={item.url}>
<item.icon />
<span className="font-medium">{item.title}</span>
</a>
) : (
<>
<item.icon />
<span className="font-medium">{item.title}</span>
</>
)}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -1,100 +0,0 @@
"use client";
import { ChevronsUpDown, LogOut } from "lucide-react";
import { graphql, useFragment } from "react-relay";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { NavUser_viewer$key } from "./__generated__/NavUser_viewer.graphql";
import { buildEndpoint } from "@/utils";
export const navUserFragment = graphql`
fragment NavUser_viewer on Viewer {
user {
id
fullName
email
}
}
`;
export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
const { isMobile } = useSidebar();
const currentUser = useFragment(navUserFragment, viewer).user;
const handleLogout = async () => {
fetch(buildEndpoint("/api/console/v1/auth/logout"), {
method: "DELETE",
credentials: "include",
}).then(() => {
window.location.href = "https://www.getprobo.com";
});
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton size="lg" variant="outline">
<Avatar className="h-9 w-9">
<AvatarFallback className="bg-highlight-bg">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{currentUser.fullName}
</span>
<span className="truncate text-xs text-secondary/70">
{currentUser.email}
</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 bg-highlight-bg">
<AvatarFallback>
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{currentUser.fullName}
</span>
<span className="truncate text-xs">{currentUser.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -1,29 +0,0 @@
"use client";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
export function NavUserSkeleton() {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
isActive
disabled
size="lg"
className="data-[state=open]:bg-accent data-[state=open]:text-accent animate-pulse"
>
<div className="bg-subtle-bg size-9 rounded-full animate-pulse" />
<div className="flex-1 space-y-[3px] animate-pulse">
<div className="h-3.5 w-20 rounded-sm bg-subtle-bg" />
<div className="h-3.5 w-30 rounded-sm bg-subtle-bg" />
</div>
<div className="ml-auto size-4 rounded-lg bg-subtle-bg" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -1,180 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { ChevronsUpDown, Plus } from "lucide-react";
import { graphql, useFragment } from "react-relay";
import { Link, useNavigate, useParams } from "react-router";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import {
OrganizationSwitcher_organizations$key,
OrganizationSwitcher_organizations$data,
} from "./__generated__/OrganizationSwitcher_organizations.graphql";
import { cn } from "@/lib/utils";
export const organizationSwitcherFragment = graphql`
fragment OrganizationSwitcher_organizations on Viewer {
organizations(first: 25)
@connection(key: "OrganizationSwitcher_organizations") {
__id
edges {
node {
id
name
logoUrl
}
}
}
}
`;
type Organization =
OrganizationSwitcher_organizations$data["organizations"]["edges"][0]["node"];
const LogoComponent = ({
org,
className,
}: {
org: Organization;
className?: string;
}) => {
if (org.logoUrl) {
return (
<img
src={org.logoUrl}
alt={org.name}
className={cn("rounded-md", className)}
/>
);
}
return org.name.substring(0, 2).toUpperCase();
};
export function OrganizationSwitcher({
organizations,
}: {
organizations: OrganizationSwitcher_organizations$key;
}) {
const { isMobile } = useSidebar();
const data = useFragment(organizationSwitcherFragment, organizations);
const navigate = useNavigate();
const { organizationId } = useParams();
const [currentOrganization, setCurrentOrganization] =
useState<Organization | null>(null);
const hasOrganizations =
data.organizations && data.organizations.edges.length > 0;
useEffect(() => {
if (hasOrganizations) {
const org = data.organizations.edges.find(
(edge) => edge.node.id === organizationId,
);
if (org) {
setCurrentOrganization(org.node);
}
}
}, [data.organizations, organizationId, hasOrganizations]);
const handleOrganizationSwitch = (org: Organization) => {
navigate(`/organizations/${org.id}`);
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
variant="ghost"
className={cn(
"gap-2.5",
!currentOrganization && "border border-dashed",
)}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-highlight-bg">
{currentOrganization ? (
<LogoComponent org={currentOrganization} className="size-8" />
) : (
<div className="size-8 bg-highlight-bg rounded-sm" />
)}
</div>
<div className="grid text-left leading-tight text-primary">
{currentOrganization ? (
<>
<span className="truncate font-medium text-lg leading-5">
{currentOrganization.name}
</span>
</>
) : (
<span className="truncate text-secondary font-medium">
Select Organization
</span>
)}
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-tertiary">
Organizations
</DropdownMenuLabel>
{hasOrganizations &&
data.organizations.edges.map((edge, index) => (
<DropdownMenuItem
key={edge.node.id}
onClick={() => handleOrganizationSwitch(edge.node)}
className={`gap-2 p-2 ${
edge.node.id === organizationId ? "bg-subtle-bg" : ""
}`}
>
<div className="flex size-6 items-center justify-center rounded-sm bg-highlight-bg">
<LogoComponent
org={edge.node}
className="size-4 shrink-0"
/>
</div>
{edge.node.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
to="/organizations/new"
className="gap-2 p-2 cursor-pointer"
>
<div className="flex size-6 items-center justify-center rounded-md border bg-secondary-bg">
<Plus className="size-4" />
</div>
<div className="font-medium text-tertiary">
Add organization
</div>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -1,31 +0,0 @@
"use client";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
export function OrganizationSwitcherSkeleton() {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-accent-bg data-[state=open]:text-accent gap-2.5"
>
<div className="size-8 items-center justify-center rounded-md bg-subtle-bg animate-pulse" />
<div className="flex flex-col items-start">
<div className="py-[1px]">
<div className="h-4.5 animate-pulse w-16 rounded-sm bg-subtle-bg" />
</div>
<div className="py-0.5">
<div className="h-3 animate-pulse w-8 rounded-sm bg-subtle-bg" />
</div>
</div>
<div className="ml-auto size-4 rounded-lg bg-subtle-bg animate-pulse" />
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -1,26 +0,0 @@
import { FC, ReactNode } from "react";
import { Helmet } from "react-helmet-async";
export interface PageContainerProps {
children: ReactNode;
title: string;
}
export const PageContainer: FC<PageContainerProps> = ({ children, title }) => {
return (
<>
<Helmet>{`${title} - Probo Console`}</Helmet>
<div className="container">{children}</div>
</>
);
};
export const PageContainerSkeleton: FC<{
title?: string;
children: ReactNode;
}> = ({ children, title }) =>
title ? (
<PageContainer title={title}>{children}</PageContainer>
) : (
<div className="container">{children}</div>
);

View File

@@ -1,110 +0,0 @@
import { cn } from "@/lib/utils";
import { FC, ReactNode } from "react";
export interface PageHeaderProps {
title: ReactNode;
actions?: ReactNode;
description?: ReactNode;
}
export function PageHeaderShell({
className,
title,
actions,
description,
}: PageHeaderProps & { className?: string }) {
return (
<div className={cn("space-y-6", className)}>
{actions ? (
<div className="flex items-center justify-between">
{title}
{actions}
</div>
) : (
title
)}
{description}
</div>
);
}
export function PageHeader({
className,
title,
actions,
description,
}: PageHeaderProps & { className?: string }) {
return (
<PageHeaderShell
className={className}
title={<PageHeading>{title}</PageHeading>}
description={<PageDescription>{description}</PageDescription>}
actions={actions}
/>
);
}
export function PageHeading({
children,
className,
}: {
className?: string;
children: ReactNode;
}) {
return <h1 className={cn("text-3xl font-medium", className)}>{children}</h1>;
}
const PageHeadingSkeleton: FC = () => {
return (
<div className="py-[3px] w-2/5">
<div className="bg-subtle-bg animate rounded-lg h-7.5" />
</div>
);
};
export function PageDescription({
children,
className,
}: {
className?: string;
children: ReactNode;
}) {
return (
<div className={cn("text-md text-tertiary text-left", className)}>
{children}
</div>
);
}
export function PageDescriptionSkeleton() {
return (
<div className="py-[5px]">
<div className="bg-subtle-bg animate rounded-md w-100 h-4.5" />
</div>
);
}
export const PageHeaderSkeleton: FC<{
className?: string;
title?: string;
actions?: ReactNode;
description?: ReactNode;
withDescription?: boolean;
}> = ({ className, title, actions, description, withDescription = false }) => {
return (
<PageHeaderShell
className={className}
title={
title ? <PageHeading>{title}</PageHeading> : <PageHeadingSkeleton />
}
description={
description ? (
<PageDescription>{description}</PageDescription>
) : withDescription ? (
<PageDescriptionSkeleton />
) : null
}
actions={actions}
/>
);
};

View File

@@ -1,47 +0,0 @@
import { FC, ReactNode } from "react";
import {
PageContainer,
PageContainerProps,
PageContainerSkeleton,
} from "./PageContainer";
import { PageHeader, PageHeaderProps, PageHeaderSkeleton } from "./PageHeader";
export const PageTemplate: FC<PageContainerProps & PageHeaderProps> = ({
actions,
title,
description,
children,
}) => {
return (
<PageContainer title={title}>
<PageHeader
className="mb-12"
title={title}
description={description}
actions={actions}
/>
{children}
</PageContainer>
);
};
export const PageTemplateSkeleton: FC<{
title?: string;
description?: ReactNode;
withDescription?: boolean;
actions?: ReactNode;
children: ReactNode;
}> = ({ actions, children, description, withDescription = false, title }) => {
return (
<PageContainerSkeleton title={title}>
<PageHeaderSkeleton
className="mb-17"
title={title}
description={description}
withDescription={withDescription}
actions={actions}
/>
{children}
</PageContainerSkeleton>
);
};

View File

@@ -1,74 +0,0 @@
import { useState, useEffect } from "react";
import { graphql, useFragment } from "react-relay";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PeopleSelector_organization$key } from "./__generated__/PeopleSelector_organization.graphql";
const peopleSelectorFragment = graphql`
fragment PeopleSelector_organization on Organization {
id
peoples(first: 100, orderBy: { direction: ASC, field: FULL_NAME })
@connection(key: "PeopleSelector_organization_peoples") {
edges {
node {
id
fullName
primaryEmailAddress
}
}
}
}
`;
interface PeopleSelectorProps {
organizationRef: PeopleSelector_organization$key;
selectedPersonId: string | null;
onSelect: (personId: string) => void;
placeholder?: string;
required?: boolean;
}
export default function PeopleSelector({
organizationRef,
selectedPersonId,
onSelect,
placeholder = "Select a person",
required = false,
}: PeopleSelectorProps) {
const organization = useFragment(peopleSelectorFragment, organizationRef);
const [value, setValue] = useState<string>(selectedPersonId || "");
useEffect(() => {
if (selectedPersonId) {
setValue(selectedPersonId);
}
}, [selectedPersonId]);
const handleValueChange = (newValue: string) => {
setValue(newValue);
onSelect(newValue);
};
return (
<Select value={value} onValueChange={handleValueChange} required={required}>
<SelectTrigger>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{organization.peoples?.edges?.map(
(edge) =>
edge?.node && (
<SelectItem key={edge.node.id} value={edge.node.id}>
{edge.node.fullName} ({edge.node.primaryEmailAddress})
</SelectItem>
),
)}
</SelectContent>
</Select>
);
}

View File

@@ -1,48 +0,0 @@
import { Component, ErrorInfo, ReactNode } from "react";
import { ErrorPage } from "@/pages/ErrorPage";
import { UnAuthenticatedError } from "@/RelayEnvironment";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class VisitorErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
if (error instanceof UnAuthenticatedError) {
return { hasError: false, error: undefined };
}
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.error("ErrorBoundary caught an error:", error);
console.error("Error info:", info.componentStack);
}
render(): ReactNode {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return <ErrorPage error={this.state.error} />;
}
return this.props.children;
}
}
export default VisitorErrorBoundary;

View File

@@ -1,240 +0,0 @@
/**
* @generated SignedSource<<72d01b98680bc82c2d5bdc8c2fa2d49d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type AppSidebarQuery$variables = Record<PropertyKey, never>;
export type AppSidebarQuery$data = {
readonly viewer: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"NavUser_viewer" | "OrganizationSwitcher_organizations">;
};
};
export type AppSidebarQuery = {
response: AppSidebarQuery$data;
variables: AppSidebarQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = [
{
"kind": "Literal",
"name": "first",
"value": 25
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "AppSidebarQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "OrganizationSwitcher_organizations"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "NavUser_viewer"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "AppSidebarQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "organizations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": "organizations(first:25)"
},
{
"alias": null,
"args": (v1/*: any*/),
"filters": null,
"handle": "connection",
"key": "OrganizationSwitcher_organizations",
"kind": "LinkedHandle",
"name": "organizations"
},
{
"alias": null,
"args": null,
"concreteType": "User",
"kind": "LinkedField",
"name": "user",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "51c72d5d6b9e1d4e5e7f45e7280a0534",
"id": null,
"metadata": {},
"name": "AppSidebarQuery",
"operationKind": "query",
"text": "query AppSidebarQuery {\n viewer {\n id\n ...OrganizationSwitcher_organizations\n ...NavUser_viewer\n }\n}\n\nfragment NavUser_viewer on Viewer {\n user {\n id\n fullName\n email\n }\n}\n\nfragment OrganizationSwitcher_organizations on Viewer {\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ebe1f481a1454380e017fb976b8bb8a5";
export default node;

View File

@@ -1,71 +0,0 @@
/**
* @generated SignedSource<<0d5ada0f912fcf3a8f2f3328b0943b28>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type NavUser_viewer$data = {
readonly user: {
readonly email: string;
readonly fullName: string;
readonly id: string;
};
readonly " $fragmentType": "NavUser_viewer";
};
export type NavUser_viewer$key = {
readonly " $data"?: NavUser_viewer$data;
readonly " $fragmentSpreads": FragmentRefs<"NavUser_viewer">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "NavUser_viewer",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "User",
"kind": "LinkedField",
"name": "user",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Viewer",
"abstractKey": null
};
(node as any).hash = "05e26e238976738b4cff26f37c5f29e7";
export default node;

View File

@@ -1,160 +0,0 @@
/**
* @generated SignedSource<<152991faff70c7ef6c4751fac48090be>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type OrganizationSwitcher_organizations$data = {
readonly organizations: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
}>;
};
readonly " $fragmentType": "OrganizationSwitcher_organizations";
};
export type OrganizationSwitcher_organizations$key = {
readonly " $data"?: OrganizationSwitcher_organizations$data;
readonly " $fragmentSpreads": FragmentRefs<"OrganizationSwitcher_organizations">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"organizations"
]
}
]
},
"name": "OrganizationSwitcher_organizations",
"selections": [
{
"alias": "organizations",
"args": null,
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "__OrganizationSwitcher_organizations_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
}
],
"type": "Viewer",
"abstractKey": null
};
(node as any).hash = "4bc97175bc64a6c7554a6ec865d12850";
export default node;

View File

@@ -1,161 +0,0 @@
/**
* @generated SignedSource<<a048edd52203c53c6736b800456816b0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PeopleSelector_organization$data = {
readonly id: string;
readonly peoples: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly fullName: string;
readonly id: string;
readonly primaryEmailAddress: string;
};
}>;
};
readonly " $fragmentType": "PeopleSelector_organization";
};
export type PeopleSelector_organization$key = {
readonly " $data"?: PeopleSelector_organization$data;
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"peoples"
]
}
]
},
"name": "PeopleSelector_organization",
"selections": [
(v0/*: any*/),
{
"alias": "peoples",
"args": [
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "FULL_NAME"
}
}
],
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "__PeopleSelector_organization_peoples_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PeopleEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "__PeopleSelector_organization_peoples_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
}
],
"type": "Organization",
"abstractKey": null
};
})();
(node as any).hash = "77094e387ff3e5dca822fc2360094744";
export default node;

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