forked from cerc-io/snowballtools-base
Merge branch 'main' into nv-handle-repo-exist-error
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
VITE_SERVER_URL='http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
VITE_GITHUB_CLIENT_ID=
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app"
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
||||
VITE_WALLET_CONNECT_ID=
|
||||
|
||||
VITE_LIT_RELAY_API_KEY=
|
||||
|
||||
VITE_ALCHEMY_API_KEY=
|
||||
|
||||
VITE_BUGSNAG_API_KEY=
|
||||
|
||||
VITE_PASSKEY_WALLET_RPID=
|
||||
VITE_TURNKEY_API_BASE_URL=
|
||||
VITE_TURNKEY_ORGANIZATION_ID=
|
||||
@@ -1 +0,0 @@
|
||||
build
|
||||
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 13,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint"],
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,3 +22,4 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*storybook.log
|
||||
@@ -0,0 +1 @@
|
||||
v20.12.1
|
||||
@@ -1,3 +0,0 @@
|
||||
# artifacts
|
||||
build
|
||||
coverage
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
import { join, dirname } from 'path';
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string): any {
|
||||
return dirname(require.resolve(join(value, 'package.json')));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
getAbsolutePath('@storybook/addon-onboarding'),
|
||||
getAbsolutePath('@storybook/addon-links'),
|
||||
getAbsolutePath('@storybook/addon-essentials'),
|
||||
getAbsolutePath('@chromatic-com/storybook'),
|
||||
getAbsolutePath('@storybook/addon-interactions'),
|
||||
getAbsolutePath('storybook-addon-remix-react-router'),
|
||||
],
|
||||
framework: {
|
||||
name: getAbsolutePath('@storybook/react-vite'),
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
import '../src/index.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
+22
-38
@@ -1,46 +1,30 @@
|
||||
# Getting Started with Create React App
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using [typescript-tailwindcss-eslint-prettier](https://github.com/cufarvid/cra-templates) template.
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
## Available Scripts
|
||||
Currently, two official plugins are available:
|
||||
|
||||
In the project directory, you can run:
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
### `yarn start`
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
### `yarn test`
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"projectId": "Project:663d04870db27ed66a48e466",
|
||||
"zip": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="snowball tools dashboard" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Snowball</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="500" height="500" fill="#0F86F5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,27 +1,48 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bugsnag/browser-performance": "^2.4.1",
|
||||
"@bugsnag/js": "^7.22.7",
|
||||
"@bugsnag/plugin-react": "^7.22.7",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@snowballtools/auth": "^0.2.0",
|
||||
"@snowballtools/auth-lit": "^0.2.0",
|
||||
"@snowballtools/js-sdk": "^0.1.1",
|
||||
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
|
||||
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
||||
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
||||
"@snowballtools/types": "^0.2.0",
|
||||
"@snowballtools/utils": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@turnkey/http": "^2.10.0",
|
||||
"@turnkey/sdk-react": "^0.1.0",
|
||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
||||
"@web3modal/siwe": "^4.0.5",
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
@@ -29,8 +50,9 @@
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"downshift": "^8.3.2",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"framer-motion": "^11.0.8",
|
||||
"gql-client": "^1.0.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"luxon": "^3.4.4",
|
||||
"octokit": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
@@ -43,54 +65,41 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-oauth-popup": "^1.0.5",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@chromatic-com/storybook": "^1.3.3",
|
||||
"@storybook/addon-essentials": "^8.0.10",
|
||||
"@storybook/addon-interactions": "^8.0.10",
|
||||
"@storybook/addon-links": "^8.0.10",
|
||||
"@storybook/addon-onboarding": "^8.0.10",
|
||||
"@storybook/blocks": "^8.0.10",
|
||||
"@storybook/react": "^8.0.10",
|
||||
"@storybook/react-vite": "^8.0.10",
|
||||
"@storybook/test": "^8.0.10",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"chromatic": "^11.3.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
"storybook": "^8.0.10",
|
||||
"storybook-addon-remix-react-router": "^3.0.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="197" height="2" viewBox="0 0 197 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0.5" y1="1.19141" x2="197" y2="1.19141" stroke="#94A7B8" stroke-dasharray="1 12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
File diff suppressed because it is too large
Load Diff
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
(cd /Users/rabbit-m2/p/snowball/js-sdk && NO_CLEAN=1 turbo build)
|
||||
|
||||
(cd ../.. && ./scripts/yarn-file-for-local-dev.sh)
|
||||
|
||||
rm -rf node_modules/.vite
|
||||
|
||||
yarn dev
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import Projects from './pages/org-slug';
|
||||
@@ -9,8 +8,10 @@ import {
|
||||
} from './pages/org-slug/projects/routes';
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import Index from './pages';
|
||||
import Login from './pages/Login';
|
||||
import { DashboardLayout } from 'pages/org-slug/layout';
|
||||
import AuthPage from './pages/AuthPage';
|
||||
import { DashboardLayout } from './pages/org-slug/layout';
|
||||
import { useEffect } from 'react';
|
||||
import Web3Provider from 'context/Web3Provider';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -46,12 +47,36 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
element: <AuthPage />,
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
// Hacky way of checking session
|
||||
// TODO: Handle redirect backs
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
|
||||
credentials: 'include',
|
||||
}).then((res) => {
|
||||
if (res.status !== 200) {
|
||||
localStorage.clear();
|
||||
const path = window.location.pathname;
|
||||
if (path !== '/login' && path !== '/signup') {
|
||||
window.location.pathname = '/login';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Web3Provider>
|
||||
<RouterProvider router={router} />
|
||||
</Web3Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -3,14 +3,14 @@ export default [
|
||||
id: '1',
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: 'pwa',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Image Upload PWA',
|
||||
icon: 'pwa',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = React.PropsWithChildren<{
|
||||
className?: string;
|
||||
snowZIndex?: number;
|
||||
}>;
|
||||
|
||||
export const CloudyFlow = ({ className, children, snowZIndex }: Props) => {
|
||||
return (
|
||||
<div className={`bg-sky-100 relative ${className || ''}`}>
|
||||
{children}
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ zIndex: snowZIndex || 0 }}
|
||||
>
|
||||
<div className="w-[3.72px] h-[3.72px] left-[587px] top-[147px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.72px] h-[4.72px] left-[742px] top-[336px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.49px] h-[3.49px] left-[36px] top-[68px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[55px] top-[114px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[1334px] top-[63px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.53px] h-[3.53px] left-[988px] top-[108px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.65px] h-[2.65px] left-[1380px] top-[16px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.60px] h-[3.60px] left-[1284px] top-[95px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[1191px] top-[376px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[1182px] top-[257px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.41px] h-[2.41px] left-[627px] top-[26px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.71px] h-[5.71px] left-[30px] top-[33px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.09px] h-[4.09px] left-[425px] top-[386px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.38px] h-[3.38px] left-[394px] top-[29px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.70px] h-[4.70px] left-[817px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[1194px] top-[332px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.89px] h-[4.89px] left-[811px] top-[76px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.25px] h-[4.25px] left-[458px] top-[366px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[936px] top-[46px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[64px] top-[132px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[763px] top-[10px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.67px] h-[3.67px] left-[861px] top-[106px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.62px] h-[3.62px] left-[710px] top-[278px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[1069px] top-[329px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.92px] h-[2.92px] left-[1286px] top-[299px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[219px] top-[269px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.39px] h-[2.39px] left-[817px] top-[121px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[168px] top-[320px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.94px] h-[5.94px] left-[419px] top-[244px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[604px] top-[309px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[1098px] top-[379px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.85px] h-[5.85px] left-[644px] top-[352px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[1361px] top-[349px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.84px] h-[2.84px] left-[1299px] top-[194px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.51px] h-[4.51px] left-[468px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.73px] h-[2.73px] left-[1084px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.43px] h-[3.43px] left-[1271px] top-[28px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.25px] h-[2.25px] left-[106px] top-[197px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[122px] top-[173px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[343px] top-[345px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[433px] top-[40px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[904px] top-[350px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.42px] h-[4.42px] left-[1066px] top-[349px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[904px] top-[317px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.54px] h-[5.54px] left-[501px] top-[336px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1149px] top-[206px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[235px] top-[362px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[1246px] top-[1px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[788px] top-[6px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[527px] top-[365px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.13px] h-[4.13px] left-[201px] top-[53px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[765px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1254px] top-[30px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[107px] top-[316px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.72px] h-[5.72px] left-[1305px] top-[8px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.46px] h-[5.46px] left-[102px] top-[316px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[1322px] top-[334px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[1370px] top-[317px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.55px] h-[5.55px] left-[945px] top-[258px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.24px] h-[2.24px] left-[266px] top-[362px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[987px] top-[156px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.46px] h-[3.46px] left-[10px] top-[168px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.67px] h-[5.67px] left-[441px] top-[291px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.07px] h-[4.07px] left-[962px] top-[364px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[599px] top-[293px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.41px] h-[4.41px] left-[358px] top-[163px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.31px] h-[2.31px] left-[670px] top-[182px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[621px] top-[257px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.16px] h-[2.16px] left-[48px] top-[322px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.91px] h-[5.91px] left-[491px] top-[5px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[1139px] top-[274px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[24px] top-[177px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1166px] top-[316px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5px] h-[5px] left-[445px] top-[326px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.01px] h-[3.01px] left-[438px] top-[252px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.14px] h-[4.14px] left-[554px] top-[131px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[1010px] top-[116px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[437px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[948px] top-[27px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.87px] h-[2.87px] left-[826px] top-[20px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.89px] h-[3.89px] left-[1222px] top-[112px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[796px] top-[395px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[272px] top-[103px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.12px] h-[4.12px] left-[76px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[226px] top-[276px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.03px] h-[3.03px] left-[723px] top-[197px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1259px] top-[17px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1244px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.45px] h-[4.45px] left-[118px] top-[128px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[490px] top-[204px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.93px] h-[4.93px] left-[552px] top-[38px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.56px] h-[5.56px] left-[115px] top-[303px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.35px] h-[2.35px] left-[509px] top-[278px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.24px] h-[5.24px] left-[804px] top-[389px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[1013px] top-[50px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1183px] top-[95px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[278px] top-[181px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.22px] h-[3.22px] left-[1316px] top-[282px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[736px] top-[119px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[483px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1135px] top-[19px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.64px] h-[3.64px] left-[39px] top-[126px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[237px] top-[369px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1156px] top-[126px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.78px] h-[2.78px] left-[1295px] top-[74px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[76px] top-[227px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.61px] h-[3.61px] left-[108px] top-[89px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[191px] top-[167px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.18px] h-[4.18px] left-[164px] top-[117px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.15px] h-[5.15px] left-[533px] top-[261px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[327px] top-[157px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.74px] h-[5.74px] left-[1242px] top-[122px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.22px] h-[4.22px] left-[129px] top-[265px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.30px] h-[2.30px] left-[1305px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.70px] h-[2.70px] left-[1235px] top-[120px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.15px] h-[2.15px] left-[596px] top-[103px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.17px] h-[2.17px] left-[483px] top-[233px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.09px] h-[5.09px] left-[706px] top-[188px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[141px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.20px] h-[4.20px] left-[48px] top-[124px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[1095px] top-[201px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.21px] h-[3.21px] left-[730px] top-[185px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.61px] h-[2.61px] left-[722px] top-[319px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.28px] h-[2.28px] left-[444px] top-[26px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.49px] h-[4.49px] left-[355px] top-[212px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1280px] top-[312px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.23px] h-[4.23px] left-[1114px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.48px] h-[3.48px] left-[729px] top-[117px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[647px] top-[276px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.16px] h-[4.16px] left-[365px] top-[116px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[94px] top-[194px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[2px] top-[84px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.43px] h-[4.43px] left-[1382px] top-[23px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.38px] h-[5.38px] left-[857px] top-[284px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.77px] h-[2.77px] left-[1228px] top-[385px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.65px] h-[4.65px] left-[165px] top-[184px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[568px] top-[354px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[1303px] top-[371px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[235px] top-[188px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.84px] h-[3.84px] left-[902px] top-[211px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[367px] top-[161px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[855px] top-[394px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[383px] top-[47px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.39px] h-[4.39px] left-[1313px] top-[165px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[697px] top-[327px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[646px] top-[370px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.13px] h-[3.13px] left-[728px] top-[122px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[203px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[424px] top-[121px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[1358px] top-[176px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.18px] h-[3.18px] left-[1212px] top-[24px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.23px] h-[5.23px] left-[260px] top-[217px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[1204px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.47px] h-[3.47px] left-[1163px] top-[159px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.77px] h-[5.77px] left-[1257px] top-[115px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.31px] h-[5.31px] left-[222px] top-[356px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.43px] h-[5.43px] left-[1141px] top-[349px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.62px] h-[5.62px] left-[683px] top-[81px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.91px] h-[3.91px] left-[269px] top-[3px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[305px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.41px] h-[5.41px] left-[530px] top-[94px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.64px] h-[4.64px] left-[730px] top-[301px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[716px] top-[14px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.77px] h-[4.77px] left-[544px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[357px] top-[281px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.42px] h-[2.42px] left-[1346px] top-[112px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.42px] h-[3.42px] left-[671px] top-[150px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.40px] h-[4.40px] left-[1324px] top-[268px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.21px] h-[5.21px] left-[1028px] top-[376px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.27px] h-[4.27px] left-[499px] top-[50px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.35px] h-[4.35px] left-[543px] top-[359px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.25px] h-[5.25px] left-[1245px] top-[296px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.52px] h-[5.52px] left-[360px] top-[98px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[741px] top-[358px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.90px] h-[3.90px] left-[1262px] top-[184px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.75px] h-[5.75px] left-[552px] top-[335px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.95px] h-[4.95px] left-[120px] top-[178px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1337px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.43px] h-[2.43px] left-[233px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[218px] top-[322px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.68px] h-[3.68px] left-[984px] top-[8px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[832px] top-[55px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.93px] h-[3.93px] left-[1105px] top-[209px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[957px] top-[23px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.33px] h-[2.33px] left-[1066px] top-[390px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[737px] top-[118px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.18px] h-[5.18px] left-[202px] top-[19px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.05px] h-[5.05px] left-[466px] top-[17px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[144px] top-[153px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[233px] top-[330px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1 h-1 left-[730px] top-[179px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[1156px] top-[342px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.22px] h-[5.22px] left-[1275px] top-[204px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[38px] top-[343px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.14px] h-[5.14px] left-[867px] top-[113px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.19px] h-[2.19px] left-[1277px] top-[314px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[1136px] top-[197px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[34px] top-[226px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.93px] h-[5.93px] left-[727px] top-[272px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[277px] top-[43px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
DayPicker,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverHandler,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import HorizontalLine from './HorizontalLine';
|
||||
|
||||
@@ -120,14 +120,11 @@ const DatePicker = ({
|
||||
handler={(value) => setIsOpen(value)}
|
||||
>
|
||||
<PopoverHandler>
|
||||
<Input
|
||||
onChange={() => null}
|
||||
value={inputValue}
|
||||
crossOrigin={undefined}
|
||||
/>
|
||||
<Input onChange={() => null} value={inputValue} />
|
||||
</PopoverHandler>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<PopoverContent placeholder={''}>
|
||||
{/* @ts-ignore */}
|
||||
<PopoverContent>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
@@ -152,7 +149,6 @@ const DatePicker = ({
|
||||
className="rounded-full mr-2"
|
||||
variant="outlined"
|
||||
onClick={() => setIsOpen(false)}
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -162,7 +158,6 @@ const DatePicker = ({
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
placeholder={''}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
default as ReactDropdown,
|
||||
Option as ReactDropdownOption,
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { Duration } from 'luxon';
|
||||
import React from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
const FormatMillisecond = ({ time }: { time: number }) => {
|
||||
export interface FormatMilliSecondProps
|
||||
extends ComponentPropsWithoutRef<'div'> {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
|
||||
const formatTime = Duration.fromMillis(time)
|
||||
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
||||
.toObject();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
{...props}
|
||||
className={cn('text-sm text-elements-mid-em', props?.className)}
|
||||
>
|
||||
{formatTime.days !== 0 && <span>{formatTime.days}d </span>}
|
||||
{formatTime.hours !== 0 && <span>{formatTime.hours}h </span>}
|
||||
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m </span>}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
const HorizontalLine = () => {
|
||||
return <hr className="h-px bg-gray-100 border-0" />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Heading } from './shared/Heading';
|
||||
|
||||
interface LogoProps {
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
export const Logo = ({ orgSlug }: LogoProps) => {
|
||||
return (
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<div className="flex items-center gap-3 px-0 lg:px-2">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Snowball Logo"
|
||||
className="lg:h-10 lg:w-10 h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<Heading className="lg:text-[24px] text-[19px] font-semibold">
|
||||
Snowball
|
||||
</Heading>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,8 @@ const SearchBar: React.ForwardRefRenderFunction<
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
appearance={'borderless'}
|
||||
appearance="borderless"
|
||||
className="w-full lg:w-[459px]"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { StepperNav } from 'vertical-stepper-nav';
|
||||
import { StepperNav } from './VerticalStepper';
|
||||
|
||||
const COLOR_COMPLETED = '#059669';
|
||||
const COLOR_ACTIVE = '#CFE6FC';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useStopwatch } from 'react-timer-hook';
|
||||
|
||||
import FormatMillisecond from './FormatMilliSecond';
|
||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
|
||||
const setStopWatchOffset = (time: string) => {
|
||||
const providedTime = new Date(time);
|
||||
@@ -11,13 +10,17 @@ const setStopWatchOffset = (time: string) => {
|
||||
return currentTime;
|
||||
};
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => {
|
||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds } = useStopwatch({
|
||||
autoStart: true,
|
||||
offsetTimestamp: offsetTimestamp,
|
||||
});
|
||||
|
||||
return <FormatMillisecond time={totalSeconds * 1000} />;
|
||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||
};
|
||||
|
||||
export { Stopwatch, setStopWatchOffset };
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as CSS from 'csstype';
|
||||
|
||||
//
|
||||
// Nav
|
||||
//
|
||||
export interface IStepDescription {
|
||||
stepContent: () => JSX.Element;
|
||||
stepStateColor?: string;
|
||||
stepStatusCircleSize?: number;
|
||||
onClickHandler?: () => void | undefined;
|
||||
}
|
||||
|
||||
export interface IStepperNavProps {
|
||||
steps: IStepDescription[];
|
||||
}
|
||||
|
||||
export const StepperNav = (props: IStepperNavProps): JSX.Element => {
|
||||
return (
|
||||
<nav>
|
||||
{props.steps.map(
|
||||
(
|
||||
{ stepContent, stepStateColor, onClickHandler, stepStatusCircleSize },
|
||||
index,
|
||||
) => (
|
||||
<div key={index}>
|
||||
<Step
|
||||
stepContent={stepContent}
|
||||
statusColor={stepStateColor}
|
||||
onClickHandler={onClickHandler}
|
||||
statusCircleSize={stepStatusCircleSize}
|
||||
/>
|
||||
{index !== props.steps.length - 1 && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: `${(stepStatusCircleSize ?? 16) / 2 + 1}px`,
|
||||
}}
|
||||
>
|
||||
<Separator />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Separator
|
||||
//
|
||||
const separatorStyles = {
|
||||
height: '5vh',
|
||||
width: 2,
|
||||
border: '1px solid #E1E1E1',
|
||||
background: '#E1E1E1',
|
||||
};
|
||||
|
||||
export interface ISeparator {
|
||||
height?: string | number;
|
||||
}
|
||||
|
||||
export const Separator = ({ height }: ISeparator): JSX.Element => {
|
||||
return <div style={{ ...separatorStyles, height: height ?? '5vh' }} />;
|
||||
};
|
||||
|
||||
//
|
||||
// Step
|
||||
//
|
||||
export interface IStep {
|
||||
stepContent: () => JSX.Element;
|
||||
statusColor?: string;
|
||||
statusCircleSize?: number;
|
||||
onClickHandler?: (
|
||||
event?: React.MouseEvent<HTMLDivElement>,
|
||||
) => void | undefined;
|
||||
}
|
||||
|
||||
const buttonContainerStyles: CSS.Properties = {
|
||||
display: 'inline-flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
export const Step = ({
|
||||
stepContent,
|
||||
statusColor,
|
||||
statusCircleSize,
|
||||
onClickHandler,
|
||||
}: IStep): JSX.Element => {
|
||||
const circleStyles = {
|
||||
borderRadius: statusCircleSize ?? 16,
|
||||
width: statusCircleSize ?? 16,
|
||||
height: statusCircleSize ?? 16,
|
||||
border: '2px solid #E1E1E1',
|
||||
background: statusColor ?? 'white',
|
||||
};
|
||||
|
||||
const keyDownHandler = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.keyCode === 13 || event.keyCode === 32) {
|
||||
onClickHandler?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
onClick={onClickHandler}
|
||||
onKeyDown={keyDownHandler}
|
||||
role="button"
|
||||
style={{ ...buttonContainerStyles }}
|
||||
>
|
||||
<div>
|
||||
<div style={circleStyles} />
|
||||
</div>
|
||||
<div style={{ paddingBottom: 2 }}>{stepContent()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
|
||||
interface CancelDeploymentDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
export const CancelDeploymentDialog = ({
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: CancelDeploymentDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Cancel deployment?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, cancel deployment"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
This will halt the deployment and you'll have to start the process
|
||||
from scratch.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import { Deployment, Domain } from 'gql-client';
|
||||
|
||||
import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ChevronDoubleDownIcon,
|
||||
LinkChainIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { TagProps } from 'components/shared/Tag';
|
||||
|
||||
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
||||
deployment: Deployment;
|
||||
newDeployment?: Deployment;
|
||||
domains: Domain[];
|
||||
}
|
||||
|
||||
export const ChangeStateToProductionDialog = ({
|
||||
deployment,
|
||||
newDeployment,
|
||||
domains,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: ChangeStateToProductionDialogProps) => {
|
||||
const currentChip = {
|
||||
value: 'Live Deployment',
|
||||
type: 'positive' as TagProps['type'],
|
||||
};
|
||||
const newChip = {
|
||||
value: 'New Deployment',
|
||||
type: 'attention' as TagProps['type'],
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
handleConfirm={handleConfirm}
|
||||
>
|
||||
<div className="flex flex-col gap-7">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</p>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={deployment}
|
||||
chip={newDeployment ? currentChip : undefined}
|
||||
/>
|
||||
{newDeployment && (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full text-elements-info">
|
||||
{Array.from({ length: 7 }).map((_, index) => (
|
||||
<ChevronDoubleDownIcon key={index} />
|
||||
))}
|
||||
</div>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={newDeployment}
|
||||
chip={newChip}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<p className="text-sm text-elements-high-em tracking-[-0.006em]">
|
||||
The new deployment will be associated with these domains:
|
||||
</p>
|
||||
{domains.length > 0 &&
|
||||
domains.map((value) => {
|
||||
return (
|
||||
<Button
|
||||
as="a"
|
||||
href={value.name}
|
||||
leftIcon={<LinkChainIcon size={18} />}
|
||||
variant="link"
|
||||
key={value.id}
|
||||
>
|
||||
{value.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
|
||||
interface DeleteDomainDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
domainName: string;
|
||||
}
|
||||
|
||||
export const DeleteDomainDialog = ({
|
||||
projectName,
|
||||
domainName,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DeleteDomainDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Delete domain?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, delete domain"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Once deleted, the project{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{projectName}
|
||||
</span>{' '}
|
||||
will not be accessible from the domain{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{domainName}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
|
||||
interface DeleteVariableDialogProps extends ConfirmDialogProps {
|
||||
variableKey: string;
|
||||
}
|
||||
|
||||
export const DeleteVariableDialog = ({
|
||||
variableKey,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DeleteVariableDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Delete variable"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm delete"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-mid-em">
|
||||
Are you sure you want to delete the variable{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{variableKey}
|
||||
</span>
|
||||
?
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
|
||||
interface DeleteWebhookDialogProps extends ConfirmDialogProps {
|
||||
webhookUrl: string;
|
||||
}
|
||||
|
||||
export const DeleteWebhookDialog = ({
|
||||
webhookUrl,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DeleteWebhookDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Delete webhook?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm delete"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-mid-em">
|
||||
Are you sure you want to delete{' '}
|
||||
<span className="text-sm font-mono text-elements-high-em px-0.5">
|
||||
{webhookUrl}
|
||||
</span>
|
||||
?
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
|
||||
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
export const DisconnectRepositoryDialog = ({
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: DisconnectRepositoryDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Disconnect repository?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm disconnect"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Any data tied to your Git project may become misconfigured. Are you sure
|
||||
you want to continue?
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
|
||||
import { formatAddress } from 'utils/format';
|
||||
|
||||
interface RemoveMemberDialogProps extends ConfirmDialogProps {
|
||||
memberName: string;
|
||||
ethAddress: string;
|
||||
emailDomain: string;
|
||||
}
|
||||
|
||||
export const RemoveMemberDialog = ({
|
||||
memberName,
|
||||
ethAddress,
|
||||
emailDomain,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: RemoveMemberDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Remove member?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, remove member"
|
||||
handleConfirm={handleConfirm}
|
||||
confirmButtonProps={{ variant: 'danger' }}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Once removed, {formatAddress(memberName)} ({formatAddress(ethAddress)}@
|
||||
{emailDomain}) will not be able to access this project.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
|
||||
interface TransferProjectDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export const TransferProjectDialog = ({
|
||||
projectName,
|
||||
from,
|
||||
to,
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
...props
|
||||
}: TransferProjectDialogProps) => {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
{...props}
|
||||
dialogTitle="Transfer project?"
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, confirm transfer"
|
||||
handleConfirm={handleConfirm}
|
||||
>
|
||||
<p className="text-sm text-elements-high-em">
|
||||
Upon confirmation, your project{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{projectName}
|
||||
</span>{' '}
|
||||
will be transferred from{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{from}
|
||||
</span>{' '}
|
||||
to{' '}
|
||||
<span className="text-sm font-mono text-elements-on-secondary bg-controls-secondary rounded px-0.5">
|
||||
{to}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
@@ -9,16 +9,18 @@ export const projectCardTheme = tv({
|
||||
'flex',
|
||||
'flex-col',
|
||||
'group',
|
||||
'cursor-pointer',
|
||||
],
|
||||
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
|
||||
content: ['flex', 'flex-col', 'gap-1', 'flex-1'],
|
||||
content: ['flex', 'flex-col', 'gap-1', 'flex-1', 'overflow-hidden'],
|
||||
title: [
|
||||
'text-sm',
|
||||
'font-medium',
|
||||
'text-elements-high-em',
|
||||
'tracking-[-0.006em]',
|
||||
'truncate',
|
||||
],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
description: ['text-xs', 'text-elements-low-em', 'truncate'],
|
||||
icons: ['flex', 'items-center', 'gap-1'],
|
||||
lowerContent: [
|
||||
'transition-colors',
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
import { Project } from 'gql-client';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import {
|
||||
BranchIcon,
|
||||
ClockIcon,
|
||||
GitHubLogo,
|
||||
HorizontalDotIcon,
|
||||
WarningDiamondIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
import { ComponentPropsWithoutRef, MouseEvent, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Project } from 'gql-client';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
BranchIcon,
|
||||
ClockIcon,
|
||||
GithubLogoIcon,
|
||||
HorizontalDotIcon,
|
||||
WarningDiamondIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tooltip } from 'components/shared/Tooltip';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||
|
||||
export interface ProjectCardProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
@@ -72,7 +69,9 @@ export const ProjectCard = ({
|
||||
/>
|
||||
{/* Title and website */}
|
||||
<div className={theme.content()}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
<Tooltip content={project.name}>
|
||||
<p className={theme.title()}>{project.name}</p>
|
||||
</Tooltip>
|
||||
<p className={theme.description()}>
|
||||
{project.deployments[0]?.domain?.name ?? 'No domain'}
|
||||
</p>
|
||||
@@ -92,11 +91,9 @@ export const ProjectCard = ({
|
||||
<HorizontalDotIcon />
|
||||
</Button>
|
||||
</MenuHandler>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuItem placeholder={''}>Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500" placeholder={''}>
|
||||
Delete project
|
||||
</MenuItem>
|
||||
<MenuList>
|
||||
<MenuItem>Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500">Delete project</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
@@ -121,7 +118,7 @@ export const ProjectCard = ({
|
||||
<div className={theme.deploymentText()}>
|
||||
{hasDeployment ? (
|
||||
<>
|
||||
<GitHubLogo />
|
||||
<GithubLogoIcon />
|
||||
<span>{relativeTimeMs(project.deployments[0].createdAt)} on</span>
|
||||
<BranchIcon />
|
||||
<span>{project.deployments[0].branch}</span>
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemPrefix,
|
||||
Card,
|
||||
Typography,
|
||||
Avatar,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../SearchBar';
|
||||
import { useGQLClient } from '../../context/GQLClientContext';
|
||||
|
||||
interface ProjectsSearchProps {
|
||||
onChange?: (data: Project) => void;
|
||||
}
|
||||
|
||||
const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
} = useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
|
||||
if (onChange) {
|
||||
onChange(newSelectedItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedInputValue = useDebounce<string>(inputValue, 500);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchBar {...getInputProps()} />
|
||||
<Card
|
||||
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
|
||||
(!inputValue || !isOpen) && 'hidden'
|
||||
}`}
|
||||
placeholder={''}
|
||||
>
|
||||
<List {...getMenuProps()}>
|
||||
{items.length ? (
|
||||
<>
|
||||
<div className="p-3">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
Suggestions
|
||||
</Typography>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<ListItem
|
||||
selected={highlightedIndex === index || selectedItem === item}
|
||||
key={item.id}
|
||||
placeholder={''}
|
||||
{...getItemProps({ item, index })}
|
||||
>
|
||||
<ListItemPrefix placeholder={''}>
|
||||
<Avatar
|
||||
src={item.icon || '/gray.png'}
|
||||
variant="rounded"
|
||||
placeholder={''}
|
||||
/>
|
||||
</ListItemPrefix>
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray" placeholder={''}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
className="font-normal"
|
||||
placeholder={''}
|
||||
>
|
||||
{item.organization.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<Typography placeholder={''}>
|
||||
^ No projects matching this name
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSearchBar;
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import SearchBar from 'components/SearchBar';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
|
||||
interface ProjectSearchBarProps {
|
||||
onChange?: (data: Project) => void;
|
||||
}
|
||||
|
||||
export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
inputValue,
|
||||
} = useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
|
||||
if (onChange) {
|
||||
onChange(newSelectedItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedInputValue = useDebounce<string>(inputValue, 300);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full lg:w-fit">
|
||||
<SearchBar {...getInputProps()} />
|
||||
<div
|
||||
{...getMenuProps()}
|
||||
className={cn(
|
||||
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
|
||||
{ hidden: !inputValue || !isOpen },
|
||||
)}
|
||||
>
|
||||
{items.length ? (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-elements-mid-em text-xs font-medium">
|
||||
Suggestions
|
||||
</p>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<ProjectSearchBarItem
|
||||
{...getItemProps({ item, index })}
|
||||
key={item.id}
|
||||
item={item}
|
||||
active={highlightedIndex === index || selectedItem === item}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<ProjectSearchBarEmpty />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCombobox } from 'downshift';
|
||||
|
||||
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
onClickItem?: (data: Project) => void;
|
||||
}
|
||||
|
||||
export const ProjectSearchBarDialog = ({
|
||||
onClose,
|
||||
onClickItem,
|
||||
...props
|
||||
}: ProjectSearchBarDialogProps) => {
|
||||
const [items, setItems] = useState<Project[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<Project | null>(null);
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { getInputProps, getItemProps, inputValue, setInputValue } =
|
||||
useCombobox({
|
||||
items,
|
||||
itemToString(item) {
|
||||
return item ? item.name : '';
|
||||
},
|
||||
selectedItem,
|
||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||
if (newSelectedItem) {
|
||||
setSelectedItem(newSelectedItem);
|
||||
onClickItem?.(newSelectedItem);
|
||||
navigate(
|
||||
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedInputValue = useDebounce<string>(inputValue, 300);
|
||||
|
||||
const fetchProjects = useCallback(
|
||||
async (inputValue: string) => {
|
||||
const { searchProjects } = await client.searchProjects(inputValue);
|
||||
setItems(searchProjects);
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue) {
|
||||
fetchProjects(debouncedInputValue);
|
||||
}
|
||||
}, [fetchProjects, debouncedInputValue]);
|
||||
|
||||
const handleClose = () => {
|
||||
setInputValue('');
|
||||
setItems([]);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root {...props}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="bg-base-bg fixed inset-0 md:hidden overflow-y-auto" />
|
||||
<Dialog.Content>
|
||||
<div className="h-full flex flex-col fixed top-0 inset-0">
|
||||
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
||||
<Input
|
||||
{...getInputProps()}
|
||||
leftIcon={<SearchIcon />}
|
||||
placeholder="Search"
|
||||
appearance="borderless"
|
||||
autoFocus
|
||||
/>
|
||||
<Button iconOnly variant="ghost" onClick={handleClose}>
|
||||
<CrossIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-col gap-1 px-2 py-2">
|
||||
{items.length > 0
|
||||
? items.map((item, index) => (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-elements-mid-em text-xs font-medium">
|
||||
Suggestions
|
||||
</p>
|
||||
</div>
|
||||
<ProjectSearchBarItem
|
||||
{...getItemProps({ item, index })}
|
||||
key={item.id}
|
||||
item={item}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
: inputValue && <ProjectSearchBarEmpty />}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { InfoRoundFilledIcon } from 'components/shared/CustomIcon';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
|
||||
|
||||
export const ProjectSearchBarEmpty = ({
|
||||
className,
|
||||
...props
|
||||
}: ProjectSearchBarEmptyProps) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex items-center px-2 py-2 gap-3', className)}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning">
|
||||
<InfoRoundFilledIcon size={16} />
|
||||
</div>
|
||||
<p className="text-elements-low-em text-sm tracking-[-0.006em]">
|
||||
No projects matching this name
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||
import { OmitCommon } from 'types/common';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
|
||||
/**
|
||||
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
|
||||
* @type {MergedComponentPropsWithoutRef}
|
||||
*/
|
||||
type MergedComponentPropsWithoutRef = OmitCommon<
|
||||
ComponentPropsWithoutRef<'button'>,
|
||||
Omit<
|
||||
Overwrite<UseComboboxGetItemPropsReturnValue, Project[]>,
|
||||
'index' | 'item'
|
||||
>
|
||||
>;
|
||||
|
||||
interface ProjectSearchBarItemProps extends MergedComponentPropsWithoutRef {
|
||||
item: Project;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const ProjectSearchBarItem = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ProjectSearchBarItemProps
|
||||
>(({ item, active, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
ref={ref}
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'px-2 py-2 flex items-center gap-3 rounded-lg text-left hover:bg-base-bg-emphasized',
|
||||
{
|
||||
'bg-base-bg-emphasized': active,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
imageSrc={item.icon}
|
||||
initials={getInitials(item.name)}
|
||||
/>
|
||||
<div className="flex flex-col flex-1">
|
||||
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
|
||||
{item.name}
|
||||
</p>
|
||||
<p className="text-xs text-elements-low-em">{item.organization.name}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectSearchBarItem.displayName = 'ProjectSearchBarItem';
|
||||
|
||||
export { ProjectSearchBarItem };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ProjectSearchBar';
|
||||
export * from './ProjectSearchBarDialog';
|
||||
@@ -2,21 +2,22 @@ import React from 'react';
|
||||
import OauthPopup from 'react-oauth-popup';
|
||||
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Button } from '../../shared/Button';
|
||||
import {
|
||||
GitIcon,
|
||||
EllipsesIcon,
|
||||
SnowballIcon,
|
||||
GithubIcon,
|
||||
GitTeaIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { IconWithFrame } from 'components/shared/IconWithFrame';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
} from '../../shared/CustomIcon';
|
||||
import { useToast } from '../../shared/Toast';
|
||||
import { IconWithFrame } from '../../shared/IconWithFrame';
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import { MockConnectGitCard } from './MockConnectGitCard';
|
||||
|
||||
const SCOPES = 'repo user';
|
||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.REACT_APP_GITHUB_CLIENT_ID
|
||||
import.meta.env.VITE_GITHUB_CLIENT_ID
|
||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
||||
|
||||
interface ConnectAccountInterface {
|
||||
@@ -93,6 +94,7 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
</div>
|
||||
|
||||
{/* TODO: Add ConnectAccountTabPanel */}
|
||||
<MockConnectGitCard />
|
||||
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||
<ConnectAccountTabPanel />
|
||||
</div> */}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import { Button } from '../../shared/Button';
|
||||
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
|
||||
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
|
||||
|
||||
const TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
@@ -31,63 +32,58 @@ const Deploy = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between mb-6">
|
||||
<div>
|
||||
<h4>Deployment started ...</h4>
|
||||
<div className="flex">
|
||||
^
|
||||
<div className="space-y-7">
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<Heading as="h4" className="md:text-lg font-medium">
|
||||
Deployment started ...
|
||||
</Heading>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Cancel deployment?"
|
||||
handleOpen={handleOpen}
|
||||
open={open}
|
||||
confirmButtonTitle="Yes, Cancel deployment"
|
||||
handleConfirm={handleCancel}
|
||||
color="red"
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
leftIcon={<WarningIcon size={16} />}
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
This will halt the deployment and you will have to start the process
|
||||
from scratch.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
Cancel
|
||||
</Button>
|
||||
<CancelDeploymentDialog
|
||||
handleCancel={handleOpen}
|
||||
open={open}
|
||||
handleConfirm={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
<DeployStep
|
||||
title="Running checks"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="3"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Assigning domains"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
</div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
<DeployStep
|
||||
title="Running checks"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="3"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Assigning domains"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Collapse, Button, Typography } from '@material-tailwind/react';
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import FormatMillisecond from '../../FormatMilliSecond';
|
||||
import processLogs from '../../../assets/process-logs.json';
|
||||
import { cn } from 'utils/classnames';
|
||||
import {
|
||||
CheckRoundFilledIcon,
|
||||
ClockOutlineIcon,
|
||||
CopyIcon,
|
||||
LoaderIcon,
|
||||
MinusCircleIcon,
|
||||
PlusIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { useIntersectionObserver } from 'usehooks-ts';
|
||||
|
||||
enum DeployStatus {
|
||||
PROCESSING = 'progress',
|
||||
@@ -28,61 +39,115 @@ const DeployStep = ({
|
||||
startTime,
|
||||
processTime,
|
||||
}: DeployStepsProps) => {
|
||||
const [collapse, setCollapse] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { toast, dismiss } = useToast();
|
||||
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
||||
threshold: 1,
|
||||
});
|
||||
|
||||
const disableCollapse = status !== DeployStatus.COMPLETE;
|
||||
|
||||
return (
|
||||
<div className="border-b-2">
|
||||
<div className="flex justify-between p-2 gap-2">
|
||||
{status === DeployStatus.NOT_STARTED && <div>{step}</div>}
|
||||
{status === DeployStatus.PROCESSING && <div>O</div>}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCollapse(!collapse);
|
||||
}}
|
||||
>
|
||||
{collapse ? '-' : '+'}
|
||||
</button>
|
||||
<div className="border-b border-border-separator">
|
||||
{/* Collapisble trigger */}
|
||||
<button
|
||||
className={cn(
|
||||
'flex justify-between w-full py-5 gap-2',
|
||||
disableCollapse && 'cursor-auto',
|
||||
)}
|
||||
tabIndex={disableCollapse ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!disableCollapse) {
|
||||
setIsOpen((val) => !val);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={cn('grow flex items-center gap-3')}>
|
||||
{/* Icon */}
|
||||
<div className="w-6 h-6 grid place-content-center">
|
||||
{status === DeployStatus.NOT_STARTED && (
|
||||
<div className="grid place-content-center w-6 h-6 rounded-[48px] bg-base-bg-emphasized font-mono text-xs">
|
||||
{step}
|
||||
</div>
|
||||
)}
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<LoaderIcon className="animate-spin text-elements-link" />
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div className="text-controls-primary">
|
||||
{!isOpen && <PlusIcon size={24} />}
|
||||
{isOpen && <MinusCircleIcon size={24} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<span
|
||||
className={cn(
|
||||
'text-left text-sm md:text-base',
|
||||
status === DeployStatus.PROCESSING && 'text-elements-link',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Timer */}
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="grow">{title}</div>
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<>
|
||||
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</>
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<>
|
||||
^<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-4.5 h-4.5 grid place-content-center">
|
||||
<CheckRoundFilledIcon
|
||||
className="text-elements-success"
|
||||
size={15}
|
||||
/>
|
||||
</div>
|
||||
<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Collapse open={collapse}>
|
||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
||||
</button>
|
||||
|
||||
{/* Collapsible */}
|
||||
<Collapse open={isOpen}>
|
||||
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
|
||||
{/* Logs */}
|
||||
{processLogs.map((log, key) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
key={key}
|
||||
placeholder={''}
|
||||
>
|
||||
<p className="font-mono" key={key}>
|
||||
{log}
|
||||
</Typography>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
<div className="sticky bottom-0 left-1/2 flex justify-center">
|
||||
|
||||
{/* End of logs ref used for hiding gradient overlay */}
|
||||
<div ref={ref} />
|
||||
|
||||
{/* Overflow gradient overlay */}
|
||||
{!hideGradientOverlay && (
|
||||
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
|
||||
)}
|
||||
|
||||
{/* Copy log button */}
|
||||
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
|
||||
<Button
|
||||
size="sm"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(processLogs.join('\n'));
|
||||
toast.success('Logs copied');
|
||||
toast({
|
||||
title: 'Logs copied',
|
||||
variant: 'success',
|
||||
id: 'logs',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}}
|
||||
color="blue"
|
||||
placeholder={''}
|
||||
leftIcon={<CopyIcon size={16} />}
|
||||
>
|
||||
^ Copy log
|
||||
Copy log
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { SegmentedControls } from 'components/shared/SegmentedControls';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
GithubIcon,
|
||||
LockIcon,
|
||||
TemplateIcon,
|
||||
TemplateIconType,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
export const MockConnectGitCard = () => {
|
||||
const [segmentedControlsValue, setSegmentedControlsValue] =
|
||||
useState<string>('import');
|
||||
|
||||
const isDesktopView = useMediaQuery('(min-width: 960px)'); // lg:
|
||||
const segmentedControlsProps = isDesktopView ? {} : { size: 'sm' as const };
|
||||
|
||||
const fiveMinutesAgo = new Date(Date.now() - 1000 * 300).toISOString();
|
||||
|
||||
const SEGMENTED_CONTROLS_OPTIONS = [
|
||||
{
|
||||
label: 'Import a repository',
|
||||
value: 'import',
|
||||
},
|
||||
{
|
||||
label: 'Start with a template',
|
||||
value: 'template',
|
||||
},
|
||||
];
|
||||
|
||||
const IMPORT_CONTENT = [
|
||||
{
|
||||
full_name: 'snowball/igloo',
|
||||
updated_at: fiveMinutesAgo,
|
||||
},
|
||||
{
|
||||
full_name: 'snowball/android-sdk',
|
||||
updated_at: fiveMinutesAgo,
|
||||
visibility: 'private',
|
||||
},
|
||||
{
|
||||
full_name: 'snowball/landing-page',
|
||||
updated_at: fiveMinutesAgo,
|
||||
},
|
||||
];
|
||||
|
||||
const TEMPLATE_CONTENT = [
|
||||
{
|
||||
name: 'Web app',
|
||||
icon: 'web',
|
||||
},
|
||||
{
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: 'pwa',
|
||||
},
|
||||
{
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
},
|
||||
{
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
},
|
||||
{
|
||||
name: 'Swift',
|
||||
icon: 'swift',
|
||||
},
|
||||
];
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (segmentedControlsValue === 'import') {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 relative z-0">
|
||||
{IMPORT_CONTENT.map((repo, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<MockProjectCard {...repo} />
|
||||
{index !== IMPORT_CONTENT.length - 1 && (
|
||||
<div className="border-b border-base-border" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 relative z-0">
|
||||
{TEMPLATE_CONTENT.map((template, index) => (
|
||||
<MockTemplateCard key={index} {...template} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, [segmentedControlsValue]);
|
||||
|
||||
return (
|
||||
<div className="relative bg-base-bg shadow-card rounded-2xl px-2 py-2 w-full max-w-[560px] flex flex-col gap-2">
|
||||
{/* Content */}
|
||||
<SegmentedControls
|
||||
value={segmentedControlsValue}
|
||||
onChange={setSegmentedControlsValue}
|
||||
options={SEGMENTED_CONTROLS_OPTIONS}
|
||||
type="full-width"
|
||||
{...segmentedControlsProps}
|
||||
/>
|
||||
{renderContent}
|
||||
|
||||
{/* Shade */}
|
||||
<div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white to-transparent" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MockProjectCard = ({
|
||||
full_name,
|
||||
updated_at,
|
||||
visibility,
|
||||
}: {
|
||||
full_name: string;
|
||||
updated_at?: string;
|
||||
visibility?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative">
|
||||
{/* Icon container */}
|
||||
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
|
||||
<GithubIcon />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-1 gap-3 flex-wrap">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
|
||||
{full_name}
|
||||
</p>
|
||||
<p className="text-elements-low-em text-xs">
|
||||
{updated_at && relativeTimeISO(updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
{visibility === 'private' && (
|
||||
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
|
||||
<LockIcon />
|
||||
Private
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MockTemplateCard = ({ icon, name }: { icon: string; name: string }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-3 py-3 hover:bg-base-bg-emphasized rounded-2xl group relative cursor-default">
|
||||
{/* Icon */}
|
||||
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
|
||||
<TemplateIcon type={icon as TemplateIconType} />
|
||||
</div>
|
||||
{/* Name */}
|
||||
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeISO } from '../../../utils/time';
|
||||
import { GitRepositoryDetails } from '../../../types';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const createProject = useCallback(async () => {
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug!, {
|
||||
name: `${repository.owner!.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch!,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
|
||||
if (Boolean(addProject)) {
|
||||
setIsLoading(false);
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
toast.error('Failed to create project');
|
||||
}
|
||||
}, [client, repository]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
||||
onClick={createProject}
|
||||
>
|
||||
<div className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
|
||||
<GithubIcon />
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div>
|
||||
<span className="text-black">{repository.full_name}</span>
|
||||
{repository.visibility === 'private' && (
|
||||
<Chip
|
||||
className="normal-case inline ml-6 font-normal text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
|
||||
size="sm"
|
||||
value="Private"
|
||||
icon={<LockIcon />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm" placeholder={''}>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRepoCard;
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Spinner } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import { GitRepositoryDetails } from '../../../../types/types';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
GithubIcon,
|
||||
LockIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
}
|
||||
|
||||
export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
repository,
|
||||
}) => {
|
||||
const client = useGQLClient();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
const createProject = useCallback(async () => {
|
||||
if (!repository || !orgSlug) {
|
||||
return toast({
|
||||
id: 'missing-repository-or-org-slug',
|
||||
title: 'Repository or organization slug is missing',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug, {
|
||||
name: `${repository.owner?.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch as string,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
if (addProject) {
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error((error as Error).message);
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-start sm:items-center gap-3 px-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"
|
||||
onClick={createProject}
|
||||
>
|
||||
{/* Icon container */}
|
||||
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
|
||||
<GithubIcon />
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="flex flex-1 gap-3 flex-wrap">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
|
||||
{repository.full_name}
|
||||
</p>
|
||||
<p className="text-elements-low-em text-xs">
|
||||
{repository.updated_at && relativeTimeISO(repository.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
{repository.visibility === 'private' && (
|
||||
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
|
||||
<LockIcon />
|
||||
Private
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Right action */}
|
||||
{isLoading ? (
|
||||
<Spinner className="h-4 w-4 absolute right-3" />
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
iconOnly
|
||||
className="sm:group-hover:flex hidden absolute right-3"
|
||||
>
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ProjectRepoCard';
|
||||
@@ -1,158 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Octokit } from 'octokit';
|
||||
import assert from 'assert';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { Button, Typography, Option } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../SearchBar';
|
||||
import ProjectRepoCard from './ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
||||
import AsyncSelect from '../../shared/AsyncSelect';
|
||||
import { GithubIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
|
||||
interface RepositoryListProps {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
// TODO: Add new type for Git user when required
|
||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||
|
||||
const [repositoryDetails, setRepositoryDetails] = useState<
|
||||
GitRepositoryDetails[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserAndOrgs = async () => {
|
||||
const user = await octokit.rest.users.getAuthenticated();
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount(user.data.login);
|
||||
};
|
||||
|
||||
fetchUserAndOrgs();
|
||||
}, [octokit]);
|
||||
|
||||
const debouncedSearchedRepo = useDebounce<string>(searchedRepo, 500);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRepos = async () => {
|
||||
if (!selectedAccount || !gitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check search input and use GitHub search API
|
||||
if (debouncedSearchedRepo) {
|
||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||
|
||||
// Check if selected account is an organization
|
||||
if (selectedAccount === gitUser.login) {
|
||||
query = query + ` user:${selectedAccount}`;
|
||||
} else {
|
||||
query = query + ` org:${selectedAccount}`;
|
||||
}
|
||||
|
||||
const result = await octokit.rest.search.repos({
|
||||
q: query,
|
||||
per_page: REPOS_PER_PAGE,
|
||||
});
|
||||
|
||||
setRepositoryDetails(result.data.items);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAccount === gitUser.login) {
|
||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
per_page: REPOS_PER_PAGE,
|
||||
affiliation: 'owner',
|
||||
});
|
||||
setRepositoryDetails(result.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOrg = orgs.find((org) => org.login === selectedAccount);
|
||||
assert(selectedOrg, 'Selected org not found in list');
|
||||
|
||||
const result = await octokit.rest.repos.listForOrg({
|
||||
org: selectedOrg.login,
|
||||
per_page: REPOS_PER_PAGE,
|
||||
type: 'all',
|
||||
});
|
||||
|
||||
setRepositoryDetails(result.data);
|
||||
};
|
||||
|
||||
fetchRepos();
|
||||
}, [selectedAccount, gitUser, orgs, debouncedSearchedRepo]);
|
||||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
assert(gitUser, 'Git user is not available');
|
||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||
setSelectedAccount(gitUser.login);
|
||||
}, [gitUser]);
|
||||
|
||||
const accounts = useMemo(() => {
|
||||
if (!octokit || !gitUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [gitUser, ...orgs];
|
||||
}, [octokit, orgs, gitUser]);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex gap-2 mb-2 items-center">
|
||||
<div className="basis-1/3">
|
||||
<AsyncSelect
|
||||
value={selectedAccount}
|
||||
onChange={(value) => setSelectedAccount(value!)}
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<Option key={account.id} value={account.login}>
|
||||
<div className="flex items-center gap-2 justify-start">
|
||||
<GithubIcon /> {account.login}
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
</div>
|
||||
<div className="basis-2/3 flex-grow flex items-center">
|
||||
<SearchBar
|
||||
value={searchedRepo}
|
||||
onChange={(event) => setSearchedRepo(event.target.value)}
|
||||
placeholder="Search for repository"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{Boolean(repositoryDetails.length) ? (
|
||||
repositoryDetails.map((repo, key) => {
|
||||
return <ProjectRepoCard repository={repo} key={key} />;
|
||||
})
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Typography placeholder={''}>No repository found</Typography>
|
||||
<Button
|
||||
className="rounded-full mt-5"
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Reset filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryList;
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import assert from 'assert';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../../types/types';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
GithubIcon,
|
||||
RefreshIcon,
|
||||
SearchIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useOctokit } from 'context/OctokitContext';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
|
||||
export const RepositoryList = () => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
// TODO: Add new type for Git user when required
|
||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||
const { octokit, isAuth } = useOctokit();
|
||||
|
||||
const [repositoryDetails, setRepositoryDetails] = useState<
|
||||
GitRepositoryDetails[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserAndOrgs = async () => {
|
||||
try {
|
||||
const user = await octokit.rest.users.getAuthenticated();
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||
} catch (error) {
|
||||
// Error handled by octokit error hook interceptor in Octokit context
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (isAuth) {
|
||||
fetchUserAndOrgs();
|
||||
}
|
||||
}, [octokit, isAuth]);
|
||||
|
||||
const debouncedSearchedRepo = useDebounce<string>(searchedRepo, 500);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRepos = async () => {
|
||||
if (!selectedAccount || !gitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check search input and use GitHub search API
|
||||
if (debouncedSearchedRepo) {
|
||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||
|
||||
// Check if selected account is an organization
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
query = query + ` user:${selectedAccount}`;
|
||||
} else {
|
||||
query = query + ` org:${selectedAccount}`;
|
||||
}
|
||||
|
||||
const result = await octokit.rest.search.repos({
|
||||
q: query,
|
||||
per_page: REPOS_PER_PAGE,
|
||||
});
|
||||
|
||||
setRepositoryDetails(result.data.items);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
per_page: REPOS_PER_PAGE,
|
||||
affiliation: 'owner',
|
||||
});
|
||||
setRepositoryDetails(result.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOrg = orgs.find(
|
||||
(org) => org.login === selectedAccount.value,
|
||||
);
|
||||
assert(selectedOrg, 'Selected org not found in list');
|
||||
|
||||
const result = await octokit.rest.repos.listForOrg({
|
||||
org: selectedOrg.login,
|
||||
per_page: REPOS_PER_PAGE,
|
||||
type: 'all',
|
||||
});
|
||||
|
||||
setRepositoryDetails(result.data);
|
||||
};
|
||||
|
||||
fetchRepos();
|
||||
}, [selectedAccount, gitUser, orgs, debouncedSearchedRepo]);
|
||||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
assert(gitUser, 'Git user is not available');
|
||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||
setSelectedAccount({ label: gitUser.login, value: gitUser.login });
|
||||
}, [gitUser]);
|
||||
|
||||
const accounts = useMemo(() => {
|
||||
if (!octokit || !gitUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [gitUser, ...orgs];
|
||||
}, [octokit, orgs, gitUser]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return accounts.map((account) => ({
|
||||
label: account.login,
|
||||
value: account.login,
|
||||
leftIcon: <GithubIcon />,
|
||||
}));
|
||||
}, [accounts]);
|
||||
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
{/* Dropdown and search */}
|
||||
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
|
||||
<div className="lg:basis-1/3 w-full">
|
||||
<Select
|
||||
options={options}
|
||||
placeholder="Select a repository"
|
||||
value={selectedAccount}
|
||||
leftIcon={selectedAccount ? <GithubIcon /> : undefined}
|
||||
rightIcon={<ChevronGrabberHorizontal />}
|
||||
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||
/>
|
||||
</div>
|
||||
<div className="basis-2/3 flex w-full flex-grow">
|
||||
<Input
|
||||
className="w-full"
|
||||
value={searchedRepo}
|
||||
placeholder="Search for repository"
|
||||
leftIcon={<SearchIcon />}
|
||||
onChange={(e) => setSearchedRepo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Repository list */}
|
||||
{Boolean(repositoryDetails.length) ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{repositoryDetails.map((repo, index) => (
|
||||
<>
|
||||
<ProjectRepoCard repository={repo} key={index} />
|
||||
{/* Horizontal line */}
|
||||
{index !== repositoryDetails.length - 1 && (
|
||||
<div className="border-b border-border-separator/[0.06] w-full" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex flex-col gap-4 items-center justify-center">
|
||||
<p className="text-elements-high-em font-sans">No repository found</p>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
leftIcon={<RefreshIcon />}
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Reset filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RepositoryList';
|
||||
@@ -55,9 +55,9 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative',
|
||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative cursor-pointer',
|
||||
{
|
||||
'cursor-default': template?.isComingSoon,
|
||||
},
|
||||
@@ -86,6 +86,6 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-7
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { CopyBlock, atomOneLight } from 'react-code-blocks';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@@ -8,7 +7,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
interface AssignDomainProps {
|
||||
open: boolean;
|
||||
@@ -17,9 +16,9 @@ interface AssignDomainProps {
|
||||
|
||||
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader placeholder={''}>Assign Domain</DialogHeader>
|
||||
<DialogBody placeholder={''}>
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader>Assign Domain</DialogHeader>
|
||||
<DialogBody>
|
||||
In order to assign a domain to your production deployments, configure it
|
||||
in the{' '}
|
||||
{/* TODO: Fix selection of project settings tab on navigation to domains */}
|
||||
@@ -36,13 +35,12 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
theme={atomOneLight}
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
onClick={handleOpen}
|
||||
placeholder={''}
|
||||
>
|
||||
<span>Okay</span>
|
||||
</Button>
|
||||
|
||||
+80
-62
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
@@ -60,79 +60,97 @@ const DeploymentDetailsCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex lg:flex gap-2 lg:gap-2 2xl:gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent ">
|
||||
<div className="flex-1 max-w-[30%] space-y-2">
|
||||
{/* DEPLOYMENT URL */}
|
||||
{deployment.url && (
|
||||
<Heading
|
||||
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||
as="h2"
|
||||
const renderDeploymentStatus = useCallback(
|
||||
(className?: string) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
<OverflownText content={deployment.url}>
|
||||
{deployment.url}
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</span>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[deployment.status, deployment.commitHash],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
||||
<div className="flex-1 flex justify-between w-full md:max-w-[25%] lg:max-w-[28%]">
|
||||
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
|
||||
{/* DEPLOYMENT URL */}
|
||||
{deployment.url && (
|
||||
<Heading
|
||||
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||
as="h2"
|
||||
>
|
||||
<OverflownText content={deployment.url}>
|
||||
{deployment.url}
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT STATUS */}
|
||||
<div className="w-[10%] max-w-[110px]">
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
{renderDeploymentStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')}
|
||||
|
||||
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||
<div className="text-sm w-[25%] space-y-2 text-elements-low-em">
|
||||
<span className="flex gap-1.5 items-center">
|
||||
<BranchStrokeIcon className="h-4 w-4" />
|
||||
{deployment.branch}
|
||||
</span>
|
||||
<span className="flex gap-2 items-center">
|
||||
<CommitIcon />
|
||||
<OverflownText content={deployment.commitMessage}>
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</OverflownText>
|
||||
</span>
|
||||
<div className="flex w-full justify-between md:w-[25%]">
|
||||
<div className="text-sm max-w-[60%] md:max-w-full space-y-2 w-full text-elements-low-em">
|
||||
<span className="flex gap-1.5 items-center">
|
||||
<BranchStrokeIcon className="h-4 w-4" />
|
||||
<OverflownText content={deployment.branch}>
|
||||
{deployment.branch}
|
||||
</OverflownText>
|
||||
</span>
|
||||
<span className="flex w-full gap-2 items-center">
|
||||
<CommitIcon />
|
||||
<OverflownText content={deployment.commitMessage}>
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</OverflownText>
|
||||
</span>
|
||||
</div>
|
||||
{renderDeploymentStatus('flex md:hidden h-fit')}
|
||||
</div>
|
||||
|
||||
{/* DEPLOYMENT INFOs */}
|
||||
<div className="ml-auto max-w-[312px] w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
|
||||
<div className="flex w-[70%] items-center gap-0.5 2xl:gap-2 flex-1">
|
||||
<ClockOutlineIcon className="h-4 w-4" />
|
||||
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</OverflownText>
|
||||
<div>
|
||||
<Avatar
|
||||
type="orange"
|
||||
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||
className="lg:size-5 2xl:size-6"
|
||||
// TODO: Add avatarUrl
|
||||
// imageSrc={deployment.createdBy.avatarUrl}
|
||||
></Avatar>
|
||||
<div className="md:ml-auto w-full md:max-w-[312px] md:w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
|
||||
<div className="flex md:w-[70%] xl:items-center gap-2 flex-1 xl:flex-row md:flex-col">
|
||||
<div className="flex gap-2 items-center">
|
||||
<ClockOutlineIcon className="h-4 w-4" />
|
||||
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</OverflownText>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div>
|
||||
<Avatar
|
||||
type="orange"
|
||||
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||
className="lg:size-5 2xl:size-6"
|
||||
// TODO: Add avatarUrl
|
||||
// imageSrc={deployment.createdBy.avatarUrl}
|
||||
></Avatar>
|
||||
</div>
|
||||
<OverflownText
|
||||
content={formatAddress(deployment.createdBy?.name ?? '')}
|
||||
>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</OverflownText>
|
||||
</div>
|
||||
|
||||
<OverflownText
|
||||
// className="min-w-[200px]"
|
||||
content={formatAddress(deployment.createdBy?.name ?? '')}
|
||||
>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</OverflownText>
|
||||
</div>
|
||||
<DeploymentMenu
|
||||
className="ml-auto"
|
||||
className="ml-auto md:static absolute top-4 right-0"
|
||||
deployment={deployment}
|
||||
currentDeployment={currentDeployment}
|
||||
onUpdate={onUpdate}
|
||||
|
||||
+55
-27
@@ -1,17 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Deployment } from 'gql-client';
|
||||
|
||||
import { Typography, Chip, Card } from '@material-tailwind/react';
|
||||
import { color } from '@material-tailwind/react/types/components/chip';
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import {
|
||||
BranchStrokeIcon,
|
||||
ClockOutlineIcon,
|
||||
CommitIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag, TagProps } from 'components/shared/Tag';
|
||||
|
||||
interface DeploymentDialogBodyCardProps {
|
||||
deployment: Deployment;
|
||||
chip?: {
|
||||
value: string;
|
||||
color?: color;
|
||||
type?: TagProps['type'];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,31 +24,54 @@ const DeploymentDialogBodyCard = ({
|
||||
chip,
|
||||
deployment,
|
||||
}: DeploymentDialogBodyCardProps) => {
|
||||
const commit =
|
||||
deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH) +
|
||||
' ' +
|
||||
deployment.commitMessage;
|
||||
|
||||
return (
|
||||
<Card className="p-2 shadow-none" placeholder={''}>
|
||||
<div className="flex flex-col gap-4 px-4 py-4 rounded-xl bg-base-bg-emphasized text-elements-low-em">
|
||||
{chip && (
|
||||
<Chip
|
||||
className={`w-fit normal-case font-normal`}
|
||||
size="sm"
|
||||
value={chip.value}
|
||||
color={chip.color}
|
||||
/>
|
||||
<Tag className="w-fit" size="xs" type={chip.type}>
|
||||
{chip.value}
|
||||
</Tag>
|
||||
)}
|
||||
{deployment.url && (
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Title */}
|
||||
<p className="text-sm font-medium text-elements-high-em tracking-[0.006em]">
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {deployment.branch} ^{' '}
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</Typography>
|
||||
</Card>
|
||||
</p>
|
||||
{/* Branch & commit */}
|
||||
<div className="flex items-center gap-6 text-elements-low-em">
|
||||
<div className="flex items-center gap-2">
|
||||
<BranchStrokeIcon size={16} />
|
||||
<p className="text-sm tracking-[0.006em]">{deployment.branch}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<CommitIcon size={16} />
|
||||
<p className="text-sm tracking-[0.006em] max-w-[67.5%] sm:max-w-[80%]">
|
||||
<OverflownText content={commit}>{commit}</OverflownText>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-elements-low-em">
|
||||
<ClockOutlineIcon size={16} />
|
||||
<p className="text-sm tracking-[0.006em]">
|
||||
{relativeTimeMs(deployment.createdAt)}
|
||||
</p>
|
||||
<Avatar
|
||||
size={20}
|
||||
type="orange"
|
||||
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||
// TODO: Add avatar image URL
|
||||
// imageSrc={deployment.createdBy.imageUrl}
|
||||
/>
|
||||
<p className="text-sm tracking-[0.006em]">
|
||||
{deployment.createdBy.name ?? 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
||||
import { Button } from 'components/shared/Button';
|
||||
@@ -15,14 +15,12 @@ import {
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
import { ComponentPropsWithRef } from 'react';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||
import { Typography } from '@material-tailwind/react';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
||||
|
||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||
deployment: Deployment;
|
||||
@@ -98,11 +96,10 @@ export const DeploymentMenu = ({
|
||||
aria-label="Toggle Menu"
|
||||
/>
|
||||
</MenuHandler>
|
||||
<MenuList className="text-elements-high-em" placeholder={''}>
|
||||
<MenuList className="text-elements-high-em">
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized"
|
||||
disabled={!Boolean(deployment.url)}
|
||||
placeholder={''}
|
||||
>
|
||||
<a
|
||||
className="flex items-center gap-3"
|
||||
@@ -116,7 +113,6 @@ export const DeploymentMenu = ({
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
placeholder={''}
|
||||
>
|
||||
<GlobeIcon /> Assign domain
|
||||
</MenuItem>
|
||||
@@ -124,7 +120,6 @@ export const DeploymentMenu = ({
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||
disabled={!(deployment.environment !== Environment.Production)}
|
||||
placeholder={''}
|
||||
>
|
||||
<RocketIcon /> Change to production
|
||||
</MenuItem>
|
||||
@@ -138,7 +133,6 @@ export const DeploymentMenu = ({
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<RefreshIcon /> Redeploy to production
|
||||
</MenuItem>
|
||||
@@ -150,7 +144,6 @@ export const DeploymentMenu = ({
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<UndoIcon /> Rollback to this version
|
||||
</MenuItem>
|
||||
@@ -158,106 +151,44 @@ export const DeploymentMenu = ({
|
||||
</Menu>
|
||||
</div>
|
||||
{/* Dialogs */}
|
||||
<ConfirmDialog
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Change to production?"
|
||||
handleOpen={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
confirmButtonTitle="Change"
|
||||
color="blue"
|
||||
handleCancel={() => setChangeToProduction((preVal) => !preVal)}
|
||||
open={changeToProduction}
|
||||
handleConfirm={async () => {
|
||||
await updateDeployment();
|
||||
setChangeToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
The new deployment will be associated with these domains:
|
||||
</Typography>
|
||||
{prodBranchDomains.length > 0 &&
|
||||
prodBranchDomains.map((value) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="small"
|
||||
color="blue"
|
||||
key={value.id}
|
||||
placeholder={''}
|
||||
>
|
||||
^ {value.name}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
deployment={deployment}
|
||||
domains={prodBranchDomains}
|
||||
/>
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Redeploy to production?"
|
||||
handleOpen={() => setRedeployToProduction((preVal) => !preVal)}
|
||||
handleCancel={() => setRedeployToProduction((preVal) => !preVal)}
|
||||
open={redeployToProduction}
|
||||
confirmButtonTitle="Redeploy"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await redeployToProd();
|
||||
setRedeployToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, new deployment will be created with the same
|
||||
source code as current deployment.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
{deployment.domain?.name && (
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
{deployment.domain?.name}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
deployment={deployment}
|
||||
domains={deployment.domain ? [deployment.domain] : []}
|
||||
/>
|
||||
{Boolean(currentDeployment) && (
|
||||
<ConfirmDialog
|
||||
<ChangeStateToProductionDialog
|
||||
dialogTitle="Rollback to this deployment?"
|
||||
handleOpen={() => setRollbackDeployment((preVal) => !preVal)}
|
||||
handleCancel={() => setRollbackDeployment((preVal) => !preVal)}
|
||||
open={rollbackDeployment}
|
||||
confirmButtonTitle="Rollback"
|
||||
color="blue"
|
||||
handleConfirm={async () => {
|
||||
await rollbackDeploymentHandler();
|
||||
setRollbackDeployment((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will replace your current
|
||||
deployment
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={currentDeployment}
|
||||
chip={{
|
||||
value: 'Live Deployment',
|
||||
color: 'green',
|
||||
}}
|
||||
/>
|
||||
<DeploymentDialogBodyCard
|
||||
deployment={deployment}
|
||||
chip={{
|
||||
value: 'New Deployment',
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
^ {currentDeployment.domain?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
deployment={currentDeployment}
|
||||
newDeployment={deployment}
|
||||
domains={currentDeployment.domain ? [currentDeployment.domain] : []}
|
||||
/>
|
||||
)}
|
||||
<AssignDomainDialog
|
||||
open={assignDomainDialog}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { IconButton, Option, Select } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../../SearchBar';
|
||||
import DatePicker from '../../../DatePicker';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import {
|
||||
CheckRadioOutlineIcon,
|
||||
CrossCircleIcon,
|
||||
LoaderIcon,
|
||||
SearchIcon,
|
||||
TrendingIcon,
|
||||
WarningTriangleIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { DatePicker } from 'components/shared/DatePicker';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Value } from 'types/vendor';
|
||||
|
||||
export enum StatusOptions {
|
||||
ALL_STATUS = 'All status',
|
||||
@@ -15,8 +22,8 @@ export enum StatusOptions {
|
||||
|
||||
export interface FilterValue {
|
||||
searchedBranch: string;
|
||||
status: StatusOptions;
|
||||
updateAtRange?: DateRange;
|
||||
status: StatusOptions | string;
|
||||
updateAtRange?: Value;
|
||||
}
|
||||
|
||||
interface FilterFormProps {
|
||||
@@ -27,7 +34,7 @@ interface FilterFormProps {
|
||||
const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch);
|
||||
const [selectedStatus, setSelectedStatus] = useState(value.status);
|
||||
const [dateRange, setDateRange] = useState<DateRange>();
|
||||
const [dateRange, setDateRange] = useState<Value>();
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
@@ -43,46 +50,68 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
setDateRange(value.updateAtRange);
|
||||
}, [value]);
|
||||
|
||||
const getOptionIcon = (status: StatusOptions) => {
|
||||
switch (status) {
|
||||
case StatusOptions.BUILDING:
|
||||
return <LoaderIcon />;
|
||||
case StatusOptions.READY:
|
||||
return <CheckRadioOutlineIcon />;
|
||||
case StatusOptions.ERROR:
|
||||
return <WarningTriangleIcon />;
|
||||
case StatusOptions.ALL_STATUS:
|
||||
default:
|
||||
return <TrendingIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const statusOptions = Object.values(StatusOptions).map((status) => ({
|
||||
label: status,
|
||||
value: status,
|
||||
leftIcon: getOptionIcon(status),
|
||||
}));
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchedBranch('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid items-center grid-cols-8 gap-2 text-sm text-gray-600">
|
||||
<div className="col-span-4">
|
||||
<SearchBar
|
||||
<div className="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-3">
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Input
|
||||
placeholder="Search branches"
|
||||
leftIcon={<SearchIcon />}
|
||||
rightIcon={
|
||||
searchedBranch && <CrossCircleIcon onClick={handleReset} />
|
||||
}
|
||||
value={searchedBranch}
|
||||
onChange={(event) => setSearchedBranch(event.target.value)}
|
||||
onChange={(e) => setSearchedBranch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
||||
<div className="col-span-2 flex items-center">
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
selectRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
onReset={() => setDateRange(undefined)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 relative">
|
||||
<div className="col-span-2 flex items-center">
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
onChange={(value) => setSelectedStatus(value as StatusOptions)}
|
||||
leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
|
||||
options={statusOptions}
|
||||
clearable
|
||||
placeholder="All status"
|
||||
>
|
||||
{Object.values(StatusOptions).map((status) => (
|
||||
<Option
|
||||
className={status === StatusOptions.ALL_STATUS ? 'hidden' : ''}
|
||||
key={status}
|
||||
value={status}
|
||||
>
|
||||
^ {status}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{selectedStatus !== StatusOptions.ALL_STATUS && (
|
||||
<div className="absolute end-1 inset-y-0 my-auto h-8">
|
||||
<IconButton
|
||||
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
value={
|
||||
selectedStatus
|
||||
? { label: selectedStatus, value: selectedStatus }
|
||||
: undefined
|
||||
}
|
||||
onChange={(item) =>
|
||||
setSelectedStatus((item as SelectOption).value as StatusOptions)
|
||||
}
|
||||
onClear={() => setSelectedStatus('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import { GitCommitWithBranch } from 'types';
|
||||
import { GitCommitWithBranch } from '../../../../../types/types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import ActivityCard from './ActivityCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { LoadingIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
export const Activity = ({
|
||||
isLoading,
|
||||
activities,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
activities: GitCommitWithBranch[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-2 mr-1">
|
||||
<div className="col-span-5 md:col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
@@ -19,9 +20,17 @@ export const Activity = ({
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
{activities.map((activity, index) => {
|
||||
return <ActivityCard activity={activity} key={`activity-${index}`} />;
|
||||
})}
|
||||
{isLoading ? (
|
||||
<div className="grid place-content-center mt-10">
|
||||
<LoadingIcon className="animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
activities.map((activity, index) => {
|
||||
return (
|
||||
<ActivityCard activity={activity} key={`activity-${index}`} />
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GitCommitWithBranch } from '../../../../../types';
|
||||
import { GitCommitWithBranch } from '../../../../../types/types';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { cloneElement } from 'utils/cloneElement';
|
||||
|
||||
|
||||
+21
-36
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
|
||||
import { Typography, Input, IconButton } from '@material-tailwind/react';
|
||||
|
||||
import { EnvironmentVariablesFormValues } from '../../../../types';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { TrashIcon } from 'components/shared/CustomIcon';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
interface AddEnvironmentVariableRowProps {
|
||||
onDelete: () => void;
|
||||
@@ -19,39 +19,24 @@ const AddEnvironmentVariableRow = ({
|
||||
isDeleteDisabled,
|
||||
}: AddEnvironmentVariableRowProps) => {
|
||||
return (
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.key`, {
|
||||
required: 'Key field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.value`, {
|
||||
required: 'Value field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => onDelete()}
|
||||
disabled={isDeleteDisabled}
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="flex gap-2 py-1 self-stretch items-end">
|
||||
<Input
|
||||
size="md"
|
||||
{...register(`variables.${index}.key`, {
|
||||
required: 'Key field cannot be empty',
|
||||
})}
|
||||
label={index === 0 ? 'Key' : undefined}
|
||||
/>
|
||||
<Input
|
||||
size="md"
|
||||
label={index === 0 ? 'Value' : undefined}
|
||||
{...register(`variables.${index}.value`, {
|
||||
required: 'Value field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Button size="md" iconOnly onClick={onDelete} disabled={isDeleteDisabled}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { AddProjectMemberInput, Permission } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Typography,
|
||||
Checkbox,
|
||||
} from '@material-tailwind/react';
|
||||
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Modal } from 'components/shared/Modal';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Checkbox } from 'components/shared/Checkbox';
|
||||
|
||||
interface AddMemberDialogProp {
|
||||
open: boolean;
|
||||
@@ -61,73 +57,47 @@ const AddMemberDialog = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Add member</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
We will send an invitation link to this email address.
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Email address
|
||||
</Typography>
|
||||
<Input
|
||||
type="email"
|
||||
crossOrigin={undefined}
|
||||
{...register('emailAddress', {
|
||||
required: 'email field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Permissions
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
You can change this later if required.
|
||||
</Typography>
|
||||
<Checkbox
|
||||
crossOrigin={undefined}
|
||||
label={Permission.View}
|
||||
{...register(`permissions.view`)}
|
||||
color="blue"
|
||||
/>
|
||||
<Checkbox
|
||||
crossOrigin={undefined}
|
||||
label={Permission.Edit}
|
||||
{...register(`permissions.edit`)}
|
||||
color="blue"
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Send invite
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
<Modal open={open} onOpenChange={handleOpen}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>Add member</Modal.Header>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<Modal.Body className="flex flex-col gap-2 p-4">
|
||||
<Typography variant="small">
|
||||
We will send an invitation link to this email address.
|
||||
</Typography>
|
||||
<Typography variant="small">Email address</Typography>
|
||||
<Input
|
||||
type="email"
|
||||
{...register('emailAddress', {
|
||||
required: 'email field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small">Permissions</Typography>
|
||||
<Typography variant="small">
|
||||
You can change this later if required.
|
||||
</Typography>
|
||||
<Checkbox
|
||||
label={Permission.View}
|
||||
{...register(`permissions.view`)}
|
||||
color="blue"
|
||||
/>
|
||||
<Checkbox
|
||||
label={Permission.Edit}
|
||||
{...register(`permissions.edit`)}
|
||||
color="blue"
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={handleOpen} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!isValid}>
|
||||
Send invite
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+44
-67
@@ -1,19 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@material-tailwind/react';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { Modal } from 'components/shared/Modal';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
interface DeleteProjectDialogProp {
|
||||
open: boolean;
|
||||
@@ -26,6 +20,7 @@ const DeleteProjectDialog = ({
|
||||
handleOpen,
|
||||
project,
|
||||
}: DeleteProjectDialogProp) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const { orgSlug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
@@ -46,66 +41,48 @@ const DeleteProjectDialog = ({
|
||||
if (deleteProject) {
|
||||
navigate(`/${orgSlug}`);
|
||||
} else {
|
||||
toast.error('Project not deleted');
|
||||
toast({
|
||||
id: 'project_not_deleted',
|
||||
title: 'Project not deleted',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
|
||||
handleOpen();
|
||||
}, [client, project, handleOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Delete project?</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2" placeholder={''}>
|
||||
<Typography variant="paragraph" placeholder={''}>
|
||||
Deleting your project is irreversible. Enter your project’s
|
||||
name
|
||||
<span className="bg-blue-100 text-blue-700">({project.name})</span>
|
||||
below to confirm you want to permanently delete it:
|
||||
</Typography>
|
||||
<Input
|
||||
id="input"
|
||||
crossOrigin={undefined}
|
||||
{...register('projectName', {
|
||||
required: 'Project name is required',
|
||||
validate: (value) => value === project.name,
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small" color="red" placeholder={''}>
|
||||
^ Deleting your project is irreversible.
|
||||
</Typography>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="red"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Yes, Delete project
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
<Modal open={open} onOpenChange={handleOpen}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>Delete project?</Modal.Header>
|
||||
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||
<Modal.Body>
|
||||
<Input
|
||||
label={
|
||||
"Deleting your project is irreversible. Enter your project's name " +
|
||||
project.name +
|
||||
' below to confirm you want to permanently delete it:'
|
||||
}
|
||||
id="input"
|
||||
{...register('projectName', {
|
||||
required: 'Project name is required',
|
||||
validate: (value) => value === project.name,
|
||||
})}
|
||||
helperText="Deleting your project is irreversible."
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-start">
|
||||
<Button onClick={handleOpen} variant="tertiary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" type="submit" disabled={!isValid}>
|
||||
Yes, delete project
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+12
-12
@@ -1,8 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
||||
import { useState } from 'react';
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
import {
|
||||
ChevronDownSmallIcon,
|
||||
ChevronUpSmallIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
interface DisplayEnvironmentVariablesProps {
|
||||
environment: Environment;
|
||||
@@ -20,23 +24,19 @@ const DisplayEnvironmentVariables = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex gap-4 p-2 "
|
||||
className="flex gap-4 p-2"
|
||||
onClick={() => setOpenCollapse((cur) => !cur)}
|
||||
>
|
||||
<div>^</div>
|
||||
{openCollapse ? <ChevronUpSmallIcon /> : <ChevronDownSmallIcon />}
|
||||
<div className="grow capitalize">{environment}</div>
|
||||
<div>{variables.length} variables</div>
|
||||
</div>
|
||||
<Collapse open={openCollapse}>
|
||||
{variables.length === 0 ? (
|
||||
<Card className="bg-gray-300 flex items-center p-4" placeholder={''}>
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
No environment variables added yet.
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once you add them, they’ll show up here.
|
||||
</Typography>
|
||||
</Card>
|
||||
<div className="bg-slate-100 rounded-xl flex-col p-4">
|
||||
No environment variables added yet. Once you add them, they'll show
|
||||
up here.
|
||||
</div>
|
||||
) : (
|
||||
variables.map((variable: EnvironmentVariable) => {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useState } from 'react';
|
||||
import { Domain, DomainStatus, Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
@@ -10,11 +9,12 @@ import {
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Card,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import EditDomainDialog from './EditDomainDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
enum RefreshStatus {
|
||||
IDLE,
|
||||
@@ -47,6 +47,7 @@ const DomainCard = ({
|
||||
project,
|
||||
onUpdate,
|
||||
}: DomainCardProps) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
@@ -58,9 +59,19 @@ const DomainCard = ({
|
||||
|
||||
if (deleteDomain) {
|
||||
onUpdate();
|
||||
toast.success(`Domain ${domain.name} deleted successfully`);
|
||||
toast({
|
||||
id: 'domain_deleted_success',
|
||||
title: 'Domain deleted',
|
||||
variant: 'success',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
} else {
|
||||
toast.error(`Error deleting domain ${domain.name}`);
|
||||
toast({
|
||||
id: 'domain_deleted_error',
|
||||
title: `Error deleting domain ${domain.name}`,
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,7 +79,7 @@ const DomainCard = ({
|
||||
<>
|
||||
<div className="flex justify-between py-3">
|
||||
<div className="flex justify-start gap-1">
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
<Typography variant="h6">
|
||||
<i>^</i> {domain.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
@@ -97,71 +108,50 @@ const DomainCard = ({
|
||||
<MenuHandler>
|
||||
<button className="border-2 rounded-full w-8 h-8">...</button>
|
||||
</MenuHandler>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
setEditDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Edit domain
|
||||
Edit Domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Delete domain
|
||||
Delete domain
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete domain?"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
<DeleteDomainDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Delete domain"
|
||||
handleConfirm={() => {
|
||||
deleteDomain();
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once deleted, the project{' '}
|
||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||
{project.name}
|
||||
</span>{' '}
|
||||
will not be accessible from the domain{' '}
|
||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||
{domain.name}.
|
||||
</span>
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
projectName={project.name}
|
||||
domainName={domain.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Production
|
||||
</Typography>
|
||||
<Typography variant="small">Production</Typography>
|
||||
{domain.status === DomainStatus.Pending && (
|
||||
<Card className="bg-gray-200 p-4 text-sm" placeholder={''}>
|
||||
<Card className="bg-slate-100 p-4 text-sm">
|
||||
{refreshStatus === RefreshStatus.IDLE ? (
|
||||
<Typography variant="small" placeholder={''}>
|
||||
<Typography variant="small">
|
||||
^ Add these records to your domain and refresh to check
|
||||
</Typography>
|
||||
) : refreshStatus === RefreshStatus.CHECKING ? (
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-blue-500"
|
||||
placeholder={''}
|
||||
>
|
||||
<Typography variant="small" className="text-blue-500">
|
||||
^ Checking records for {domain.name}
|
||||
</Typography>
|
||||
) : (
|
||||
<div className="flex gap-2 text-red-500 mb-2">
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
Failed to verify records. DNS propagation can take up to 48
|
||||
hours. Please ensure you added the correct records and refresh.
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Domain } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Typography,
|
||||
Select,
|
||||
Option,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { Modal } from 'components/shared/Modal';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
||||
|
||||
@@ -122,95 +119,67 @@ const EditDomainDialog = ({
|
||||
}, [domain]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Edit domain</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain name
|
||||
</Typography>
|
||||
<Input crossOrigin={undefined} {...register('name')} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Redirect to
|
||||
</Typography>
|
||||
<Controller
|
||||
name="redirectedTo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select {...field} disabled={isDisableDropdown} placeholder={''}>
|
||||
{redirectOptions.map((option, key) => (
|
||||
<Option key={key} value={option}>
|
||||
^ {option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Modal open={open} onOpenChange={handleOpen}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>Edit domain</Modal.Header>
|
||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||
<Modal.Body className="flex flex-col gap-2">
|
||||
<Typography variant="small">Domain name</Typography>
|
||||
<Input {...register('name')} />
|
||||
<Typography variant="small">Redirect to</Typography>
|
||||
<Controller
|
||||
name="redirectedTo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select {...field} disabled={isDisableDropdown}>
|
||||
{redirectOptions.map((option, key) => (
|
||||
<Option key={key} value={option}>
|
||||
^ {option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
{isDisableDropdown && (
|
||||
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
||||
<div>^</div>
|
||||
<Typography variant="small">
|
||||
Domain “
|
||||
{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
||||
redirects to this domain so you can not redirect this doman
|
||||
further.
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{isDisableDropdown && (
|
||||
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
||||
<div>^</div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain “{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
||||
redirects to this domain so you can not redirect this doman
|
||||
further.
|
||||
<Typography variant="small">Git branch</Typography>
|
||||
<Input
|
||||
{...register('branch', {
|
||||
validate: (value) =>
|
||||
Boolean(branches.length) ? branches.includes(value) : true,
|
||||
})}
|
||||
disabled={
|
||||
!Boolean(branches.length) ||
|
||||
watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]
|
||||
}
|
||||
/>
|
||||
{!isValid && (
|
||||
<Typography variant="small" className="text-red-500">
|
||||
We couldn't find this branch in the connected Git
|
||||
repository.
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Git branch
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register('branch', {
|
||||
validate: (value) =>
|
||||
Boolean(branches.length) ? branches.includes(value) : true,
|
||||
})}
|
||||
disabled={
|
||||
!Boolean(branches.length) ||
|
||||
watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]
|
||||
}
|
||||
/>
|
||||
{!isValid && (
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-red-500"
|
||||
placeholder={''}
|
||||
>
|
||||
We couldn't find this branch in the connected Git repository.
|
||||
</Typography>
|
||||
)}
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
placeholder={''}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={handleOpen} className="mr-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!isDirty}>
|
||||
Save changes
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+102
-105
@@ -1,12 +1,20 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
CheckRoundFilledIcon,
|
||||
CrossIcon,
|
||||
EditBigIcon,
|
||||
HideEyeOffIcon,
|
||||
ShowEyeIcon,
|
||||
TrashIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import { IconButton, Input, Typography } from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
const ShowPasswordIcon = ({
|
||||
handler,
|
||||
@@ -22,19 +30,24 @@ const ShowPasswordIcon = ({
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{isVisible ? '-' : '+'}
|
||||
{isVisible ? <ShowEyeIcon size={16} /> : <HideEyeOffIcon size={16} />}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export interface EditEnvironmentVariableRowProps {
|
||||
variable: EnvironmentVariable;
|
||||
onUpdate: () => Promise<void>;
|
||||
isFirstVariable?: boolean;
|
||||
}
|
||||
|
||||
const EditEnvironmentVariableRow = ({
|
||||
variable,
|
||||
onUpdate,
|
||||
}: {
|
||||
variable: EnvironmentVariable;
|
||||
onUpdate: () => Promise<void>;
|
||||
}) => {
|
||||
isFirstVariable = false,
|
||||
}: EditEnvironmentVariableRowProps) => {
|
||||
const client = useGQLClient();
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
const { handleSubmit, register, reset } = useForm({
|
||||
defaultValues: {
|
||||
@@ -54,9 +67,19 @@ const EditEnvironmentVariableRow = ({
|
||||
if (isEnvironmentVariableRemoved) {
|
||||
await onUpdate();
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
toast.success('Variable deleted');
|
||||
toast({
|
||||
id: 'variable_deleted',
|
||||
title: 'Variable deleted',
|
||||
variant: 'success',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
} else {
|
||||
toast.error('Variable not deleted');
|
||||
toast({
|
||||
id: 'variable_not_deleted',
|
||||
title: 'Variable not deleted',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
}, [variable, onUpdate]);
|
||||
|
||||
@@ -67,10 +90,21 @@ const EditEnvironmentVariableRow = ({
|
||||
|
||||
if (isEnvironmentVariableUpdated) {
|
||||
await onUpdate();
|
||||
toast.success('Variable edited');
|
||||
toast({
|
||||
id: 'variable_updated',
|
||||
title: 'Variable edited',
|
||||
variant: 'success',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
|
||||
setEdit((preVal) => !preVal);
|
||||
} else {
|
||||
toast.error('Variable not edited');
|
||||
toast({
|
||||
id: 'variable_not_updated',
|
||||
title: 'Variable not edited',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
},
|
||||
[variable, onUpdate],
|
||||
@@ -82,99 +116,62 @@ const EditEnvironmentVariableRow = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
{...register(`key`)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
icon={
|
||||
<ShowPasswordIcon
|
||||
handler={() => {
|
||||
setShowPassword((prevShowPassword) => !prevShowPassword);
|
||||
}}
|
||||
isVisible={showPassword}
|
||||
/>
|
||||
<div className="flex gap-1 items-end">
|
||||
<Input
|
||||
disabled={!edit}
|
||||
{...register(`key`)}
|
||||
label={isFirstVariable ? 'Key' : undefined}
|
||||
/>
|
||||
<Input
|
||||
disabled={!edit}
|
||||
type={showPassword || edit ? 'text' : 'password'}
|
||||
label={isFirstVariable ? 'Value' : undefined}
|
||||
rightIcon={
|
||||
<ShowPasswordIcon
|
||||
handler={() => {
|
||||
setShowPassword((prevShowPassword) => !prevShowPassword);
|
||||
}}
|
||||
isVisible={showPassword || edit}
|
||||
/>
|
||||
}
|
||||
{...register(`value`)}
|
||||
/>
|
||||
<Button
|
||||
iconOnly
|
||||
size="md"
|
||||
onClick={() => {
|
||||
edit
|
||||
? handleSubmit(updateEnvironmentVariableHandler)()
|
||||
: setEdit((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
{edit ? (
|
||||
<CheckRoundFilledIcon size={16} />
|
||||
) : (
|
||||
<EditBigIcon size={16} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
iconOnly
|
||||
size="md"
|
||||
onClick={() => {
|
||||
if (edit) {
|
||||
reset();
|
||||
setEdit((preVal) => !preVal);
|
||||
} else {
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
}
|
||||
{...register(`value`)}
|
||||
/>
|
||||
</div>
|
||||
{edit ? (
|
||||
<>
|
||||
<div className="self-end">
|
||||
<IconButton
|
||||
onClick={handleSubmit(updateEnvironmentVariableHandler)}
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
{'S'}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
reset();
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'C'}
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="self-end">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'E'}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
{'D'}
|
||||
</IconButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
{edit ? <CrossIcon size={16} /> : <TrashIcon size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete variable"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
<DeleteVariableDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Confirm delete"
|
||||
handleConfirm={removeEnvironmentVariableHandler}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Are you sure you want to delete the variable
|
||||
<span className="bg-blue-100">{variable.key}</span>?
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
variableKey={variable.key}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { GitSelect } from '../../../../types';
|
||||
|
||||
const GitSelectionSection = ({
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { LoaderIcon } from 'components/shared/CustomIcon';
|
||||
import { Tooltip } from 'components/shared/Tooltip';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Permission, User } from 'gql-client';
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
import {
|
||||
Select,
|
||||
Typography,
|
||||
Option,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
|
||||
const PERMISSION_OPTIONS = [
|
||||
const PERMISSION_OPTIONS: SelectOption[] = [
|
||||
{
|
||||
label: 'View only',
|
||||
value: 'View',
|
||||
@@ -24,7 +21,7 @@ const PERMISSION_OPTIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
const DROPDOWN_OPTIONS = [
|
||||
const DROPDOWN_OPTIONS: SelectOption[] = [
|
||||
...PERMISSION_OPTIONS,
|
||||
{ label: 'Remove member', value: 'remove' },
|
||||
];
|
||||
@@ -51,16 +48,21 @@ const MemberCard = ({
|
||||
onUpdateProjectMember,
|
||||
}: MemberCardProps) => {
|
||||
const [ethAddress, emailDomain] = member.email.split('@');
|
||||
const [selectedPermission, setSelectedPermission] = useState(
|
||||
permissions.join('+'),
|
||||
const [selectedPermission, setSelectedPermission] = useState<SelectOption>(
|
||||
PERMISSION_OPTIONS.map((value) => {
|
||||
permissions.join('+') === value.value;
|
||||
}).pop() ?? {
|
||||
label: 'View only',
|
||||
value: 'View',
|
||||
},
|
||||
);
|
||||
const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false);
|
||||
|
||||
const handlePermissionChange = useCallback(
|
||||
async (value: string) => {
|
||||
async (value: SelectOption) => {
|
||||
setSelectedPermission(value);
|
||||
|
||||
if (value === 'remove') {
|
||||
if (value.value === 'remove') {
|
||||
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
||||
// To display updated label in next render
|
||||
setTimeout(() => {
|
||||
@@ -68,7 +70,7 @@ const MemberCard = ({
|
||||
});
|
||||
} else {
|
||||
if (onUpdateProjectMember) {
|
||||
const permissions = value.split('+') as Permission[];
|
||||
const permissions = value.value.split('+') as Permission[];
|
||||
await onUpdateProjectMember({ permissions });
|
||||
}
|
||||
}
|
||||
@@ -78,9 +80,8 @@ const MemberCard = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex p-1 ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
|
||||
className={`flex py-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
|
||||
>
|
||||
<div>^</div>
|
||||
<div className="basis-1/2">
|
||||
{member.name && (
|
||||
<Tooltip content={member.name}>
|
||||
@@ -95,71 +96,53 @@ const MemberCard = ({
|
||||
</div>
|
||||
<div className="basis-1/2">
|
||||
{!isPending ? (
|
||||
<Select
|
||||
size="lg"
|
||||
label={isOwner ? 'Owner' : ''}
|
||||
disabled={isOwner}
|
||||
value={selectedPermission}
|
||||
onChange={(value) => handlePermissionChange(value!)}
|
||||
selected={(_, index) => (
|
||||
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
||||
)}
|
||||
placeholder={''}
|
||||
>
|
||||
{DROPDOWN_OPTIONS.map((permission, key) => (
|
||||
<Option key={key} value={permission.value}>
|
||||
^ {permission.label}
|
||||
{permission.value === selectedPermission && (
|
||||
<p className="float-right">^</p>
|
||||
)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
isOwner ? (
|
||||
<Input size="md" value="Owner" disabled />
|
||||
) : (
|
||||
<Select
|
||||
options={DROPDOWN_OPTIONS}
|
||||
size="md"
|
||||
placeholder="Select permission"
|
||||
value={selectedPermission}
|
||||
onChange={(value) =>
|
||||
handlePermissionChange(value as SelectOption)
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="flex justify-end gap-2">
|
||||
<div>
|
||||
<Chip
|
||||
value="Pending"
|
||||
variant="outlined"
|
||||
color="orange"
|
||||
size="sm"
|
||||
icon={'^'}
|
||||
/>
|
||||
<Tag type="positive" size="sm" leftIcon={<LoaderIcon />}>
|
||||
Pending
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
<Button
|
||||
size="md"
|
||||
iconOnly
|
||||
onClick={() => {
|
||||
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
D
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Remove member?"
|
||||
handleOpen={() => setRemoveMemberDialogOpen((preVal) => !preVal)}
|
||||
<RemoveMemberDialog
|
||||
handleCancel={() => setRemoveMemberDialogOpen((preVal) => !preVal)}
|
||||
open={removeMemberDialogOpen}
|
||||
confirmButtonTitle="Yes, Remove member"
|
||||
handleConfirm={() => {
|
||||
setRemoveMemberDialogOpen((preVal) => !preVal);
|
||||
if (onRemoveProjectMember) {
|
||||
onRemoveProjectMember();
|
||||
}
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once removed, {formatAddress(member.name ?? '')} (
|
||||
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
||||
this project.
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
memberName={member.name ?? ''}
|
||||
ethAddress={ethAddress}
|
||||
emailDomain={emailDomain}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { ProjectSettingHeader } from './ProjectSettingHeader';
|
||||
|
||||
export interface ProjectSettingContainerProps extends PropsWithChildren {
|
||||
headingText: string;
|
||||
className?: string;
|
||||
button?: React.ReactNode;
|
||||
badge?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProjectSettingContainer: React.FC<ProjectSettingContainerProps> = ({
|
||||
headingText,
|
||||
className,
|
||||
button,
|
||||
children,
|
||||
badge,
|
||||
...props
|
||||
}: ProjectSettingContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
className={'flex-col justify-start gap-8 space-y-3 px-2 pb-6'}
|
||||
{...props}
|
||||
>
|
||||
<ProjectSettingHeader
|
||||
headingText={headingText}
|
||||
button={button}
|
||||
badge={badge}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProjectSettingContainer };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
|
||||
export interface ProjectSettingHeaderProps extends PropsWithChildren {
|
||||
headingText: string;
|
||||
button?: React.ReactNode;
|
||||
badge?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProjectSettingHeader: React.FC<ProjectSettingHeaderProps> = ({
|
||||
headingText,
|
||||
button,
|
||||
badge,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center" {...props}>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Heading className="text-lg font-medium leading-normal">
|
||||
{headingText}
|
||||
</Heading>
|
||||
{badge}
|
||||
</div>
|
||||
{button ?? button}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProjectSettingHeader };
|
||||
+11
-27
@@ -1,9 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { GitRepositoryDetails } from '../../../../types';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { DisconnectRepositoryDialog } from 'components/projects/Dialog/DisconnectRepositoryDialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
const RepoConnectedSection = ({
|
||||
linkedRepo,
|
||||
@@ -17,38 +18,21 @@ const RepoConnectedSection = ({
|
||||
<div className="flex gap-4">
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
{linkedRepo.full_name}
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Connected just now
|
||||
</Typography>
|
||||
<Typography variant="small">{linkedRepo.full_name}</Typography>
|
||||
<Typography variant="small">Connected just now</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setDisconnectRepoDialogOpen(true)}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Disconnect
|
||||
<Button onClick={() => setDisconnectRepoDialogOpen(true)} size="sm">
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Disconnect repository?"
|
||||
handleOpen={() => setDisconnectRepoDialogOpen((preVal) => !preVal)}
|
||||
<DisconnectRepositoryDialog
|
||||
handleCancel={() => setDisconnectRepoDialogOpen((preVal) => !preVal)}
|
||||
open={disconnectRepoDialogOpen}
|
||||
confirmButtonTitle="Yes, confirm disconnect"
|
||||
handleConfirm={() => {
|
||||
setDisconnectRepoDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Any data tied to your Git project may become misconfigured. Are you
|
||||
sure you want to continue?
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import {
|
||||
Radio,
|
||||
Typography,
|
||||
Button,
|
||||
Input,
|
||||
Alert,
|
||||
} from '@material-tailwind/react';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Radio } from 'components/shared/Radio';
|
||||
|
||||
interface SetupDomainFormValues {
|
||||
domainName: string;
|
||||
isWWW: string;
|
||||
}
|
||||
|
||||
const SetupDomain = () => {
|
||||
const {
|
||||
@@ -17,32 +20,38 @@ const SetupDomain = () => {
|
||||
formState: { isValid },
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm({
|
||||
} = useForm<SetupDomainFormValues>({
|
||||
defaultValues: {
|
||||
domainName: '',
|
||||
isWWW: 'false',
|
||||
},
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const [domainStr, setDomainStr] = useState('');
|
||||
const [domainStr, setDomainStr] = useState<string>('');
|
||||
const navigate = useNavigate();
|
||||
const isWWWRadioOptions = [
|
||||
{ label: domainStr, value: 'false' },
|
||||
{ label: `www.${domainStr}`, value: 'true' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch((value, { name }) => {
|
||||
if (name === 'domainName' && value.domainName) {
|
||||
const domainArr = value.domainName.split('www.');
|
||||
setDomainStr(domainArr.length > 1 ? domainArr[1] : domainArr[0]);
|
||||
const cleanedDomain =
|
||||
domainArr.length > 1 ? domainArr[1] : domainArr[0];
|
||||
setDomainStr(cleanedDomain);
|
||||
|
||||
if (value.domainName.startsWith('www.')) {
|
||||
setValue('isWWW', 'true');
|
||||
} else {
|
||||
setValue('isWWW', 'false');
|
||||
}
|
||||
setValue(
|
||||
'isWWW',
|
||||
value.domainName.startsWith('www.') ? 'true' : 'false',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch]);
|
||||
}, [watch, setValue]);
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -54,68 +63,49 @@ const SetupDomain = () => {
|
||||
className="flex flex-col gap-6 w-full"
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h5" placeholder={''}>
|
||||
<Heading className="text-sky-950 text-lg font-medium leading-normal">
|
||||
Setup domain name
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
</Heading>
|
||||
<p className="text-slate-500 text-sm font-normal leading-tight">
|
||||
Add your domain and setup redirects
|
||||
</Typography>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-auto">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain name
|
||||
</Typography>
|
||||
<Input
|
||||
type="text"
|
||||
variant="outlined"
|
||||
size="lg"
|
||||
crossOrigin={undefined}
|
||||
className="w-full"
|
||||
{...register('domainName', {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
size="md"
|
||||
placeholder="example.com"
|
||||
{...register('domainName', {
|
||||
required: true,
|
||||
})}
|
||||
label="Domain name"
|
||||
/>
|
||||
|
||||
{isValid && (
|
||||
<div>
|
||||
<Typography placeholder={''}>Primary domain</Typography>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Radio
|
||||
label={domainStr}
|
||||
crossOrigin={undefined}
|
||||
{...register('isWWW')}
|
||||
value="false"
|
||||
type="radio"
|
||||
/>
|
||||
<Radio
|
||||
label={`www.${domainStr}`}
|
||||
crossOrigin={undefined}
|
||||
{...register('isWWW')}
|
||||
value="true"
|
||||
type="radio"
|
||||
/>
|
||||
</div>
|
||||
<Alert color="blue">
|
||||
<i>^</i> For simplicity, we’ll redirect the{' '}
|
||||
{watch('isWWW') === 'true'
|
||||
? `non-www variant to www.${domainStr}`
|
||||
: `www variant to ${domainStr}`}
|
||||
. Redirect preferences can be changed later
|
||||
</Alert>
|
||||
<div className="self-stretch flex flex-col gap-4">
|
||||
<Heading className="text-sky-950 text-lg font-medium leading-normal">
|
||||
Primary domain
|
||||
</Heading>
|
||||
<Radio
|
||||
options={isWWWRadioOptions}
|
||||
onValueChange={(value) => setValue('isWWW', value)}
|
||||
value={watch('isWWW')}
|
||||
/>
|
||||
<InlineNotification
|
||||
variant="info"
|
||||
title={`For simplicity, we'll redirect the ${
|
||||
watch('isWWW') === 'true'
|
||||
? `non-www variant to www.${domainStr}`
|
||||
: `www variant to ${domainStr}`
|
||||
}. Redirect preferences can be changed later`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={!isValid}
|
||||
className="w-fit"
|
||||
color={isValid ? 'blue' : 'gray'}
|
||||
type="submit"
|
||||
placeholder={''}
|
||||
>
|
||||
<i>^</i> Next
|
||||
</Button>
|
||||
<div className="self-stretch">
|
||||
<Button disabled={!isValid} type="submit">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
interface WebhookCardProps {
|
||||
webhookUrl: string;
|
||||
@@ -11,50 +10,46 @@ interface WebhookCardProps {
|
||||
}
|
||||
|
||||
const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
return (
|
||||
<div className="flex justify-between w-full mb-3">
|
||||
{webhookUrl}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(webhookUrl);
|
||||
toast.success('Copied to clipboard');
|
||||
toast({
|
||||
id: 'webhook_copied',
|
||||
title: 'Webhook copied to clipboard',
|
||||
variant: 'success',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
C
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
dialogTitle="Delete webhook"
|
||||
handleOpen={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
<DeleteWebhookDialog
|
||||
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
open={deleteDialogOpen}
|
||||
confirmButtonTitle="Yes, Confirm delete"
|
||||
handleConfirm={() => {
|
||||
setDeleteDialogOpen((preVal) => !preVal);
|
||||
onDelete();
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Are you sure you want to delete the variable{' '}
|
||||
<span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
webhookUrl={webhookUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// https://github.com/creativetimofficial/material-tailwind/issues/419#issuecomment-1760474312
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select, SelectProps } from '@material-tailwind/react';
|
||||
import {
|
||||
Select,
|
||||
SelectProps,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
// TODO: Use correct type for ref
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -9,7 +12,7 @@ const AsyncSelect = React.forwardRef((props: SelectProps, ref: any) => {
|
||||
|
||||
useEffect(() => setKey((preVal) => preVal + 1), [props]);
|
||||
|
||||
return <Select key={key} ref={ref} {...props} placeholder={''} />;
|
||||
return <Select key={key} ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
AsyncSelect.displayName = 'AsyncSelect';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const avatarTheme = tv(
|
||||
{
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden', 'shrink-0'],
|
||||
slots: {
|
||||
image: [
|
||||
'h-full',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { type ComponentPropsWithoutRef, type ComponentProps } from 'react';
|
||||
import { avatarTheme, type AvatarVariants } from './Avatar.theme';
|
||||
import * as PrimitiveAvatar from '@radix-ui/react-avatar';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { BadgeTheme, badgeTheme } from './Badge.theme';
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './Badge';
|
||||
export * from './Badge.theme';
|
||||
|
||||
@@ -20,7 +20,7 @@ export const buttonTheme = tv(
|
||||
variants: {
|
||||
size: {
|
||||
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
|
||||
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
md: ['gap-2', 'py-3', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
|
||||
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import { buttonTheme } from './Button.theme';
|
||||
|
||||
@@ -30,7 +30,7 @@ abbr[title] {
|
||||
@apply !text-elements-disabled !bg-transparent;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
.react-calendar__month-view__days__day--neighboringMonth:focus-visible {
|
||||
@apply !text-elements-disabled !bg-transparent;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {
|
||||
import {
|
||||
ComponentPropsWithRef,
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
|
||||
import './Calendar.css';
|
||||
import { format } from 'date-fns';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
export type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
@@ -63,6 +64,11 @@ export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
|
||||
* @returns None
|
||||
*/
|
||||
onCancel?: () => void;
|
||||
/**
|
||||
* Optional callback function that is called when a reset action is triggered.
|
||||
* @returns None
|
||||
*/
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +86,7 @@ export const Calendar = ({
|
||||
actions,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onReset,
|
||||
onChange: onChangeProp,
|
||||
...props
|
||||
}: CalendarProps): JSX.Element => {
|
||||
@@ -217,6 +224,11 @@ export const Calendar = ({
|
||||
[setValue, setActiveDate, changeNavigationLabel, selectRange],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setValue(null);
|
||||
onReset?.();
|
||||
}, [setValue, onReset]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
@@ -276,21 +288,30 @@ export const Calendar = ({
|
||||
{/* Footer or CTA */}
|
||||
<div
|
||||
{...footerProps}
|
||||
className={footer({ className: footerProps?.className })}
|
||||
className={cn(footer({ className: footerProps?.className }), {
|
||||
'justify-between': Boolean(value),
|
||||
})}
|
||||
>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<>
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!value}
|
||||
onClick={() => (value ? onSelect?.(value) : null)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
{value && (
|
||||
<Button variant="danger" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<div className="space-x-3">
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!value}
|
||||
onClick={() => (value ? onSelect?.(value) : null)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import * as CheckboxRadix from '@radix-ui/react-checkbox';
|
||||
import { type CheckboxProps as CheckboxRadixProps } from '@radix-ui/react-checkbox';
|
||||
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
import React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { Modal, ModalProps } from './Modal';
|
||||
import { Button, ButtonOrLinkProps } from './Button';
|
||||
|
||||
import { color } from '@material-tailwind/react/types/components/button';
|
||||
import {
|
||||
Typography,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
type ConfirmDialogProp = {
|
||||
children: React.ReactNode;
|
||||
dialogTitle: string;
|
||||
export type ConfirmDialogProps = ModalProps & {
|
||||
children?: ReactNode;
|
||||
dialogTitle?: string;
|
||||
open: boolean;
|
||||
handleOpen: () => void;
|
||||
confirmButtonTitle: string;
|
||||
handleCancel: () => void;
|
||||
confirmButtonTitle?: string;
|
||||
handleConfirm?: () => void;
|
||||
color: color;
|
||||
cancelButtonProps?: ButtonOrLinkProps;
|
||||
confirmButtonProps?: ButtonOrLinkProps;
|
||||
};
|
||||
|
||||
const ConfirmDialog = ({
|
||||
children,
|
||||
dialogTitle,
|
||||
open,
|
||||
handleOpen,
|
||||
handleCancel,
|
||||
confirmButtonTitle,
|
||||
handleConfirm,
|
||||
color,
|
||||
}: ConfirmDialogProp) => {
|
||||
cancelButtonProps,
|
||||
confirmButtonProps,
|
||||
...props
|
||||
}: ConfirmDialogProps) => {
|
||||
// Close the dialog when the user clicks outside of it
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) return handleCancel?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
{dialogTitle}{' '}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className=" rounded-full"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<DialogBody placeholder={''}>{children}</DialogBody>
|
||||
<DialogFooter className="flex justify-start gap-2" placeholder={''}>
|
||||
<Button variant="outlined" onClick={handleOpen} placeholder={''}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color={color}
|
||||
onClick={handleConfirm}
|
||||
placeholder={''}
|
||||
>
|
||||
{confirmButtonTitle}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
<Modal {...props} onOpenChange={handleOpenChange}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>{dialogTitle}</Modal.Header>
|
||||
<Modal.Body>{children}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={handleCancel}
|
||||
{...cancelButtonProps}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} {...confirmButtonProps}>
|
||||
{confirmButtonTitle}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const AppleIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_2415_13467)">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18.234 16.046c-.29.67-.633 1.286-1.03 1.853-.543.772-.986 1.307-1.328 1.604-.53.487-1.098.737-1.706.751-.437 0-.963-.124-1.576-.376-.615-.25-1.18-.375-1.696-.375-.542 0-1.123.124-1.745.375-.623.252-1.124.383-1.508.396-.583.025-1.164-.232-1.744-.771-.37-.323-.834-.877-1.389-1.661-.595-.838-1.084-1.809-1.468-2.916-.41-1.197-.616-2.355-.616-3.476 0-1.284.277-2.392.833-3.32a4.89 4.89 0 011.746-1.767 4.696 4.696 0 012.36-.665c.463 0 1.07.143 1.825.424.753.283 1.236.426 1.448.426.159 0 .695-.167 1.606-.501.86-.31 1.587-.438 2.182-.388 1.612.13 2.824.766 3.63 1.911-1.443.874-2.156 2.098-2.142 3.668.013 1.223.457 2.24 1.329 3.049.395.375.836.665 1.327.87-.106.31-.219.605-.338.889zM14.535 1.493c0 .958-.35 1.853-1.048 2.682-.842.985-1.861 1.554-2.966 1.464a3 3 0 01-.022-.363c0-.92.4-1.905 1.112-2.71a4.282 4.282 0 011.354-1.018c.547-.266 1.064-.413 1.55-.439.015.129.02.257.02.384z"
|
||||
></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2415_13467">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M0 0H20V20H0z"
|
||||
transform="translate(.5 .691)"
|
||||
></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ArrowLeftCircleFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM10.4697 15.7803C10.7626 16.0732 11.2374 16.0732 11.5303 15.7803C11.8232 15.4874 11.8232 15.0126 11.5303 14.7197L9.56066 12.75H16.25C16.6642 12.75 17 12.4142 17 12C17 11.5858 16.6642 11.25 16.25 11.25H9.56066L11.5303 9.28033C11.8232 8.98744 11.8232 8.51256 11.5303 8.21967C11.2374 7.92678 10.7626 7.92678 10.4697 8.21967L7.21967 11.4697C6.92678 11.7626 6.92678 12.2374 7.21967 12.5303L10.4697 15.7803Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ArrowRightCircleFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
{...props}
|
||||
fill="none"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
width="32"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ArrowRightCircleIcon = (props: CustomIconProps) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const BranchIcon = (props: CustomIconProps) => {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const BranchStrokeIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.66667 4.99984C5.58714 4.99984 6.33333 4.25365 6.33333 3.33317C6.33333 2.4127 5.58714 1.6665 4.66667 1.6665C3.74619 1.6665 3 2.4127 3 3.33317C3 4.25365 3.74619 4.99984 4.66667 4.99984ZM4.66667 4.99984V10.9998M4.66667 10.9998C3.74619 10.9998 3 11.746 3 12.6665C3 13.587 3.74619 14.3332 4.66667 14.3332C5.58714 14.3332 6.33333 13.587 6.33333 12.6665C6.33333 11.746 5.58714 10.9998 4.66667 10.9998ZM4.66667 10.9998V9.33317C4.66667 8.59679 5.26362 7.99984 6 7.99984H10C10.7364 7.99984 11.3333 7.40288 11.3333 6.6665V4.99984M11.3333 4.99984C12.2538 4.99984 13 4.25365 13 3.33317C13 2.4127 12.2538 1.6665 11.3333 1.6665C10.4129 1.6665 9.66667 2.4127 9.66667 3.33317C9.66667 4.25365 10.4129 4.99984 11.3333 4.99984Z"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
d="M14.1667 9.99967V10.4997H14.6667V9.99967H14.1667ZM5.83333 9.99967V9.49967H5.33333V9.99967H5.83333ZM7.41667 4.16634C7.41667 5.04079 6.70778 5.74967 5.83333 5.74967V6.74967C7.26007 6.74967 8.41667 5.59308 8.41667 4.16634H7.41667ZM5.83333 5.74967C4.95888 5.74967 4.25 5.04079 4.25 4.16634H3.25C3.25 5.59308 4.4066 6.74967 5.83333 6.74967V5.74967ZM4.25 4.16634C4.25 3.29189 4.95888 2.58301 5.83333 2.58301V1.58301C4.4066 1.58301 3.25 2.73961 3.25 4.16634H4.25ZM5.83333 2.58301C6.70778 2.58301 7.41667 3.29189 7.41667 4.16634H8.41667C8.41667 2.73961 7.26007 1.58301 5.83333 1.58301V2.58301ZM7.41667 15.833C7.41667 16.7075 6.70778 17.4163 5.83333 17.4163V18.4163C7.26007 18.4163 8.41667 17.2597 8.41667 15.833H7.41667ZM5.83333 17.4163C4.95888 17.4163 4.25 16.7075 4.25 15.833H3.25C3.25 17.2597 4.4066 18.4163 5.83333 18.4163V17.4163ZM4.25 15.833C4.25 14.9586 4.95888 14.2497 5.83333 14.2497V13.2497C4.4066 13.2497 3.25 14.4063 3.25 15.833H4.25ZM5.83333 14.2497C6.70778 14.2497 7.41667 14.9586 7.41667 15.833H8.41667C8.41667 14.4063 7.26007 13.2497 5.83333 13.2497V14.2497ZM5.33333 6.24967V13.7497H6.33333V6.24967H5.33333ZM13.6667 6.24967V9.99967H14.6667V6.24967H13.6667ZM14.1667 9.49967H5.83333V10.4997H14.1667V9.49967ZM5.33333 9.99967V13.7497H6.33333V9.99967H5.33333ZM15.75 4.16634C15.75 5.04079 15.0411 5.74967 14.1667 5.74967V6.74967C15.5934 6.74967 16.75 5.59308 16.75 4.16634H15.75ZM14.1667 5.74967C13.2922 5.74967 12.5833 5.04079 12.5833 4.16634H11.5833C11.5833 5.59308 12.7399 6.74967 14.1667 6.74967V5.74967ZM12.5833 4.16634C12.5833 3.29189 13.2922 2.58301 14.1667 2.58301V1.58301C12.7399 1.58301 11.5833 2.73961 11.5833 4.16634H12.5833ZM14.1667 2.58301C15.0411 2.58301 15.75 3.29189 15.75 4.16634H16.75C16.75 2.73961 15.5934 1.58301 14.1667 1.58301V2.58301Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const BuildingIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3.75 19.25H14.25M3.75 19.25V5.75C3.75 4.64543 4.64543 3.75 5.75 3.75H12.25C13.3546 3.75 14.25 4.64543 14.25 5.75V8M3.75 19.25H1.75M14.25 19.25V8M14.25 19.25H20.25M14.25 8H18.25C19.3546 8 20.25 8.89543 20.25 10V19.25M20.25 19.25H22.25M10.25 8.75H7.75M7.75 12.75H10.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarDaysIcon = (props: CustomIconProps) => {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
|
||||
fill="currentColor"
|
||||
d="M2.625 7.125H15.375M5.625 3.375V1.875M12.375 3.375V1.875M4.125 15.375H13.875C14.7034 15.375 15.375 14.7034 15.375 13.875V4.875C15.375 4.04657 14.7034 3.375 13.875 3.375H4.125C3.29657 3.375 2.625 4.04657 2.625 4.875V13.875C2.625 14.7034 3.29657 15.375 4.125 15.375Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user