forked from cerc-io/snowballtools-base
commit
8376aff7bd
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,3 +5,5 @@ yarn-error.log
|
||||
.yarnrc
|
||||
|
||||
packages/backend/environments/local.toml
|
||||
packages/backend/dev/
|
||||
packages/frontend/dist/
|
@ -3,10 +3,11 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@cerc-io/laconic-sdk": "^0.1.16",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
"@snowballtools/laconic-sdk": "^0.1.17",
|
||||
"@turnkey/sdk-server": "^0.1.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.0",
|
||||
|
@ -52,4 +52,10 @@ export interface Config {
|
||||
gitHub: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
misc: MiscConfig;
|
||||
turnkey: {
|
||||
apiBaseUrl: string;
|
||||
apiPublicKey: string;
|
||||
apiPrivateKey: string;
|
||||
defaultOrganizationId: string;
|
||||
};
|
||||
}
|
||||
|
@ -39,6 +39,12 @@ export class User {
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column()
|
||||
subOrgId!: string;
|
||||
|
||||
@Column()
|
||||
turnkeyWalletId!: string;
|
||||
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
|
@ -3,7 +3,7 @@ import assert from 'assert';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import {
|
||||
|
@ -1,45 +1,50 @@
|
||||
import { Router } from 'express';
|
||||
import { SiweMessage } from 'siwe';
|
||||
import { Service } from '../service';
|
||||
import { authenticateUser, createUser } from '../turnkey-backend';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/validate', async (req, res) => {
|
||||
const { message, signature, action } = req.body;
|
||||
const { success, data } = await new SiweMessage(message).verify({
|
||||
signature,
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
return res.send({ success });
|
||||
}
|
||||
|
||||
router.get('/registration/:email', async (req, res) => {
|
||||
const service: Service = req.app.get('service');
|
||||
const user = await service.getUserByEthAddress(data.address);
|
||||
|
||||
if (action === 'signup') {
|
||||
if (user) {
|
||||
return res.send({ success: false, error: 'user_already_exists' });
|
||||
}
|
||||
const newUser = await service.loadOrCreateUser(data.address);
|
||||
req.session.userId = newUser.id;
|
||||
} else if (action === 'login') {
|
||||
if (!user) {
|
||||
return res.send({ success: false, error: 'user_not_found' });
|
||||
}
|
||||
req.session.userId = user.id;
|
||||
const user = await service.getUserByEmail(req.params.email);
|
||||
if (user) {
|
||||
return res.send({ subOrganizationId: user?.subOrgId });
|
||||
} else {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
});
|
||||
|
||||
req.session.address = data.address;
|
||||
router.post('/register', async (req, res) => {
|
||||
const { email, challenge, attestation } = req.body;
|
||||
const user = await createUser(req.app.get('service'), {
|
||||
challenge,
|
||||
attestation,
|
||||
userEmail: email,
|
||||
userName: email.split('@')[0],
|
||||
});
|
||||
req.session.userId = user.id;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
res.send({ success });
|
||||
router.post('/authenticate', async (req, res) => {
|
||||
const { signedWhoamiRequest } = req.body;
|
||||
const user = await authenticateUser(
|
||||
req.app.get('service'),
|
||||
signedWhoamiRequest,
|
||||
);
|
||||
if (user) {
|
||||
req.session.userId = user.id;
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.address) {
|
||||
if (req.session.userId) {
|
||||
res.send({
|
||||
userId: req.session.userId,
|
||||
address: req.session.address,
|
||||
});
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||
|
@ -24,7 +24,6 @@ const log = debug('snowball:server');
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
userId: string;
|
||||
address: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,14 +53,13 @@ export const createAndStartServer = async (
|
||||
context: async ({ req }) => {
|
||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||
|
||||
const { address } = req.session;
|
||||
const { userId } = req.session;
|
||||
|
||||
if (!address) {
|
||||
if (!userId) {
|
||||
throw new AuthenticationError('Unauthorized: No active session');
|
||||
}
|
||||
|
||||
// Find/create user from ETH address in request session
|
||||
const user = await service.loadOrCreateUser(address);
|
||||
const user = await service.getUser(userId);
|
||||
|
||||
return { user };
|
||||
},
|
||||
|
@ -161,6 +161,22 @@ export class Service {
|
||||
});
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | null> {
|
||||
return await this.db.getUser({
|
||||
where: {
|
||||
email
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getUserBySubOrgId(subOrgId: string): Promise<User | null> {
|
||||
return await this.db.getUser({
|
||||
where: {
|
||||
subOrgId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getUserByEthAddress (ethAddress: string): Promise<User | null> {
|
||||
return await this.db.getUser({
|
||||
where: {
|
||||
@ -169,28 +185,31 @@ export class Service {
|
||||
});
|
||||
}
|
||||
|
||||
async loadOrCreateUser (ethAddress: string): Promise<User> {
|
||||
// Get user by ETH address
|
||||
let user = await this.getUserByEthAddress(ethAddress);
|
||||
async createUser (params: {
|
||||
name: string
|
||||
email: string
|
||||
subOrgId: string
|
||||
ethAddress: string
|
||||
turnkeyWalletId: string
|
||||
}): Promise<User> {
|
||||
const [org] = await this.db.getOrganizations({});
|
||||
assert(org, 'No organizations exists in database');
|
||||
|
||||
if (!user) {
|
||||
const [org] = await this.db.getOrganizations({});
|
||||
assert(org, 'No organizations exists in database');
|
||||
// Create user with new address
|
||||
const user = await this.db.addUser({
|
||||
email: params.email,
|
||||
name: params.name,
|
||||
subOrgId: params.subOrgId,
|
||||
ethAddress: params.ethAddress,
|
||||
isVerified: true,
|
||||
turnkeyWalletId: params.turnkeyWalletId,
|
||||
});
|
||||
|
||||
// Create user with new address
|
||||
user = await this.db.addUser({
|
||||
email: `${ethAddress}@example.com`,
|
||||
name: ethAddress,
|
||||
isVerified: true,
|
||||
ethAddress
|
||||
});
|
||||
|
||||
await this.db.addUserOrganization({
|
||||
member: user,
|
||||
organization: org,
|
||||
role: Role.Owner
|
||||
});
|
||||
}
|
||||
await this.db.addUserOrganization({
|
||||
member: user,
|
||||
organization: org,
|
||||
role: Role.Owner
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
130
packages/backend/src/turnkey-backend.ts
Normal file
130
packages/backend/src/turnkey-backend.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { Turnkey, TurnkeyApiTypes } from '@turnkey/sdk-server';
|
||||
|
||||
// Default path for the first Ethereum address in a new HD wallet.
|
||||
// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form:
|
||||
// m / purpose' / coin_type' / account' / change / address_index
|
||||
// - Purpose is a constant set to 44' following the BIP43 recommendation.
|
||||
// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
// - Account, Change, and Address Index are set to 0
|
||||
import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server';
|
||||
import { getConfig } from './utils';
|
||||
import { Service } from './service';
|
||||
|
||||
type TAttestation = TurnkeyApiTypes['v1Attestation'];
|
||||
|
||||
type CreateUserParams = {
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
challenge: string;
|
||||
attestation: TAttestation;
|
||||
};
|
||||
|
||||
export async function createUser(
|
||||
service: Service,
|
||||
{ userName, userEmail, challenge, attestation }: CreateUserParams,
|
||||
) {
|
||||
try {
|
||||
if (await service.getUserByEmail(userEmail)) {
|
||||
throw new Error(`User already exists: ${userEmail}`);
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
const turnkey = new Turnkey(config.turnkey);
|
||||
|
||||
const apiClient = turnkey.api();
|
||||
|
||||
const walletName = `Default ETH Wallet`;
|
||||
|
||||
const createSubOrgResponse = await apiClient.createSubOrganization({
|
||||
subOrganizationName: `Default SubOrg for ${userEmail}`,
|
||||
rootQuorumThreshold: 1,
|
||||
rootUsers: [
|
||||
{
|
||||
userName,
|
||||
userEmail,
|
||||
apiKeys: [],
|
||||
authenticators: [
|
||||
{
|
||||
authenticatorName: 'Passkey',
|
||||
challenge,
|
||||
attestation,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
wallet: {
|
||||
walletName: walletName,
|
||||
accounts: DEFAULT_ETHEREUM_ACCOUNTS,
|
||||
},
|
||||
});
|
||||
|
||||
const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId);
|
||||
const wallet = refineNonNull(createSubOrgResponse.wallet);
|
||||
|
||||
const result = {
|
||||
id: wallet.walletId,
|
||||
address: wallet.addresses[0],
|
||||
subOrgId: subOrgId,
|
||||
};
|
||||
console.log('Turnkey success', result);
|
||||
|
||||
const user = await service.createUser({
|
||||
name: userName,
|
||||
email: userEmail,
|
||||
subOrgId,
|
||||
ethAddress: wallet.addresses[0],
|
||||
turnkeyWalletId: wallet.walletId,
|
||||
});
|
||||
console.log('New user', user);
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error('Failed to create user:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticateUser(
|
||||
service: Service,
|
||||
signedWhoamiRequest: {
|
||||
url: string;
|
||||
body: any;
|
||||
stamp: {
|
||||
stampHeaderName: string;
|
||||
stampHeaderValue: string;
|
||||
};
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const tkRes = await fetch(signedWhoamiRequest.url, {
|
||||
method: 'POST',
|
||||
body: signedWhoamiRequest.body,
|
||||
headers: {
|
||||
[signedWhoamiRequest.stamp.stampHeaderName]:
|
||||
signedWhoamiRequest.stamp.stampHeaderValue,
|
||||
},
|
||||
});
|
||||
console.log('AUTH RESULT', tkRes.status);
|
||||
if (tkRes.status !== 200) {
|
||||
console.log(await tkRes.text());
|
||||
return null;
|
||||
}
|
||||
const orgId = (await tkRes.json()).organizationId;
|
||||
const user = await service.getUserBySubOrgId(orgId);
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error('Failed to authenticate:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function refineNonNull<T>(
|
||||
input: T | null | undefined,
|
||||
errorMessage?: string,
|
||||
): T {
|
||||
if (input == null) {
|
||||
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { Config } from '../src/config';
|
||||
|
@ -2,7 +2,7 @@ import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
@ -4,6 +4,6 @@
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cerc-io/laconic-registry-cli": "^0.1.10"
|
||||
"@snowballtools/laconic-registry-cli": "^0.1.13"
|
||||
}
|
||||
}
|
||||
|
@ -10,4 +10,8 @@ VITE_LIT_RELAY_API_KEY=
|
||||
|
||||
VITE_ALCHEMY_API_KEY=
|
||||
|
||||
LOCAL_SNOWBALL_SDK_DIR=
|
||||
VITE_BUGSNAG_API_KEY=
|
||||
|
||||
VITE_PASSKEY_WALLET_RPID=
|
||||
VITE_TURNKEY_API_BASE_URL=
|
||||
VITE_TURNKEY_ORGANIZATION_ID=
|
@ -5,6 +5,7 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
1
packages/frontend/.gitignore
vendored
1
packages/frontend/.gitignore
vendored
@ -22,3 +22,4 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*storybook.log
|
31
packages/frontend/.storybook/main.ts
Normal file
31
packages/frontend/.storybook/main.ts
Normal file
@ -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;
|
16
packages/frontend/.storybook/preview.ts
Normal file
16
packages/frontend/.storybook/preview.ts
Normal file
@ -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;
|
4
packages/frontend/chromatic.config.json
Normal file
4
packages/frontend/chromatic.config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"projectId": "Project:663d04870db27ed66a48e466",
|
||||
"zip": true
|
||||
}
|
@ -5,11 +5,17 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"build": "tsc && vite build",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit",
|
||||
"preview": "vite preview"
|
||||
"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",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@ -33,6 +39,9 @@
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@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",
|
||||
@ -62,11 +71,20 @@
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.66",
|
||||
@ -74,8 +92,12 @@
|
||||
"@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",
|
||||
"storybook": "^8.0.10",
|
||||
"storybook-addon-remix-react-router": "^3.0.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.0"
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
(cd /Users/rabbit-m2/p/snowball/snowball-ts-sdk && NO_CLEAN=1 turbo build)
|
||||
(cd /Users/rabbit-m2/p/snowball/js-sdk && NO_CLEAN=1 turbo build)
|
||||
|
||||
(cd ../.. && ./scripts/yarn-file-for-local-dev.sh)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = React.PropsWithChildren<{
|
||||
className?: string;
|
||||
snowZIndex?: number;
|
||||
|
@ -12,7 +12,7 @@ import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
BranchIcon,
|
||||
ClockIcon,
|
||||
GitHubLogo,
|
||||
GithubLogoIcon,
|
||||
HorizontalDotIcon,
|
||||
WarningDiamondIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
@ -118,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,12 +1,9 @@
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
|
||||
import {
|
||||
Typography,
|
||||
Input,
|
||||
IconButton,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { EnvironmentVariablesFormValues } from '../../../../types/types';
|
||||
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;
|
||||
@ -22,31 +19,30 @@ const AddEnvironmentVariableRow = ({
|
||||
isDeleteDisabled,
|
||||
}: AddEnvironmentVariableRowProps) => {
|
||||
return (
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small">Key</Typography>
|
||||
<Input
|
||||
{...register(`variables.${index}.key`, {
|
||||
required: 'Key field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small">Value</Typography>
|
||||
<Input
|
||||
{...register(`variables.${index}.value`, {
|
||||
required: 'Value field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex py-4 self-stretch">
|
||||
<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',
|
||||
})}
|
||||
/>
|
||||
<div className="self-end">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => onDelete()}
|
||||
<Button
|
||||
size="md"
|
||||
iconOnly
|
||||
onClick={onDelete}
|
||||
disabled={isDeleteDisabled}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,16 +2,12 @@ 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 '@snowballtools/material-tailwind-react-fork';
|
||||
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,59 +57,47 @@ const AddMemberDialog = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<div>Add member</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<DialogBody 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"
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
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 {
|
||||
@ -13,7 +12,9 @@ import {
|
||||
Input,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
interface DeleteProjectDialogProp {
|
||||
open: boolean;
|
||||
@ -26,6 +27,7 @@ const DeleteProjectDialog = ({
|
||||
handleOpen,
|
||||
project,
|
||||
}: DeleteProjectDialogProp) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const { orgSlug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
@ -46,7 +48,12 @@ 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();
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Collapse,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
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;
|
||||
@ -24,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">
|
||||
<Typography variant="small" className="text-black">
|
||||
No environment variables added yet.
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
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 { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Domain, DomainStatus, Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
@ -15,6 +14,7 @@ import {
|
||||
import EditDomainDialog from './EditDomainDialog';
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -104,13 +115,13 @@ const DomainCard = ({
|
||||
setEditDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
^ Edit domain
|
||||
Edit Domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
>
|
||||
^ Delete domain
|
||||
Delete domain
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
@ -130,7 +141,7 @@ const DomainCard = ({
|
||||
|
||||
<Typography variant="small">Production</Typography>
|
||||
{domain.status === DomainStatus.Pending && (
|
||||
<Card className="bg-gray-200 p-4 text-sm">
|
||||
<Card className="bg-slate-100 p-4 text-sm">
|
||||
{refreshStatus === RefreshStatus.IDLE ? (
|
||||
<Typography variant="small">
|
||||
^ Add these records to your domain and refresh to check
|
||||
@ -141,7 +152,6 @@ const DomainCard = ({
|
||||
</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.
|
||||
|
@ -4,18 +4,15 @@ import toast from 'react-hot-toast';
|
||||
import { Domain } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Typography,
|
||||
Select,
|
||||
Option,
|
||||
} 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,77 +119,67 @@ const EditDomainDialog = ({
|
||||
}, [domain]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<div>Edit domain</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4">
|
||||
<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>
|
||||
<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">
|
||||
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">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>
|
||||
)}
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -5,12 +5,12 @@ import { EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import {
|
||||
IconButton,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
const ShowPasswordIcon = ({
|
||||
handler,
|
||||
@ -96,7 +96,7 @@ const EditEnvironmentVariableRow = ({
|
||||
<Input
|
||||
disabled={!edit}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
icon={
|
||||
leftIcon={
|
||||
<ShowPasswordIcon
|
||||
handler={() => {
|
||||
setShowPassword((prevShowPassword) => !prevShowPassword);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GitSelect } from '../../../../types/types';
|
||||
import { GitSelect } from '../../../../types';
|
||||
|
||||
const GitSelectionSection = ({
|
||||
gitSelectionHandler,
|
||||
|
@ -1,18 +1,16 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Permission, User } from 'gql-client';
|
||||
|
||||
import {
|
||||
Select,
|
||||
Option,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { formatAddress } from 'utils/format';
|
||||
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';
|
||||
|
||||
const PERMISSION_OPTIONS = [
|
||||
const PERMISSION_OPTIONS: SelectOption[] = [
|
||||
{
|
||||
label: 'View only',
|
||||
value: 'View',
|
||||
@ -23,7 +21,7 @@ const PERMISSION_OPTIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
const DROPDOWN_OPTIONS = [
|
||||
const DROPDOWN_OPTIONS: SelectOption[] = [
|
||||
...PERMISSION_OPTIONS,
|
||||
{ label: 'Remove member', value: 'remove' },
|
||||
];
|
||||
@ -50,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(() => {
|
||||
@ -67,7 +70,7 @@ const MemberCard = ({
|
||||
});
|
||||
} else {
|
||||
if (onUpdateProjectMember) {
|
||||
const permissions = value.split('+') as Permission[];
|
||||
const permissions = value.value.split('+') as Permission[];
|
||||
await onUpdateProjectMember({ permissions });
|
||||
}
|
||||
}
|
||||
@ -77,9 +80,8 @@ const MemberCard = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex p-1 ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
|
||||
className={`flex p-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
|
||||
>
|
||||
<div>^</div>
|
||||
<div className="basis-1/2">
|
||||
{member.name && (
|
||||
<Tooltip content={member.name}>
|
||||
@ -94,46 +96,36 @@ 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>
|
||||
)}
|
||||
>
|
||||
{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);
|
||||
}}
|
||||
>
|
||||
D
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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 };
|
@ -1,12 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { GitRepositoryDetails } from '../../../../types/types';
|
||||
import { GitRepositoryDetails } from '../../../../types';
|
||||
import { DisconnectRepositoryDialog } from 'components/projects/Dialog/DisconnectRepositoryDialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
const RepoConnectedSection = ({
|
||||
linkedRepo,
|
||||
@ -24,12 +22,8 @@ const RepoConnectedSection = ({
|
||||
<Typography variant="small">Connected just now</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setDisconnectRepoDialogOpen(true)}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
>
|
||||
^ Disconnect
|
||||
<Button onClick={() => setDisconnectRepoDialogOpen(true)} size="sm">
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
<DisconnectRepositoryDialog
|
||||
|
@ -2,13 +2,16 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import {
|
||||
Radio,
|
||||
Typography,
|
||||
Button,
|
||||
Input,
|
||||
Alert,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
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,60 +63,49 @@ const SetupDomain = () => {
|
||||
className="flex flex-col gap-6 w-full"
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h5">Setup domain name</Typography>
|
||||
<Typography variant="small">
|
||||
<Heading className="text-sky-950 text-lg font-medium leading-normal">
|
||||
Setup domain name
|
||||
</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">Domain name</Typography>
|
||||
<Input
|
||||
type="text"
|
||||
variant="outlined"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
{...register('domainName', {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
size="md"
|
||||
placeholder="example.com"
|
||||
{...register('domainName', {
|
||||
required: true,
|
||||
})}
|
||||
label="Domain name"
|
||||
/>
|
||||
|
||||
{isValid && (
|
||||
<div>
|
||||
<Typography>Primary domain</Typography>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Radio
|
||||
label={domainStr}
|
||||
{...register('isWWW')}
|
||||
value="false"
|
||||
type="radio"
|
||||
/>
|
||||
<Radio
|
||||
label={`www.${domainStr}`}
|
||||
{...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"
|
||||
>
|
||||
<i>^</i> Next
|
||||
</Button>
|
||||
<div className="self-stretch">
|
||||
<Button disabled={!isValid} type="submit">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Button } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
interface WebhookCardProps {
|
||||
webhookUrl: string;
|
||||
@ -11,6 +10,8 @@ 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">
|
||||
@ -20,14 +21,19 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
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,
|
||||
});
|
||||
}}
|
||||
>
|
||||
C
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './Badge';
|
||||
export * from './Badge.theme';
|
||||
|
@ -3,16 +3,15 @@ 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,20 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronDownSmallIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.4697 14.5303C11.7626 14.8232 12.2374 14.8232 12.5303 14.5303L16.5303 10.5303C16.8232 10.2374 16.8232 9.76256 16.5303 9.46967C16.2374 9.17678 15.7626 9.17678 15.4697 9.46967L12 12.9393L8.53033 9.46967C8.23744 9.17678 7.76256 9.17678 7.46967 9.46967C7.17678 9.76256 7.17678 10.2374 7.46967 10.5303L11.4697 14.5303Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronUpSmallIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.4697 9.46967C11.7626 9.17678 12.2374 9.17678 12.5303 9.46967L16.5303 13.4697C16.8232 13.7626 16.8232 14.2374 16.5303 14.5303C16.2374 14.8232 15.7626 14.8232 15.4697 14.5303L12 11.0607L8.53033 14.5303C8.23744 14.8232 7.76256 14.8232 7.46967 14.5303C7.17678 14.2374 7.17678 13.7626 7.46967 13.4697L11.4697 9.46967Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CollaboratorsIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12.5002 2.08301C14.3411 2.08301 15.8335 3.57539 15.8335 5.41634C15.8335 7.25729 14.3411 8.74968 12.5002 8.74968M14.5835 11.1659C17.2451 12.0374 19.1668 14.498 19.1668 17.083H16.6668M7.50016 8.74968C5.65921 8.74968 4.16683 7.25729 4.16683 5.41634C4.16683 3.57539 5.65921 2.08301 7.50016 2.08301C9.34111 2.08301 10.8335 3.57539 10.8335 5.41634C10.8335 7.25729 9.34111 8.74968 7.50016 8.74968ZM0.833496 17.083C0.833496 13.8613 3.81826 10.833 7.50016 10.833C11.1821 10.833 14.1668 13.8613 14.1668 17.083H0.833496Z"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CopyUnfilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6 5.625V5.025C6 4.18492 6 3.76488 6.16349 3.44401C6.3073 3.16177 6.53677 2.9323 6.81901 2.78849C7.13988 2.625 7.55992 2.625 8.4 2.625H12.975C13.8151 2.625 14.2351 2.625 14.556 2.78849C14.8382 2.9323 15.0677 3.16177 15.2115 3.44401C15.375 3.76488 15.375 4.18492 15.375 5.025V9.975C15.375 10.8151 15.375 11.2351 15.2115 11.556C15.0677 11.8382 14.8382 12.0677 14.556 12.2115C14.2351 12.375 13.8151 12.375 12.975 12.375H12.375M12.375 8.025V12.975C12.375 13.8151 12.375 14.2351 12.2115 14.556C12.0677 14.8382 11.8382 15.0677 11.556 15.2115C11.2351 15.375 10.8151 15.375 9.975 15.375H5.025C4.18492 15.375 3.76488 15.375 3.44401 15.2115C3.16177 15.0677 2.9323 14.8382 2.78849 14.556C2.625 14.2351 2.625 13.8151 2.625 12.975V8.025C2.625 7.18492 2.625 6.76488 2.78849 6.44401C2.9323 6.16177 3.16177 5.9323 3.44401 5.78849C3.76488 5.625 4.18492 5.625 5.025 5.625H9.975C10.8151 5.625 11.2351 5.625 11.556 5.78849C11.8382 5.9323 12.0677 6.16177 12.2115 6.44401C12.375 6.76488 12.375 7.18492 12.375 8.025Z"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const GearIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M7.62516 4.45898L5.05225 3.86523L3.86475 5.05273L4.4585 7.62565L2.0835 9.20898V10.7923L4.4585 12.3757L3.86475 14.9486L5.05225 16.1361L7.62516 15.5423L9.2085 17.9173H10.7918L12.3752 15.5423L14.9481 16.1361L16.1356 14.9486L15.5418 12.3757L17.9168 10.7923V9.20898L15.5418 7.62565L16.1356 5.05273L14.9481 3.86523L12.3752 4.45898L10.7918 2.08398H9.2085L7.62516 4.45898Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M12.5002 10.0007C12.5002 11.3814 11.3809 12.5007 10.0002 12.5007C8.61945 12.5007 7.50016 11.3814 7.50016 10.0007C7.50016 8.61994 8.61945 7.50065 10.0002 7.50065C11.3809 7.50065 12.5002 8.61994 12.5002 10.0007Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const GitHubLogo = (props: CustomIconProps) => {
|
||||
export const GithubLogoIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="12"
|
@ -0,0 +1,22 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const SwitchIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.25 9.99967C1.25 7.00813 3.67512 4.58301 6.66667 4.58301H13.3333C16.3249 4.58301 18.75 7.00813 18.75 9.99967C18.75 12.9912 16.3249 15.4163 13.3333 15.4163H6.66667C3.67512 15.4163 1.25 12.9912 1.25 9.99967Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10.4167 9.99967C10.4167 8.38884 11.7225 7.08301 13.3333 7.08301C14.9442 7.08301 16.25 8.38884 16.25 9.99967C16.25 11.6105 14.9442 12.9163 13.3333 12.9163C11.7225 12.9163 10.4167 11.6105 10.4167 9.99967Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const TrashIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.05612 3.33398C5.52062 2.16265 6.66341 1.33398 8.00079 1.33398C9.33816 1.33398 10.481 2.16265 10.9455 3.33398H14.1668C14.443 3.33398 14.6668 3.55784 14.6668 3.83398C14.6668 4.11013 14.443 4.33398 14.1668 4.33398H13.3023L12.7463 12.952C12.684 13.9167 11.8834 14.6673 10.9167 14.6673H5.08358C4.11688 14.6673 3.31629 13.9167 3.25405 12.952L2.69805 4.33398H1.8335C1.55735 4.33398 1.3335 4.11013 1.3335 3.83398C1.3335 3.55784 1.55735 3.33398 1.8335 3.33398H5.05612ZM6.17457 3.33398C6.55973 2.73248 7.23408 2.33398 8.00079 2.33398C8.76749 2.33398 9.44184 2.73248 9.827 3.33398H6.17457ZM7.00016 7.16732C7.00016 6.89118 6.77631 6.66732 6.50016 6.66732C6.22402 6.66732 6.00016 6.89118 6.00016 7.16732V10.834C6.00016 11.1101 6.22402 11.334 6.50016 11.334C6.77631 11.334 7.00016 11.1101 7.00016 10.834V7.16732ZM9.50016 6.66732C9.77631 6.66732 10.0002 6.89118 10.0002 7.16732V10.834C10.0002 11.1101 9.77631 11.334 9.50016 11.334C9.22402 11.334 9.00016 11.1101 9.00016 10.834V7.16732C9.00016 6.89118 9.22402 6.66732 9.50016 6.66732Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -32,7 +32,7 @@ export * from './BuildingIcon';
|
||||
export * from './CheckRadioIcon';
|
||||
export * from './ChevronDownIcon';
|
||||
export * from './BranchIcon';
|
||||
export * from './GitHubLogo';
|
||||
export * from './GitHubLogoIcon';
|
||||
export * from './ClockIcon';
|
||||
export * from './HorizontalDotIcon';
|
||||
export * from './WarningDiamondIcon';
|
||||
@ -62,6 +62,17 @@ export * from './WarningTriangleIcon';
|
||||
export * from './CheckRadioOutlineIcon';
|
||||
export * from './TrendingIcon';
|
||||
export * from './ChevronDoubleDownIcon';
|
||||
export * from './GearIcon';
|
||||
export * from './AppleIcon';
|
||||
export * from './CalendarDaysIcon';
|
||||
export * from './GoogleIcon';
|
||||
export * from './KeyIcon';
|
||||
export * from './TrashIcon';
|
||||
export * from './CopyUnfilledIcon';
|
||||
export * from './SwitchIcon';
|
||||
export * from './CollaboratorsIcon';
|
||||
export * from './ChevronUpSmallIcon';
|
||||
export * from './ChevronDownSmallIcon';
|
||||
|
||||
// Templates
|
||||
export * from './templates';
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './InlineNotification';
|
||||
export * from './InlineNotification.theme';
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { InputTheme, inputTheme } from './Input.theme';
|
||||
import {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
ComponentPropsWithoutRef,
|
||||
} from 'react';
|
||||
import { FieldValues, UseFormRegister } from 'react-hook-form';
|
||||
|
||||
import { WarningIcon } from 'components/shared/CustomIcon';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
export interface InputProps
|
||||
import { InputTheme, inputTheme } from './Input.theme';
|
||||
|
||||
export interface InputProps<T extends FieldValues = FieldValues>
|
||||
extends InputTheme,
|
||||
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
|
||||
label?: string;
|
||||
@ -13,93 +20,108 @@ export interface InputProps
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
helperText?: string;
|
||||
|
||||
// react-hook-form optional register
|
||||
register?: ReturnType<UseFormRegister<T>>;
|
||||
}
|
||||
|
||||
export const Input = ({
|
||||
className,
|
||||
label,
|
||||
description,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
helperText,
|
||||
size,
|
||||
state,
|
||||
appearance,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
const styleProps = useMemo(
|
||||
() => ({
|
||||
size: size || 'md',
|
||||
state: state || 'default',
|
||||
appearance, // Pass appearance to inputTheme
|
||||
}),
|
||||
[size, state, appearance],
|
||||
);
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
label,
|
||||
description,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
helperText,
|
||||
register,
|
||||
size,
|
||||
state,
|
||||
appearance,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const styleProps = useMemo(
|
||||
() => ({
|
||||
size: size || 'md',
|
||||
state: state || 'default',
|
||||
appearance, // Pass appearance to inputTheme
|
||||
}),
|
||||
[size, state, appearance],
|
||||
);
|
||||
|
||||
const {
|
||||
container: containerCls,
|
||||
label: labelCls,
|
||||
description: descriptionCls,
|
||||
input: inputCls,
|
||||
icon: iconCls,
|
||||
iconContainer: iconContainerCls,
|
||||
helperText: helperTextCls,
|
||||
helperIcon: helperIconCls,
|
||||
} = inputTheme({ ...styleProps });
|
||||
const {
|
||||
container: containerCls,
|
||||
label: labelCls,
|
||||
description: descriptionCls,
|
||||
input: inputCls,
|
||||
icon: iconCls,
|
||||
iconContainer: iconContainerCls,
|
||||
helperText: helperTextCls,
|
||||
helperIcon: helperIconCls,
|
||||
} = inputTheme({ ...styleProps });
|
||||
|
||||
const renderLabels = useMemo(() => {
|
||||
if (!label && !description) return null;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className={labelCls()}>{label}</p>
|
||||
<p className={descriptionCls()}>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}, [labelCls, descriptionCls, label, description]);
|
||||
|
||||
const renderLeftIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
|
||||
{cloneIcon(leftIcon, { className: iconCls(), 'aria-hidden': true })}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
|
||||
|
||||
const renderRightIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
|
||||
{cloneIcon(rightIcon, { className: iconCls(), 'aria-hidden': true })}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
|
||||
|
||||
const renderHelperText = useMemo(() => {
|
||||
if (!helperText) return null;
|
||||
return (
|
||||
<div className={helperTextCls()}>
|
||||
{state &&
|
||||
cloneIcon(<WarningIcon className={helperIconCls()} />, {
|
||||
'aria-hidden': true,
|
||||
})}
|
||||
<p>{helperText}</p>
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
|
||||
|
||||
const renderLabels = useMemo(() => {
|
||||
if (!label && !description) return null;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className={labelCls()}>{label}</p>
|
||||
<p className={descriptionCls()}>{description}</p>
|
||||
<div className="flex flex-col gap-y-2 w-full">
|
||||
{renderLabels}
|
||||
<div className={containerCls({ class: className })}>
|
||||
{leftIcon && renderLeftIcon}
|
||||
<input
|
||||
{...(register ? register : {})}
|
||||
className={cn(inputCls(), {
|
||||
'pl-10': leftIcon,
|
||||
})}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
{rightIcon && renderRightIcon}
|
||||
</div>
|
||||
{renderHelperText}
|
||||
</div>
|
||||
);
|
||||
}, [labelCls, descriptionCls, label, description]);
|
||||
},
|
||||
);
|
||||
|
||||
const renderLeftIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
|
||||
{cloneIcon(leftIcon, { className: iconCls(), 'aria-hidden': true })}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
const renderRightIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
|
||||
{cloneIcon(rightIcon, { className: iconCls(), 'aria-hidden': true })}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
|
||||
|
||||
const renderHelperText = useMemo(() => {
|
||||
if (!helperText) return null;
|
||||
return (
|
||||
<div className={helperTextCls()}>
|
||||
{state &&
|
||||
cloneIcon(<WarningIcon className={helperIconCls()} />, {
|
||||
'aria-hidden': true,
|
||||
})}
|
||||
<p>{helperText}</p>
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 w-full">
|
||||
{renderLabels}
|
||||
<div className={containerCls({ class: className })}>
|
||||
{leftIcon && renderLeftIcon}
|
||||
<input
|
||||
className={cn(inputCls(), {
|
||||
'pl-10': leftIcon,
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
{rightIcon && renderRightIcon}
|
||||
</div>
|
||||
{renderHelperText}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { Input };
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './Radio';
|
||||
export * from './RadioItem';
|
||||
export * from './Radio.theme';
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './SegmentedControlItem';
|
||||
export * from './SegmentedControls';
|
||||
export * from './SegmentedControls.theme';
|
||||
|
@ -1 +1,3 @@
|
||||
export * from './Select';
|
||||
export * from './SelectItem';
|
||||
export * from './Select.theme';
|
||||
|
@ -3,8 +3,6 @@ import { NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Organization, User } from 'gql-client';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { useDisconnect } from 'wagmi';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import {
|
||||
GlobeIcon,
|
||||
@ -31,7 +29,6 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
const { orgSlug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
const { disconnect } = useDisconnect();
|
||||
const isDesktop = useMediaQuery('(min-width: 960px)');
|
||||
|
||||
const [user, setUser] = useState<User>();
|
||||
@ -90,10 +87,9 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
disconnect();
|
||||
localStorage.clear();
|
||||
navigate('/login');
|
||||
}, [disconnect, navigate]);
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<motion.nav
|
||||
|
@ -2,7 +2,7 @@ import { Fragment, ComponentPropsWithoutRef } from 'react';
|
||||
import { stepsTheme, StepsTheme } from './Steps.theme';
|
||||
import { Step, StepProps, StepTheme } from './Step';
|
||||
|
||||
interface StepsProps
|
||||
export interface StepsProps
|
||||
extends ComponentPropsWithoutRef<'ul'>,
|
||||
StepsTheme,
|
||||
Pick<StepTheme, 'orientation'> {
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './Steps';
|
||||
export * from './Steps.theme';
|
||||
export * from './Step';
|
||||
|
49
packages/frontend/src/components/shared/Table/Table.theme.ts
Normal file
49
packages/frontend/src/components/shared/Table/Table.theme.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { tv, VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const tableTheme = tv({
|
||||
slots: {
|
||||
root: ['min-w-full', 'border-collapse'],
|
||||
header: [
|
||||
'p-2',
|
||||
'border-b',
|
||||
'border-sky-950/opacity-5',
|
||||
'text-sky-950',
|
||||
'text-sm',
|
||||
'font-medium',
|
||||
'leading-tight',
|
||||
],
|
||||
body: [],
|
||||
row: ['border-b', 'border-sky-950/opacity-5'],
|
||||
columnHeaderCell: [
|
||||
'p-4',
|
||||
'text-sky-950',
|
||||
'text-sm',
|
||||
'font-medium',
|
||||
'uppercase',
|
||||
'tracking-wider',
|
||||
'text-left',
|
||||
],
|
||||
rowHeaderCell: [
|
||||
'p-4',
|
||||
'text-slate-600',
|
||||
'text-sm',
|
||||
'font-normal',
|
||||
'leading-tight',
|
||||
'text-left',
|
||||
],
|
||||
cell: [
|
||||
'p-4',
|
||||
'whitespace-nowrap',
|
||||
'text-sm',
|
||||
'text-slate-600',
|
||||
'font-normal',
|
||||
'text-left',
|
||||
],
|
||||
},
|
||||
variants: {},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
},
|
||||
});
|
||||
|
||||
export type TableTheme = VariantProps<typeof tableTheme>;
|
57
packages/frontend/src/components/shared/Table/Table.tsx
Normal file
57
packages/frontend/src/components/shared/Table/Table.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { tableTheme } from './Table.theme';
|
||||
|
||||
type TableComponentProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Table: React.FC<TableComponentProps> & {
|
||||
Header: typeof Header;
|
||||
Body: typeof Body;
|
||||
Row: typeof Row;
|
||||
ColumnHeaderCell: typeof ColumnHeaderCell;
|
||||
RowHeaderCell: typeof RowHeaderCell;
|
||||
Cell: typeof Cell;
|
||||
} = ({ children }) => {
|
||||
const theme = tableTheme();
|
||||
return <table className={theme.root()}>{children}</table>;
|
||||
};
|
||||
|
||||
const Header: React.FC<TableComponentProps> = ({ children }) => {
|
||||
const theme = tableTheme();
|
||||
return <thead className={theme.header()}>{children}</thead>;
|
||||
};
|
||||
|
||||
const Body: React.FC<TableComponentProps> = ({ children }) => {
|
||||
const theme = tableTheme();
|
||||
return <tbody className={theme.body()}>{children}</tbody>;
|
||||
};
|
||||
|
||||
const Row: React.FC<TableComponentProps> = ({ children }) => {
|
||||
const theme = tableTheme();
|
||||
return <tr className={theme.row()}>{children}</tr>;
|
||||
};
|
||||
|
||||
const ColumnHeaderCell: React.FC<TableComponentProps> = ({ children }) => {
|
||||
const theme = tableTheme();
|
||||
return <th className={theme.columnHeaderCell()}>{children}</th>;
|
||||
};
|
||||
|
||||
const RowHeaderCell: React.FC<TableComponentProps> = ({ children }) => {
|
||||
const theme = tableTheme();
|
||||
return <th className={theme.rowHeaderCell()}>{children}</th>;
|
||||
};
|
||||
|
||||
const Cell: React.FC<TableComponentProps> = ({ children }) => {
|
||||
const theme = tableTheme();
|
||||
return <td className={theme.cell()}>{children}</td>;
|
||||
};
|
||||
|
||||
Table.Header = Header;
|
||||
Table.Body = Body;
|
||||
Table.Row = Row;
|
||||
Table.ColumnHeaderCell = ColumnHeaderCell;
|
||||
Table.RowHeaderCell = RowHeaderCell;
|
||||
Table.Cell = Cell;
|
||||
|
||||
export { Table };
|
1
packages/frontend/src/components/shared/Table/index.ts
Normal file
1
packages/frontend/src/components/shared/Table/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './Table';
|
@ -92,3 +92,5 @@ export const tabsTheme = tv({
|
||||
fillWidth: false,
|
||||
},
|
||||
});
|
||||
|
||||
export type TabsTheme = VariantProps<typeof tabsTheme>;
|
||||
|
@ -1 +1,5 @@
|
||||
export * from './Tabs';
|
||||
export * from './Tabs.theme';
|
||||
export { default as TabsList } from './TabsList';
|
||||
export { default as TabsTrigger } from './TabsTrigger';
|
||||
export { default as TabsContent } from './TabsContent';
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './Tooltip';
|
||||
export * from './TooltipBase';
|
||||
export * from './TooltipContent';
|
||||
|
@ -1,36 +1,14 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { WagmiProvider } from 'wagmi';
|
||||
import { arbitrum, mainnet } from 'wagmi/chains';
|
||||
|
||||
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const metadata = {
|
||||
name: 'Snowball Tools',
|
||||
description: 'Snowball Tools Dashboard',
|
||||
url: window.location.origin,
|
||||
icons: [
|
||||
'https://raw.githubusercontent.com/snowball-tools/mediakit/main/assets/logo.svg',
|
||||
],
|
||||
};
|
||||
|
||||
const chains = [mainnet, arbitrum] as const;
|
||||
const config = defaultWagmiConfig({
|
||||
chains,
|
||||
projectId: import.meta.env.VITE_WALLET_CONNECT_ID,
|
||||
metadata,
|
||||
});
|
||||
|
||||
if (!import.meta.env.VITE_WALLET_CONNECT_ID) {
|
||||
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
||||
}
|
||||
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<WagmiProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
@ -3,22 +3,20 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-Thin.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-Thin.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-ThinItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-ThinItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -26,7 +24,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-ExtraLight.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-ExtraLight.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -34,7 +32,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-ExtraLightItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-ExtraLightItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -42,15 +40,14 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-Light.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-Light.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-LightItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-LightItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -58,31 +55,28 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-Regular.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-Regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-Italic.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-Italic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-Medium.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-Medium.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-MediumItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-MediumItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -90,15 +84,14 @@
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-SemiBold.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-SemiBoldItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-SemiBoldItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -106,15 +99,14 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-Bold.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-Bold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-BoldItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-BoldItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -122,15 +114,14 @@
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-ExtraBold.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-ExtraBold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-ExtraBoldItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-ExtraBoldItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
@ -138,15 +129,14 @@
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-Black.woff2')
|
||||
format('woff2');
|
||||
src: url('/fonts/InterDisplay/InterDisplay-Black.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('../public/fonts/InterDisplay/InterDisplay-BlackItalic.woff2')
|
||||
src: url('/fonts/InterDisplay/InterDisplay-BlackItalic.woff2')
|
||||
format('woff2');
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import reportWebVitals from './reportWebVitals';
|
||||
import { GQLClientProvider } from './context/GQLClientContext';
|
||||
import { SERVER_GQL_PATH } from './constants';
|
||||
import { Toaster } from 'components/shared/Toast';
|
||||
import { LogErrorBoundary } from 'utils/log-error';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement,
|
||||
);
|
||||
@ -26,14 +28,16 @@ const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
|
||||
const gqlClient = new GQLClient({ gqlEndpoint });
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</GQLClientProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
<LogErrorBoundary>
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</GQLClientProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
</LogErrorBoundary>,
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
@ -6,8 +6,6 @@ import HorizontalLine from 'components/HorizontalLine';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { NotificationBellIcon, PlusIcon } from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { ProjectSearchBar } from 'components/projects/ProjectSearchBar';
|
||||
|
||||
@ -61,10 +59,9 @@ const ProjectSearch = () => {
|
||||
<NotificationBellIcon />
|
||||
</Button>
|
||||
{user?.name && (
|
||||
<Avatar
|
||||
size={44}
|
||||
initials={getInitials(formatAddress(user.name))}
|
||||
/>
|
||||
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
|
||||
{formatAddress(user.name)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,7 +20,7 @@ export const Done = ({ continueTo }: Props) => {
|
||||
You're in!
|
||||
</div>
|
||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
||||
It's time to get your next project rolling!
|
||||
It's time to get your project rolling 😎
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@ import { Link } from 'react-router-dom';
|
||||
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
||||
import { signInWithEthereum } from 'utils/siwe';
|
||||
import { useSnowball } from 'utils/use-snowball';
|
||||
import { logError } from 'utils/log-error';
|
||||
|
||||
type Provider = 'google' | 'github' | 'apple' | 'email' | 'passkey';
|
||||
|
||||
@ -52,11 +53,12 @@ export const Login = ({ onDone }: Props) => {
|
||||
setError(result.error);
|
||||
setProvider(false);
|
||||
wallet = undefined;
|
||||
logError(new Error(result.error));
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
console.log(err.message, err.name, err.details);
|
||||
logError(err);
|
||||
setProvider(false);
|
||||
return;
|
||||
}
|
||||
|
@ -9,13 +9,18 @@ import { DotBorder } from 'components/shared/DotBorder';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSnowball } from 'utils/use-snowball';
|
||||
import { CreatePasskey } from './CreatePasskey';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { AppleIcon } from 'components/shared/CustomIcon/AppleIcon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
||||
import { signInWithEthereum } from 'utils/siwe';
|
||||
import { logError } from 'utils/log-error';
|
||||
import {
|
||||
subOrganizationIdForEmail,
|
||||
turnkeySignin,
|
||||
turnkeySignup,
|
||||
} from 'utils/turnkey-frontend';
|
||||
|
||||
type Provider = 'google' | 'github' | 'apple' | 'email';
|
||||
|
||||
@ -46,11 +51,13 @@ export const SignUp = ({ onDone }: Props) => {
|
||||
setError({ type: 'provider', message: result.error });
|
||||
setProvider(false);
|
||||
wallet = undefined;
|
||||
logError(new Error(result.error));
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError({ type: 'provider', message: err.message });
|
||||
setProvider(false);
|
||||
logError(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -78,6 +85,23 @@ export const SignUp = ({ onDone }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
async function authEmail() {
|
||||
setProvider('email');
|
||||
try {
|
||||
const orgId = await subOrganizationIdForEmail(email);
|
||||
console.log('orgId', orgId);
|
||||
if (orgId) {
|
||||
await turnkeySignin(orgId);
|
||||
window.location.href = '/dashboard';
|
||||
} else {
|
||||
await turnkeySignup(email);
|
||||
onDone();
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError({ type: 'email', message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleSignupRedirect();
|
||||
}, []);
|
||||
@ -85,10 +109,6 @@ export const SignUp = ({ onDone }: Props) => {
|
||||
const loading = provider;
|
||||
const emailValid = /.@./.test(email);
|
||||
|
||||
if (provider === 'email') {
|
||||
return <CreatePasskey onDone={onDone} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
||||
@ -197,9 +217,15 @@ export const SignUp = ({ onDone }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
rightIcon={<ArrowRightCircleFilledIcon height="16" />}
|
||||
rightIcon={
|
||||
loading && loading === 'email' ? (
|
||||
<LoaderIcon className="animate-spin" />
|
||||
) : (
|
||||
<ArrowRightCircleFilledIcon height="16" />
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
setProvider('email');
|
||||
authEmail();
|
||||
}}
|
||||
variant={'secondary'}
|
||||
disabled={!email || !emailValid || !!loading}
|
||||
|
@ -64,9 +64,16 @@ const CreateRepo = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refetch to always get correct default branch
|
||||
const templateRepo = await octokit.rest.repos.get({
|
||||
owner: template.repoFullName.split('/')[0],
|
||||
repo: template.repoFullName.split('/')[1],
|
||||
});
|
||||
const prodBranch = templateRepo.data.default_branch ?? 'main';
|
||||
|
||||
const { addProject } = await client.addProject(orgSlug!, {
|
||||
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`,
|
||||
prodBranch: gitRepo.data.default_branch ?? 'main',
|
||||
prodBranch,
|
||||
repository: gitRepo.data.full_name,
|
||||
// TODO: Set selected template
|
||||
template: 'webapp',
|
||||
@ -189,12 +196,8 @@ const CreateRepo = () => {
|
||||
<Controller
|
||||
name="isPrivate"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
label="Make this repo private"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
render={({}) => (
|
||||
<Checkbox label="Make this repo private" disabled={true} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@ import FilterForm, {
|
||||
FilterValue,
|
||||
StatusOptions,
|
||||
} from 'components/projects/project/deployments/FilterForm';
|
||||
import { OutletContextType } from '../../../../types/types';
|
||||
import { OutletContextType } from '../../../../types';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { RefreshIcon } from 'components/shared/CustomIcon';
|
||||
|
@ -4,10 +4,7 @@ import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { RequestError } from 'octokit';
|
||||
|
||||
import { useOctokit } from '../../../../context/OctokitContext';
|
||||
import {
|
||||
GitCommitWithBranch,
|
||||
OutletContextType,
|
||||
} from '../../../../types/types';
|
||||
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
|
@ -1,40 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Link, Outlet, useLocation, useOutletContext } from 'react-router-dom';
|
||||
|
||||
import { OutletContextType } from '../../../../types';
|
||||
import {
|
||||
Tabs,
|
||||
TabsHeader,
|
||||
TabsBody,
|
||||
Tab,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { OutletContextType } from '../../../../types/types';
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from 'components/shared/Tabs';
|
||||
import {
|
||||
BranchStrokeIcon,
|
||||
CollaboratorsIcon,
|
||||
GearIcon,
|
||||
GlobeIcon,
|
||||
SwitchIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
const tabsData = [
|
||||
{
|
||||
label: 'General',
|
||||
icon: '^',
|
||||
icon: <GearIcon />,
|
||||
value: 'general',
|
||||
},
|
||||
{
|
||||
label: 'Domains',
|
||||
icon: '^',
|
||||
icon: <GlobeIcon />,
|
||||
value: 'domains',
|
||||
},
|
||||
{
|
||||
label: 'Git',
|
||||
icon: '^',
|
||||
icon: <BranchStrokeIcon />,
|
||||
value: 'git',
|
||||
},
|
||||
{
|
||||
label: 'Environment variables',
|
||||
icon: '^',
|
||||
icon: <SwitchIcon />,
|
||||
value: 'environment-variables',
|
||||
},
|
||||
{
|
||||
label: 'Members',
|
||||
icon: '^',
|
||||
value: 'members',
|
||||
label: 'Collaborators',
|
||||
icon: <CollaboratorsIcon />,
|
||||
value: 'collaborators',
|
||||
},
|
||||
];
|
||||
|
||||
@ -57,31 +63,26 @@ const SettingsTabPanel = () => {
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
orientation="vertical"
|
||||
className="grid grid-cols-4"
|
||||
className="grid grid-cols-5"
|
||||
>
|
||||
<TabsHeader
|
||||
className="bg-transparent col-span-1"
|
||||
indicatorProps={{
|
||||
className: 'bg-gray-900/10 shadow-none !text-gray-900',
|
||||
}}
|
||||
>
|
||||
<TabsList className="col-span-1">
|
||||
{tabsData.map(({ label, value, icon }) => (
|
||||
<Link key={value} to={value === 'general' ? '' : value}>
|
||||
<Tab
|
||||
<TabsTrigger
|
||||
value={value === 'general' ? '' : `/${value}`}
|
||||
className="flex justify-start"
|
||||
className="col-span-1"
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<div>{icon}</div>
|
||||
<div>{label}</div>
|
||||
<div className="items-center gap-2 inline-flex">
|
||||
<div className="items-center">{icon}</div>
|
||||
<div className="items-center">{label}</div>
|
||||
</div>
|
||||
</Tab>
|
||||
</TabsTrigger>
|
||||
</Link>
|
||||
))}
|
||||
</TabsHeader>
|
||||
<TabsBody className="col-span-2">
|
||||
</TabsList>
|
||||
<TabsContent value={currentTab ?? ''} className="col-span-3">
|
||||
<Outlet context={{ project, onUpdate }} />
|
||||
</TabsBody>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import SettingsTabPanel from './Settings';
|
||||
import GeneralTabPanel from './settings/General';
|
||||
import GitTabPanel from './settings/Git';
|
||||
import { EnvironmentVariablesTabPanel } from './settings/EnvironmentVariables';
|
||||
import MembersTabPanel from './settings/Members';
|
||||
import CollaboratorsTabPanel from './settings/Collaborators';
|
||||
import Domains from './settings/Domains';
|
||||
|
||||
const Integrations = () => (
|
||||
@ -34,8 +34,8 @@ export const settingsTabRoutes = [
|
||||
element: <EnvironmentVariablesTabPanel />,
|
||||
},
|
||||
{
|
||||
path: 'members',
|
||||
element: <MembersTabPanel />,
|
||||
path: 'collaborators',
|
||||
element: <CollaboratorsTabPanel />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -1,23 +1,22 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Permission, AddProjectMemberInput, ProjectMember } from 'gql-client';
|
||||
|
||||
import {
|
||||
Chip,
|
||||
Button,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import MemberCard from '../../../../../components/projects/project/settings/MemberCard';
|
||||
import AddMemberDialog from '../../../../../components/projects/project/settings/AddMemberDialog';
|
||||
import MemberCard from 'components/projects/project/settings/MemberCard';
|
||||
import AddMemberDialog from 'components/projects/project/settings/AddMemberDialog';
|
||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||
import { OutletContextType } from '../../../../../types/types';
|
||||
import { OutletContextType } from '../../../../../types';
|
||||
import { useToast } from '../../../../../components/shared/Toast';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import { Badge } from 'components/shared/Badge';
|
||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||
|
||||
const FIRST_MEMBER_CARD = 0;
|
||||
|
||||
const MembersTabPanel = () => {
|
||||
const CollaboratorsTabPanel = () => {
|
||||
const client = useGQLClient();
|
||||
const { toast } = useToast();
|
||||
const { project } = useOutletContext<OutletContextType>();
|
||||
|
||||
const [addmemberDialogOpen, setAddMemberDialogOpen] = useState(false);
|
||||
@ -36,9 +35,19 @@ const MembersTabPanel = () => {
|
||||
|
||||
if (isProjectMemberAdded) {
|
||||
await fetchProjectMembers();
|
||||
toast.success('Invitation sent');
|
||||
toast({
|
||||
id: 'member_added',
|
||||
title: 'Member added to project',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
} else {
|
||||
toast.error('Invitation not sent');
|
||||
toast({
|
||||
id: 'member_not_added',
|
||||
title: 'Invitation not sent',
|
||||
variant: 'error',
|
||||
onDismiss() {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[project],
|
||||
@ -50,9 +59,19 @@ const MembersTabPanel = () => {
|
||||
|
||||
if (isMemberRemoved) {
|
||||
await fetchProjectMembers();
|
||||
toast.success('Member removed from project');
|
||||
toast({
|
||||
id: 'member_removed',
|
||||
title: 'Member removed from project',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
} else {
|
||||
toast.error('Not able to remove member');
|
||||
toast({
|
||||
id: 'member_not_removed',
|
||||
title: 'Not able to remove member',
|
||||
variant: 'error',
|
||||
onDismiss() {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -63,9 +82,19 @@ const MembersTabPanel = () => {
|
||||
|
||||
if (isProjectMemberUpdated) {
|
||||
await fetchProjectMembers();
|
||||
toast.success('Project member permission updated');
|
||||
toast({
|
||||
id: 'member_permission_updated',
|
||||
title: 'Project member permission updated',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
} else {
|
||||
toast.error('Project member permission not updated');
|
||||
toast({
|
||||
id: 'member_permission_not_updated',
|
||||
title: 'Project member permission not updated',
|
||||
variant: 'error',
|
||||
onDismiss() {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
@ -76,27 +105,24 @@ const MembersTabPanel = () => {
|
||||
}, [project.id, fetchProjectMembers]);
|
||||
|
||||
return (
|
||||
<div className="p-2 mb-20">
|
||||
<div className="flex justify-between mb-2">
|
||||
<div className="flex">
|
||||
<Typography variant="h6">Members</Typography>
|
||||
<div>
|
||||
<Chip
|
||||
className="normal-case ml-3 font-normal"
|
||||
size="sm"
|
||||
value={projectMembers.length + 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setAddMemberDialogOpen((preVal) => !preVal)}
|
||||
>
|
||||
+ Add member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectSettingContainer
|
||||
headingText="Collaborators"
|
||||
badge={
|
||||
<Badge size="sm" variant="inset">
|
||||
{projectMembers.length + 1}
|
||||
</Badge>
|
||||
}
|
||||
button={
|
||||
<Button
|
||||
size="md"
|
||||
onClick={() => setAddMemberDialogOpen((preVal) => !preVal)}
|
||||
leftIcon={<PlusIcon />}
|
||||
variant="secondary"
|
||||
>
|
||||
Add member
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<MemberCard
|
||||
member={project.owner}
|
||||
isFirstCard={true}
|
||||
@ -129,8 +155,8 @@ const MembersTabPanel = () => {
|
||||
open={addmemberDialogOpen}
|
||||
handleAddMember={addMemberHandler}
|
||||
/>
|
||||
</div>
|
||||
</ProjectSettingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MembersTabPanel;
|
||||
export default CollaboratorsTabPanel;
|
@ -1,17 +1,15 @@
|
||||
import { RequestError } from 'octokit';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { Domain } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import DomainCard from '../../../../../components/projects/project/settings/DomainCard';
|
||||
import DomainCard from 'components/projects/project/settings/DomainCard';
|
||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||
import { OutletContextType } from '../../../../../types/types';
|
||||
import { OutletContextType } from '../../../../../types';
|
||||
import { useOctokit } from '../../../../../context/OctokitContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||
|
||||
const Domains = () => {
|
||||
const client = useGQLClient();
|
||||
@ -60,16 +58,20 @@ const Domains = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between p-2">
|
||||
<Typography variant="h3">Domain</Typography>
|
||||
<Link to="add">
|
||||
<Button color="blue" variant="outlined" className="rounded-full">
|
||||
<i>^</i> Add domain
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ProjectSettingContainer
|
||||
headingText="Domains"
|
||||
button={
|
||||
<Button
|
||||
as="a"
|
||||
href="add"
|
||||
variant="secondary"
|
||||
leftIcon={<PlusIcon />}
|
||||
size="md"
|
||||
>
|
||||
Add domain
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{domains.map((domain) => {
|
||||
return (
|
||||
<DomainCard
|
||||
@ -83,7 +85,7 @@ const Domains = () => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</ProjectSettingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -4,20 +4,19 @@ import toast from 'react-hot-toast';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import {
|
||||
Typography,
|
||||
Collapse,
|
||||
Card,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import AddEnvironmentVariableRow from '../../../../../components/projects/project/settings/AddEnvironmentVariableRow';
|
||||
import DisplayEnvironmentVariables from '../../../../../components/projects/project/settings/DisplayEnvironmentVariables';
|
||||
import HorizontalLine from '../../../../../components/HorizontalLine';
|
||||
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
|
||||
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
|
||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||
import { EnvironmentVariablesFormValues } from '../../../../../types/types';
|
||||
import { EnvironmentVariablesFormValues } from '../../../../../types';
|
||||
import HorizontalLine from 'components/HorizontalLine';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Checkbox } from 'components/shared/Checkbox';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||
|
||||
export const EnvironmentVariablesTabPanel = () => {
|
||||
const { id } = useParams();
|
||||
@ -132,20 +131,22 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6">Environment variables</Typography>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
<ProjectSettingContainer headingText="Environment variables">
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
A new deployment is required for your changes to take effect.
|
||||
</Typography>
|
||||
<div className="bg-gray-300 rounded-lg p-2">
|
||||
<div
|
||||
className="text-black"
|
||||
</p>
|
||||
<div className="bg-slate-100 rounded-xl flex-col">
|
||||
<Heading
|
||||
onClick={() => setCreateNewVariable((cur) => !cur)}
|
||||
className="p-4"
|
||||
>
|
||||
+ Create new variable
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<PlusIcon />
|
||||
<span>Create new variable</span>
|
||||
</div>
|
||||
</Heading>
|
||||
<Collapse open={createNewVariable}>
|
||||
<Card className="bg-white p-2">
|
||||
<div className="p-4 bg-slate-100">
|
||||
<form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
@ -160,8 +161,7 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
})}
|
||||
<div className="flex gap-1 p-2">
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
size="md"
|
||||
onClick={() =>
|
||||
append({
|
||||
key: '',
|
||||
@ -172,19 +172,18 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
+ Add variable
|
||||
</Button>
|
||||
{/* TODO: Implement import environment varible functionality */}
|
||||
<Button variant="outlined" size="sm" disabled>
|
||||
^ Import .env
|
||||
<Button size="md" disabled>
|
||||
Import .env
|
||||
</Button>
|
||||
</div>
|
||||
{isFieldEmpty && (
|
||||
<Chip
|
||||
value="^ Please ensure no fields are empty before saving."
|
||||
variant="outlined"
|
||||
color="red"
|
||||
size="sm"
|
||||
<InlineNotification
|
||||
title="Please ensure no fields are empty before saving."
|
||||
variant="danger"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<div className="flex gap-2 p-2">
|
||||
<Checkbox
|
||||
label="Production"
|
||||
{...register(`environment.production`)}
|
||||
@ -202,12 +201,12 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Button size="lg" color="blue" type="submit">
|
||||
<Button size="md" type="submit">
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
@ -235,6 +234,6 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</ProjectSettingContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,54 +1,36 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Organization } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Input,
|
||||
Option,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import DeleteProjectDialog from 'components/projects/project/settings/DeleteProjectDialog';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import AsyncSelect from 'components/shared/AsyncSelect';
|
||||
import { OutletContextType } from '../../../../../types/types';
|
||||
import { OutletContextType } from '../../../../../types';
|
||||
import { TransferProjectDialog } from 'components/projects/Dialog/TransferProjectDialog';
|
||||
|
||||
const CopyIcon = ({ value }: { value: string }) => {
|
||||
return (
|
||||
<span
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
toast.success('Project ID copied');
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
^
|
||||
</span>
|
||||
);
|
||||
};
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { TrashIcon, CopyUnfilledIcon } from 'components/shared/CustomIcon';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||
|
||||
const GeneralTabPanel = () => {
|
||||
const client = useGQLClient();
|
||||
const { toast } = useToast();
|
||||
const { project, onUpdate } = useOutletContext<OutletContextType>();
|
||||
|
||||
const [transferOrganizations, setTransferOrganizations] = useState<
|
||||
Organization[]
|
||||
SelectOption[]
|
||||
>([]);
|
||||
const [selectedTransferOrganization, setSelectedTransferOrganization] =
|
||||
useState('');
|
||||
useState<SelectOption>();
|
||||
|
||||
const {
|
||||
handleSubmit: handleTransfer,
|
||||
control,
|
||||
formState,
|
||||
reset: transferFormReset,
|
||||
} = useForm({
|
||||
const { handleSubmit: handleTransfer, reset: transferFormReset } = useForm({
|
||||
defaultValues: {
|
||||
orgId: '',
|
||||
org: {
|
||||
value: '',
|
||||
label: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -75,33 +57,47 @@ const GeneralTabPanel = () => {
|
||||
const orgsToTransfer = organizations.filter(
|
||||
(org) => org.id !== project.organization.id,
|
||||
);
|
||||
setTransferOrganizations(orgsToTransfer);
|
||||
const selectableOrgs: SelectOption[] = orgsToTransfer.map((org) => ({
|
||||
value: org.id,
|
||||
label: org.name,
|
||||
}));
|
||||
|
||||
setTransferOrganizations(selectableOrgs);
|
||||
}, [project]);
|
||||
|
||||
const handleTransferProject = useCallback(async () => {
|
||||
const { updateProject: isTransferred } = await client.updateProject(
|
||||
project.id,
|
||||
{
|
||||
organizationId: selectedTransferOrganization,
|
||||
organizationId: selectedTransferOrganization?.value,
|
||||
},
|
||||
);
|
||||
setOpenTransferDialog(!openTransferDialog);
|
||||
|
||||
if (isTransferred) {
|
||||
toast.success('Project transferred');
|
||||
toast({
|
||||
id: 'project_transferred',
|
||||
title: 'Project transferred successfully',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
await fetchUserOrganizations();
|
||||
await onUpdate();
|
||||
transferFormReset();
|
||||
} else {
|
||||
toast.error('Project not transrfered');
|
||||
toast({
|
||||
id: 'project_transfer_failed',
|
||||
title: 'Project transfer failed',
|
||||
variant: 'error',
|
||||
onDismiss() {},
|
||||
});
|
||||
}
|
||||
}, [project, selectedTransferOrganization]);
|
||||
|
||||
const selectedUserOrgName = useMemo(() => {
|
||||
return (
|
||||
transferOrganizations.find(
|
||||
(org) => org.id === selectedTransferOrganization,
|
||||
)?.name || ''
|
||||
transferOrganizations.find((org) => org === selectedTransferOrganization)
|
||||
?.label || ''
|
||||
);
|
||||
}, [transferOrganizations, selectedTransferOrganization]);
|
||||
|
||||
@ -114,7 +110,7 @@ const GeneralTabPanel = () => {
|
||||
}, [project]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProjectSettingContainer headingText="Project Info">
|
||||
<form
|
||||
onSubmit={handleSubmit(async ({ appName, description }) => {
|
||||
const { updateProject } = await client.updateProject(project.id, {
|
||||
@ -125,110 +121,97 @@ const GeneralTabPanel = () => {
|
||||
await onUpdate();
|
||||
}
|
||||
})}
|
||||
className="self-stretch space-y-3"
|
||||
>
|
||||
<Typography variant="h6">Project info</Typography>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
App name
|
||||
</Typography>
|
||||
<Input
|
||||
variant="outlined"
|
||||
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
|
||||
label="App name"
|
||||
size="md"
|
||||
{...register('appName')}
|
||||
/>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
Description (Optional)
|
||||
</Typography>
|
||||
<Input variant="outlined" size="md" {...register('description')} />
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
Project ID
|
||||
</Typography>
|
||||
<Input
|
||||
variant="outlined"
|
||||
value={project.id}
|
||||
size="md"
|
||||
disabled
|
||||
icon={<CopyIcon value={project.id} />}
|
||||
label="Description (Optional)"
|
||||
{...register('description')}
|
||||
/>
|
||||
<div
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(project.id);
|
||||
toast({
|
||||
id: 'copied_project_id',
|
||||
title: 'Project ID copied to clipboard',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={project.id}
|
||||
size="md"
|
||||
disabled
|
||||
label="Project ID"
|
||||
rightIcon={<CopyUnfilledIcon />}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
size="md"
|
||||
disabled={!updateProjectFormState.isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mb-1">
|
||||
<Typography variant="h6">Transfer project</Typography>
|
||||
<Typography variant="small">
|
||||
<form
|
||||
onSubmit={handleTransfer((org) => {
|
||||
setSelectedTransferOrganization(org.org);
|
||||
setOpenTransferDialog(!openTransferDialog);
|
||||
})}
|
||||
className="self-stretch space-y-3 px-2"
|
||||
>
|
||||
<Heading className="text-sky-950 text-lg font-medium leading-normal">
|
||||
Transfer project
|
||||
</Heading>
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
Transfer this app to your personal account or a team you are a member
|
||||
of.
|
||||
<Link to="" className="text-blue-500">
|
||||
Learn more
|
||||
</Link>
|
||||
</Typography>
|
||||
<form
|
||||
onSubmit={handleTransfer(({ orgId }) => {
|
||||
setSelectedTransferOrganization(orgId);
|
||||
setOpenTransferDialog(!openTransferDialog);
|
||||
})}
|
||||
>
|
||||
<Typography variant="small" className="font-medium text-gray-800">
|
||||
Choose team
|
||||
</Typography>
|
||||
<Controller
|
||||
name="orgId"
|
||||
rules={{ required: 'This field is required' }}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<AsyncSelect
|
||||
{...field}
|
||||
// TODO: Implement placeholder for select
|
||||
label={!field.value ? 'Select an account / team' : ''}
|
||||
>
|
||||
{transferOrganizations.map((org, key) => (
|
||||
<Option key={key} value={org.id}>
|
||||
^ {org.name}
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
disabled={!formState.isValid}
|
||||
type="submit"
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
</form>
|
||||
<TransferProjectDialog
|
||||
handleCancel={() => setOpenTransferDialog(!openTransferDialog)}
|
||||
open={openTransferDialog}
|
||||
handleConfirm={handleTransferProject}
|
||||
projectName={project.name}
|
||||
from={project.organization.name}
|
||||
to={selectedUserOrgName}
|
||||
</p>
|
||||
<Select
|
||||
disabled
|
||||
size="md"
|
||||
placeholder="Select an account / team"
|
||||
options={transferOrganizations}
|
||||
value={selectedTransferOrganization}
|
||||
onChange={(value) =>
|
||||
setSelectedTransferOrganization(value as SelectOption)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<Typography variant="h6">Delete project</Typography>
|
||||
<Typography variant="small">
|
||||
<Button disabled type="submit" size="md">
|
||||
Transfer
|
||||
</Button>
|
||||
</form>
|
||||
<TransferProjectDialog
|
||||
handleCancel={() => setOpenTransferDialog(!openTransferDialog)}
|
||||
open={openTransferDialog}
|
||||
handleConfirm={handleTransferProject}
|
||||
projectName={project.name}
|
||||
from={project.organization.name}
|
||||
to={selectedUserOrgName}
|
||||
/>
|
||||
<div className="self-stretch space-y-3 px-2">
|
||||
<Heading className="text-sky-950 text-lg font-medium leading-normal">
|
||||
Delete project
|
||||
</Heading>
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
The project will be permanently deleted, including its deployments and
|
||||
domains. This action is irreversible and can not be undone.
|
||||
</Typography>
|
||||
</p>
|
||||
<Button
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
color="red"
|
||||
size="md"
|
||||
variant="danger"
|
||||
onClick={handleDeleteProjectDialog}
|
||||
leftIcon={<TrashIcon />}
|
||||
>
|
||||
^ Delete project
|
||||
Delete project
|
||||
</Button>
|
||||
<DeleteProjectDialog
|
||||
handleOpen={handleDeleteProjectDialog}
|
||||
@ -236,7 +219,7 @@ const GeneralTabPanel = () => {
|
||||
project={project}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</ProjectSettingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,18 +1,16 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import WebhookCard from '../../../../../components/projects/project/settings/WebhookCard';
|
||||
import WebhookCard from 'components/projects/project/settings/WebhookCard';
|
||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||
import { OutletContextType } from '../../../../../types/types';
|
||||
import { OutletContextType } from '../../../../../types';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Switch } from 'components/shared/Switch';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||
import { ProjectSettingHeader } from 'components/projects/project/settings/ProjectSettingHeader';
|
||||
|
||||
type UpdateProdBranchValues = {
|
||||
prodBranch: string;
|
||||
@ -24,8 +22,12 @@ type UpdateWebhooksValues = {
|
||||
|
||||
const GitTabPanel = () => {
|
||||
const client = useGQLClient();
|
||||
const { toast } = useToast();
|
||||
const { project, onUpdate } = useOutletContext<OutletContextType>();
|
||||
|
||||
const [pullRequestComments, updatePullRequestComments] = useState(true);
|
||||
const [commitComments, updateCommitComments] = useState(false);
|
||||
|
||||
const {
|
||||
register: registerProdBranch,
|
||||
handleSubmit: handleSubmitProdBranch,
|
||||
@ -45,9 +47,19 @@ const GitTabPanel = () => {
|
||||
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
toast.success('Production branch upadated successfully');
|
||||
toast({
|
||||
id: 'prod_branch_updated',
|
||||
title: 'Production branch updated successfully',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
} else {
|
||||
toast.error('Error updating production branch');
|
||||
toast({
|
||||
id: 'prod_branch_update_failed',
|
||||
title: 'Error updating production branch',
|
||||
variant: 'error',
|
||||
onDismiss() {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[project],
|
||||
@ -72,9 +84,19 @@ const GitTabPanel = () => {
|
||||
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
toast.success('Webhook added successfully');
|
||||
toast({
|
||||
id: 'webhook_added',
|
||||
title: 'Webhook added successfully',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
} else {
|
||||
toast.error('Error adding webhook');
|
||||
toast({
|
||||
id: 'webhook_add_failed',
|
||||
title: 'Error adding webhook',
|
||||
variant: 'error',
|
||||
onDismiss() {},
|
||||
});
|
||||
}
|
||||
|
||||
resetWebhooks();
|
||||
@ -96,80 +118,99 @@ const GitTabPanel = () => {
|
||||
|
||||
if (updateProject) {
|
||||
await onUpdate();
|
||||
toast.success('Webhook deleted successfully');
|
||||
toast({
|
||||
id: 'webhook_deleted',
|
||||
title: 'Webhook deleted successfully',
|
||||
variant: 'success',
|
||||
onDismiss() {},
|
||||
});
|
||||
} else {
|
||||
toast.error('Error deleting webhook');
|
||||
toast({
|
||||
id: 'webhook_delete_failed',
|
||||
title: 'Error deleting webhook',
|
||||
variant: 'error',
|
||||
onDismiss() {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 p-2">
|
||||
<Typography variant="h6" className="text-black">
|
||||
Git repository
|
||||
</Typography>
|
||||
|
||||
<ProjectSettingContainer headingText="Git repository">
|
||||
<div className="self-stretch space-y-3">
|
||||
<div className="flex justify-between mt-4">
|
||||
<div>
|
||||
<Typography variant="small">Pull request comments</Typography>
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
Pull request comments
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch defaultChecked />
|
||||
<Switch
|
||||
checked={pullRequestComments}
|
||||
onChange={() => updatePullRequestComments(!pullRequestComments)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<Typography variant="small">Commit comments</Typography>
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
Commit comments
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch />
|
||||
<Switch
|
||||
checked={commitComments}
|
||||
onChange={() => updateCommitComments(!commitComments)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmitProdBranch(updateProdBranchHandler)}>
|
||||
<div className="mb-2 p-2">
|
||||
<Typography variant="h6" className="text-black">
|
||||
Production branch
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
By default, each commit pushed to the{' '}
|
||||
<span className="font-bold">{project.prodBranch}</span> branch
|
||||
initiates a production deployment. You can opt for a different
|
||||
branch for deployment in the settings.
|
||||
</Typography>
|
||||
<Typography variant="small">Branch name</Typography>
|
||||
<Input {...registerProdBranch('prodBranch')} />
|
||||
<Button size="sm" className="mt-1" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmitProdBranch(updateProdBranchHandler)}
|
||||
className="space-y-3"
|
||||
>
|
||||
<ProjectSettingHeader headingText="Production branch" />
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
By default, each commit pushed to the{' '}
|
||||
<span className="font-bold">{project.prodBranch}</span> branch
|
||||
initiates a production deployment. You can opt for a different branch
|
||||
for deployment in the settings.
|
||||
</p>
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
Branch name
|
||||
</p>
|
||||
<Input {...registerProdBranch('prodBranch')} />
|
||||
<Button size="md" variant="primary">
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleSubmitWebhooks(updateWebhooksHandler)}>
|
||||
<div className="mb-2 p-2">
|
||||
<Typography variant="h6" className="text-black">
|
||||
Deploy webhooks
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
Webhooks configured to trigger when there is a change in a
|
||||
project's build or deployment status.
|
||||
</Typography>
|
||||
<div className="flex gap-1">
|
||||
<div className="grow">
|
||||
<Typography variant="small">Webhook URL</Typography>
|
||||
<Input {...registerWebhooks('webhookUrl')} />
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<Button size="sm" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmitWebhooks(updateWebhooksHandler)}
|
||||
className="space-y-3"
|
||||
>
|
||||
<ProjectSettingHeader headingText="Deploy webhooks" />
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
{' '}
|
||||
Webhooks configured to trigger when there is a change in a
|
||||
project's build or deployment status.
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
<div className="grow">
|
||||
<p className="text-slate-600 text-sm font-normal leading-tight">
|
||||
Webhook URL
|
||||
</p>
|
||||
<Input {...registerWebhooks('webhookUrl')} />
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<Button size="sm" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="mb-2 p-2">
|
||||
<div className="space-y-3 px-2">
|
||||
{project.webhooks.map((webhookUrl, index) => {
|
||||
return (
|
||||
<WebhookCard
|
||||
@ -180,7 +221,7 @@ const GitTabPanel = () => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
</ProjectSettingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Typography,
|
||||
Alert,
|
||||
Button,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { useGQLClient } from '../../../../../../../context/GQLClientContext';
|
||||
import { Table } from 'components/shared/Table';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
||||
import { ArrowRightCircleIcon } from 'components/shared/CustomIcon';
|
||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||
|
||||
const Config = () => {
|
||||
const { id, orgSlug } = useParams();
|
||||
@ -38,46 +38,54 @@ const Config = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Figure out DNS Provider if possible and update appropriatly
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<div>
|
||||
<Typography variant="h5">Configure DNS</Typography>
|
||||
<Typography variant="small">
|
||||
Add the following records to your domain.
|
||||
<a href="https://www.namecheap.com/" target="_blank" rel="noreferrer">
|
||||
<span className="underline">Go to NameCheap</span> ^
|
||||
</a>
|
||||
</Typography>
|
||||
</div>
|
||||
<ProjectSettingContainer headingText="Setup domain name">
|
||||
<p className="text-blue-gray-500">
|
||||
Add the following records to your domain.
|
||||
<a href="https://www.namecheap.com/" target="_blank" rel="noreferrer">
|
||||
<span className="underline">Go to NameCheap</span>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<table className="rounded-lg w-3/4 text-blue-gray-600">
|
||||
<tbody>
|
||||
<tr className="border-b-2 border-gray-300">
|
||||
<th className="text-left p-2">Type</th>
|
||||
<th className="text-left p-2">Name</th>
|
||||
<th className="text-left p-2">Value</th>
|
||||
</tr>
|
||||
<tr className="border-b-2 border-gray-300">
|
||||
<td className="text-left p-2">A</td>
|
||||
<td className="text-left p-2">@</td>
|
||||
<td className="text-left p-2">56.49.19.21</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="text-left p-2">CNAME</td>
|
||||
<td className="text-left p-2">www</td>
|
||||
<td className="text-left p-2">cname.snowballtools.xyz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>Type</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>Host</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>Value</Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Alert color="blue">
|
||||
<i>^</i>It can take up to 48 hours for these updates to reflect
|
||||
globally.
|
||||
</Alert>
|
||||
<Button className="w-fit" color="blue" onClick={handleSubmitDomain}>
|
||||
Finish <i>{'>'}</i>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>A</Table.RowHeaderCell>
|
||||
<Table.Cell>@</Table.Cell>
|
||||
<Table.Cell>56.49.19.21</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>CNAME</Table.RowHeaderCell>
|
||||
<Table.Cell>www</Table.Cell>
|
||||
<Table.Cell>cname.snowballtools.xyz</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
<InlineNotification
|
||||
variant="info"
|
||||
title={`It can take up to 48 hours for these updates to reflect
|
||||
globally.`}
|
||||
/>
|
||||
<Button
|
||||
className="w-fit"
|
||||
onClick={handleSubmitDomain}
|
||||
variant="primary"
|
||||
rightIcon={<ArrowRightCircleIcon />}
|
||||
>
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</ProjectSettingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
IconButton,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import Stepper from '../../../../../../../components/Stepper';
|
||||
import Stepper from 'components/Stepper';
|
||||
|
||||
const AddDomain = () => {
|
||||
const { id, orgSlug } = useParams();
|
||||
|
76
packages/frontend/src/stories/Components/Avatar.stories.tsx
Normal file
76
packages/frontend/src/stories/Components/Avatar.stories.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Avatar, AvatarVariants } from 'components/shared/Avatar';
|
||||
import { avatars, avatarsFallback } from 'pages/components/renders/avatar';
|
||||
|
||||
const avatarSizes: AvatarVariants['size'][] = [18, 20, 24, 28, 32, 36, 40, 44];
|
||||
const avatarVariants: AvatarVariants['type'][] = ['gray', 'orange', 'blue'];
|
||||
|
||||
const meta: Meta<typeof Avatar> = {
|
||||
component: Avatar,
|
||||
title: 'Components/Avatar',
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: avatarSizes,
|
||||
},
|
||||
imageSrc: {
|
||||
control: 'text',
|
||||
},
|
||||
initials: {
|
||||
control: 'text',
|
||||
},
|
||||
type: {
|
||||
control: 'select',
|
||||
options: avatarVariants,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Avatar>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ initials, imageSrc, size, type, ...arg }) => (
|
||||
<Avatar
|
||||
initials={initials}
|
||||
imageSrc={imageSrc}
|
||||
size={size}
|
||||
type={type}
|
||||
{...arg}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const Fallback: Story = {
|
||||
render: ({ initials, imageSrc, size, type, ...arg }) => (
|
||||
<Avatar
|
||||
initials={initials}
|
||||
imageSrc={imageSrc}
|
||||
size={size}
|
||||
type={type}
|
||||
{...arg}
|
||||
/>
|
||||
),
|
||||
args: {
|
||||
initials: 'SY',
|
||||
},
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-5 flex-wrap">
|
||||
{avatars.map((avatar) => avatar)}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const FallbackAll: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-5 flex-wrap">
|
||||
{avatarsFallback.map((avatar) => avatar)}
|
||||
</div>
|
||||
),
|
||||
};
|
76
packages/frontend/src/stories/Components/Badge.stories.tsx
Normal file
76
packages/frontend/src/stories/Components/Badge.stories.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Badge, BadgeTheme } from 'components/shared/Badge';
|
||||
|
||||
const badgeVariants: BadgeTheme['variant'][] = [
|
||||
'primary',
|
||||
'secondary',
|
||||
'tertiary',
|
||||
'inset',
|
||||
];
|
||||
const badgeSizes: BadgeTheme['size'][] = ['xs', 'sm'];
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: 'Components/Badge',
|
||||
component: Badge,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: badgeVariants,
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: badgeSizes,
|
||||
},
|
||||
children: {
|
||||
control: 'object',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
variant: 'primary',
|
||||
size: 'sm',
|
||||
children: '1',
|
||||
},
|
||||
} as Meta<typeof Badge>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Badge>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ variant, size, children, ...args }) => (
|
||||
<Badge variant={variant} size={size} {...args}>
|
||||
{children}
|
||||
</Badge>
|
||||
),
|
||||
args: {
|
||||
variant: 'primary',
|
||||
size: 'sm',
|
||||
children: '1',
|
||||
},
|
||||
};
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
children: '1',
|
||||
},
|
||||
};
|
||||
|
||||
export const All: Story = {
|
||||
render: () => (
|
||||
<>
|
||||
{badgeVariants.map((variant, index) => (
|
||||
<div className="flex gap-5" key={index}>
|
||||
{badgeSizes.map((size) => (
|
||||
<Badge key={variant} variant={variant} size={size}>
|
||||
{size}
|
||||
</Badge>
|
||||
))}
|
||||
{variant}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
};
|
137
packages/frontend/src/stories/Components/Button.stories.tsx
Normal file
137
packages/frontend/src/stories/Components/Button.stories.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Button, ButtonTheme } from 'components/shared/Button';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import {
|
||||
renderButtonIcons,
|
||||
renderButtons,
|
||||
renderDisabledButtons,
|
||||
renderLinks,
|
||||
} from 'pages/components/renders/button';
|
||||
|
||||
const buttonVariants: ButtonTheme['variant'][] = [
|
||||
'primary',
|
||||
'secondary',
|
||||
'tertiary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
'link',
|
||||
'link-emphasized',
|
||||
];
|
||||
const buttonSizes: ButtonTheme['size'][] = ['lg', 'md', 'sm', 'xs'];
|
||||
const buttonShapes: ButtonTheme['shape'][] = ['default', 'rounded'];
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: buttonSizes,
|
||||
},
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: buttonVariants,
|
||||
},
|
||||
fullWidth: {
|
||||
control: 'boolean',
|
||||
},
|
||||
iconOnly: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
shape: {
|
||||
control: 'select',
|
||||
options: buttonShapes,
|
||||
},
|
||||
children: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ children, size, variant, iconOnly, fullWidth, shape }) => (
|
||||
<Button
|
||||
size={size}
|
||||
variant={variant}
|
||||
fullWidth={fullWidth}
|
||||
iconOnly={iconOnly}
|
||||
shape={shape}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
),
|
||||
args: {
|
||||
children: 'Button',
|
||||
size: 'md',
|
||||
variant: 'primary',
|
||||
fullWidth: false,
|
||||
shape: 'rounded',
|
||||
iconOnly: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
leftIcon: <PlusIcon />,
|
||||
rightIcon: <PlusIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const FullWidth: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
fullWidth: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const IconOnly: Story = {
|
||||
render: ({ leftIcon }) => <Button iconOnly>{leftIcon}</Button>,
|
||||
args: {
|
||||
...Default.args,
|
||||
leftIcon: <PlusIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const ButtonAll: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-5 flex-col items-center">
|
||||
{/* Button */}
|
||||
<h1 className="text-2xl font-bold items-center justify-between">
|
||||
Button
|
||||
</h1>
|
||||
<div className="flex flex-col gap-10">
|
||||
{renderButtons()}
|
||||
{renderButtonIcons()}
|
||||
</div>
|
||||
|
||||
{/* Link */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Link</h1>
|
||||
<div className="flex gap-4 items-center justify-center">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disabled button, icon only, and link */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-bold text-center">Disabled</h1>
|
||||
<p className="text-lg text-center text-gray-500">
|
||||
Button - icon only - link
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-10 items-center justify-between">
|
||||
{renderDisabledButtons()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
@ -0,0 +1,86 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import { Calendar } from 'components/shared/Calendar';
|
||||
|
||||
const meta: Meta<typeof Calendar> = {
|
||||
title: 'Components/Calendar',
|
||||
component: Calendar,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
wrapperProps: {
|
||||
control: 'object',
|
||||
},
|
||||
calendarWrapperProps: {
|
||||
control: 'object',
|
||||
},
|
||||
footerProps: {
|
||||
control: 'object',
|
||||
},
|
||||
actions: {
|
||||
control: 'object',
|
||||
},
|
||||
onSelect: {
|
||||
action: 'select',
|
||||
},
|
||||
onCancel: {
|
||||
action: 'cancel',
|
||||
},
|
||||
onReset: {
|
||||
action: 'reset',
|
||||
},
|
||||
selectRange: {
|
||||
control: 'boolean',
|
||||
},
|
||||
activeStartDate: {
|
||||
control: 'date',
|
||||
},
|
||||
value: {
|
||||
control: 'date',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Calendar>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const ToShowCode: Story = {
|
||||
render: ({
|
||||
wrapperProps,
|
||||
calendarWrapperProps,
|
||||
footerProps,
|
||||
actions,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onReset,
|
||||
selectRange,
|
||||
activeStartDate,
|
||||
value,
|
||||
...arg
|
||||
}) => (
|
||||
<Calendar
|
||||
wrapperProps={wrapperProps}
|
||||
calendarWrapperProps={calendarWrapperProps}
|
||||
footerProps={footerProps}
|
||||
actions={actions}
|
||||
onSelect={onSelect}
|
||||
onCancel={onCancel}
|
||||
onReset={onReset}
|
||||
selectRange={selectRange}
|
||||
activeStartDate={activeStartDate}
|
||||
value={value}
|
||||
{...arg}
|
||||
/>
|
||||
),
|
||||
args: {
|
||||
actions: <div>Actions</div>,
|
||||
onSelect: (value) => console.log(value),
|
||||
onCancel: () => console.log('Cancel'),
|
||||
onReset: () => console.log('Reset'),
|
||||
selectRange: false,
|
||||
activeStartDate: new Date(),
|
||||
value: new Date(),
|
||||
},
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import { Checkbox } from 'components/shared/Checkbox';
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: 'Components/Checkbox',
|
||||
component: Checkbox,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
label: {
|
||||
control: 'text',
|
||||
},
|
||||
description: {
|
||||
control: 'text',
|
||||
},
|
||||
checked: {
|
||||
control: 'boolean',
|
||||
},
|
||||
defaultChecked: {
|
||||
control: 'boolean',
|
||||
},
|
||||
required: {
|
||||
control: 'boolean',
|
||||
},
|
||||
onCheckedChange: {
|
||||
action: 'checkedChange',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Checkbox>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
defaultChecked,
|
||||
required,
|
||||
onCheckedChange,
|
||||
...arg
|
||||
}) => (
|
||||
<Checkbox
|
||||
label={label}
|
||||
description={description}
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
required={required}
|
||||
onCheckedChange={onCheckedChange}
|
||||
{...arg}
|
||||
/>
|
||||
),
|
||||
args: {
|
||||
label: 'Label',
|
||||
description: 'Description',
|
||||
checked: false,
|
||||
defaultChecked: false,
|
||||
required: false,
|
||||
onCheckedChange: (checked: boolean) => console.log(checked),
|
||||
},
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import { CloudyFlow } from 'components/CloudyFlow';
|
||||
|
||||
const meta: Meta<typeof CloudyFlow> = {
|
||||
title: 'Components/CloudyFlow',
|
||||
component: CloudyFlow,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
className: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof CloudyFlow>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ ...arg }) => {
|
||||
return <CloudyFlow {...arg} />;
|
||||
},
|
||||
args: {
|
||||
className: 'flex flex-col min-h-screen',
|
||||
},
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import Page from 'pages/components';
|
||||
|
||||
const meta: Meta<typeof Page> = {
|
||||
component: Page,
|
||||
title: 'Components/All',
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Page>;
|
||||
|
||||
export const Default: Story = {};
|
@ -0,0 +1,50 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import { DatePicker } from 'components/shared/DatePicker';
|
||||
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
title: 'Components/DatePicker',
|
||||
component: DatePicker,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
calendarProps: {
|
||||
control: 'object',
|
||||
},
|
||||
onChange: {
|
||||
action: 'change',
|
||||
},
|
||||
value: {
|
||||
control: 'text',
|
||||
},
|
||||
selectRange: {
|
||||
control: 'boolean',
|
||||
},
|
||||
onReset: {
|
||||
action: 'reset',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DatePicker>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({
|
||||
calendarProps,
|
||||
onChange,
|
||||
value,
|
||||
selectRange,
|
||||
onReset,
|
||||
...args
|
||||
}) => (
|
||||
<DatePicker
|
||||
calendarProps={calendarProps}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
selectRange={selectRange}
|
||||
onReset={onReset}
|
||||
{...args}
|
||||
/>
|
||||
),
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import { DotBorder } from 'components/shared/DotBorder';
|
||||
|
||||
const meta: Meta<typeof DotBorder> = {
|
||||
title: 'Components/DotBorder',
|
||||
component: DotBorder,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DotBorder>;
|
||||
|
||||
export const Default: Story = {};
|
15
packages/frontend/src/stories/Components/Heading.stories.tsx
Normal file
15
packages/frontend/src/stories/Components/Heading.stories.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
|
||||
const meta: Meta<typeof Heading> = {
|
||||
title: 'Components/Heading',
|
||||
component: Heading,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Heading>;
|
||||
|
||||
export const Default: Story = {};
|
@ -0,0 +1,32 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
|
||||
import { IconWithFrame } from 'components/shared/IconWithFrame';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof IconWithFrame> = {
|
||||
title: 'Components/IconWithFrame',
|
||||
component: IconWithFrame,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
icon: {
|
||||
control: 'object',
|
||||
},
|
||||
hasHighlight: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof IconWithFrame>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ icon, hasHighlight }) => (
|
||||
<IconWithFrame icon={icon} hasHighlight={hasHighlight} />
|
||||
),
|
||||
args: {
|
||||
hasHighlight: true,
|
||||
icon: <PlusIcon />,
|
||||
},
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { AppleIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof AppleIcon> = {
|
||||
title: 'Icons/AppleIcon',
|
||||
component: AppleIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof AppleIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => <AppleIcon size={size} name={name} />,
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ArrowLeftCircleFilledIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof ArrowLeftCircleFilledIcon> = {
|
||||
title: 'Icons/ArrowLeftCircleFilledIcon',
|
||||
component: ArrowLeftCircleFilledIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ArrowLeftCircleFilledIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => (
|
||||
<ArrowLeftCircleFilledIcon size={size} name={name} />
|
||||
),
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ArrowRightCircleFilledIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof ArrowRightCircleFilledIcon> = {
|
||||
title: 'Icons/ArrowRightCircleFilledIcon',
|
||||
component: ArrowRightCircleFilledIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ArrowRightCircleFilledIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => (
|
||||
<ArrowRightCircleFilledIcon size={size} name={name} />
|
||||
),
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ArrowRightCircleIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof ArrowRightCircleIcon> = {
|
||||
title: 'Icons/ArrowRightCircleIcon',
|
||||
component: ArrowRightCircleIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ArrowRightCircleIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => <ArrowRightCircleIcon size={size} name={name} />,
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { BranchIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof BranchIcon> = {
|
||||
title: 'Icons/BranchIcon',
|
||||
component: BranchIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof BranchIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => <BranchIcon size={size} name={name} />,
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { BranchStrokeIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof BranchStrokeIcon> = {
|
||||
title: 'Icons/BranchStrokeIcon',
|
||||
component: BranchStrokeIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof BranchStrokeIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => <BranchStrokeIcon size={size} name={name} />,
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { BuildingIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof BuildingIcon> = {
|
||||
title: 'Icons/BuildingIcon',
|
||||
component: BuildingIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof BuildingIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => <BuildingIcon size={size} name={name} />,
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { CalendarDaysIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const meta: Meta<typeof CalendarDaysIcon> = {
|
||||
title: 'Icons/CalendarDaysIcon',
|
||||
component: CalendarDaysIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'text',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof CalendarDaysIcon>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: ({ size, name }) => <CalendarDaysIcon size={size} name={name} />,
|
||||
args: {
|
||||
size: '24px',
|
||||
name: 'plus',
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user