Compare commits

...
Author SHA1 Message Date
Wahyu Kurniawan 6c4d8af14d Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4861-inline-notification 2024-02-22 10:41:46 +07:00
Andre Hadianto 2369f4498a Merge pull request #85 from snowball-tools/andrehadianto/T-4863-input-field
feat: input field
2024-02-22 10:18:59 +08:00
Wahyu Kurniawan eb6a727425 [T-4862: feat] Link component (#88) 2024-02-22 09:17:22 +07:00
Andre H da2f7ede42 🔧 chore: add dependency to usememo 2024-02-22 09:15:17 +07:00
Wahyu Kurniawan ab6bf7ffef 🐛 fix: use the right method of `useCallback 2024-02-22 01:10:01 +07:00
Andre H 448d0ceb7c 🔧 chore: 1 classname per object 2024-02-21 17:10:45 +07:00
Andre H 195d957251 🔧 chore: restructure tailwind classnames 2024-02-21 17:08:33 +07:00
Andre H 807a4b1197 🔧 chore: lint 2024-02-21 17:08:14 +07:00
Andre H 5f6723fad6 ♻️ refactor: create new renderInput for component page 2024-02-21 17:07:59 +07:00
Andre H b663067035 🔧 chore: use absolute import 2024-02-21 17:07:30 +07:00
Wahyu Kurniawan bbb5abbd42 Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4861-inline-notification 2024-02-21 16:43:42 +07:00
Wahyu Kurniawan 2cd1776ded 📝 docs: add js doc comment and add inline notification component to the example page 2024-02-21 16:42:09 +07:00
Wahyu Kurniawan d1cd144dfc ️ feat: create inline notification component 2024-02-21 16:40:12 +07:00
Wahyu Kurniawan 10fae09d15 ️ feat: create info square icon component 2024-02-21 16:39:46 +07:00
Andre Hadianto b3a99475af Merge branch 'andrehadianto/design-system-components' into andrehadianto/T-4863-input-field 2024-02-21 17:39:29 +08:00
Wahyu Kurniawan ea44efa0f2 [T-4867: feat] Horizontal and vertical tabs component (#84)
* ️ feat: create tabs component

* ♻️ refactor: avoid big conflict on the example page

* 🔧 chore: upgrade tailwindcss and install `@radix-ui/react-tabs`

* ️ feat: create globe icon component

* 🎨 style: adjust vertical tab theme

* 📝 docs: add vertical tabs to the example page
2024-02-21 16:13:16 +07:00
Andre H 7d5963d776 🔧 chore: add input to page/components 2024-02-21 15:44:21 +07:00
Andre H 33b6191539 ️ feat: implement Input Field component 2024-02-21 15:43:53 +07:00
Andre H 636f68d7a4 🔧 chore: add more icons 2024-02-21 15:43:32 +07:00
Andre H 237e9e5cb9 🔧 chore: implement classnames utils 2024-02-21 15:43:09 +07:00
Andre H e70bb34190 ⚒️ build: reorder packages 2024-02-21 15:42:35 +07:00
Andre H b5eef95d15 Merge remote-tracking branch 'origin/main' 2024-02-21 11:00:14 +07:00
Zachery f0121605c4 feat: avatar component (#76) 2024-02-21 11:39:38 +08:00
Wahyu Kurniawan c8f1c58507 Merge pull request #81 from snowball-tools/ayungavis/T-4837-badge
[T-4837: feat] Badge component
2024-02-20 23:42:56 +07:00
Wahyu Kurniawan 83508cd81d Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4837-badge 2024-02-20 23:38:35 +07:00
Wahyu Kurniawan 76c59e2c58 Merge pull request #80 from snowball-tools/ayungavis/T-4838-checkbox
[T-4838: feat] Checkbox component
2024-02-20 23:37:30 +07:00
Wahyu Kurniawan a5bce0cda2 🎨 style: adjust disabled hover text color 2024-02-20 23:34:20 +07:00
Wahyu Kurniawan 3a085a79ae Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4838-checkbox 2024-02-20 23:29:18 +07:00
Wahyu Kurniawan abfe6a523c Merge pull request #79 from snowball-tools/ayungavis/T-4842-datepicker
[T-4842: feat] Calendar component
2024-02-20 23:26:38 +07:00
Wahyu Kurniawan f764bea6d1 📝 docs: add badge to the example page 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan cc9c24cca1 🎨 style: add new font sizes 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan e482f998a1 ️ feat: create badge comopnent 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan b255078431 📝 docs: add checkbox to example component page 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 26260976fb ️ feat: create check icon for checkbox component 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 5e3a6ad2b5 ️ feat: create checkbox component 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan f31ee7349f 🔧 chore: install @radix-ui/react-checkbox 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 5c5d759c10 ♻️ refactor: fix typo and change to readable name 2024-02-20 23:24:28 +07:00
Wahyu Kurniawan dc6bf3794a ️ feat: add interaction on the example page 2024-02-20 23:24:28 +07:00
Wahyu Kurniawan af0d28e654 🎨 style: add new shadows 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 4f7f9cf914 📝 docs: add calendar component to the example page 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan ad7dd1920a ️ feat: create calendar component 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan cc97ddff9d ️ feat: create chevron left, right, and grabbed horizontal component 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 0a5a1b8c55 🔧 chore: add vscode settings to enable tailwind variant intellisense 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 4f2944a974 🔧 chore: install date-fns and react-calendar 2024-02-20 23:24:26 +07:00
Wahyu Kurniawan 69198307fb Merge pull request #77 from snowball-tools/ayungavis/T-4834-button
[T-4834: feat] Button component
2024-02-20 23:23:38 +07:00
Wahyu Kurniawan 0f7c6c73c9 ️ feat: make the button component to forward ref 2024-02-20 22:59:31 +07:00
Wahyu Kurniawan 2ac657c32e 🎨 style: add new spacing 2024-02-19 21:16:55 +07:00
Wahyu Kurniawan 943d427db1 📝 docs: add button icon only on component example 2024-02-19 21:16:39 +07:00
Wahyu Kurniawan 7e6b74208d ️ feat: create cloneIcon util function 2024-02-19 21:16:17 +07:00
Wahyu Kurniawan 2b78cab849 ️ feat: create custom icon and plus icon component 2024-02-19 21:15:51 +07:00
Wahyu Kurniawan 862862a9c5 🎨 style: update button and button icon only theme 2024-02-19 21:15:29 +07:00
Wahyu Kurniawan 6a3af491ca Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4834-button 2024-02-19 20:20:31 +07:00
Wahyu Kurniawan 92f7d07f3f 🔧 chore: install tailwind-variants and @fontsource/inter 2024-02-19 20:16:25 +07:00
Wahyu Kurniawan 6fb94aeb11 🔧 chore: add absolute path 2024-02-19 20:15:56 +07:00
Wahyu Kurniawan 1c848c5a89 🎨 style: add inter font family 2024-02-19 20:15:32 +07:00
Wahyu Kurniawan 17b5ff9a74 🎨 style: add colors, font family, and shadow 2024-02-19 20:14:49 +07:00
Wahyu Kurniawan 56adfc4806 🐛 fix: typescript error 2024-02-19 20:14:23 +07:00
Wahyu Kurniawan b51d050ee6 📝 docs: add button to the example comopnent pag 2024-02-19 20:13:56 +07:00
Wahyu Kurniawan 8021ff7ff9 🔧 chore: add vscode settings on frontend package 2024-02-19 20:13:36 +07:00
Wahyu Kurniawan e850435c2d ️ feat: create button component 2024-02-19 20:13:11 +07:00
Zachery 8a43698c43 Merge pull request #75 from snowball-tools/zachery/T-4843-focus-ring-class
feat: focus ring class
2024-02-19 20:55:52 +08:00
Zachery Ng 499dba2c5e chore: add full palette colors 2024-02-19 20:12:25 +08:00
Zachery Ng d498ba7e9a feat: focus ring class 2024-02-19 20:05:29 +08:00
Andre H 082353e9bc ️ feat: implement components page to act as storybook 2024-02-16 12:31:13 +07:00
103 changed files with 3470 additions and 398 deletions
+6
View File
@@ -0,0 +1,6 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+2 -2
View File
@@ -31,7 +31,7 @@
- Load fixtures in database
```bash
yarn db:load:fixtures
yarn test:db:load:fixtures
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
@@ -66,7 +66,7 @@
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn registry:init
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
+1
View File
@@ -13,6 +13,7 @@
clientSecret = ""
[registryConfig]
fetchDeploymentRecordDelay = 5000
restEndpoint = "http://localhost:1317"
gqlEndpoint = "http://localhost:9473/api"
chainId = "laconic_9000-1"
+4 -3
View File
@@ -36,9 +36,10 @@
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
"db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
"db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
+1
View File
@@ -22,6 +22,7 @@ export interface RegistryConfig {
chainId: string;
privateKey: string;
bondId: string;
fetchDeploymentRecordDelay: number;
fee: {
amount: string;
denom: string;
+25 -8
View File
@@ -126,10 +126,18 @@ export class Database {
return projects;
}
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
/**
* Get deployments with specified filter
*/
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find(options);
const deployments = await deploymentRepository.find({
return deployments;
}
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
return this.getDeployments({
relations: {
project: true,
domain: true,
@@ -144,8 +152,6 @@ export class Database {
createdAt: 'DESC'
}
});
return deployments;
}
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
@@ -162,16 +168,14 @@ export class Database {
return domains;
}
async addDeployement (data: DeepPartial<Deployment>): Promise<Deployment> {
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const id = nanoid();
const url = `${data.project!.name}-${id}.${PROJECT_DOMAIN}`;
const updatedData = {
...data,
id,
url
id
};
const deployment = await deploymentRepository.save(updatedData);
@@ -312,6 +316,19 @@ export class Database {
return Boolean(updateResult.affected);
}
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository
.createQueryBuilder()
.update(Deployment)
.set(data)
.where('projectId IN (:...projectIds)', { projectIds })
.execute();
return Boolean(updateResult.affected);
}
async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project);
+15 -5
View File
@@ -12,6 +12,7 @@ import {
import { Project } from './Project';
import { Domain } from './Domain';
import { User } from './User';
import { AppDeploymentRecordAttributes } from '../types';
export enum Environment {
Production = 'Production',
@@ -28,7 +29,7 @@ export enum DeploymentStatus {
export interface ApplicationRecord {
type: string;
version:string
name?: string
name: string
description?: string
homepage?: string
license?: string
@@ -45,6 +46,9 @@ export class Deployment {
@PrimaryColumn('varchar')
id!: string;
@Column()
projectId!: string;
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'projectId' })
project!: Project;
@@ -65,14 +69,20 @@ export class Deployment {
@Column('varchar')
commitMessage!: string;
@Column('varchar')
url!: string;
@Column('varchar', { nullable: true })
url!: string | null;
@Column('varchar')
registryRecordId!: string;
applicationRecordId!: string;
@Column('simple-json')
registryRecordData!: ApplicationRecord;
applicationRecordData!: ApplicationRecord;
@Column('varchar', { nullable: true })
applicationDeploymentRecordId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
@Column({
enum: Environment
+2 -2
View File
@@ -53,10 +53,10 @@ export class Project {
prodBranch!: string;
@Column('varchar', { nullable: true })
registryRecordId!: string | null;
applicationDeploymentRequestId!: string | null;
@Column('simple-json', { nullable: true })
registryRecordData!: ApplicationDeploymentRequest | null;
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
@Column('text', { default: '' })
description!: string;
+1 -1
View File
@@ -31,7 +31,7 @@ export const main = async (): Promise<void> => {
await db.init();
const registry = new Registry(registryConfig);
const service = new Service({ gitHubConfig: gitHub }, db, app, registry);
const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
const resolvers = await createResolvers(service);
+27 -11
View File
@@ -7,13 +7,14 @@ import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config';
import { ApplicationDeploymentRequest } from './entity/Project';
import { ApplicationRecord } from './entity/Deployment';
import { PackageJSON } from './types';
import { ApplicationRecord, Deployment } from './entity/Deployment';
import { AppDeploymentRecord, PackageJSON } from './types';
const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord';
const DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
// TODO: Move registry code to laconic-sdk/watcher-ts
export class Registry {
@@ -35,7 +36,8 @@ export class Registry {
commitHash: string,
appType: string,
repoUrl: string
}): Promise<{registryRecordId: string, registryRecordData: ApplicationRecord}> {
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
assert(packageJSON.name, "name field doesn't exist in package.json");
// Use laconic-sdk to publish record
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
// Fetch previous records
@@ -58,7 +60,7 @@ export class Registry {
repository_ref: commitHash,
repository: [repoUrl],
app_type: appType,
...(packageJSON.name && { name: packageJSON.name }),
name: packageJSON.name,
...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }),
@@ -79,14 +81,14 @@ export class Registry {
log('Application record data:', applicationRecord);
// TODO: Discuss computation of CRN
const crn = this.getCrn(packageJSON.name ?? '');
const crn = this.getCrn(packageJSON.name);
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee);
return { registryRecordId: result.data.id, registryRecordData: applicationRecord };
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
}
async createApplicationDeploymentRequest (data: {
@@ -95,8 +97,8 @@ export class Registry {
repository: string,
environmentVariables: { [key: string]: string }
}): Promise<{
registryRecordId: string,
registryRecordData: ApplicationDeploymentRequest
applicationDeploymentRequestId: string,
applicationDeploymentRequestData: ApplicationDeploymentRequest
}> {
const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]);
@@ -108,7 +110,7 @@ export class Registry {
// Create record of type ApplicationDeploymentRequest and publish
const applicationDeploymentRequest = {
type: DEPLOYMENT_RECORD_TYPE,
type: APP_DEPLOYMENT_REQUEST_TYPE,
version: '1.0.0',
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
application: `${crn}@${applicationRecord.attributes.app_version}`,
@@ -140,7 +142,21 @@ export class Registry {
log(`Application deployment request record published: ${result.data.id}`);
log('Application deployment request data:', applicationDeploymentRequest);
return { registryRecordId: result.data.id, registryRecordData: applicationDeploymentRequest };
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
}
/**
* Fetch ApplicationDeploymentRecords for deployments
*/
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
const records = await this.registry.queryRecords({
type: APP_DEPLOYMENT_RECORD_TYPE
}, true);
// Filter records with ApplicationRecord ids
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
}
getCrn (packageJsonName: string): string {
+1 -1
View File
@@ -30,7 +30,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
},
deployments: async (_: any, { projectId }: { projectId: string }) => {
return service.getDeployementsByProjectId(projectId);
return service.getDeploymentsByProjectId(projectId);
},
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
+1 -1
View File
@@ -91,7 +91,7 @@ type Deployment {
branch: String!
commitHash: String!
commitMessage: String!
url: String!
url: String
environment: Environment!
isCurrent: Boolean!
status: DeploymentStatus!
+121 -34
View File
@@ -14,14 +14,16 @@ import { Project } from './entity/Project';
import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User';
import { Registry } from './registry';
import { GitHubConfig } from './config';
import { GitPushEventPayload } from './types';
import { GitHubConfig, RegistryConfig } from './config';
import { AppDeploymentRecord, GitPushEventPayload } from './types';
const log = debug('snowball:service');
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
interface Config {
gitHubConfig: GitHubConfig
registryConfig: RegistryConfig
}
export class Service {
@@ -30,11 +32,112 @@ export class Service {
private registry: Registry;
private config: Config;
private deployRecordCheckTimeout?: NodeJS.Timeout;
constructor (config: Config, db: Database, app: OAuthApp, registry: Registry) {
this.db = db;
this.oauthApp = app;
this.registry = registry;
this.config = config;
this.init();
}
/**
* Initialize services
*/
init (): void {
// Start check for ApplicationDeploymentRecords asynchronously
this.checkDeployRecordsAndUpdate();
}
/**
* Destroy services
*/
destroy (): void {
clearTimeout(this.deployRecordCheckTimeout);
}
/**
* Checks for ApplicationDeploymentRecord and update corresponding deployments
* Continues check in loop after a delay of DEPLOY_RECORD_CHECK_DELAY_MS
*/
async checkDeployRecordsAndUpdate (): Promise<void> {
// Fetch deployments in building state
const deployments = await this.db.getDeployments({
where: {
status: DeploymentStatus.Building
// TODO: Fetch and check records for recent deployments
}
});
if (deployments.length) {
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`);
// Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRecords`);
// Update deployments for which ApplicationDeploymentRecords were returned
if (records.length) {
await this.updateDeploymentsWithRecordData(records);
}
}
this.deployRecordCheckTimeout = setTimeout(() => {
this.checkDeployRecordsAndUpdate();
}, this.config.registryConfig.fetchDeploymentRecordDelay);
}
/**
* Update deployments with ApplicationDeploymentRecord data
*/
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
// Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({
where: records.map(record => ({
applicationRecordId: record.attributes.application
})),
order: {
createdAt: 'DESC'
}
});
// Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId);
}
return acc;
}, new Set<string>());
// Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false });
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => {
acc[deployment.applicationRecordId] = deployment;
return acc;
}, {});
// Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.application];
await this.db.updateDeploymentById(
deployment.id,
{
applicationDeploymentRecordId: record.id,
applicationDeploymentRecordData: record.attributes,
url: record.attributes.url,
status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production
}
);
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`);
});
await Promise.all(deploymentUpdatePromises);
}
async getUser (userId: string): Promise<User | null> {
@@ -67,7 +170,7 @@ export class Service {
return dbProjects;
}
async getDeployementsByProjectId (projectId: string): Promise<Deployment[]> {
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
const dbDeployments = await this.db.getDeploymentsByProjectId(projectId);
return dbDeployments;
}
@@ -187,11 +290,10 @@ export class Service {
const octokit = await this.getOctokit(userId);
const newDeployement = await this.createDeployment(userId,
const newDeployment = await this.createDeployment(userId,
octokit,
{
project: oldDeployment.project,
isCurrent: true,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
@@ -199,7 +301,7 @@ export class Service {
commitMessage: oldDeployment.commitMessage
});
return newDeployement;
return newDeployment;
}
async createDeployment (
@@ -232,24 +334,13 @@ export class Service {
}
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { registryRecordId, registryRecordData } = await this.registry.createApplicationRecord({
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl: recordData.repoUrl
});
// Check if new deployment is set to current
if (data.isCurrent) {
// Update previous current deployment
await this.db.updateDeployment({
project: {
id: data.project.id
},
isCurrent: true
}, { isCurrent: false });
}
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
await this.db.updateDeployment({
@@ -258,24 +349,23 @@ export class Service {
domain: null
});
const newDeployement = await this.db.addDeployement({
const newDeployment = await this.db.addDeployment({
project: data.project,
branch: data.branch,
commitHash: data.commitHash,
commitMessage: data.commitMessage,
environment: data.environment,
isCurrent: data.isCurrent,
status: DeploymentStatus.Building,
registryRecordId,
registryRecordData,
applicationRecordId,
applicationRecordData,
domain: data.domain,
createdBy: Object.assign(new User(), {
id: userId
})
});
log(`Created deployment ${newDeployement.id} and published application record ${registryRecordId}`);
return newDeployement;
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
return newDeployment;
}
async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
@@ -307,7 +397,6 @@ export class Service {
octokit,
{
project,
isCurrent: true,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
@@ -327,17 +416,17 @@ export class Service {
return acc;
}, {} as { [key: string]: string });
const { registryRecordId, registryRecordData } = await this.registry.createApplicationDeploymentRequest(
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
{
appName: newDeployment.registryRecordData.name!,
appName: newDeployment.applicationRecordData.name!,
commitHash: latestCommit.sha,
repository: repoDetails.html_url,
environmentVariables: environmentVariablesObj
});
await this.db.updateProjectById(project.id, {
registryRecordId,
registryRecordData
applicationDeploymentRequestId,
applicationDeploymentRequestData
});
await this.createRepoHook(octokit, project);
@@ -393,7 +482,6 @@ export class Service {
octokit,
{
project,
isCurrent: project.prodBranch === branch,
branch,
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
domain,
@@ -444,20 +532,19 @@ export class Service {
const octokit = await this.getOctokit(userId);
const newDeployement = await this.createDeployment(userId,
const newDeployment = await this.createDeployment(userId,
octokit,
{
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
isCurrent: true,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
});
return newDeployement;
return newDeployment;
}
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
@@ -475,7 +562,7 @@ export class Service {
});
if (!oldCurrentDeployment) {
throw new Error('Current deployement doesnot exist');
throw new Error('Current deployment doesnot exist');
}
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
+24
View File
@@ -25,3 +25,27 @@ export interface GitPushEventPayload {
message: string;
};
}
export interface AppDeploymentRecordAttributes {
application: string;
dns: string;
meta: string;
name: string;
request: string;
type: string;
url: string;
version: string;
}
interface RegistryRecord {
id: string;
names: string[] | null;
owners: string[];
bondId: string;
createTime: string;
expiryTime: string;
}
export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes
}
+29 -29
View File
@@ -4,11 +4,11 @@
"domainIndex":0,
"createdByIndex": 0,
"id":"ffhae3zq",
"status": "Building",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -22,8 +22,8 @@
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"registryRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -34,11 +34,11 @@
"domainIndex":2,
"createdByIndex": 0,
"id":"qmgekyte",
"status": "Error",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -52,8 +52,8 @@
"status": "Ready",
"environment": "Production",
"isCurrent": false,
"registryRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "prod",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -64,11 +64,11 @@
"domainIndex":3,
"createdByIndex": 1,
"id":"eO8cckxk",
"status": "Building",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -82,8 +82,8 @@
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"registryRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -94,11 +94,11 @@
"domainIndex":5,
"createdByIndex": 1,
"id":"hwwr6sbx",
"status": "Error",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"registryRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
"applicationRecordData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -109,11 +109,11 @@
"domainIndex":9,
"createdByIndex": 2,
"id":"ndxje48a",
"status": "Building",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "main",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -127,8 +127,8 @@
"status": "Ready",
"environment": "Preview",
"isCurrent": false,
"registryRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationRecordData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -139,11 +139,11 @@
"domainIndex":8,
"createdByIndex": 2,
"id":"b4bpthjr",
"status": "Error",
"status": "Ready",
"environment": "Development",
"isCurrent": false,
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"registryRecordData": {},
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
@@ -154,11 +154,11 @@
"domainIndex": 6,
"createdByIndex": 2,
"id":"b4bpthjr",
"status": "Building",
"status": "Ready",
"environment": "Production",
"isCurrent": true,
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"registryRecordData": {},
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
"applicationRecordData": {},
"branch": "test",
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
"commitMessage": "subscription added",
+2 -2
View File
@@ -6,7 +6,7 @@
},
{
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
"name": "AirFoil",
"slug": "airfoil-2"
"name": "Laconic",
"slug": "laconic-2"
}
]
+6 -6
View File
@@ -1,37 +1,37 @@
[
{
"projectIndex": 0,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "Live",
"branch": "main"
},
{
"projectIndex": 0,
"name": "saugatt.com",
"name": "example.org",
"status": "Pending",
"branch": "test"
},
{
"projectIndex": 1,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "Live",
"branch": "main"
},
{
"projectIndex": 1,
"name": "saugatt.com",
"name": "example.org",
"status": "Pending",
"branch": "test"
},
{
"projectIndex": 2,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "Live",
"branch": "main"
},
{
"projectIndex": 2,
"name": "saugatt.com",
"name": "example.org",
"status": "Pending",
"branch": "test"
},
+10 -10
View File
@@ -10,8 +10,8 @@
"framework": "test",
"webhooks": [],
"icon": "",
"registryRecordId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject.snowball.xyz"
},
{
@@ -25,8 +25,8 @@
"framework": "test-2",
"webhooks": [],
"icon": "",
"registryRecordId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "testProject-2.snowball.xyz"
},
{
@@ -40,8 +40,8 @@
"framework": "test-3",
"webhooks": [],
"icon": "",
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools.snowball.xyz"
},
{
@@ -55,8 +55,8 @@
"framework": "test-4",
"webhooks": [],
"icon": "",
"registryRecordId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "iglootools-2.snowball.xyz"
},
{
@@ -70,8 +70,8 @@
"framework": "test-5",
"webhooks": [],
"icon": "",
"registryRecordId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"registryRecordData": {},
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
"applicationDeploymentRequestData": {},
"subDomain": "snowball-2.snowball.xyz"
}
]
+3 -3
View File
@@ -1,21 +1,21 @@
[
{
"projectIndex": 0,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "Pending",
"redirectToIndex": 1,
"branch": "test"
},
{
"projectIndex": 1,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "Pending",
"redirectToIndex": 3,
"branch": "test"
},
{
"projectIndex": 2,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "Pending",
"redirectToIndex": 5,
"branch": "test"
+6 -6
View File
@@ -1,20 +1,20 @@
[
{
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
"name": "Saugat Yadav",
"email": "saugaty@airfoil.studio",
"name": "Snowball",
"email": "snowball@snowballtools.xyz",
"isVerified": true
},
{
"id": "e505b212-8da6-48b2-9614-098225dab34b",
"name": "Gideon Low",
"email": "gideonl@airfoil.studio",
"name": "Alice Anderson",
"email": "alice@snowballtools.xyz",
"isVerified": true
},
{
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
"name": "Sushan Yadav",
"email": "sushany@airfoil.studio",
"name": "Bob Banner",
"email": "bob@snowballtools.xyz",
"isVerified": true
}
]
+1 -1
View File
@@ -20,7 +20,7 @@ const log = debug('snowball:initialize-database');
const USER_DATA_PATH = './fixtures/users.json';
const PROJECT_DATA_PATH = './fixtures/projects.json';
const ORGANIZATION_DATA_PATH = './fixtures/organizations.json';
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-orgnizations.json';
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-organizations.json';
const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json';
const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json';
const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
@@ -0,0 +1,77 @@
import debug from 'debug';
import { DataSource } from 'typeorm';
import path from 'path';
import { Registry } from '@cerc-io/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH, PROJECT_DOMAIN } from '../src/constants';
import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records');
async function main () {
const { registryConfig, database } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
const dataSource = new DataSource({
type: 'better-sqlite3',
database: database.dbPath,
synchronize: true,
entities: [path.join(__dirname, '../src/entity/*')]
});
await dataSource.initialize();
const deploymentRepository = dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find({
relations: {
project: true
},
where: {
status: DeploymentStatus.Building
}
});
for await (const deployment of deployments) {
const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`;
const applicationDeploymentRecord = {
type: 'ApplicationDeploymentRecord',
version: '0.0.1',
name: deployment.applicationRecordData.name,
application: deployment.applicationRecordId,
// TODO: Create DNS record
dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm',
// Using dummy values
meta: JSON.stringify({
config: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
}),
request: deployment.project.applicationDeploymentRequestId,
url
};
const result = await registry.setRecord(
{
privateKey: registryConfig.privateKey,
record: applicationDeploymentRecord,
bondId: registryConfig.bondId
},
'',
registryConfig.fee
);
log('Application deployment record data:', applicationDeploymentRecord);
log(`Application deployment record published: ${result.data.id}`);
}
}
main().catch((err) => {
log(err);
});
+39
View File
@@ -0,0 +1,39 @@
{
// eslint extension options
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"css.customData": [".vscode/tailwind.json"],
// prettier extension setting
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.rulers": [80],
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.fixAll",
"source.organizeImports"
],
// Show in vscode "Problems" tab when there are errors
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
// Use absolute import for typescript files
"typescript.preferences.importModuleSpecifier": "non-relative",
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+9 -2
View File
@@ -3,7 +3,11 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-tabs": "^1.0.4",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -12,13 +16,15 @@
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"assert": "^2.1.0",
"date-fns": "^3.0.1",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"downshift": "^8.2.3",
"eslint-config-react-app": "^7.0.1",
"gql-client": "^1.0.0",
"luxon": "^3.4.4",
"octokit": "^3.1.2",
"react": "^18.2.0",
"react-calendar": "^4.8.0",
"react-code-blocks": "^0.1.6",
"react-day-picker": "^8.9.1",
"react-dom": "^18.2.0",
@@ -29,6 +35,7 @@
"react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-timer-hook": "^3.0.7",
"tailwind-variants": "^0.2.0",
"typescript": "^4.9.5",
"usehooks-ts": "^2.10.0",
"vertical-stepper-nav": "^1.0.2",
@@ -71,6 +78,6 @@
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "^7.33.2",
"prettier": "^3.1.0",
"tailwindcss": "^3.3.6"
"tailwindcss": "^3.4.1"
}
}
+3 -3
View File
@@ -2,7 +2,7 @@
{
"id": 1,
"projectid": 1,
"name": "randomurl.snowballtools.xyz",
"name": "example.snowballtools.xyz",
"status": "live",
"record": null,
"isRedirectedto": false
@@ -10,7 +10,7 @@
{
"id": 2,
"projectid": 1,
"name": "saugatt.com",
"name": "example.org",
"status": "pending",
"record": {
"type": "A",
@@ -22,7 +22,7 @@
{
"id": 3,
"projectid": 1,
"name": "www.saugatt.com",
"name": "www.example.org",
"status": "pending",
"record": {
"type": "CNAME",
-17
View File
@@ -1,17 +0,0 @@
[
{
"name": "Saugat Yadav",
"email": "saugaty@airfoil.studio",
"id": 1
},
{
"name": "Gideon Low",
"email": "gideonl@airfoil.studio",
"id": 2
},
{
"name": "Sushan Yadav",
"email": "sushany@airfoil.studio",
"id": 3
}
]
@@ -1,6 +1,6 @@
[
"[20:50:03.502] Running build in Washington, D.C., USA (East) iad1",
"[20:50:03.641] Cloning github.com/saugatyadav11/nextjs2 (Branch: main, Commit: 4a5f47f)",
"[20:50:03.641] Cloning github.com/cerc-io/nextjs2 (Branch: main, Commit: 4a5f47f)",
"[20:50:04.004] Previous build cache not available",
"[20:50:04.118] Cloning completed: 480.574ms",
"[20:50:04.382] Running 'vercel build'",
-190
View File
@@ -1,190 +0,0 @@
[
{
"id": 1,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"domain": null,
"organization": "Airfoil",
"url": "iglootools.co",
"createdAt": "2023-12-07T04:20:00",
"createdBy": "Alice",
"deployment": "iglootools.snowballtools.co",
"source": "feature/add-remote-control",
"latestCommit": {
"message": "subscription added",
"createdAt": "2023-12-11T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1
},
{
"id": 2,
"icon": "^",
"name": "snowball-starter-kit",
"title": "Snowball Starter Kit",
"domain": null,
"organization": "Snowball",
"url": "starterkit.snowballtools.com",
"createdAt": "2023-12-04T04:20:00",
"createdBy": "Bob",
"deployment": "deploy.snowballtools.com",
"source": "prod/add-docker-compose",
"latestCommit": {
"message": "component updates",
"createdAt": "2023-12-11T04:20:00",
"branch": "staging"
},
"repositoryId": 1,
"members": [
{
"id": 2,
"permissions": []
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 2
},
{
"id": 3,
"icon": "^",
"name": "web3-android",
"title": "Web3 Android",
"domain": null,
"organization": "Personal",
"url": "web3fordroids.com",
"createdAt": "2023-12-01T04:20:00",
"createdBy": "Charlie",
"deployment": "deploy.web3fordroids.com",
"source": "dev/style-page",
"latestCommit": {
"message": "No repo connected",
"createdAt": "2023-12-01T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1
},
{
"id": 4,
"icon": "^",
"name": "passkeys-demo",
"title": "Passkeys Demo",
"domain": null,
"organization": "Airfoil",
"url": "passkeys.iglootools.xyz",
"createdAt": "2023-12-01T04:20:00",
"createdBy": "David",
"deployment": "demo.passkeys.xyz",
"source": "dev/style-page",
"latestCommit": {
"message": "hello world",
"createdAt": "2023-12-01T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 1,
"permissions": []
},
{
"id": 2,
"permissions": ["view", "edit"]
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 1
},
{
"id": 5,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"domain": null,
"organization": "Airfoil",
"url": "iglootools.xyz",
"createdAt": "2023-12-11T04:20:00",
"createdBy": "Erin",
"deployment": "staging.snowballtools.com",
"source": "prod/fix-error",
"latestCommit": {
"message": "404 added",
"createdAt": "2023-12-09T04:20:00",
"branch": "main"
},
"repositoryId": 1,
"members": [
{
"id": 3,
"permissions": []
}
],
"ownerId": 3
},
{
"id": 6,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"domain": null,
"organization": "Airfoil",
"url": "iglootools.xyz",
"createdAt": "2023-12-11T04:20:00",
"createdBy": "Frank",
"deployment": "iglootools.snowballtools.com",
"source": "prod/fix-error",
"latestCommit": {
"message": "design system integrated",
"createdAt": "2023-12-09T04:20:00",
"branch": "prod"
},
"repositoryId": 1,
"members": [
{
"id": 2,
"permissions": []
},
{
"id": 3,
"permissions": ["view"]
}
],
"ownerId": 2
}
]
@@ -3,11 +3,12 @@ import { Octokit } from 'octokit';
import assert from 'assert';
import { useDebounce } from 'usehooks-ts';
import { Button, Typography, Option, Select } from '@material-tailwind/react';
import { Button, Typography, Option } from '@material-tailwind/react';
import SearchBar from '../../SearchBar';
import ProjectRepoCard from './ProjectRepoCard';
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
import AsyncSelect from '../../shared/AsyncSelect';
const DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5;
@@ -109,8 +110,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
<div className="p-4">
<div className="flex gap-2 mb-2">
<div className="basis-1/3">
{/* TODO: Fix selection of Git user at start */}
<Select
<AsyncSelect
value={selectedAccount}
onChange={(value) => setSelectedAccount(value!)}
>
@@ -119,7 +119,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
^ {account.login}
</Option>
))}
</Select>
</AsyncSelect>
</div>
<div className="basis-2/3">
<SearchBar
@@ -90,7 +90,9 @@ const DeploymentDetailsCard = ({
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
<div className="col-span-2">
<div className="flex">
<Typography className=" basis-3/4">{deployment.url}</Typography>
{deployment.url && (
<Typography className=" basis-3/4">{deployment.url}</Typography>
)}
<Chip
value={deployment.status}
color={STATUS_COLORS[deployment.status] ?? 'gray'}
@@ -120,12 +122,8 @@ const DeploymentDetailsCard = ({
<button className="self-start">...</button>
</MenuHandler>
<MenuList>
<a
href={'https://' + deployment.url}
target="_blank"
rel="noreferrer"
>
<MenuItem>^ Visit</MenuItem>
<a href={deployment.url} target="_blank" rel="noreferrer">
<MenuItem disabled={!Boolean(deployment.url)}>^ Visit</MenuItem>
</a>
<MenuItem
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
@@ -28,9 +28,11 @@ const DeploymentDialogBodyCard = ({
color={chip.color}
/>
)}
<Typography variant="small" className="text-black">
{deployment.url}
</Typography>
{deployment.url && (
<Typography variant="small" className="text-black">
{deployment.url}
</Typography>
)}
<Typography variant="small">
^ {deployment.branch} ^{' '}
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
@@ -1,9 +1,8 @@
import React, { useState } from 'react';
import { Card, Collapse, Typography } from '@material-tailwind/react';
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
import { Environment, EnvironmentVariable } from 'gql-client';
interface DisplayEnvironmentVariablesProps {
environment: Environment;
@@ -12,7 +12,6 @@ import {
Card,
} from '@material-tailwind/react';
import { RepositoryDetails } from '../../../../types';
import ConfirmDialog from '../../../shared/ConfirmDialog';
import EditDomainDialog from './EditDomainDialog';
import { useGQLClient } from '../../../../context/GQLClientContext';
@@ -27,7 +26,7 @@ enum RefreshStatus {
interface DomainCardProps {
domains: Domain[];
domain: Domain;
repo: RepositoryDetails;
branches: string[];
project: Project;
onUpdate: () => Promise<void>;
}
@@ -44,7 +43,7 @@ const DOMAIN_RECORD = {
const DomainCard = ({
domains,
domain,
repo,
branches,
project,
onUpdate,
}: DomainCardProps) => {
@@ -188,7 +187,7 @@ const DomainCard = ({
domains={domains}
open={editDialogOpen}
domain={domain}
repo={repo}
branches={branches}
onUpdate={onUpdate}
/>
</>
@@ -15,7 +15,6 @@ import {
Option,
} from '@material-tailwind/react';
import { RepositoryDetails } from '../../../../types';
import { useGQLClient } from '../../../../context/GQLClientContext';
const DEFAULT_REDIRECT_OPTIONS = ['none'];
@@ -25,7 +24,7 @@ interface EditDomainDialogProp {
open: boolean;
handleOpen: () => void;
domain: Domain;
repo: RepositoryDetails;
branches: string[];
onUpdate: () => Promise<void>;
}
@@ -40,7 +39,7 @@ const EditDomainDialog = ({
open,
handleOpen,
domain,
repo,
branches,
onUpdate,
}: EditDomainDialogProp) => {
const client = useGQLClient();
@@ -120,7 +119,7 @@ const EditDomainDialog = ({
branch: domain.branch,
redirectedTo: getRedirectUrl(domain),
});
}, [domain, repo]);
}, [domain]);
return (
<Dialog open={open} handler={handleOpen}>
@@ -166,9 +165,13 @@ const EditDomainDialog = ({
<Input
crossOrigin={undefined}
{...register('branch', {
validate: (value) => repo.branch.includes(value),
validate: (value) =>
Boolean(branches.length) ? branches.includes(value) : true,
})}
disabled={watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]}
disabled={
!Boolean(branches.length) ||
watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]
}
/>
{!isValid && (
<Typography variant="small" className="text-red-500">
@@ -0,0 +1,71 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const avatarTheme = tv(
{
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
slots: {
image: [
'h-full',
'w-full',
'rounded-[inherit]',
'object-cover',
'object-center',
],
fallback: [
'grid',
'select-none',
'place-content-center',
'h-full',
'w-full',
'rounded-[inherit]',
'font-medium',
],
},
variants: {
type: {
gray: {
fallback: ['text-elements-highEm', 'bg-base-bg-emphasized'],
},
orange: {
fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'],
},
blue: {
fallback: ['text-elements-info', 'bg-base-bg-emphasized-info'],
},
},
size: {
18: {
base: ['rounded-md', 'h-[18px]', 'w-[18px]', 'text-[0.625rem]'],
},
20: {
base: ['rounded-md', 'h-5', 'w-5', 'text-[0.625rem]'],
},
24: {
base: ['rounded-md', 'h-6', 'w-6', 'text-[0.625rem]'],
},
28: {
base: ['rounded-lg', 'h-[28px]', 'w-[28px]', 'text-[0.625rem]'],
},
32: {
base: ['rounded-lg', 'h-8', 'w-8', 'text-xs'],
},
36: {
base: ['rounded-xl', 'h-[36px]', 'w-[36px]', 'text-xs'],
},
40: {
base: ['rounded-xl', 'h-10', 'w-10', 'text-sm'],
},
44: {
base: ['rounded-xl', 'h-[44px]', 'w-[44px]', 'text-sm'],
},
},
},
defaultVariants: {
size: 24,
type: 'gray',
},
},
{ responsiveVariants: true },
);
export type AvatarVariants = VariantProps<typeof avatarTheme>;
@@ -0,0 +1,40 @@
import React from 'react';
import { type ComponentPropsWithoutRef, type ComponentProps } from 'react';
import { avatarTheme, type AvatarVariants } from './Avatar.theme';
import * as PrimitiveAvatar from '@radix-ui/react-avatar';
export type AvatarProps = ComponentPropsWithoutRef<'div'> & {
imageSrc?: string | null;
initials?: string;
imageProps?: ComponentProps<typeof PrimitiveAvatar.Image>;
fallbackProps?: ComponentProps<typeof PrimitiveAvatar.Fallback>;
} & AvatarVariants;
export const Avatar = ({
className,
size,
type,
imageSrc,
imageProps,
fallbackProps,
initials,
}: AvatarProps) => {
const { base, image, fallback } = avatarTheme({ size, type });
return (
<PrimitiveAvatar.Root className={base({ className })}>
{imageSrc && (
<PrimitiveAvatar.Image
{...imageProps}
className={image({ className: imageProps?.className })}
src={imageSrc}
/>
)}
<PrimitiveAvatar.Fallback asChild {...fallbackProps}>
<div className={fallback({ className: fallbackProps?.className })}>
{initials}
</div>
</PrimitiveAvatar.Fallback>
</PrimitiveAvatar.Root>
);
};
@@ -0,0 +1,2 @@
export * from './Avatar';
export * from './Avatar.theme';
@@ -0,0 +1,43 @@
import { VariantProps, tv } from 'tailwind-variants';
export const badgeTheme = tv({
slots: {
wrapper: ['rounded-full', 'grid', 'place-content-center'],
},
variants: {
variant: {
primary: {
wrapper: ['bg-controls-primary', 'text-elements-on-primary'],
},
secondary: {
wrapper: ['bg-controls-secondary', 'text-elements-on-secondary'],
},
tertiary: {
wrapper: [
'bg-controls-tertiary',
'border',
'border-border-interactive/10',
'text-elements-high-em',
'shadow-button',
],
},
inset: {
wrapper: ['bg-controls-inset', 'text-elements-high-em'],
},
},
size: {
sm: {
wrapper: ['h-5', 'w-5', 'text-xs'],
},
xs: {
wrapper: ['h-4', 'w-4', 'text-2xs', 'font-medium'],
},
},
},
defaultVariants: {
variant: 'primary',
size: 'sm',
},
});
export type BadgeTheme = VariantProps<typeof badgeTheme>;
@@ -0,0 +1,33 @@
import React from 'react';
import { ComponentPropsWithoutRef } from 'react';
import { BadgeTheme, badgeTheme } from './Badge.theme';
export interface BadgeProps
extends ComponentPropsWithoutRef<'div'>,
BadgeTheme {}
/**
* A badge is a small status descriptor for UI elements.
* It can be used to indicate a status, a count, or a category.
* It is typically used in lists, tables, or navigation elements.
*
* @example
* ```tsx
* <Badge variant="primary" size="sm">1</Badge
* ```
*/
export const Badge = ({
className,
children,
variant,
size,
...props
}: BadgeProps) => {
const { wrapper } = badgeTheme();
return (
<div {...props} className={wrapper({ className, variant, size })}>
{children}
</div>
);
};
@@ -0,0 +1 @@
export * from './Badge';
@@ -0,0 +1,185 @@
import { tv } from 'tailwind-variants';
import type { VariantProps } from 'tailwind-variants';
/**
* Defines the theme for a button component.
*/
export const buttonTheme = tv(
{
base: [
'h-fit',
'inline-flex',
'items-center',
'justify-center',
'whitespace-nowrap',
'focus-ring',
'disabled:cursor-not-allowed',
],
variants: {
size: {
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
},
fullWidth: {
true: 'w-full',
},
shape: {
default: '',
rounded: 'rounded-full',
},
iconOnly: {
true: '',
},
variant: {
primary: [
'text-elements-on-primary',
'border',
'border-transparent',
'bg-controls-primary',
'shadow-button',
'hover:bg-controls-primary-hovered',
'focus-visible:bg-controls-primary-hovered',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
secondary: [
'text-elements-on-secondary',
'border',
'border-transparent',
'bg-controls-secondary',
'hover:bg-controls-secondary-hovered',
'focus-visible:bg-controls-secondary-hovered',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
tertiary: [
'text-elements-on-tertiary',
'border',
'border-border-interactive/10',
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
ghost: [
'text-elements-on-tertiary',
'border',
'border-transparent',
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
danger: [
'text-elements-on-danger',
'border',
'border-transparent',
'bg-border-danger',
'hover:bg-controls-danger-hovered',
'focus-visible:bg-controls-danger-hovered',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
'danger-ghost': [
'text-elements-danger',
'border',
'border-transparent',
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
link: [
'p-0',
'gap-1.5',
'text-elements-link',
'rounded',
'focus-ring',
'hover:underline',
'hover:text-elements-link-hovered',
'disabled:text-controls-disabled',
'disabled:hover:no-underline',
],
'link-emphasized': [
'p-0',
'gap-1.5',
'text-elements-high-em',
'rounded',
'underline',
'focus-ring',
'hover:text-elements-link-hovered',
'disabled:text-controls-disabled',
'disabled:hover:no-underline',
'dark:text-elements-on-high-contrast',
],
unstyled: [],
},
},
compoundVariants: [
{
size: 'lg',
iconOnly: true,
class: ['py-3.5', 'px-3.5'],
},
{
size: 'md',
iconOnly: true,
class: ['py-3.25', 'px-3.25'],
},
{
size: 'sm',
iconOnly: true,
class: ['py-2', 'px-2'],
},
{
size: 'xs',
iconOnly: true,
class: ['py-1', 'px-1'],
},
],
defaultVariants: {
size: 'md',
variant: 'primary',
fullWidth: false,
iconOnly: false,
shape: 'rounded',
},
},
{
responsiveVariants: true,
},
);
/**
* Represents the type of a button theme, which is derived from the `buttonTheme` variant props.
*/
export type ButtonTheme = VariantProps<typeof buttonTheme>;
@@ -0,0 +1,178 @@
import React, { forwardRef, useCallback } from 'react';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { buttonTheme } from './Button.theme';
import type { ButtonTheme } from './Button.theme';
import { Link } from 'react-router-dom';
import { cloneIcon } from 'utils/cloneIcon';
/**
* Represents the properties of a base button component.
*/
interface ButtonBaseProps {
/**
* The optional left icon element for a component.
* @type {ReactNode}
*/
leftIcon?: ReactNode;
/**
* The optional right icon element to display.
* @type {ReactNode}
*/
rightIcon?: ReactNode;
}
/**
* Interface for the props of a button link component.
*/
export interface ButtonLinkProps
extends Omit<ComponentPropsWithoutRef<'a'>, 'color'> {
/**
* Specifies the optional property `as` with a value of `'a'`.
* @type {'a'}
*/
as?: 'a';
/**
* Indicates whether the item is external or not.
* @type {boolean}
*/
external?: boolean;
/**
* The URL of a web page or resource.
* @type {string}
*/
href: string;
}
export interface ButtonProps
extends Omit<ComponentPropsWithoutRef<'button'>, 'color'> {
/**
* Specifies the optional property `as` with a value of `'button'`.
* @type {'button'}
*/
as?: 'button';
}
/**
* Interface representing the props for a button component.
* Extends the ComponentPropsWithoutRef<'button'> and ButtonTheme interfaces.
*/
export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) &
ButtonBaseProps &
ButtonTheme;
/**
* A custom button component that can be used in React applications.
*/
const Button = forwardRef<
HTMLButtonElement | HTMLAnchorElement,
ButtonOrLinkProps
>(
(
{
children,
className,
leftIcon,
rightIcon,
fullWidth,
iconOnly,
shape,
variant,
...props
},
ref,
) => {
// Conditionally render between <NextLink>, <a> or <button> depending on props
// useCallback to prevent unnecessary re-rendering
const Component = useCallback(
({ children: _children, ..._props }: ButtonOrLinkProps) => {
if (_props.as === 'a') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { external, href, as, ...baseLinkProps } = _props;
// External link
if (external) {
const externalLinkProps = {
target: '_blank',
rel: 'noopener',
href,
...baseLinkProps,
};
return <a {...externalLinkProps}>{_children}</a>;
}
// Internal link
return (
<Link {...baseLinkProps} to={href}>
{_children}
</Link>
);
} else {
const { ...buttonProps } = _props;
// @ts-expect-error - as prop is not a valid prop for button elements
return <button {...buttonProps}>{_children}</button>;
}
},
[],
);
/**
* Extracts specific style properties from the given props object and returns them as a new object.
*/
const styleProps = (({
variant = 'primary',
size = 'md',
fullWidth = false,
iconOnly = false,
shape = 'rounded',
as,
}) => ({
variant,
size,
fullWidth,
iconOnly,
shape,
as,
}))({ ...props, fullWidth, iconOnly, shape, variant });
/**
* Validates that a button component has either children or an aria-label prop.
*/
if (typeof children === 'undefined' && !props['aria-label']) {
throw new Error(
'Button components must have either children or an aria-label prop',
);
}
const iconSize = useCallback(() => {
switch (styleProps.size) {
case 'lg':
return { width: 20, height: 20 };
case 'sm':
case 'xs':
return { width: 16, height: 16 };
case 'md':
default: {
return { width: 18, height: 18 };
}
}
}, [styleProps.size])();
return (
<Component
{...props}
// @ts-expect-error - ref is not a valid prop for button elements
ref={ref}
className={buttonTheme({ ...styleProps, class: className })}
>
{cloneIcon(leftIcon, { ...iconSize })}
{children}
{cloneIcon(rightIcon, { ...iconSize })}
</Component>
);
},
);
Button.displayName = 'Button';
export { Button };
@@ -0,0 +1,2 @@
export * from './Button';
export * from './Button.theme';
@@ -0,0 +1,128 @@
/* React Calendar */
.react-calendar {
@apply border-none font-sans;
}
/* Weekdays -- START */
.react-calendar__month-view__weekdays {
@apply p-0 flex items-center justify-center;
}
.react-calendar__month-view__weekdays__weekday {
@apply h-8 w-12 flex items-center justify-center p-0 font-medium text-xs text-elements-disabled mb-2;
}
abbr[title] {
text-decoration: none;
}
/* Weekdays -- END */
/* Days -- START */
.react-calendar__month-view__days {
@apply p-0 gap-0;
}
.react-calendar__month-view__days__day--neighboringMonth {
@apply !text-elements-disabled;
}
.react-calendar__month-view__days__day--neighboringMonth:hover {
@apply !text-elements-disabled !bg-transparent;
}
.react-calendar__month-view__days__day--neighboringMonth {
@apply !text-elements-disabled !bg-transparent;
}
/* For weekend days */
.react-calendar__month-view__days__day--weekend {
/* color: ${colors.grey[950]} !important; */
}
.react-calendar__tile {
@apply h-12 w-12 text-elements-high-em;
}
.react-calendar__tile:hover {
@apply bg-base-bg-emphasized rounded-lg;
}
.react-calendar__tile:focus-visible {
@apply bg-base-bg-emphasized rounded-lg focus-ring z-10;
}
.react-calendar__tile--now {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
}
.react-calendar__tile--now:hover {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
}
.react-calendar__tile--now:focus-visible {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg focus-ring;
}
.react-calendar__tile--active {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--active:hover {
@apply bg-controls-primary-hovered;
}
.react-calendar__tile--active:focus-visible {
@apply bg-controls-primary-hovered focus-ring;
}
/* Range -- START */
.react-calendar__tile--range {
@apply bg-controls-secondary text-elements-on-secondary rounded-none;
}
.react-calendar__tile--range:hover {
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-none;
}
.react-calendar__tile--range:focus-visible {
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-lg;
}
.react-calendar__tile--rangeStart {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeStart:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeStart:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
}
.react-calendar__tile--rangeEnd {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeEnd:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeEnd:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
}
/* Range -- END */
/* Days -- END */
/* Months -- START */
.react-calendar__tile--hasActive {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--hasActive:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--hasActive:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
}
@@ -0,0 +1,49 @@
import { VariantProps, tv } from 'tailwind-variants';
export const calendarTheme = tv({
slots: {
wrapper: [
'max-w-[352px]',
'bg-surface-floating',
'shadow-calendar',
'rounded-xl',
],
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
navigation: [
'flex',
'items-center',
'justify-between',
'gap-3',
'py-2.5',
'px-1',
],
actions: ['flex', 'items-center', 'justify-center', 'gap-1.5', 'flex-1'],
button: [
'flex',
'items-center',
'gap-2',
'px-2',
'py-2',
'rounded-lg',
'border',
'border-border-interactive',
'text-elements-high-em',
'shadow-field',
'bg-white',
'hover:bg-base-bg-alternate',
'focus-visible:bg-base-bg-alternate',
],
footer: [
'flex',
'items-center',
'justify-end',
'py-3',
'px-2',
'gap-3',
'border-t',
'border-border-separator',
],
},
});
export type CalendarTheme = VariantProps<typeof calendarTheme>;
@@ -0,0 +1,298 @@
import React, {
ComponentPropsWithRef,
MouseEvent,
ReactNode,
useCallback,
useState,
} from 'react';
import {
Calendar as ReactCalendar,
CalendarProps as ReactCalendarProps,
} from 'react-calendar';
import { Value } from 'react-calendar/dist/cjs/shared/types';
import { CalendarTheme, calendarTheme } from './Calendar.theme';
import { Button } from 'components/shared/Button';
import {
ChevronGrabberHorizontal,
ChevronLeft,
ChevronRight,
} from 'components/shared/CustomIcon';
import './Calendar.css';
import { format } from 'date-fns';
const CALENDAR_VIEW = ['month', 'year', 'decade'] as const;
export type CalendarView = (typeof CALENDAR_VIEW)[number];
/**
* Defines a custom set of props for a React calendar component by excluding specific props
* from the original ReactCalendarProps type.
* @type {CustomReactCalendarProps}
*/
type CustomReactCalendarProps = Omit<
ReactCalendarProps,
'view' | 'showNavigation' | 'onClickMonth' | 'onClickYear'
>;
export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
/**
* Optional props for wrapping a component with a div element.
*/
wrapperProps?: ComponentPropsWithRef<'div'>;
/**
* Props for the calendar wrapper component.
*/
calendarWrapperProps?: ComponentPropsWithRef<'div'>;
/**
* Optional props for the footer component.
*/
footerProps?: ComponentPropsWithRef<'div'>;
/**
* Optional custom actions to be rendered.
*/
actions?: ReactNode;
/**
* Optional callback function that is called when a value is selected.
* @param {Value} value - The selected value
* @returns None
*/
onSelect?: (value: Value) => void;
/**
* Optional callback function that is called when a cancel action is triggered.
* @returns None
*/
onCancel?: () => void;
}
/**
* Calendar component that allows users to select dates and navigate through months and years.
* @param {Object} CalendarProps - Props for the Calendar component.
* @returns {JSX.Element} A calendar component with navigation, date selection, and actions.
*/
export const Calendar = ({
selectRange,
activeStartDate: activeStartDateProp,
value: valueProp,
wrapperProps,
calendarWrapperProps,
footerProps,
actions,
onSelect,
onCancel,
onChange: onChangeProp,
...props
}: CalendarProps): JSX.Element => {
const {
wrapper,
calendar,
navigation,
actions: actionsClass,
button,
footer,
} = calendarTheme();
const today = new Date();
const currentMonth = format(today, 'MMM');
const currentYear = format(today, 'yyyy');
const [view, setView] = useState<CalendarView>('month');
const [activeDate, setActiveDate] = useState<Date>(
activeStartDateProp ?? today,
);
const [value, setValue] = useState<Value>(valueProp as Value);
const [month, setMonth] = useState(currentMonth);
const [year, setYear] = useState(currentYear);
/**
* Update the navigation label based on the active date
*/
const changeNavigationLabel = useCallback(
(date: Date) => {
setMonth(format(date, 'MMM'));
setYear(format(date, 'yyyy'));
},
[setMonth, setYear],
);
/**
* Change the active date base on the action and range
*/
const handleNavigate = useCallback(
(action: 'previous' | 'next', view: CalendarView) => {
setActiveDate((date) => {
const newDate = new Date(date);
switch (view) {
case 'month':
newDate.setMonth(
action === 'previous' ? date.getMonth() - 1 : date.getMonth() + 1,
);
break;
case 'year':
newDate.setFullYear(
action === 'previous'
? date.getFullYear() - 1
: date.getFullYear() + 1,
);
break;
case 'decade':
newDate.setFullYear(
action === 'previous'
? date.getFullYear() - 10
: date.getFullYear() + 10,
);
break;
}
changeNavigationLabel(newDate);
return newDate;
});
},
[setActiveDate, changeNavigationLabel],
);
/**
* Change the view of the calendar
*/
const handleChangeView = useCallback(
(view: CalendarView) => {
setView(view);
},
[setView],
);
/**
* Change the active date and set the view to the selected type
* and also update the navigation label
*/
const handleChangeNavigation = useCallback(
(view: 'month' | 'year', date: Date) => {
setActiveDate(date);
changeNavigationLabel(date);
setView(view);
},
[setActiveDate, changeNavigationLabel, setView],
);
const handlePrevious = useCallback(() => {
switch (view) {
case 'month':
return handleNavigate('previous', 'month');
case 'year':
return handleNavigate('previous', 'year');
case 'decade':
return handleNavigate('previous', 'decade');
}
}, [view]);
const handleNext = useCallback(() => {
switch (view) {
case 'month':
return handleNavigate('next', 'month');
case 'year':
return handleNavigate('next', 'year');
case 'decade':
return handleNavigate('next', 'decade');
}
}, [view]);
const handleChange = useCallback(
(newValue: Value, event: MouseEvent<HTMLButtonElement>) => {
setValue(newValue);
// Call the onChange prop if it exists
onChangeProp?.(newValue, event);
/**
* Update the active date and navigation label
*
* NOTE:
* For range selection, the active date is not updated
* The user only can select multiple dates within the same month
*/
if (!selectRange) {
setActiveDate(newValue as Date);
changeNavigationLabel(newValue as Date);
}
},
[setValue, setActiveDate, changeNavigationLabel, selectRange],
);
return (
<div
{...wrapperProps}
className={wrapper({ className: wrapperProps?.className })}
>
{/* Calendar wrapper */}
<div
{...calendarWrapperProps}
className={calendar({ className: calendarWrapperProps?.className })}
>
{/* Navigation */}
<div className={navigation()}>
<Button iconOnly size="sm" variant="ghost" onClick={handlePrevious}>
<ChevronLeft />
</Button>
<div className={actionsClass()}>
<Button
variant="unstyled"
className={button()}
rightIcon={
<ChevronGrabberHorizontal className="text-elements-low-em" />
}
onClick={() => handleChangeView('year')}
>
{month}
</Button>
<Button
variant="unstyled"
className={button()}
rightIcon={
<ChevronGrabberHorizontal className="text-elements-low-em" />
}
onClick={() => handleChangeView('decade')}
>
{year}
</Button>
</div>
<Button iconOnly size="sm" variant="ghost" onClick={handleNext}>
<ChevronRight />
</Button>
</div>
{/* Calendar */}
<ReactCalendar
{...props}
activeStartDate={activeDate}
view={view}
value={value}
showNavigation={false}
selectRange={selectRange}
onChange={handleChange}
onClickMonth={(date) => handleChangeNavigation('month', date)}
onClickYear={(date) => handleChangeNavigation('year', date)}
/>
</div>
{/* Footer or CTA */}
<div
{...footerProps}
className={footer({ className: footerProps?.className })}
>
{actions ? (
actions
) : (
<>
<Button variant="tertiary" onClick={onCancel}>
Cancel
</Button>
<Button
disabled={!value}
onClick={() => (value ? onSelect?.(value) : null)}
>
Select
</Button>
</>
)}
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from './Calendar';
@@ -0,0 +1,68 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const getCheckboxVariant = tv({
slots: {
wrapper: ['group', 'flex', 'gap-3'],
indicator: [
'grid',
'place-content-center',
'text-transparent',
'group-hover:text-controls-disabled',
'focus-visible:text-controls-disabled',
'group-focus-visible:text-controls-disabled',
'data-[state=checked]:text-elements-on-primary',
'data-[state=checked]:group-focus-visible:text-elements-on-primary',
'data-[state=indeterminate]:text-elements-on-primary',
'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active',
],
icon: ['w-3', 'h-3', 'stroke-current', 'text-current'],
input: [
'h-5',
'w-5',
'group',
'border',
'border-border-interactive/10',
'bg-controls-tertiary',
'rounded-md',
'transition-all',
'duration-150',
'focus-ring',
'shadow-button',
'group-hover:border-border-interactive/[0.14]',
'group-hover:bg-controls-tertiary',
'data-[state=checked]:bg-controls-primary',
'data-[state=checked]:hover:bg-controls-primary-hovered',
'data-[state=checked]:focus-visible:bg-controls-primary-hovered',
'data-[disabled]:bg-controls-disabled',
'data-[disabled]:shadow-none',
'data-[disabled]:hover:border-border-interactive/10',
'data-[disabled]:cursor-not-allowed',
'data-[state=checked]:data-[disabled]:bg-controls-disabled-active',
],
label: [
'text-sm',
'tracking-[-0.006em]',
'text-elements-high-em',
'flex',
'flex-col',
'gap-1',
'px-1',
],
description: ['text-xs', 'text-elements-low-em'],
},
variants: {
disabled: {
true: {
wrapper: ['cursor-not-allowed'],
indicator: ['group-hover:text-transparent'],
input: [
'group-hover:border-border-interactive/[0.14]',
'group-hover:bg-controls-disabled',
],
label: ['cursor-not-allowed'],
},
},
},
});
export type CheckboxVariants = VariantProps<typeof getCheckboxVariant>;
@@ -0,0 +1,76 @@
import React from 'react';
import * as CheckboxRadix from '@radix-ui/react-checkbox';
import { type CheckboxProps as CheckboxRadixProps } from '@radix-ui/react-checkbox';
import { getCheckboxVariant, type CheckboxVariants } from './Checkbox.theme';
import { CheckIcon } from 'components/shared/CustomIcon';
interface CheckBoxProps extends CheckboxRadixProps, CheckboxVariants {
/**
* The label of the checkbox.
*/
label?: string;
/**
* The description of the checkbox.
*/
description?: string;
}
/**
* Checkbox component is used to allow users to select one or more items from a set.
* It is a wrapper around the `@radix-ui/react-checkbox` component.
*
* It accepts all the props from `@radix-ui/react-checkbox` component and the variants from the theme.
*
* It also accepts `label` and `description` props to display the label and description of the checkbox.
*
* @example
* ```tsx
* <Checkbox
* id="checkbox"
* label="Checkbox"
* description="This is a checkbox"
* />
* ```
*/
export const Checkbox = ({
id,
className,
label,
description,
onCheckedChange,
...props
}: CheckBoxProps) => {
const {
wrapper: wrapperStyle,
indicator: indicatorStyle,
icon: iconStyle,
input: inputStyle,
label: labelStyle,
description: descriptionStyle,
} = getCheckboxVariant({
disabled: props?.disabled,
});
return (
<div className={wrapperStyle()}>
<CheckboxRadix.Root
{...props}
className={inputStyle({ className })}
id={id}
onCheckedChange={onCheckedChange}
>
<CheckboxRadix.Indicator forceMount className={indicatorStyle()}>
<CheckIcon className={iconStyle()} />
</CheckboxRadix.Indicator>
</CheckboxRadix.Root>
{label && (
<label className={labelStyle()} htmlFor={id}>
{label}
{description && (
<span className={descriptionStyle()}>{description}</span>
)}
</label>
)}
</div>
);
};
@@ -0,0 +1 @@
export * from './Checkbox';
@@ -0,0 +1,22 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CheckIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
{...props}
>
<path
d="M1.5 7.5L4.64706 10L10.5 2"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,20 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronGrabberHorizontal = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M6.66666 12.5L9.99999 15.8333L13.3333 12.5M6.66666 7.5L9.99999 4.16666L13.3333 7.5"
stroke="currentColor"
strokeLinecap="square"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronLeft = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.7071 2.66666L5.37378 8.00001L10.7071 13.3333L10 14.0404L3.95956 8.00001L10 1.95956L10.7071 2.66666Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronRight = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.00001 1.95956L12.0405 7.99998L6.00001 14.0404L5.29291 13.3333L10.6262 7.99999L5.29291 2.66666L6.00001 1.95956Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CrossIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M5 5L19 19M19 5L5 19"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,30 @@
import React, { ComponentPropsWithoutRef } from 'react';
export interface CustomIconProps extends ComponentPropsWithoutRef<'svg'> {
size?: number | string; // width and height will both be set as the same value
name?: string;
}
export const CustomIcon = ({
children,
width = 24,
height = 24,
size,
viewBox = '0 0 24 24',
name,
...rest
}: CustomIconProps) => {
return (
<svg
aria-labelledby={name}
height={size || height}
role="presentation"
viewBox={viewBox}
width={size || width}
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
{children}
</svg>
);
};
@@ -0,0 +1,20 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const GlobeIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M10 17.9167C14.3723 17.9167 17.9167 14.3723 17.9167 10C17.9167 5.62776 14.3723 2.08334 10 2.08334M10 17.9167C5.62776 17.9167 2.08334 14.3723 2.08334 10C2.08334 5.62776 5.62776 2.08334 10 2.08334M10 17.9167C8.15906 17.9167 6.66668 14.3723 6.66668 10C6.66668 5.62776 8.15906 2.08334 10 2.08334M10 17.9167C11.841 17.9167 13.3333 14.3723 13.3333 10C13.3333 5.62776 11.841 2.08334 10 2.08334M17.5 10H2.50001"
stroke="currentColor"
strokeLinecap="square"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const InfoSquareIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.96786 2.5C5.52858 2.49999 5.14962 2.49997 4.83748 2.52548C4.50802 2.55239 4.18034 2.61182 3.86503 2.77249C3.39462 3.01217 3.01217 3.39462 2.77249 3.86503C2.61182 4.18034 2.55239 4.50802 2.52548 4.83748C2.49997 5.14962 2.49999 5.52857 2.5 5.96785V14.0321C2.49999 14.4714 2.49997 14.8504 2.52548 15.1625C2.55239 15.492 2.61182 15.8197 2.77249 16.135C3.01217 16.6054 3.39462 16.9878 3.86503 17.2275C4.18034 17.3882 4.50802 17.4476 4.83748 17.4745C5.14962 17.5 5.52858 17.5 5.96786 17.5H14.0321C14.4714 17.5 14.8504 17.5 15.1625 17.4745C15.492 17.4476 15.8197 17.3882 16.135 17.2275C16.6054 16.9878 16.9878 16.6054 17.2275 16.135C17.3882 15.8197 17.4476 15.492 17.4745 15.1625C17.5 14.8504 17.5 14.4714 17.5 14.0321V5.96786C17.5 5.52858 17.5 5.14962 17.4745 4.83748C17.4476 4.50802 17.3882 4.18034 17.2275 3.86503C16.9878 3.39462 16.6054 3.01217 16.135 2.77249C15.8197 2.61182 15.492 2.55239 15.1625 2.52548C14.8504 2.49997 14.4714 2.49999 14.0322 2.5H5.96786ZM8.33333 9.16667C8.33333 8.70643 8.70643 8.33333 9.16667 8.33333H10C10.4602 8.33333 10.8333 8.70643 10.8333 9.16667V13.3333C10.8333 13.7936 10.4602 14.1667 10 14.1667C9.53976 14.1667 9.16667 13.7936 9.16667 13.3333V10C8.70643 10 8.33333 9.6269 8.33333 9.16667ZM10 5.83333C9.53976 5.83333 9.16667 6.20643 9.16667 6.66667C9.16667 7.1269 9.53976 7.5 10 7.5C10.4602 7.5 10.8333 7.1269 10.8333 6.66667C10.8333 6.20643 10.4602 5.83333 10 5.83333Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const PlusIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.66666 9.99999C1.66666 5.39762 5.39762 1.66666 9.99999 1.66666C14.6024 1.66666 18.3333 5.39762 18.3333 9.99999C18.3333 14.6024 14.6024 18.3333 9.99999 18.3333C5.39762 18.3333 1.66666 14.6024 1.66666 9.99999ZM10.625 6.46483C10.625 6.11966 10.3452 5.83983 9.99999 5.83983C9.65481 5.83983 9.37499 6.11966 9.37499 6.46483V9.37537H6.46446C6.11928 9.37537 5.83946 9.65519 5.83946 10.0004C5.83946 10.3455 6.11928 10.6254 6.46446 10.6254H9.37499V13.5359C9.37499 13.8811 9.65481 14.1609 9.99999 14.1609C10.3452 14.1609 10.625 13.8811 10.625 13.5359V10.6254H13.5355C13.8807 10.6254 14.1605 10.3455 14.1605 10.0004C14.1605 9.65519 13.8807 9.37537 13.5355 9.37537H10.625V6.46483Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,24 @@
# 1. What icons are compatible with this component?
- Viewbox "0 0 24 24": From where you're exporting from, please make sure the icon is using viewBox="0 0 24 24" before downloading/exporting. Not doing so will result in incorrect icon scaling
# 2. How to add a new icon?
**2.1 Sanitising the icon**
1. Duplicate a current icon e.g. CrossIcon and rename it accordingly.
2. Rename the function inside the new file you duplicated too
3. Replace the markup with your SVG markup (make sure it complies with the above section's rule)
4. Depending on the svg you pasted...
A. If the `<svg>` has only 1 child, remove the `<svg>` parent entirely so you only have the path left
B. If your component has more than 1 paths, rename `<svg>` tag with the `<g>` tag. Then, remove all attributes of this `<g>` tag so that it's just `<g>`
5. Usually, icons are single colored. If that's the case, replace all fill/stroke color with `currentColor`. E.g. <path d="..." fill="currentColor">. Leave the other attributes without removing them.
6. If your icon has more than one colour, then it's up to you to decide whether we want to use tailwind to help set the fill and stroke colors
7. Lastly, export your icon in `index.ts` by following what was done for CrossIcon
8. Make sure to provide a name to the `<CustomIcon>` component for accessibility sake
9. Done!
**2.3 Use your newly imported icon**
1. You can change simply use `<BellIcon size="32" />` to quickly change both width and height with the same value (square). For custom viewBox, width and height, simply provide all three props.
2. Coloring the icon: Simply add a className with text color. E.g. `<BellIcon className="text-gray-500" />`
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const SearchIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M20 20L16.05 16.05M18 11C18 14.866 14.866 18 11 18C7.13401 18 4 14.866 4 11C4 7.13401 7.13401 4 11 4C14.866 4 18 7.13401 18 11Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const WarningIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,11 @@
export * from './PlusIcon';
export * from './CustomIcon';
export * from './CheckIcon';
export * from './ChevronGrabberHorizontal';
export * from './ChevronLeft';
export * from './ChevronRight';
export * from './InfoSquareIcon';
export * from './WarningIcon';
export * from './SearchIcon';
export * from './CrossIcon';
export * from './GlobeIcon';
@@ -0,0 +1,78 @@
import { VariantProps, tv } from 'tailwind-variants';
export const inlineNotificationTheme = tv({
slots: {
wrapper: ['rounded-xl', 'flex', 'gap-2', 'items-start', 'w-full', 'border'],
content: ['flex', 'flex-col', 'gap-1'],
title: [],
description: [],
icon: ['flex', 'items-start'],
},
variants: {
variant: {
info: {
wrapper: ['border-border-info-light', 'bg-base-bg-emphasized-info'],
title: ['text-elements-on-emphasized-info'],
description: ['text-elements-on-emphasized-info'],
icon: ['text-elements-info'],
},
danger: {
wrapper: ['border-border-danger-light', 'bg-base-bg-emphasized-danger'],
title: ['text-elements-on-emphasized-danger'],
description: ['text-elements-on-emphasized-danger'],
icon: ['text-elements-danger'],
},
warning: {
wrapper: [
'border-border-warning-light',
'bg-base-bg-emphasized-warning',
],
title: ['text-elements-on-emphasized-warning'],
description: ['text-elements-on-emphasized-warning'],
icon: ['text-elements-warning'],
},
success: {
wrapper: [
'border-border-success-light',
'bg-base-bg-emphasized-success',
],
title: ['text-elements-on-emphasized-success'],
description: ['text-elements-on-emphasized-success'],
icon: ['text-elements-success'],
},
generic: {
wrapper: ['border-border-separator', 'bg-base-bg-emphasized'],
title: ['text-elements-high-em'],
description: ['text-elements-on-emphasized-info'],
icon: ['text-elements-high-em'],
},
},
size: {
sm: {
wrapper: ['px-2', 'py-2'],
title: ['leading-4', 'text-xs'],
description: ['leading-4', 'text-xs'],
icon: ['h-4', 'w-4'],
},
md: {
wrapper: ['px-3', 'py-3'],
title: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
description: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
icon: ['h-5', 'w-5'],
},
},
hasDescription: {
true: {
title: ['font-medium'],
},
},
},
defaultVariants: {
variant: 'generic',
size: 'md',
},
});
export type InlineNotificationTheme = VariantProps<
typeof inlineNotificationTheme
>;
@@ -0,0 +1,68 @@
import React, { ReactNode, useCallback } from 'react';
import { ComponentPropsWithoutRef } from 'react';
import {
InlineNotificationTheme,
inlineNotificationTheme,
} from './InlineNotification.theme';
import { InfoSquareIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
export interface InlineNotificationProps
extends ComponentPropsWithoutRef<'div'>,
InlineNotificationTheme {
/**
* The title of the notification
*/
title: string;
/**
* The description of the notification
*/
description?: string;
/**
* The icon to display in the notification
* @default <InfoSquareIcon />
*/
icon?: ReactNode;
}
/**
* A notification that is displayed inline with the content
*
* @example
* ```tsx
* <InlineNotification title="Notification title goes here" />
* ```
*/
export const InlineNotification = ({
className,
title,
description,
size,
variant,
icon,
...props
}: InlineNotificationProps) => {
const {
wrapper,
content,
title: titleClass,
description: descriptionClass,
icon: iconClass,
} = inlineNotificationTheme({ size, variant, hasDescription: !!description });
// Render custom icon or default icon
const renderIcon = useCallback(() => {
if (!icon) return <InfoSquareIcon className={iconClass()} />;
return cloneIcon(icon, { className: iconClass() });
}, [icon]);
return (
<div {...props} className={wrapper({ className })}>
{renderIcon()}
<div className={content()}>
<p className={titleClass()}>{title}</p>
{description && <p className={descriptionClass()}>{description}</p>}
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from './InlineNotification';
@@ -0,0 +1,84 @@
import { VariantProps, tv } from 'tailwind-variants';
export const inputTheme = tv(
{
slots: {
container: [
'flex',
'items-center',
'rounded-lg',
'relative',
'placeholder:text-elements-disabled',
'disabled:cursor-not-allowed',
'disabled:bg-controls-disabled',
],
label: ['text-sm', 'text-elements-high-em'],
description: ['text-xs', 'text-elements-low-em'],
input: [
'focus-ring',
'block',
'w-full',
'h-full',
'rounded-lg',
'text-elements-mid-em',
'shadow-sm',
'border',
'border-border-interactive',
'disabled:shadow-none',
'disabled:border-none',
],
icon: ['text-elements-mid-em'],
iconContainer: [
'absolute',
'inset-y-0',
'flex',
'items-center',
'z-10',
'cursor-pointer',
],
helperIcon: [],
helperText: ['flex', 'gap-2', 'items-center', 'text-elements-danger'],
},
variants: {
state: {
default: {
input: '',
},
error: {
input: [
'outline',
'outline-offset-0',
'outline-border-danger',
'shadow-none',
],
helperText: 'text-elements-danger',
},
},
size: {
md: {
container: 'h-11',
input: ['text-sm pl-4 pr-4'],
icon: ['h-[18px] w-[18px]'],
helperText: 'text-sm',
helperIcon: ['h-5 w-5'],
},
sm: {
container: 'h-8',
input: ['text-xs pl-3 pr-3'],
icon: ['h-4 w-4'],
helperText: 'text-xs',
helperIcon: ['h-4 w-4'],
},
},
},
defaultVariants: {
size: 'md',
state: 'default',
},
},
{
responsiveVariants: true,
},
);
export type InputTheme = VariantProps<typeof inputTheme>;
@@ -0,0 +1,100 @@
import React, { ReactNode, useMemo } from 'react';
import { ComponentPropsWithoutRef } from 'react';
import { InputTheme, inputTheme } from './Input.theme';
import { WarningIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
import { cn } from 'utils/classnames';
export interface InputProps
extends InputTheme,
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
label?: string;
description?: string;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
helperText?: string;
}
export const Input = ({
className,
label,
description,
leftIcon,
rightIcon,
helperText,
size,
state,
...props
}: InputProps) => {
const styleProps = (({ size = 'md', state }) => ({
size,
state,
}))({ size, state });
const {
container: containerCls,
label: labelCls,
description: descriptionCls,
input: inputCls,
icon: iconCls,
iconContainer: iconContainerCls,
helperText: helperTextCls,
helperIcon: helperIconCls,
} = inputTheme({ ...styleProps });
const renderLabels = useMemo(
() => (
<div className="space-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(), ariaHidden: true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
const renderRightIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
{cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
const renderHelperText = useMemo(
() => (
<div className={helperTextCls()}>
{state &&
cloneIcon(<WarningIcon className={helperIconCls()} />, {
ariaHidden: true,
})}
<p>{helperText}</p>
</div>
),
[cloneIcon, state, helperIconCls, helperText, helperTextCls],
);
return (
<div className="space-y-2">
{renderLabels}
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
className={cn(inputCls({ class: 'w-80' }), {
'pl-10': leftIcon,
})}
{...props}
/>
{rightIcon && renderRightIcon}
</div>
{renderHelperText}
</div>
);
};
@@ -0,0 +1,2 @@
export * from './Input';
export * from './Input.theme';
@@ -0,0 +1,80 @@
import { tv, type VariantProps } from 'tailwind-variants';
export type TabsVariants = VariantProps<typeof tabsTheme>;
export const tabsTheme = tv({
slots: {
root: ['flex', 'data-[orientation=horizontal]:w-full'],
triggerWrapper: [
// Horizontal default
'px-1',
'pb-5',
'text-elements-low-em',
'border-b-2',
'border-transparent',
'hover:border-border-interactive/10',
'hover:text-elements-mid-em',
'focus-within:border-border-interactive/10',
'data-[state=active]:font-medium',
'data-[state=active]:text-elements-high-em',
'data-[state=active]:border-elements-high-em',
// Vertical
'data-[orientation=vertical]:px-3',
'data-[orientation=vertical]:py-3',
'data-[orientation=vertical]:min-w-[240px]',
'data-[orientation=vertical]:focus-ring',
'data-[orientation=vertical]:rounded-xl',
'data-[orientation=vertical]:border-transparent',
'data-[orientation=vertical]:hover:bg-base-bg-emphasized',
'data-[orientation=vertical]:hover:text-elements-mid-em',
'data-[orientation=vertical]:hover:border-transparent',
'data-[orientation=vertical]:focus-visible:border-transparent',
'data-[orientation=vertical]:focus-visible:bg-base-bg-emphasized',
'data-[orientation=vertical]:focus-visible:text-elements-mid-em',
'data-[orientation=vertical]:data-[state=active]:font-normal',
'data-[orientation=vertical]:data-[state=active]:bg-base-bg-emphasized',
'data-[orientation=vertical]:data-[state=active]:border-transparent',
'data-[orientation=vertical]:data-[state=active]:hover:text-elements-high-em',
'data-[orientation=vertical]:data-[state=active]:focus-visible:text-elements-high-em',
],
trigger: [
'flex',
'gap-1.5',
'cursor-default',
'select-none',
'items-center',
'justify-center',
'outline-none',
'leading-none',
'tracking-[-0.006em]',
'rounded-md',
// Horizontal default
'data-[orientation=horizontal]:focus-ring',
// Vertical
'data-[orientation=vertical]:gap-2',
],
triggerList: [
'flex',
'shrink-0',
'gap-5',
'border-b',
'border-transparent',
// Horizontal default
'data-[orientation=horizontal]:border-border-interactive/10',
// Vertical
'data-[orientation=vertical]:flex-col',
'data-[orientation=vertical]:gap-0.5',
],
content: ['text-elements-high-em', 'grow', 'outline-none', 'tab-content'],
},
variants: {
fillWidth: {
true: {
trigger: ['flex-1'],
},
},
},
defaultVariants: {
fillWidth: false,
},
});
@@ -0,0 +1,51 @@
import React, { type ComponentPropsWithoutRef } from 'react';
import { Root as TabsRoot } from '@radix-ui/react-tabs';
import { tabsTheme } from './Tabs.theme';
import TabsContent from './TabsContent';
import TabsList from './TabsList';
import TabsTrigger from './TabsTrigger';
import TabsProvider, { TabsProviderProps } from './TabsProvider';
export interface TabsProps extends ComponentPropsWithoutRef<typeof TabsRoot> {
/**
* The configuration for the tabs component.
*/
config?: TabsProviderProps;
}
/**
* A component that allows users to switch between different tabs.
* @returns JSX element representing the tabs component.
*/
export const Tabs = ({
config,
className,
orientation = 'horizontal',
...props
}: TabsProps) => {
const { root } = tabsTheme(config);
return (
<TabsProvider {...config} orientation={orientation}>
<TabsRoot
{...props}
orientation={orientation}
className={root({ className })}
/>
</TabsProvider>
);
};
/**
* Assigns the TabsTrigger class to the Trigger property of the Tabs object.
*/
Tabs.Trigger = TabsTrigger;
/**
* Assigns the TabsList object to the List property of the Tabs object.
*/
Tabs.List = TabsList;
/**
* Assigns the TabsContent component to the Content property of the Tabs component.
*/
Tabs.Content = TabsContent;
@@ -0,0 +1,26 @@
import React, {
forwardRef,
type ComponentPropsWithoutRef,
type ElementRef,
} from 'react';
import { Content } from '@radix-ui/react-tabs';
import { tabsTheme } from '../Tabs.theme';
export interface TabsContentProps
extends ComponentPropsWithoutRef<typeof Content> {}
/**
* A component that represents the content of the tabs component.
*/
const TabsContent = forwardRef<ElementRef<typeof Content>, TabsContentProps>(
({ className, ...props }, ref) => {
const { content } = tabsTheme();
return <Content ref={ref} className={content({ className })} {...props} />;
},
);
// Assigns the display name to the TabsContent component.
TabsContent.displayName = 'TabsContent';
export { TabsContent };
@@ -0,0 +1,3 @@
import { TabsContent } from './TabsContent';
export default TabsContent;
@@ -0,0 +1,25 @@
import React, {
forwardRef,
type ComponentPropsWithoutRef,
type ElementRef,
} from 'react';
import { List } from '@radix-ui/react-tabs';
import { tabsTheme } from 'components/shared/Tabs/Tabs.theme';
export interface TabsListProps extends ComponentPropsWithoutRef<typeof List> {}
/**
* A component that represents the list of tabs.
*/
const TabsList = forwardRef<ElementRef<typeof List>, TabsListProps>(
({ className, ...props }, ref) => {
const { triggerList } = tabsTheme({ className });
return <List ref={ref} className={triggerList({ className })} {...props} />;
},
);
// Assigns the display name to the TabsList component.
TabsList.displayName = 'TabsList';
export { TabsList };
@@ -0,0 +1,3 @@
import { TabsList } from './TabsList';
export default TabsList;
@@ -0,0 +1,47 @@
import React, {
createContext,
useContext,
type PropsWithChildren,
ComponentPropsWithoutRef,
} from 'react';
import { TabsVariants } from './Tabs.theme';
import { Root as TabsRoot } from '@radix-ui/react-tabs';
export interface TabsProviderProps
extends Partial<TabsVariants>,
ComponentPropsWithoutRef<typeof TabsRoot> {}
type TabsProviderContext = ReturnType<typeof useTabsValues>;
const TabsContext = createContext<Partial<TabsProviderContext>>({});
// For inferring return type
const useTabsValues = (props: TabsProviderProps) => {
return props;
};
/**
* A provider component that allows users to switch between different tabs.
* @returns JSX element representing the tabs provider component.
*/
export const TabsProvider = ({
children,
...props
}: PropsWithChildren<TabsProviderProps>): JSX.Element => {
const values = useTabsValues(props);
return <TabsContext.Provider value={values}>{children}</TabsContext.Provider>;
};
/**
* A hook that returns the context of the tabs provider.
* @returns The context of the tabs provider.
*/
export const useTabs = () => {
const context = useContext(TabsContext);
if (context === undefined) {
throw new Error('useTabs was used outside of its Provider');
}
return context;
};
export default TabsProvider;
@@ -0,0 +1,59 @@
import React, {
forwardRef,
type ComponentPropsWithoutRef,
type ElementRef,
type PropsWithChildren,
type ReactNode,
} from 'react';
import { Trigger } from '@radix-ui/react-tabs';
import { tabsTheme } from 'components/shared/Tabs/Tabs.theme';
import { useTabs } from 'components/shared/Tabs/TabsProvider';
export interface TabsTriggerProps
extends ComponentPropsWithoutRef<typeof Trigger> {
/**
* The icon to display in the trigger.
*/
icon?: ReactNode;
}
/**
* A component that represents the trigger for the tabs component.
*/
const TabsTrigger = forwardRef<
ElementRef<typeof Trigger>,
PropsWithChildren<TabsTriggerProps>
>(({ className, icon, children, ...props }, ref) => {
const config = useTabs();
const { triggerWrapper, trigger } = tabsTheme(config);
const orientation = config.orientation;
return (
<Trigger
ref={ref}
// Disabled focus state for horizontal tabs
tabIndex={orientation === 'horizontal' ? -1 : undefined}
className={triggerWrapper({ className })}
{...props}
>
{/* Need to add button in the trigger children because there's focus state inside the children */}
<button
data-orientation={orientation}
// Disabled focus state for vertical tabs
tabIndex={orientation === 'vertical' ? -1 : undefined}
className={trigger()}
>
{/* Put the icon on the left of the text for veritcal tab */}
{orientation === 'vertical' && icon}
{children}
{/* Put the icon on the right of the text for horizontal tab */}
{orientation === 'horizontal' && icon}
</button>
</Trigger>
);
});
TabsTrigger.displayName = 'TabsTrigger';
export { TabsTrigger };
@@ -0,0 +1,3 @@
import { TabsTrigger } from './TabsTrigger';
export default TabsTrigger;
@@ -0,0 +1 @@
export * from './Tabs';
@@ -14,13 +14,13 @@ import { useGQLClient } from './GQLClientContext';
const UNAUTHORIZED_ERROR_CODE = 401;
interface ContextValue {
octokit: Octokit | null;
octokit: Octokit;
isAuth: boolean;
updateAuth: () => void;
}
const OctokitContext = createContext<ContextValue>({
octokit: null,
octokit: new Octokit(),
isAuth: false,
updateAuth: () => {},
});
+6
View File
@@ -1,3 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.focus-ring {
@apply focus-visible:ring-[3px] focus-visible:ring-snowball-200 focus-visible:ring-offset-1 focus-visible:ring-offset-snowball-500 focus-visible:outline-none;
}
}
+1
View File
@@ -7,6 +7,7 @@ import { GQLClient } from 'gql-client';
import { ThemeProvider } from '@material-tailwind/react';
import './index.css';
import '@fontsource/inter';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { GQLClientProvider } from './context/GQLClientContext';
@@ -0,0 +1,158 @@
import React, { useState } from 'react';
import { Calendar } from 'components/shared/Calendar';
import { Value } from 'react-calendar/dist/cjs/shared/types';
import {
renderCheckbox,
renderCheckboxWithDescription,
} from './renders/checkbox';
import { avatars, avatarsFallback } from './renders/avatar';
import { renderBadges } from './renders/badge';
import {
renderButtonIcons,
renderButtons,
renderLinks,
} from './renders/button';
import {
renderTabWithBadges,
renderTabs,
renderVerticalTabs,
} from './renders/tabs';
import {
renderInlineNotificationWithDescriptions,
renderInlineNotifications,
} from './renders/inlineNotifications';
import { renderInputs } from './renders/input';
const Page = () => {
const [singleDate, setSingleDate] = useState<Value>();
const [dateRange, setDateRange] = useState<Value>();
return (
<div className="relative h-full min-h-full">
<div className="flex flex-col items-center justify-center container mx-auto px-20 py-20">
<h1 className="text-4xl font-bold">Manual Storybook</h1>
<p className="mt-4 text-lg text-center text-gray-500">
Get started by editing{' '}
<code className="p-2 font-mono text-sm bg-gray-100 rounded-md">
packages/frontend/src/pages/components/index.tsx
</code>
</p>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Button */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Input</h1>
<div className="flex w-full flex-col gap-10">{renderInputs()}</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
<h1 className="text-2xl font-bold">Button</h1>
<div className="flex flex-col gap-10">
{renderButtons()}
{renderButtonIcons()}
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Badge */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Badge</h1>
<div className="space-y-5">{renderBadges()}</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Checkbox */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Checkbox</h1>
<div className="flex gap-10 flex-wrap">{renderCheckbox()}</div>
<div className="flex gap-10 flex-wrap">
{renderCheckboxWithDescription()}
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Calendar */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Calendar</h1>
<div className="flex flex-col gap-10">
<div className="space-y-5 flex flex-col items-center">
<p>Selected date: {singleDate?.toString()}</p>
<Calendar
value={singleDate}
onChange={setSingleDate}
onSelect={setSingleDate}
/>
</div>
<div className="space-y-5 flex flex-col items-center">
<p>
Start date:{' '}
{dateRange instanceof Array ? dateRange[0]?.toString() : ''}{' '}
<br />
End date:{' '}
{dateRange instanceof Array ? dateRange[1]?.toString() : ''}
</p>
<Calendar
selectRange
value={dateRange}
onChange={setDateRange}
/>
</div>
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Avatar */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Avatar</h1>
<div className="flex gap-10 flex-wrap max-w-[522px]">
{avatars}
{avatarsFallback}
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Tabs */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Tabs</h1>
<div className="flex flex-col gap-10 items-center justify-center">
{renderTabs()}
{renderTabWithBadges()}
</div>
<h1 className="text-2xl font-bold">Vertical Tabs</h1>
{renderVerticalTabs()}
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Inline notification */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Inline Notification</h1>
<div className="flex gap-1 flex-wrap">
{renderInlineNotifications()}
</div>
<div className="flex gap-1 flex-wrap">
{renderInlineNotificationWithDescriptions()}
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* 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>
</div>
</div>
</div>
);
};
export default Page;
@@ -0,0 +1,19 @@
import React from 'react';
import { Avatar, AvatarVariants } from 'components/shared/Avatar';
const avatarSizes: AvatarVariants['size'][] = [18, 20, 24, 28, 32, 36, 40, 44];
const avatarVariants: AvatarVariants['type'][] = ['gray', 'orange', 'blue'];
export const avatars = avatarSizes.map((size) => {
return (
<Avatar initials="SY" key={String(size)} size={size} imageSrc="/gray.png" />
);
});
export const avatarsFallback = avatarVariants.map((color) => {
return avatarSizes.map((size) => {
return (
<Avatar initials="SY" key={`${color}-${size}`} type={color} size={size} />
);
});
});
@@ -0,0 +1,20 @@
import React from 'react';
import { Badge } from 'components/shared/Badge';
import { BadgeTheme } from 'components/shared/Badge/Badge.theme';
export const renderBadges = () => {
return ['primary', 'secondary', 'tertiary', 'inset'].map((variant, index) => (
<div className="flex gap-5" key={index}>
{['sm', 'xs'].map((size) => (
<Badge
key={size}
variant={variant as BadgeTheme['variant']}
size={size as BadgeTheme['size']}
>
1
</Badge>
))}
</div>
));
};
@@ -0,0 +1,65 @@
import { Button, ButtonTheme } from 'components/shared/Button';
import { PlusIcon } from 'components/shared/CustomIcon';
import React from 'react';
export const renderButtons = () => {
return ['primary', 'secondary', 'tertiary', 'danger'].map(
(variant, index) => (
<div className="flex gap-5 flex-wrap" key={index}>
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
<Button
leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />}
variant={variant as ButtonTheme['variant']}
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'}
key={`${variant}-${size}`}
disabled={size === 'disabled'}
>
Button
</Button>
))}
</div>
),
);
};
export const renderButtonIcons = () => {
return [
'primary',
'secondary',
'tertiary',
'ghost',
'danger',
'danger-ghost',
].map((variant, index) => (
<div className="flex gap-5 flex-wrap" key={index}>
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
<Button
iconOnly
variant={variant as ButtonTheme['variant']}
size={size !== 'disabled' ? (size as ButtonTheme['size']) : 'md'}
key={`${variant}-${size}`}
disabled={size === 'disabled'}
>
<PlusIcon />
</Button>
))}
</div>
));
};
export const renderLinks = () => {
return ['link', 'link-emphasized', 'disabled'].map((variant) => (
<Button
variant={
variant !== 'disabled' ? (variant as ButtonTheme['variant']) : 'link'
}
key={variant}
leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />}
disabled={variant === 'disabled'}
>
Link
</Button>
));
};
@@ -0,0 +1,27 @@
import React from 'react';
import { Checkbox } from 'components/shared/Checkbox';
export const renderCheckbox = () => {
return Array.from({ length: 5 }).map((_, index) => (
<Checkbox
id={`checkbox-${index + 1}`}
key={index}
label={`Label ${index + 1}`}
disabled={index === 2 || index === 4 ? true : false}
checked={index === 4 ? true : undefined}
value={`value-${index + 1}`}
/>
));
};
export const renderCheckboxWithDescription = () => {
return Array.from({ length: 2 }).map((_, index) => (
<Checkbox
id={`checkbox-description-${index + 1}`}
key={index}
label={`Label ${index + 1}`}
description={`Description of the checkbox ${index + 1}`}
value={`value-with-description-${index + 1}`}
/>
));
};
@@ -0,0 +1,43 @@
import { InlineNotification } from 'components/shared/InlineNotification';
import { InlineNotificationTheme } from 'components/shared/InlineNotification/InlineNotification.theme';
import React from 'react';
const inlineNotificationVariants = [
'info',
'danger',
'warning',
'success',
'generic',
];
const inlineNotificationSizes = ['md', 'sm'];
export const renderInlineNotifications = () => {
return inlineNotificationVariants.map((variant) => (
<div className="space-y-2" key={variant}>
{inlineNotificationSizes.map((size) => (
<InlineNotification
size={size as InlineNotificationTheme['size']}
variant={variant as InlineNotificationTheme['variant']}
key={`${variant}-${size}`}
title="Notification title goes here"
/>
))}
</div>
));
};
export const renderInlineNotificationWithDescriptions = () => {
return inlineNotificationVariants.map((variant) => (
<div className="space-y-2" key={variant}>
{inlineNotificationSizes.map((size) => (
<InlineNotification
size={size as InlineNotificationTheme['size']}
variant={variant as InlineNotificationTheme['variant']}
key={`${variant}-${size}`}
title="Notification title goes here"
description="Description goes here"
/>
))}
</div>
));
};
@@ -0,0 +1,57 @@
import React from 'react';
import { Input } from 'components/shared/Input';
import { SearchIcon, CrossIcon } from 'components/shared/CustomIcon';
export const renderInputs = () => {
return (
<>
<div className="flex w-full gap-10">
<Input
label="Label"
description="Additional information or context"
leftIcon={<SearchIcon />}
rightIcon={<CrossIcon />}
placeholder="Placeholder text"
/>
<Input
disabled
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
/>
<Input
state="error"
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
helperText="The error goes here"
/>
</div>
<div className="flex w-full gap-10">
<Input
label="Label"
leftIcon={<SearchIcon />}
rightIcon={<CrossIcon />}
description="Additional information or context"
placeholder="Placeholder text"
size="sm"
/>
<Input
disabled
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
size="sm"
/>
<Input
state="error"
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
size="sm"
helperText="The error goes here"
/>
</div>
</>
);
};
@@ -0,0 +1,56 @@
import React from 'react';
import { Tabs } from 'components/shared/Tabs';
import { Badge } from 'components/shared/Badge';
import { GlobeIcon } from 'components/shared/CustomIcon';
const tabs = Array.from({ length: 8 });
export const renderTabs = () => {
return (
<Tabs defaultValue="A">
<Tabs.List>
{tabs.map((_, index) => (
<Tabs.Trigger key={index} value={index.toString()}>
Tab item {index}
</Tabs.Trigger>
))}
</Tabs.List>
</Tabs>
);
};
export const renderTabWithBadges = () => {
return (
<Tabs defaultValue="A">
<Tabs.List>
{tabs.map((_, index) => (
<Tabs.Trigger
key={index}
value={index.toString()}
icon={<Badge variant="tertiary">{index}</Badge>}
>
Tab item
</Tabs.Trigger>
))}
</Tabs.List>
</Tabs>
);
};
export const renderVerticalTabs = () => {
return (
<Tabs defaultValue="A" orientation="vertical">
<Tabs.List>
{tabs.slice(0, 4).map((_, index) => (
<Tabs.Trigger
key={index}
icon={<GlobeIcon />}
value={index.toString()}
>
Tab item {index}
</Tabs.Trigger>
))}
</Tabs.List>
</Tabs>
);
};
@@ -24,7 +24,7 @@ const NewProject = () => {
})}
</div>
<h5 className="mt-4 ml-4">Import a repository</h5>
<RepositoryList octokit={octokit!} />
<RepositoryList octokit={octokit} />
</>
) : (
<ConnectAccount onAuth={updateAuth} />
@@ -66,8 +66,10 @@ const DeploymentsTabPanel = () => {
const dateMatch =
!filterValue.updateAtRange ||
(new Date(deployment.updatedAt) >= filterValue.updateAtRange!.from! &&
new Date(deployment.updatedAt) <= filterValue.updateAtRange!.to!);
(new Date(Number(deployment.createdAt)) >=
filterValue.updateAtRange!.from! &&
new Date(Number(deployment.createdAt)) <=
filterValue.updateAtRange!.to!);
return branchMatch && statusMatch && dateMatch;
});
@@ -24,10 +24,6 @@ const OverviewTabPanel = () => {
const { project } = useOutletContext<OutletContextType>();
useEffect(() => {
if (!octokit) {
return;
}
// TODO: Save repo commits in DB and avoid using GitHub API in frontend
// TODO: Figure out fetching latest commits for all branches
const fetchRepoActivity = async () => {
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { RequestError } from 'octokit';
import React, { useCallback, useEffect, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { Domain } from 'gql-client';
@@ -6,14 +7,41 @@ import { Button, Typography } from '@material-tailwind/react';
import DomainCard from '../../../../../components/projects/project/settings/DomainCard';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import repositories from '../../../../../assets/repositories.json';
import { OutletContextType } from '../../../../../types';
import { useOctokit } from '../../../../../context/OctokitContext';
const Domains = () => {
const client = useGQLClient();
const { octokit } = useOctokit();
const { project } = useOutletContext<OutletContextType>();
const [domains, setDomains] = useState<Domain[]>([]);
const [branches, setBranches] = useState<string[]>([]);
const fetchBranches = useCallback(async () => {
const [owner, repo] = project.repository.split('/');
try {
const result = await octokit.rest.repos.listBranches({
owner,
repo,
});
const branches = result.data.map((repo) => repo.name);
setBranches(branches);
} catch (err) {
if (!(err instanceof RequestError && err.status === 404)) {
throw err;
}
console.error(err);
}
}, []);
useEffect(() => {
fetchBranches();
}, []);
const fetchDomains = async () => {
if (project === undefined) {
@@ -46,7 +74,7 @@ const Domains = () => {
domain={domain}
key={domain.id}
// TODO: Use github API for getting linked repository
repo={repositories[0]!}
branches={branches}
project={project}
onUpdate={fetchDomains}
/>
@@ -171,7 +171,8 @@ export const EnvironmentVariablesTabPanel = () => {
>
+ Add variable
</Button>
<Button variant="outlined" size="sm">
{/* TODO: Implement import environment varible functionality */}
<Button variant="outlined" size="sm" disabled>
^ Import .env
</Button>
</div>
-9
View File
@@ -6,15 +6,6 @@ export interface GitOrgDetails {
avatar_url: string;
}
// TODO: Use GitRepositoryDetails
export interface RepositoryDetails {
title: string;
updatedAt: string;
user: string;
private: boolean;
branch: string[];
}
export interface GitRepositoryDetails {
id: number;
name: string;
+13
View File
@@ -0,0 +1,13 @@
import { clsx } from 'clsx';
import type { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Returns a merged class name string by merging and processing multiple class names and Tailwind CSS styles.
*
* @param {...string[]} args - One or more class names and/or Tailwind CSS styles to be merged.
* @returns {string} - The merged class name string.
*/
export function cn(...args: ClassValue[]): string {
return twMerge(clsx(args));
}
+17
View File
@@ -0,0 +1,17 @@
import { Children, cloneElement, isValidElement } from 'react';
import type { Attributes, ReactElement, ReactNode } from 'react';
/**
* Clones an icon element with optional additional props.
* @param {ReactNode} icon - The icon element to clone.
* @param {Attributes & P} [props] - Additional props to apply to the cloned icon.
* @returns {ReactNode} - The cloned icon element with the applied props.
*/
export function cloneIcon<P extends object>(
icon: ReactNode,
props?: Attributes & P,
): ReactNode {
return Children.map(icon, (child) =>
isValidElement(child) ? cloneElement(child as ReactElement, props) : child,
);
}
+150 -1
View File
@@ -8,7 +8,156 @@ export default withMT({
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
extend: {
fontSize: {
'2xs': '0.625rem',
'3xs': '0.5rem',
},
colors: {
emerald: {
100: '#d1fae5',
200: '#a9f1d0',
300: '#6ee7b7',
400: '#34d399',
50: '#ecfdf5',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
},
gray: {
0: '#ffffff',
100: '#f1f5f9',
200: '#e2e9f0',
300: '#cbd6e1',
400: '#94a7b8',
50: '#f8fafc',
500: '#60788f',
600: '#475969',
700: '#334555',
800: '#1b2d3e',
900: '#0b1d2e',
},
orange: {
100: '#ffedd5',
200: '#fed7aa',
300: '#fdba74',
400: '#fb923c',
50: '#fff7ed',
500: '#f97316',
600: '#ea580c',
700: '#c2410c',
800: '#9a3412',
900: '#7c2d12',
},
rose: {
100: '#ffe4e6',
200: '#fecdd3',
300: '#fda4af',
400: '#fb7185',
50: '#fff1f2',
500: '#f43f5e',
600: '#e11d48',
700: '#be123c',
800: '#9f1239',
900: '#881337',
},
snowball: {
100: '#e1f1fe',
200: '#ddeefd',
300: '#cfe6fc',
400: '#74bafb',
50: '#ecf6fe',
500: '#47a4fa',
600: '#0f86f5',
700: '#0977dc',
800: '#075185',
900: '#0a3a5c',
},
base: {
bg: '#ffffff',
'bg-alternate': '#f8fafc',
'bg-emphasized': '#f1f5f9',
'bg-emphasized-danger': '#fff1f2',
'bg-emphasized-info': '#ecf6fe',
'bg-emphasized-success': '#ecfdf5',
'bg-emphasized-warning': '#fff7ed',
},
border: {
active: '#0f86f5',
danger: '#e11d48',
'danger-light': '#ffe4e6',
'info-light': '#ddeefd',
interactive: '#082f561a',
'interactive-hovered': '#082f5624',
separator: '#082f560f',
'success-light': '#d1fae5',
'warning-light': '#ffedd5',
},
controls: {
danger: '#e11d48',
'danger-hovered': '#be123c',
disabled: '#e2e9f0',
'disabled-active': '#74bafb',
elevated: '#ffffff',
inset: '#e2e9f0',
'inset-hovered': '#cbd6e1',
primary: '#0f86f5',
'primary-hovered': '#0977dc',
secondary: '#ddeefd',
'secondary-hovered': '#cfe6fc',
tertiary: '#ffffff',
'tertiary-hovered': '#f8fafc',
},
elements: {
danger: '#e11d48',
disabled: '#94a7b8',
'high-em': '#0b1d2e',
info: '#0f86f5',
link: '#0f86f5',
'link-hovered': '#0977dc',
'low-em': '#60788f',
'mid-em': '#475969',
'on-danger': '#ffffff',
'on-disabled': '#60788f',
'on-disabled-active': '#0a3a5c',
'on-emphasized-danger': '#9f1239',
'on-emphasized-info': '#0a3a5c',
'on-emphasized-success': '#065f46',
'on-emphasized-warning': '#9a3412',
'on-high-contrast': '#ffffff',
'on-primary': '#ffffff',
'on-secondary': '#0977dc',
'on-secondary-tinted': '#075185',
'on-tertiary': '#1b2d3e',
success: '#059669',
warning: '#ea580c',
},
surface: {
card: '#ffffff',
'card-hovered': '#f8fafc',
floating: '#ffffff',
'floating-hovered': '#f1f5f9',
'high-contrast': '#0b1d2e',
},
},
boxShadow: {
button:
'inset 0px 0px 4px rgba(255, 255, 255, 0.25), inset 0px -2px 0px rgba(0, 0, 0, 0.04)',
calendar:
'0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)',
field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
},
spacing: {
2.5: '0.625rem',
3.25: '0.8125rem',
3.5: '0.875rem',
},
},
},
plugins: [],
});

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