Compare commits

...

14 Commits

Author SHA1 Message Date
c8183ba355 chore: rename to laconic 2024-10-30 11:45:25 -04:00
1cb7c5a1f4 style: projects and settings 2024-10-30 11:45:20 -04:00
f50f4b9a4d style: rename color 2024-10-30 11:40:15 -04:00
017d773d81 style: more colors 2024-10-30 11:40:15 -04:00
284721234b style: first pass at laconic colors 2024-10-30 11:40:10 -04:00
ea9a56eb65 Display DNS deployment URLs in overview section (#21)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

- Disable `Deploy` button in configure step if account and deployer not selected
- Update organization slug
- Only display project if current user is project owner

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#21
2024-10-30 13:11:04 +00:00
05bd766133 Display project URLs in Overview tab (#20)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

- Fix project create not working after failed tx
- Poll for project details for auction details
- Update wallet connect metadata

![image](/attachments/cd0217c9-8a2f-4bc5-ad4c-2654fa92f958)

Co-authored-by: Neeraj <neeraj.rtly@gmail.com>
Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#20
2024-10-29 14:10:01 +00:00
0f18bc978e Pass payment tx hash in deployment request (#19)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#19
2024-10-29 09:12:39 +00:00
519e318190 Check if repo with same name already exists when creating project (#18)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

![image](/attachments/6e0efb39-db83-4140-b840-3eca84c3e0f2)

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#18
2024-10-28 11:23:22 +00:00
63969ae25a Implement payments for app deployments (#17)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)
- Implement funtionality to pay for deployments by connecting wallet using `WalletConnect`

![image](/attachments/842e33e8-7de6-4d91-9008-1c67a259b586)

![image](/attachments/94b2fe39-f753-4e99-a8c2-bda4c0b84897)

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#17
2024-10-28 09:46:18 +00:00
b449c299dc Comment out bugsnag code (#16)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Reviewed-on: cerc-io/snowballtools-base#16
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2024-10-25 12:40:34 +00:00
2a35ec1cd5 Check deployment status while creating project with single deployer (#15)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)
- Use deployer API to get status of the deployments

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#15
Co-authored-by: Nabarun Gogoi <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun Gogoi <nabarun@deepstacksoft.com>
2024-10-25 10:47:04 +00:00
be90fc76c1 Update script to pay webapp deployer before deployment request (#14)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#14
2024-10-25 10:01:22 +00:00
3fa60f3cdf Handle account sequence mismatch error (#13)
Part of [Service provider auctions for web deployments](https://www.notion.so/Service-provider-auctions-for-web-deployments-104a6b22d47280dbad51d28aa3a91d75)

- Handle failed txs due to `account sequence mismatch` error by creating a wrapper for all tx methods and retry the tx if `account sequence mismatch` error occurs

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/snowballtools-base#13
2024-10-24 11:38:17 +00:00
127 changed files with 1780 additions and 2144 deletions

View File

@ -15,6 +15,7 @@ VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo' VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo' VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id' VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key' VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key' VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid' VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'

View File

@ -20,16 +20,6 @@
clientId = "" clientId = ""
clientSecret = "" clientSecret = ""
[google]
clientId = ""
clientSecret = ""
[turnkey]
apiBaseUrl = "https://api.turnkey.com"
apiPrivateKey = ""
apiPublicKey = ""
defaultOrganizationId = ""
[registryConfig] [registryConfig]
fetchDeploymentRecordDelay = 5000 fetchDeploymentRecordDelay = 5000
checkAuctionStatusDelay = 5000 checkAuctionStatusDelay = 5000
@ -51,6 +41,3 @@
revealFee = "100000" revealFee = "100000"
revealsDuration = "120s" revealsDuration = "120s"
denom = "alnt" denom = "alnt"
[misc]
projectDomain = "apps.snowballtools.com"

View File

@ -51,17 +51,12 @@ export interface AuctionConfig {
denom: string; denom: string;
} }
export interface MiscConfig {
projectDomain: string;
}
export interface Config { export interface Config {
server: ServerConfig; server: ServerConfig;
database: DatabaseConfig; database: DatabaseConfig;
gitHub: GitHubConfig; gitHub: GitHubConfig;
registryConfig: RegistryConfig; registryConfig: RegistryConfig;
auction: AuctionConfig; auction: AuctionConfig;
misc: MiscConfig;
turnkey: { turnkey: {
apiBaseUrl: string; apiBaseUrl: string;
apiPublicKey: string; apiPublicKey: string;

View File

@ -13,7 +13,7 @@ import assert from 'assert';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { lowercase, numbers } from 'nanoid-dictionary'; import { lowercase, numbers } from 'nanoid-dictionary';
import { DatabaseConfig, MiscConfig } from './config'; import { DatabaseConfig } from './config';
import { User } from './entity/User'; import { User } from './entity/User';
import { Organization } from './entity/Organization'; import { Organization } from './entity/Organization';
import { Project } from './entity/Project'; import { Project } from './entity/Project';
@ -140,6 +140,7 @@ export class Database {
) )
.leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.createdBy', 'user')
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoinAndSelect('deployments.deployer', 'deployer')
.leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.owner', 'owner')
.leftJoinAndSelect('project.deployers', 'deployers') .leftJoinAndSelect('project.deployers', 'deployers')
.leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.organization', 'organization')

View File

@ -15,6 +15,12 @@ export class Deployer {
@Column('varchar') @Column('varchar')
baseDomain!: string; baseDomain!: string;
@Column('varchar', { nullable: true })
minimumPayment!: string | null;
@Column('varchar', { nullable: true })
paymentAddress!: string | null;
@ManyToMany(() => Project, (project) => project.deployers) @ManyToMany(() => Project, (project) => project.deployers)
projects!: Project[]; projects!: Project[];
} }

View File

@ -38,19 +38,17 @@ export interface ApplicationDeploymentRequest {
auction?: string; auction?: string;
config: string; config: string;
meta: string; meta: string;
payment?: string;
} }
export interface ApplicationDeploymentRemovalRequest { export interface ApplicationDeploymentRemovalRequest {
type: string; type: string;
version: string; version: string;
deployment: string; deployment: string;
auction?: string;
payment?: string;
} }
export interface ApplicationDeploymentRemovalRequest {
type: string;
version: string;
deployment: string;
}
export interface ApplicationRecord { export interface ApplicationRecord {
type: string; type: string;

View File

@ -52,6 +52,10 @@ export class Project {
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
auctionId!: string | null; auctionId!: string | null;
// Tx hash for sending coins from snowball to deployer
@Column('varchar', { nullable: true })
txHash!: string | null;
@ManyToMany(() => Deployer, (deployer) => (deployer.projects)) @ManyToMany(() => Deployer, (deployer) => (deployer.projects))
@JoinTable() @JoinTable()
deployers!: Deployer[] deployers!: Deployer[]
@ -66,6 +70,10 @@ export class Project {
@Column('varchar', { nullable: true }) @Column('varchar', { nullable: true })
framework!: string | null; framework!: string | null;
// Address of the user who created the project i.e. requested deployments
@Column('varchar')
paymentAddress!: string;
@Column({ @Column({
type: 'simple-array' type: 'simple-array'
}) })

View File

@ -17,7 +17,7 @@ const log = debug('snowball:server');
const OAUTH_CLIENT_TYPE = 'oauth-app'; const OAUTH_CLIENT_TYPE = 'oauth-app';
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
const { server, database, gitHub, registryConfig, misc } = await getConfig(); const { server, database, gitHub, registryConfig } = await getConfig();
const app = new OAuthApp({ const app = new OAuthApp({
clientType: OAUTH_CLIENT_TYPE, clientType: OAUTH_CLIENT_TYPE,

View File

@ -5,7 +5,8 @@ import { Octokit } from 'octokit';
import { inc as semverInc } from 'semver'; import { inc as semverInc } from 'semver';
import { DeepPartial } from 'typeorm'; import { DeepPartial } from 'typeorm';
import { Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk'; import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { import {
@ -15,7 +16,7 @@ import {
ApplicationDeploymentRemovalRequest ApplicationDeploymentRemovalRequest
} from './entity/Deployment'; } from './entity/Deployment';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types'; import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
import { getConfig, getRepoDetails, sleep } from './utils'; import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
@ -108,14 +109,16 @@ export class Registry {
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord( const result = await registryTransactionWithRetry(() =>
{ this.registry.setRecord(
privateKey: this.registryConfig.privateKey, {
record: applicationRecord, privateKey: this.registryConfig.privateKey,
bondId: this.registryConfig.bondId record: applicationRecord,
}, bondId: this.registryConfig.bondId
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
log(`Published application record ${result.id}`); log(`Published application record ${result.id}`);
@ -126,33 +129,39 @@ export class Registry {
log(`Setting name: ${lrn} for record ID: ${result.id}`); log(`Setting name: ${lrn} for record ID: ${result.id}`);
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await this.registry.setName( await registryTransactionWithRetry(() =>
{ this.registry.setName(
cid: result.id, {
lrn cid: result.id,
}, lrn
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await this.registry.setName( await registryTransactionWithRetry(() =>
{ this.registry.setName(
cid: result.id, {
lrn: `${lrn}@${applicationRecord.app_version}` cid: result.id,
}, lrn: `${lrn}@${applicationRecord.app_version}`
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
await this.registry.setName( await registryTransactionWithRetry(() =>
{ this.registry.setName(
cid: result.id, {
lrn: `${lrn}@${applicationRecord.repository_ref}` cid: result.id,
}, lrn: `${lrn}@${applicationRecord.repository_ref}`
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
return { return {
@ -183,19 +192,21 @@ export class Registry {
const auctionConfig = config.auction; const auctionConfig = config.auction;
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const auctionResult = await this.registry.createProviderAuction( const auctionResult = await registryTransactionWithRetry(() =>
{ this.registry.createProviderAuction(
commitFee: auctionConfig.commitFee, {
commitsDuration: auctionConfig.commitsDuration, commitFee: auctionConfig.commitFee,
revealFee: auctionConfig.revealFee, commitsDuration: auctionConfig.commitsDuration,
revealsDuration: auctionConfig.revealsDuration, revealFee: auctionConfig.revealFee,
denom: auctionConfig.denom, revealsDuration: auctionConfig.revealsDuration,
maxPrice: auctionParams.maxPrice, denom: auctionConfig.denom,
numProviders: auctionParams.numProviders, maxPrice: auctionParams.maxPrice,
}, numProviders: auctionParams.numProviders,
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
) fee
)
);
if (!auctionResult.auction) { if (!auctionResult.auction) {
throw new Error('Error creating auction'); throw new Error('Error creating auction');
@ -208,14 +219,16 @@ export class Registry {
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE, type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
}; };
const result = await this.registry.setRecord( const result = await registryTransactionWithRetry(() =>
{ this.registry.setRecord(
privateKey: this.registryConfig.privateKey, {
record: applicationDeploymentAuction, privateKey: this.registryConfig.privateKey,
bondId: this.registryConfig.bondId record: applicationDeploymentAuction,
}, bondId: this.registryConfig.bondId
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
log(`Application deployment auction created: ${auctionResult.auction.id}`); log(`Application deployment auction created: ${auctionResult.auction.id}`);
@ -231,10 +244,11 @@ export class Registry {
deployment: Deployment, deployment: Deployment,
appName: string, appName: string,
repository: string, repository: string,
auctionId?: string, auctionId?: string | null,
lrn: string, lrn: string,
environmentVariables: { [key: string]: string }, environmentVariables: { [key: string]: string },
dns: string, dns: string,
payment?: string | null
}): Promise<{ }): Promise<{
applicationDeploymentRequestId: string; applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest; applicationDeploymentRequestData: ApplicationDeploymentRequest;
@ -268,20 +282,23 @@ export class Registry {
}), }),
deployer: data.lrn, deployer: data.lrn,
...(data.auctionId && { auction: data.auctionId }), ...(data.auctionId && { auction: data.auctionId }),
...(data.payment && { payment: data.payment }),
}; };
await sleep(SLEEP_DURATION); await sleep(SLEEP_DURATION);
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord( const result = await registryTransactionWithRetry(() =>
{ this.registry.setRecord(
privateKey: this.registryConfig.privateKey, {
record: applicationDeploymentRequest, privateKey: this.registryConfig.privateKey,
bondId: this.registryConfig.bondId record: applicationDeploymentRequest,
}, bondId: this.registryConfig.bondId
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
log(`Application deployment request record published: ${result.id}`); log(`Application deployment request record published: ${result.id}`);
@ -307,7 +324,11 @@ export class Registry {
paymentAddress: auctionWinner, paymentAddress: auctionWinner,
}); });
for (const record of records) { const newRecords = records.filter(record => {
return record.names !== null && record.names.length > 0;
});
for (const record of newRecords) {
if (record.id) { if (record.id) {
deployerRecords.push(record); deployerRecords.push(record);
break; break;
@ -322,12 +343,14 @@ export class Registry {
auctionId: string auctionId: string
): Promise<any> { ): Promise<any> {
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const auction = await this.registry.releaseFunds( const auction = await registryTransactionWithRetry(() =>
{ this.registry.releaseFunds(
auctionId {
}, auctionId
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
return auction; return auction;
@ -411,6 +434,8 @@ export class Registry {
async createApplicationDeploymentRemovalRequest(data: { async createApplicationDeploymentRemovalRequest(data: {
deploymentId: string; deploymentId: string;
deployerLrn: string; deployerLrn: string;
auctionId?: string | null;
payment?: string | null;
}): Promise<{ }): Promise<{
applicationDeploymentRemovalRequestId: string; applicationDeploymentRemovalRequestId: string;
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest; applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
@ -419,19 +444,23 @@ export class Registry {
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE, type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
version: '1.0.0', version: '1.0.0',
deployment: data.deploymentId, deployment: data.deploymentId,
deployer: data.deployerLrn deployer: data.deployerLrn,
...(data.auctionId && { auction: data.auctionId }),
...(data.payment && { payment: data.payment }),
}; };
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees); const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord( const result = await registryTransactionWithRetry(() =>
{ this.registry.setRecord(
privateKey: this.registryConfig.privateKey, {
record: applicationDeploymentRemovalRequest, privateKey: this.registryConfig.privateKey,
bondId: this.registryConfig.bondId record: applicationDeploymentRemovalRequest,
}, bondId: this.registryConfig.bondId
this.registryConfig.privateKey, },
fee this.registryConfig.privateKey,
fee
)
); );
log(`Application deployment removal request record published: ${result.id}`); log(`Application deployment removal request record published: ${result.id}`);
@ -465,6 +494,40 @@ export class Registry {
return this.registry.getAuctionsByIds([auctionId]); return this.registry.getAuctionsByIds([auctionId]);
} }
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const account = await this.getAccount();
const laconicClient = await this.registry.getLaconicClient(account);
const txResponse: DeliverTxResponse =
await registryTransactionWithRetry(() =>
laconicClient.sendTokens(account.address, receiverAddress,
[
{
denom: 'alnt',
amount
}
],
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
);
return txResponse;
}
async getAccount(): Promise<Account> {
const account = new Account(Buffer.from(this.registryConfig.privateKey, 'hex'));
await account.init();
return account;
}
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
const account = await this.getAccount();
const laconicClient = await this.registry.getLaconicClient(account);
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash);
return txResponse;
}
getLrn(appName: string): string { getLrn(appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist"); assert(this.registryConfig.authority, "Authority doesn't exist");
return `lrn://${this.registryConfig.authority}/applications/${appName}`; return `lrn://${this.registryConfig.authority}/applications/${appName}`;

View File

@ -22,8 +22,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getOrganizationsByUserId(context.user); return service.getOrganizationsByUserId(context.user);
}, },
project: async (_: any, { projectId }: { projectId: string }) => { project: async (_: any, { projectId }: { projectId: string }, context: any) => {
return service.getProjectById(projectId); return service.getProjectById(context.user, projectId);
}, },
projectsInOrganization: async ( projectsInOrganization: async (
@ -80,6 +80,21 @@ export const createResolvers = async (service: Service): Promise<any> => {
deployers: async (_: any, __: any, context: any) => { deployers: async (_: any, __: any, context: any) => {
return service.getDeployers(); return service.getDeployers();
}, },
address: async (_: any, __: any, context: any) => {
return service.getAddress();
},
verifyTx: async (
_: any,
{
txHash,
amount,
senderAddress,
}: { txHash: string; amount: string; senderAddress: string },
) => {
return service.verifyTx(txHash, amount, senderAddress);
},
}, },
// TODO: Return error in GQL response // TODO: Return error in GQL response
@ -221,7 +236,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
organizationSlug: string; organizationSlug: string;
data: AddProjectFromTemplateInput; data: AddProjectFromTemplateInput;
lrn: string; lrn: string;
auctionParams: AuctionParams, auctionParams: AuctionParams;
environmentVariables: EnvironmentVariables[]; environmentVariables: EnvironmentVariables[];
}, },
context: any, context: any,

View File

@ -77,6 +77,8 @@ type Project {
fundsReleased: Boolean fundsReleased: Boolean
template: String template: String
framework: String framework: String
paymentAddress: String!
txHash: String!
webhooks: [String!] webhooks: [String!]
members: [ProjectMember!] members: [ProjectMember!]
environmentVariables: [EnvironmentVariable!] environmentVariables: [EnvironmentVariable!]
@ -137,8 +139,11 @@ type Deployer {
deployerLrn: String! deployerLrn: String!
deployerId: String! deployerId: String!
deployerApiUrl: String! deployerApiUrl: String!
minimumPayment: String
paymentAddress: String
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
baseDomain: String
} }
type AuthResult { type AuthResult {
@ -157,6 +162,8 @@ input AddProjectFromTemplateInput {
owner: String! owner: String!
name: String! name: String!
isPrivate: Boolean! isPrivate: Boolean!
paymentAddress: String!
txHash: String!
} }
input AddProjectInput { input AddProjectInput {
@ -164,6 +171,8 @@ input AddProjectInput {
repository: String! repository: String!
prodBranch: String! prodBranch: String!
template: String template: String
paymentAddress: String!
txHash: String!
} }
input UpdateProjectInput { input UpdateProjectInput {
@ -258,6 +267,8 @@ type Query {
getAuctionData(auctionId: String!): Auction! getAuctionData(auctionId: String!): Auction!
domains(projectId: String!, filter: FilterDomainsInput): [Domain] domains(projectId: String!, filter: FilterDomainsInput): [Domain]
deployers: [Deployer] deployers: [Deployer]
address: String!
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
} }
type Mutation { type Mutation {

View File

@ -212,6 +212,9 @@ export class Service {
if (!deployment.project.fundsReleased) { if (!deployment.project.fundsReleased) {
const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId); const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId);
// Return remaining amount to owner
await this.returnUserFundsByProjectId(deployment.projectId, true);
await this.db.updateProjectById(deployment.projectId, { await this.db.updateProjectById(deployment.projectId, {
fundsReleased, fundsReleased,
}); });
@ -309,6 +312,9 @@ export class Service {
if (!deployerRecords) { if (!deployerRecords) {
log(`No winning deployer for auction ${project!.auctionId}`); log(`No winning deployer for auction ${project!.auctionId}`);
// Return all funds to the owner
await this.returnUserFundsByProjectId(project.id, false)
} else { } else {
const deployers = await this.saveDeployersByDeployerRecords(deployerRecords); const deployers = await this.saveDeployersByDeployerRecords(deployerRecords);
for (const deployer of deployers) { for (const deployer of deployers) {
@ -401,8 +407,13 @@ export class Service {
return dbOrganizations; return dbOrganizations;
} }
async getProjectById(projectId: string): Promise<Project | null> { async getProjectById(user: User, projectId: string): Promise<Project | null> {
const dbProject = await this.db.getProjectById(projectId); const dbProject = await this.db.getProjectById(projectId);
if (dbProject && dbProject.owner.id !== user.id) {
return null;
}
return dbProject; return dbProject;
} }
@ -642,7 +653,9 @@ export class Service {
repository: repoUrl, repository: repoUrl,
environmentVariables: environmentVariablesObj, environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`, dns: `${newDeployment.project.name}`,
lrn: deployer!.deployerLrn! lrn: deployer!.deployerLrn!,
payment: data.project.txHash,
auctionId: data.project.auctionId
}); });
} }
@ -654,6 +667,8 @@ export class Service {
lrn: deployer!.deployerLrn!, lrn: deployer!.deployerLrn!,
environmentVariables: environmentVariablesObj, environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`, dns: `${newDeployment.project.name}-${newDeployment.id}`,
payment: data.project.txHash,
auctionId: data.project.auctionId
}); });
await this.db.updateDeploymentById(newDeployment.id, { await this.db.updateDeploymentById(newDeployment.id, {
@ -829,6 +844,8 @@ export class Service {
repository: gitRepo.data.full_name, repository: gitRepo.data.full_name,
// TODO: Set selected template // TODO: Set selected template
template: 'webapp', template: 'webapp',
paymentAddress: data.paymentAddress,
txHash: data.txHash
}, lrn, auctionParams, environmentVariables); }, lrn, auctionParams, environmentVariables);
if (!project || !project.id) { if (!project || !project.id) {
@ -878,21 +895,53 @@ export class Service {
per_page: 1, per_page: 1,
}); });
// Create deployment with prod branch and latest commit
const deploymentData = {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message,
};
if (auctionParams) { if (auctionParams) {
// Create deployment with prod branch and latest commit
const deploymentData = {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message,
};
const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData); const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData);
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId }); await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId });
} else { } else {
const newDeployment = await this.createDeployment(user.id, octokit, deploymentData, lrn); const deployer = await this.db.getDeployerByLRN(lrn!);
if (!deployer) {
log('Invalid deployer LRN');
return;
}
if (deployer.minimumPayment && project.txHash) {
const amountToBePaid = deployer?.minimumPayment.replace(/\D/g, '').toString();
const txResponse = await this.laconicRegistry.sendTokensToAccount(
deployer?.paymentAddress!,
amountToBePaid
);
const txHash = txResponse.transactionHash;
if (txHash) {
await this.updateProject(project.id, { txHash });
project.txHash = txHash;
log('Funds transferrend to deployer');
}
}
const deploymentData = {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message,
deployer
};
const newDeployment = await this.createDeployment(user.id, octokit, deploymentData);
// Update project with deployer // Update project with deployer
await this.updateProjectWithDeployer(newDeployment.projectId, newDeployment.deployer); await this.updateProjectWithDeployer(newDeployment.projectId, newDeployment.deployer);
} }
@ -1133,14 +1182,18 @@ export class Service {
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
deploymentId: latestRecord.id, deploymentId: latestRecord.id,
deployerLrn: deployment.deployer.deployerLrn deployerLrn: deployment.deployer.deployerLrn,
auctionId: deployment.project.auctionId,
payment: deployment.project.txHash
}); });
} }
const result = const result =
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({ await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
deploymentId: deployment.applicationDeploymentRecordId, deploymentId: deployment.applicationDeploymentRecordId,
deployerLrn: deployment.deployer.deployerLrn deployerLrn: deployment.deployer.deployerLrn,
auctionId: deployment.project.auctionId,
payment: deployment.project.txHash
}); });
await this.db.updateDeploymentById(deployment.id, { await this.db.updateDeploymentById(deployment.id, {
@ -1324,6 +1377,33 @@ export class Service {
return false; return false;
} }
async returnUserFundsByProjectId(projectId: string, winningDeployersPresent: boolean) {
const project = await this.db.getProjectById(projectId);
if (!project || !project.auctionId) {
log(`Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}`);
return false;
}
const auction = await this.getAuctionData(project.auctionId);
const totalAuctionPrice = Number(auction.maxPrice.quantity) * auction.numProviders;
let amountToBeReturned;
if (winningDeployersPresent) {
amountToBeReturned = totalAuctionPrice - auction.winnerAddresses.length * Number(auction.winnerPrice.quantity);
} else {
amountToBeReturned = totalAuctionPrice;
}
if (amountToBeReturned !== 0) {
await this.laconicRegistry.sendTokensToAccount(
project.paymentAddress,
amountToBeReturned.toString()
);
}
}
async getDeployers(): Promise<Deployer[]> { async getDeployers(): Promise<Deployer[]> {
const dbDeployers = await this.db.getDeployers(); const dbDeployers = await this.db.getDeployers();
@ -1348,17 +1428,21 @@ export class Service {
const deployers: Deployer[] = []; const deployers: Deployer[] = [];
for (const record of deployerRecords) { for (const record of deployerRecords) {
if (record.names.length > 0) { if (record.names && record.names.length > 0) {
const deployerId = record.id; const deployerId = record.id;
const deployerLrn = record.names[0]; const deployerLrn = record.names[0];
const deployerApiUrl = record.attributes.apiUrl; const deployerApiUrl = record.attributes.apiUrl;
const minimumPayment = record.attributes.minimumPayment;
const paymentAddress = record.attributes.paymentAddress;
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1); const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
const deployerData = { const deployerData = {
deployerLrn, deployerLrn,
deployerId, deployerId,
deployerApiUrl, deployerApiUrl,
baseDomain baseDomain,
minimumPayment,
paymentAddress
}; };
// TODO: Update deployers table in a separate job // TODO: Update deployers table in a separate job
@ -1369,4 +1453,32 @@ export class Service {
return deployers; return deployers;
} }
async getAddress(): Promise<any> {
const account = await this.laconicRegistry.getAccount();
return account.address;
}
async verifyTx(txHash: string, amountSent: string, senderAddress: string): Promise<boolean> {
const txResponse = await this.laconicRegistry.getTxResponse(txHash);
if (!txResponse) {
log('Transaction response not found');
return false;
}
const transfer = txResponse.events.find(e => e.type === 'transfer' && e.attributes.some(a => a.key === 'msg_index'));
if (!transfer) {
log('No transfer event found');
return false;
}
const sender = transfer.attributes.find(a => a.key === 'sender')?.value;
const recipient = transfer.attributes.find(a => a.key === 'recipient')?.value;
const amount = transfer.attributes.find(a => a.key === 'amount')?.value;
const recipientAddress = await this.getAddress();
return amount === amountSent && sender === senderAddress && recipient === recipientAddress;
}
} }

View File

@ -70,6 +70,8 @@ export interface AddProjectFromTemplateInput {
owner: string; owner: string;
name: string; name: string;
isPrivate: boolean; isPrivate: boolean;
paymentAddress: string;
txHash: string;
} }
export interface AuctionParams { export interface AuctionParams {
@ -92,6 +94,7 @@ export interface DeployerRecord {
expiryTime: string; expiryTime: string;
attributes: { attributes: {
apiUrl: string; apiUrl: string;
minimumPayment: string | null;
name: string; name: string;
paymentAddress: string; paymentAddress: string;
publicKey: string; publicKey: string;

View File

@ -120,3 +120,24 @@ export const getRepoDetails = async (
repoUrl repoUrl
}; };
} }
// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs
export const registryTransactionWithRetry = async (
txMethod: () => Promise<any>
): Promise<any> => {
try {
return await txMethod();
} catch (error: any) {
if (!error.message.includes('account sequence mismatch')) {
throw error;
}
console.error(`Transaction failed due to account sequence mismatch. Retrying...`);
try {
return await txMethod();
} catch (retryError: any) {
throw new Error(`Transaction failed again after retry: ${retryError.message}`);
}
}
}

View File

@ -1,8 +1,8 @@
[ [
{ {
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26", "id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
"name": "Snowball Tools", "name": "Deploy Tools",
"slug": "snowball-tools-1" "slug": "deploy-tools"
}, },
{ {
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb", "id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",

View File

@ -1,2 +1,3 @@
REGISTRY_BOND_ID= REGISTRY_BOND_ID=
DEPLOYER_LRN= DEPLOYER_LRN=
AUTHORITY=

View File

@ -1,10 +1,8 @@
services: services:
registry: registry:
rpcEndpoint: http://laconicd.laconic.com:26657 rpcEndpoint: https://laconicd-sapo.laconic.com
gqlEndpoint: http://laconicd.laconic.com:9473/api gqlEndpoint: https://laconicd-sapo.laconic.com/api
userKey: 08c0d30ed23706330468e6936316a3bc3e69e451e394f05027ad56119bb485b9 userKey:
bondId: 820587f916d9a6a056f1e6a5a250151d9fa0c1e771347a6b8bb3d6f2090fd11b bondId:
chainId: laconic_9000-2 chainId: laconic_9000-2
gas:
fees:
gasPrice: 1alnt gasPrice: 1alnt

View File

@ -3,6 +3,7 @@
source .env source .env
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID" echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN" echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
echo "Using AUTHORITY: $AUTHORITY"
# Repository URL # Repository URL
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base" REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
@ -21,40 +22,13 @@ CONFIG_FILE=config.yml
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts # Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
# Get latest version from registry and increment application-record version # Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}') NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found # Set application-record version if no previous records were found
NEW_APPLICATION_VERSION=0.0.1 NEW_APPLICATION_VERSION=0.0.1
fi fi
# Generate application-deployment-request.yml
cat >./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION
application: lrn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
deployer: $DEPLOYER_LRN
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowball-backend.pwa.laconic.com
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.pwa.laconic.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version # Generate application-record.yml with incremented version
cat >./records/application-record.yml <<EOF cat >./records/application-record.yml <<EOF
record: record:
@ -63,11 +37,11 @@ record:
repository_ref: $LATEST_HASH repository_ref: $LATEST_HASH
repository: ["$REPO_URL"] repository: ["$REPO_URL"]
app_type: webapp app_type: webapp
name: snowballtools-base-frontend name: deploy-frontend
app_version: $PACKAGE_VERSION app_version: $PACKAGE_VERSION
EOF EOF
echo "Files generated successfully." echo "Files generated successfully"
RECORD_FILE=records/application-record.yml RECORD_FILE=records/application-record.yml
@ -83,7 +57,7 @@ echo "ApplicationRecord published"
echo $RECORD_ID echo $RECORD_ID
# Set name to record # Set name to record
REGISTRY_APP_LRN="lrn://snowballtools/applications/snowballtools-base-frontend" REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
sleep 2 sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID" yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
@ -122,6 +96,45 @@ if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
exit 1 exit 1
fi fi
# Get payment address for deployer
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
# Pay deployer if paymentAmount is not null
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
# Extract the transaction hash
txHash=$(echo "$payment" | jq -r '.tx.hash')
echo "Paid deployer with txHash as $txHash"
else
echo "Payment amount is null; skipping payment."
fi
# Generate application-deployment-request.yml
cat >./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: deploy-frontend@$PACKAGE_VERSION
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
deployer: $DEPLOYER_LRN
dns: deploy
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
payment: $txHash
EOF
RECORD_FILE=records/application-deployment-request.yml RECORD_FILE=records/application-deployment-request.yml
sleep 2 sleep 2

View File

@ -41,6 +41,7 @@ record:
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2 LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073 LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073

View File

@ -1,17 +1,16 @@
record: record:
type: ApplicationDeploymentRequest type: ApplicationDeploymentRequest
version: "1.0.0" version: '1.0.0'
name: snowballtools-base-frontend@0.1.8 name: deploy-frontend@1.0.0
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8 application: lrn://vaasl/applications/deploy-frontend@1.0.0
dns: dashboard dns: deploy
config: config:
env: env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta: meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024 note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base" repository: "https://git.vdb.to/cerc-io/snowballtools-base"

View File

@ -4,5 +4,5 @@ record:
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"] repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp app_type: webapp
name: snowballtools-base-frontend name: deploy-frontend
app_version: 0.1.8 app_version: 1.0.0

View File

@ -15,3 +15,5 @@ VITE_BUGSNAG_API_KEY=
VITE_PASSKEY_WALLET_RPID= VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL= VITE_TURNKEY_API_BASE_URL=
VITE_TURNKEY_ORGANIZATION_ID= VITE_TURNKEY_ORGANIZATION_ID=
VITE_LACONICD_CHAIN_ID=

View File

@ -1,19 +1,21 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en" class="dark dark:bg-background dark:text-foreground">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="snowball tools dashboard" /> <meta name="description" content="laconic tools dashboard" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<meta name="msapplication-TileColor" content="#2d89ef" /> <meta name="msapplication-TileColor" content="#2d89ef" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<title>Snowball</title> <title>Laconic</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet"
/>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 3000", "dev": "vite --port 3000",
@ -30,10 +30,6 @@
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@snowballtools/auth": "^0.2.0",
"@snowballtools/auth-lit": "^0.2.0",
"@snowballtools/js-sdk": "^0.1.1",
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
"@snowballtools/material-tailwind-react-fork": "^2.1.10", "@snowballtools/material-tailwind-react-fork": "^2.1.10",
"@snowballtools/smartwallet-alchemy-light": "^0.2.0", "@snowballtools/smartwallet-alchemy-light": "^0.2.0",
"@snowballtools/types": "^0.2.0", "@snowballtools/types": "^0.2.0",

View File

@ -0,0 +1 @@
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,4 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="4" fill="#29292E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0494 24.6233C18.8425 21.8302 20.5713 17.973 20.5706 13.7142C20.5717 13.1361 20.5396 12.5645 20.4762 12L12 12.0008L12.0003 28.2867C11.9996 30.2608 12.7522 32.2356 14.2578 33.7411C15.7633 35.2466 17.7395 36.0001 19.7139 35.9991L19.7134 35.9996L36 36L35.9995 27.5227C35.4362 27.4605 34.8645 27.4285 34.2852 27.4284C30.0275 27.4289 26.1701 29.1577 23.377 31.9507C21.3446 33.9321 18.0858 33.9325 16.0785 31.9252C14.0722 29.9191 14.0715 26.6593 16.0494 24.6233ZM34.2419 13.7624C31.9012 11.4217 28.0982 11.4208 25.7566 13.7624C23.4151 16.1038 23.4159 19.9067 25.7566 22.2473C28.0986 24.5892 31.9004 24.5889 34.2419 22.2473C36.5835 19.9059 36.5839 16.1042 34.2419 13.7624Z" fill="#FBFBFB"/>
</svg>

After

Width:  |  Height:  |  Size: 892 B

View File

@ -1 +1,10 @@
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg> <svg width="115" height="20" viewBox="0 0 115 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.37388 10.5194C5.70149 8.19185 7.14225 4.97748 7.1416 1.42853C7.14246 0.94681 7.11586 0.470456 7.063 0L-0.000488281 0.000643078L-0.000273922 13.5723C-0.000917354 15.2174 0.62632 16.863 1.88091 18.1175C3.1356 19.3721 4.78235 20.0001 6.42772 19.9993L6.42729 19.9997L19.9995 20L19.999 12.9355C19.5296 12.8838 19.0532 12.857 18.5704 12.8569C15.0224 12.8574 11.8079 14.298 9.48026 16.6255C7.78654 18.2768 5.07093 18.2771 3.39812 16.6043C1.72638 14.9325 1.72562 12.2161 3.37388 10.5194ZM18.5344 1.46863C16.5837 -0.481929 13.4146 -0.48268 11.4633 1.46863C9.512 3.41984 9.51276 6.58895 11.4633 8.53941C13.415 10.491 16.5831 10.4907 18.5344 8.53941C20.4857 6.5882 20.4861 3.42016 18.5344 1.46863Z" fill="#FBFBFB"/>
<path d="M31.4741 18.5838H39.2552V16.3302H34.075V1.41351H31.4741V18.5838Z" fill="#FBFBFB"/>
<path d="M49.8108 1.41351H45.4976L40.9893 18.5838H43.6769L44.8039 14.2913H50.3744L51.5014 18.5838H54.3191L49.8108 1.41351ZM45.3458 12.145L47.6 3.2593H47.6866L49.8541 12.145H45.3458Z" fill="#FBFBFB"/>
<path d="M62.9292 8.06885H65.9636C65.9636 3.17534 64.3813 1.07196 60.6967 1.07196C56.8169 1.07196 55.1479 3.73341 55.1479 9.97909C55.1479 16.2462 56.8169 18.9291 60.6967 18.9291C64.3813 18.9291 65.9636 16.8901 65.9853 12.1468H62.9508C62.9292 15.8599 62.474 16.7828 60.6967 16.7828C58.6593 16.7828 58.1607 15.4307 58.1824 9.97909C58.1824 4.54896 58.6809 3.19678 60.6967 3.21823C62.474 3.21823 62.9292 4.18413 62.9292 8.06885Z" fill="#FBFBFB"/>
<path d="M73.7781 1.07209C77.7229 1.09364 79.4135 3.77643 79.4135 10.0007C79.4135 16.2249 77.7229 18.9078 73.7781 18.9292C69.8117 18.9507 68.1211 16.2678 68.1211 10.0007C68.1211 3.73354 69.8117 1.05064 73.7781 1.07209ZM71.1555 10.0007C71.1555 15.4308 71.6757 16.783 73.7781 16.783C75.8589 16.783 76.3791 15.4308 76.3791 10.0007C76.3791 4.54909 75.8589 3.19691 73.7781 3.21847C71.6757 3.23992 71.1555 4.59209 71.1555 10.0007Z" fill="#FBFBFB"/>
<path d="M85.0819 18.5624L82.481 18.5838V1.41351H87.0544L91.3243 15.4073H91.3676V1.41351H93.968V18.5838H89.677L85.1254 3.51689H85.0819V18.5624Z" fill="#FBFBFB"/>
<path d="M100.468 1.41351H97.8677V18.5838H100.468V1.41351Z" fill="#FBFBFB"/>
<path d="M111.139 8.06885H114.174C114.174 3.17534 112.591 1.07196 108.906 1.07196C105.028 1.07196 103.358 3.73341 103.358 9.97909C103.358 16.2462 105.028 18.9291 108.906 18.9291C112.591 18.9291 114.174 16.8901 114.195 12.1468H111.161C111.139 15.8599 110.684 16.7828 108.906 16.7828C106.869 16.7828 106.371 15.4307 106.393 9.97909C106.393 4.54896 106.891 3.19678 108.906 3.21823C110.684 3.21823 111.139 4.18413 111.139 8.06885Z" fill="#FBFBFB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,5 +1,5 @@
{ {
"name": "Snowball Tools Dashboard", "name": "Laconic Tools Dashboard",
"short_name": "snowball tools", "short_name": "snowball tools",
"icons": [ "icons": [
{ {

View File

@ -1,5 +1,4 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Heading } from './shared/Heading';
interface LogoProps { interface LogoProps {
orgSlug?: string; orgSlug?: string;
@ -9,14 +8,7 @@ export const Logo = ({ orgSlug }: LogoProps) => {
return ( return (
<Link to={`/${orgSlug}`}> <Link to={`/${orgSlug}`}>
<div className="flex items-center gap-3 px-0 lg:px-2"> <div className="flex items-center gap-3 px-0 lg:px-2">
<img <img src="/logo.svg" alt="Snowball Logo" />
src="/logo.svg"
alt="Snowball Logo"
className="lg:h-10 lg:w-10 h-8 w-8 rounded-lg"
/>
<Heading className="lg:text-[24px] text-[19px] font-semibold">
Snowball
</Heading>
</div> </div>
</Link> </Link>
); );

View File

@ -10,7 +10,7 @@ const SearchBar: React.ForwardRefRenderFunction<
return ( return (
<div className="relative flex w-full"> <div className="relative flex w-full">
<Input <Input
leftIcon={<SearchIcon />} leftIcon={<SearchIcon className="text-foreground-secondary" />}
onChange={onChange} onChange={onChange}
value={value} value={value}
type="search" type="search"

View File

@ -24,8 +24,8 @@ const Stepper = ({ activeStep, stepperValues }: StepperProps) => {
<div <div
className={`text-sm ${ className={`text-sm ${
activeStep === stepperValue.step activeStep === stepperValue.step
? 'text-black font-semibold' ? 'text-black font-semibold dark:text-foreground'
: 'text-gray-600' : 'text-gray-600 dark:text-foreground-secondary'
}`} }`}
> >
{stepperValue.label} {stepperValue.label}

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { useStopwatch } from 'react-timer-hook'; import { useStopwatch } from 'react-timer-hook';
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond'; import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
@ -12,14 +13,19 @@ const setStopWatchOffset = (time: string) => {
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> { interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
offsetTimestamp: Date; offsetTimestamp: Date;
isPaused: boolean;
} }
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => { const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
const { totalSeconds } = useStopwatch({ const { totalSeconds, pause, start } = useStopwatch({
autoStart: true, autoStart: true,
offsetTimestamp: offsetTimestamp, offsetTimestamp: offsetTimestamp,
}); });
useEffect(() => {
isPaused ? pause() : start();
}, [isPaused]);
return <FormatMillisecond time={totalSeconds * 1000} {...props} />; return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
}; };

View File

@ -1,7 +1,10 @@
import ConfirmDialog, { import ConfirmDialog, {
ConfirmDialogProps, ConfirmDialogProps,
} from 'components/shared/ConfirmDialog'; } from 'components/shared/ConfirmDialog';
import { ArrowRightCircleFilledIcon, LoadingIcon } from 'components/shared/CustomIcon'; import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
interface DeleteDeploymentDialogProps extends ConfirmDialogProps { interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
isConfirmButtonLoading?: boolean; isConfirmButtonLoading?: boolean;
@ -20,7 +23,11 @@ export const DeleteDeploymentDialog = ({
dialogTitle="Delete deployment?" dialogTitle="Delete deployment?"
handleCancel={handleCancel} handleCancel={handleCancel}
open={open} open={open}
confirmButtonTitle={isConfirmButtonLoading ? "Deleting deployment" : "Yes, delete deployment"} confirmButtonTitle={
isConfirmButtonLoading
? 'Deleting deployment'
: 'Yes, delete deployment'
}
handleConfirm={handleConfirm} handleConfirm={handleConfirm}
confirmButtonProps={{ confirmButtonProps={{
variant: 'danger', variant: 'danger',

View File

@ -4,7 +4,9 @@ export const projectCardTheme = tv({
slots: { slots: {
wrapper: [ wrapper: [
'bg-surface-card', 'bg-surface-card',
'dark:bg-overlay2',
'shadow-card', 'shadow-card',
'dark:shadow-background',
'rounded-2xl', 'rounded-2xl',
'flex', 'flex',
'flex-col', 'flex-col',
@ -17,10 +19,16 @@ export const projectCardTheme = tv({
'text-sm', 'text-sm',
'font-medium', 'font-medium',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground',
'tracking-[-0.006em]', 'tracking-[-0.006em]',
'truncate', 'truncate',
], ],
description: ['text-xs', 'text-elements-low-em', 'truncate'], description: [
'text-xs',
'text-elements-low-em',
'dark:text-foreground-secondary',
'truncate',
],
icons: ['flex', 'items-center', 'gap-1'], icons: ['flex', 'items-center', 'gap-1'],
lowerContent: [ lowerContent: [
'transition-colors', 'transition-colors',
@ -32,6 +40,7 @@ export const projectCardTheme = tv({
'gap-2', 'gap-2',
'rounded-b-2xl', 'rounded-b-2xl',
'group-hover:bg-surface-card-hovered', 'group-hover:bg-surface-card-hovered',
'dark:group-hover:bg-overlay3',
], ],
latestDeployment: ['flex', 'items-center', 'gap-2'], latestDeployment: ['flex', 'items-center', 'gap-2'],
deploymentStatusContainer: [ deploymentStatusContainer: [
@ -42,10 +51,15 @@ export const projectCardTheme = tv({
'justify-center', 'justify-center',
], ],
deploymentStatus: ['w-1', 'h-1', 'rounded-full'], deploymentStatus: ['w-1', 'h-1', 'rounded-full'],
deploymentName: ['text-xs', 'text-elements-low-em'], deploymentName: [
'text-xs',
'text-elements-low-em',
'dark:text-foreground-secondary',
],
deploymentText: [ deploymentText: [
'text-xs', 'text-xs',
'text-elements-low-em', 'text-elements-low-em',
'dark:text-foreground-secondary',
'font-mono', 'font-mono',
'flex', 'flex',
'items-center', 'items-center',
@ -53,9 +67,11 @@ export const projectCardTheme = tv({
], ],
wavyBorder: [ wavyBorder: [
'bg-surface-card', 'bg-surface-card',
'dark:bg-background',
'transition-colors', 'transition-colors',
'duration-150', 'duration-150',
'group-hover:bg-surface-card-hovered', 'group-hover:bg-surface-card-hovered',
'dark:group-hover:bg-overlay2',
], ],
}, },
variants: { variants: {
@ -67,7 +83,7 @@ export const projectCardTheme = tv({
deploymentStatus: ['bg-orange-400'], deploymentStatus: ['bg-orange-400'],
}, },
failure: { failure: {
deploymentStatus: ['bg-rose-500'], deploymentStatus: ['bg-error'],
}, },
pending: { pending: {
deploymentStatus: ['bg-gray-500'], deploymentStatus: ['bg-gray-500'],

View File

@ -88,7 +88,7 @@ export const ProjectCard = ({
</div> </div>
{/* Icons */} {/* Icons */}
<div className={theme.icons()}> <div className={theme.icons()}>
{hasError && <WarningDiamondIcon className="text-elements-danger" />} {hasError && <WarningDiamondIcon className="text-error" />}
<Menu placement="bottom-end"> <Menu placement="bottom-end">
<MenuHandler> <MenuHandler>
<Button <Button
@ -101,12 +101,15 @@ export const ProjectCard = ({
<HorizontalDotIcon /> <HorizontalDotIcon />
</Button> </Button>
</MenuHandler> </MenuHandler>
<MenuList> <MenuList className="dark:bg-overlay3 dark:shadow-background dark:border-none">
<MenuItem onClick={navigateToSettingsOnClick}> <MenuItem
onClick={navigateToSettingsOnClick}
className="text-foreground"
>
Project settings Project settings
</MenuItem> </MenuItem>
<MenuItem <MenuItem
className="text-red-500" className="text-error"
onClick={navigateToSettingsOnClick} onClick={navigateToSettingsOnClick}
> >
Delete project Delete project

View File

@ -59,12 +59,12 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
}, [fetchProjects, debouncedInputValue]); }, [fetchProjects, debouncedInputValue]);
return ( return (
<div className="relative w-full lg:w-fit"> <div className="relative w-full lg:w-fit dark:bg-overlay">
<SearchBar {...getInputProps()} /> <SearchBar {...getInputProps()} />
<div <div
{...getMenuProps({}, { suppressRefError: true })} {...getMenuProps({}, { suppressRefError: true })}
className={cn( className={cn(
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50', 'flex flex-col shadow-dropdown rounded-xl dark:bg-overlay2 bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen }, { hidden: !inputValue || !isOpen },
)} )}
> >

View File

@ -13,10 +13,10 @@ export const ProjectSearchBarEmpty = ({
{...props} {...props}
className={cn('flex items-center px-2 py-2 gap-3', className)} className={cn('flex items-center px-2 py-2 gap-3', className)}
> >
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning"> <div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning dark:bg-red-50 text-error">
<InfoRoundFilledIcon size={16} /> <InfoRoundFilledIcon size={16} />
</div> </div>
<p className="text-elements-low-em text-sm tracking-[-0.006em]"> <p className="text-elements-low-em text-sm dark:text-foreground-secondary tracking-[-0.006em]">
No projects matching this name No projects matching this name
</p> </p>
</div> </div>

View File

@ -3,7 +3,11 @@ import { useForm, Controller } from 'react-hook-form';
import { FormProvider, FieldValues } from 'react-hook-form'; import { FormProvider, FieldValues } from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
import { AddEnvironmentVariableInput, AuctionParams, Deployer } from 'gql-client'; import {
AddEnvironmentVariableInput,
AuctionParams,
Deployer,
} from 'gql-client';
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material'; import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
@ -18,6 +22,8 @@ import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm'; import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types'; import { EnvironmentVariablesFormValues } from 'types/types';
import ConnectWallet from './ConnectWallet';
import { useWalletConnectClient } from 'context/WalletConnectContext';
type ConfigureDeploymentFormValues = { type ConfigureDeploymentFormValues = {
option: string; option: string;
@ -29,9 +35,17 @@ type ConfigureDeploymentFormValues = {
type ConfigureFormValues = ConfigureDeploymentFormValues & type ConfigureFormValues = ConfigureDeploymentFormValues &
EnvironmentVariablesFormValues; EnvironmentVariablesFormValues;
const DEFAULT_MAX_PRICE = '10000';
const Configure = () => { const Configure = () => {
const { signClient, session, accounts } = useWalletConnectClient();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [deployers, setDeployers] = useState<Deployer[]>([]); const [deployers, setDeployers] = useState<Deployer[]>([]);
const [selectedAccount, setSelectedAccount] = useState<string>();
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [isPaymentDone, setIsPaymentDone] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId'); const templateId = searchParams.get('templateId');
@ -51,7 +65,13 @@ const Configure = () => {
const client = useGQLClient(); const client = useGQLClient();
const methods = useForm<ConfigureFormValues>({ const methods = useForm<ConfigureFormValues>({
defaultValues: { option: 'Auction' }, defaultValues: {
option: 'Auction',
maxPrice: DEFAULT_MAX_PRICE,
lrn: '',
numProviders: 1,
variables: [],
},
}); });
const selectedOption = methods.watch('option'); const selectedOption = methods.watch('option');
@ -62,6 +82,8 @@ const Configure = () => {
const createProject = async ( const createProject = async (
data: FieldValues, data: FieldValues,
envVariables: AddEnvironmentVariableInput[], envVariables: AddEnvironmentVariableInput[],
senderAddress: string,
txHash: string,
): Promise<string> => { ): Promise<string> => {
setIsLoading(true); setIsLoading(true);
let projectId: string | null = null; let projectId: string | null = null;
@ -86,6 +108,8 @@ const Configure = () => {
owner, owner,
name, name,
isPrivate, isPrivate,
paymentAddress: senderAddress,
txHash,
}; };
const { addProjectFromTemplate } = await client.addProjectFromTemplate( const { addProjectFromTemplate } = await client.addProjectFromTemplate(
@ -105,6 +129,8 @@ const Configure = () => {
prodBranch: defaultBranch!, prodBranch: defaultBranch!,
repository: fullName!, repository: fullName!,
template: 'webapp', template: 'webapp',
paymentAddress: senderAddress,
txHash,
}, },
lrn, lrn,
auctionParams, auctionParams,
@ -132,56 +158,219 @@ const Configure = () => {
} }
}; };
const verifyTx = async (
senderAddress: string,
txHash: string,
amount: string,
): Promise<boolean> => {
const isValid = await client.verifyTx(
txHash,
`${amount.toString()}alnt`,
senderAddress,
);
return isValid;
};
const handleFormSubmit = useCallback( const handleFormSubmit = useCallback(
async (createFormData: FieldValues) => { async (createFormData: FieldValues) => {
const environmentVariables = createFormData.variables.map( try {
(variable: any) => { const deployerLrn = createFormData.lrn;
return { const deployer = deployers.find(
key: variable.key, (deployer) => deployer.deployerLrn === deployerLrn,
value: variable.value, );
environments: Object.entries(createFormData.environment)
.filter(([, value]) => value === true)
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
};
},
);
const projectId = await createProject( let amount: string;
createFormData, let senderAddress: string;
environmentVariables, let txHash: string;
); if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
toast({
id: 'no-payment-required',
title: 'No payment required. Deploying app...',
variant: 'info',
onDismiss: dismiss,
});
await client.getEnvironmentVariables(projectId); txHash = '';
senderAddress = '';
} else {
if (!selectedAccount) return;
if (templateId) { senderAddress = selectedAccount.split(':')[2];
createFormData.option === 'Auction'
? navigate( if (createFormData.option === 'LRN') {
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`, amount = deployer?.minimumPayment!;
) } else {
: navigate( amount = (
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`, createFormData.numProviders * createFormData.maxPrice
).toString();
}
const amountToBePaid = amount.replace(/\D/g, '').toString();
const txHashResponse = await cosmosSendTokensHandler(
selectedAccount,
amountToBePaid,
); );
} else {
createFormData.option === 'Auction' if (!txHashResponse) {
? navigate( console.error('Tx not successful');
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`, return;
) }
: navigate(
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`, txHash = txHashResponse;
const isTxHashValid = await verifyTx(
senderAddress,
txHash,
amountToBePaid.toString(),
); );
if (isTxHashValid === false) {
console.error('Invalid Tx hash', txHash);
return;
}
}
const environmentVariables = createFormData.variables.map(
(variable: any) => {
return {
key: variable.key,
value: variable.value,
environments: Object.entries(createFormData.environment)
.filter(([, value]) => value === true)
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
};
},
);
const projectId = await createProject(
createFormData,
environmentVariables,
senderAddress,
txHash,
);
await client.getEnvironmentVariables(projectId);
if (templateId) {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
)
: navigate(
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`,
);
} else {
createFormData.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
)
: navigate(
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`,
);
}
} catch (error) {
console.error(error);
toast({
id: 'error-deploying-app',
title: 'Error deploying app',
variant: 'error',
onDismiss: dismiss,
});
} }
}, },
[client, createProject, dismiss, toast], [client, createProject, dismiss, toast],
); );
const fetchDeployers = useCallback(async () => { const fetchDeployers = useCallback(async () => {
const res = await client.getDeployers() const res = await client.getDeployers();
setDeployers(res.deployers) setDeployers(res.deployers);
}, [client]) }, [client]);
const onAccountChange = useCallback((account: string) => {
setSelectedAccount(account);
}, []);
const onDeployerChange = useCallback(
(selectedLrn: string) => {
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
setSelectedDeployer(deployer);
},
[deployers],
);
const cosmosSendTokensHandler = useCallback(
async (selectedAccount: string, amount: string) => {
if (!signClient || !session || !selectedAccount) {
return;
}
const chainId = selectedAccount.split(':')[1];
const senderAddress = selectedAccount.split(':')[2];
const snowballAddress = await client.getAddress();
try {
setIsPaymentDone(false);
setIsPaymentLoading(true);
toast({
id: 'sending-payment-request',
title: 'Check your wallet and approve payment request',
variant: 'loading',
onDismiss: dismiss,
});
const result: { signature: string } = await signClient.request({
topic: session.topic,
chainId: `cosmos:${chainId}`,
request: {
method: 'cosmos_sendTokens',
params: [
{
from: senderAddress,
to: snowballAddress,
value: amount,
},
],
},
});
if (!result) {
throw new Error('Error completing transaction');
}
toast({
id: 'payment-successful',
title: 'Payment successful',
variant: 'success',
onDismiss: dismiss,
});
setIsPaymentDone(true);
return result.signature;
} catch (error: any) {
console.error('Error sending tokens', error);
toast({
id: 'error-sending-tokens',
title: 'Error sending tokens',
variant: 'error',
onDismiss: dismiss,
});
setIsPaymentDone(false);
} finally {
setIsPaymentLoading(false);
}
},
[session, signClient, toast],
);
useEffect(() => { useEffect(() => {
fetchDeployers() fetchDeployers();
}, []) }, []);
return ( return (
<div className="space-y-7 px-4 py-6"> <div className="space-y-7 px-4 py-6">
@ -209,11 +398,18 @@ const Configure = () => {
<Select <Select
value={value} value={value}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
size='small' size="small"
displayEmpty displayEmpty
sx={{
fontFamily: 'inherit',
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#e0e0e0',
borderRadius: '8px',
},
}}
> >
<MenuItem value="LRN">Deployer LRN</MenuItem>
<MenuItem value="Auction">Create Auction</MenuItem> <MenuItem value="Auction">Create Auction</MenuItem>
<MenuItem value="LRN">Deployer LRN</MenuItem>
</Select> </Select>
)} )}
/> />
@ -238,17 +434,27 @@ const Configure = () => {
</span> </span>
<Select <Select
value={value} value={value}
onChange={(event) => onChange(event.target.value)} onChange={(event) => {
onChange(event.target.value);
onDeployerChange(event.target.value);
}}
displayEmpty displayEmpty
size='small' size="small"
> >
{deployers.map((deployer) => ( {deployers.map((deployer) => (
<MenuItem key={deployer.deployerLrn} value={deployer.deployerLrn}> <MenuItem
{deployer.deployerLrn} key={deployer.deployerLrn}
value={deployer.deployerLrn}
>
{`${deployer.deployerLrn} ${deployer.minimumPayment ? `(${deployer.minimumPayment})` : ''}`}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{fieldState.error && <FormHelperText>{fieldState.error.message}</FormHelperText>} {fieldState.error && (
<FormHelperText>
{fieldState.error.message}
</FormHelperText>
)}
</FormControl> </FormControl>
)} )}
/> />
@ -273,7 +479,11 @@ const Configure = () => {
control={methods.control} control={methods.control}
rules={{ required: true }} rules={{ required: true }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} /> <Input
type="number"
value={value}
onChange={(e) => onChange(e)}
/>
)} )}
/> />
</div> </div>
@ -300,22 +510,57 @@ const Configure = () => {
<EnvironmentVariablesForm /> <EnvironmentVariablesForm />
</div> </div>
<div> {selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
<Button <div>
{...buttonSize} <Button
type="submit" {...buttonSize}
disabled={isLoading} type="submit"
rightIcon={ disabled={isLoading || !selectedDeployer || !selectedAccount}
isLoading ? ( rightIcon={
<LoadingIcon className="animate-spin" /> isLoading ? (
) : ( <LoadingIcon className="animate-spin" />
<ArrowRightCircleFilledIcon /> ) : (
) <ArrowRightCircleFilledIcon />
} )
> }
{isLoading ? 'Deploying repo' : 'Deploy repo'} >
</Button> {isLoading ? 'Deploying' : 'Deploy'}
</div> </Button>
</div>
) : (
<>
<Heading as="h4" className="md:text-lg font-medium mb-3">
Connect to your wallet
</Heading>
<ConnectWallet onAccountChange={onAccountChange} />
{accounts && accounts?.length > 0 && (
<div>
<Button
{...buttonSize}
type="submit"
disabled={
isLoading || isPaymentLoading || !selectedAccount
}
rightIcon={
isLoading || isPaymentLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
>
{!isPaymentDone
? isPaymentLoading
? 'Transaction Requested'
: 'Pay and Deploy'
: isLoading
? 'Deploying'
: 'Deploy'}
</Button>
</div>
)}
</>
)}
</form> </form>
</FormProvider> </FormProvider>
</div> </div>

View File

@ -6,7 +6,6 @@ import { Button } from '../../shared/Button';
import { import {
GitIcon, GitIcon,
EllipsesIcon, EllipsesIcon,
SnowballIcon,
GithubIcon, GithubIcon,
GitTeaIcon, GitTeaIcon,
} from '../../shared/CustomIcon'; } from '../../shared/CustomIcon';
@ -15,6 +14,7 @@ import { IconWithFrame } from '../../shared/IconWithFrame';
import { Heading } from '../../shared/Heading'; import { Heading } from '../../shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard'; import { MockConnectGitCard } from './MockConnectGitCard';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants'; import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
import { LaconicIcon } from 'components/shared/CustomIcon/LaconicIcon';
const SCOPES = 'repo user'; const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`; const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
@ -46,20 +46,24 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
// TODO: Use correct height // TODO: Use correct height
return ( return (
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl"> <div className="dark:bg-overlay bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
<div className="flex flex-col items-center max-w-[420px]"> <div className="flex flex-col items-center max-w-[420px]">
{/** Icons */} {/** Icons */}
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7"> <div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
<IconWithFrame icon={<GitIcon />} /> <IconWithFrame icon={<GitIcon />} hasHighlight={false} />
<EllipsesIcon className="items-center gap-1.5 flex" /> <EllipsesIcon className="items-center gap-1.5 flex" />
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} /> <IconWithFrame
className="bg-background"
icon={<LaconicIcon />}
hasHighlight={false}
/>
</div> </div>
{/** Text */} {/** Text */}
<div className="flex flex-col gap-1.5 mb-6"> <div className="flex flex-col gap-1.5 mb-6">
<Heading className="text-xl font-medium"> <Heading className="text-xl font-medium dark:text-foreground">
Connect to your Git account Connect to your Git account
</Heading> </Heading>
<p className="text-center text-elements-mid-em"> <p className="text-center text-elements-mid-em dark:text-foreground-secondary">
Once connected, you can import a repository from your account or Once connected, you can import a repository from your account or
start with one of our templates. start with one of our templates.
</p> </p>
@ -70,14 +74,14 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
url={GITHUB_OAUTH_URL} url={GITHUB_OAUTH_URL}
onCode={handleCode} onCode={handleCode}
onClose={() => {}} onClose={() => {}}
title="Snowball" title="Laconic"
width={1000} width={1000}
height={1000} height={1000}
> >
<Button <Button
className="w-full sm:w-auto" className="w-full sm:w-auto"
leftIcon={<GithubIcon />} leftIcon={<GithubIcon />}
variant="tertiary" variant="primary"
> >
Connect to GitHub Connect to GitHub
</Button> </Button>
@ -85,7 +89,7 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
<Button <Button
className="w-full sm:w-auto" className="w-full sm:w-auto"
leftIcon={<GitTeaIcon />} leftIcon={<GitTeaIcon />}
variant="tertiary" variant="primary"
> >
Connect to GitTea Connect to GitTea
</Button> </Button>

View File

@ -0,0 +1,46 @@
import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
import { Button } from '../../shared/Button';
import { useWalletConnectClient } from 'context/WalletConnectContext';
const ConnectWallet = ({
onAccountChange,
}: {
onAccountChange: (selectedAccount: string) => void;
}) => {
const { onConnect, accounts } = useWalletConnectClient();
const handleConnect = async () => {
await onConnect();
};
return (
<div className="p-4 bg-slate-100 rounded-lg mb-6">
{!accounts ? (
<div>
<Button type={'button'} onClick={handleConnect}>
Connect Wallet
</Button>
</div>
) : (
<div>
<Select
label="Select Account"
defaultValue={accounts[0].address}
onChange={(value) => {
value && onAccountChange(value);
}}
>
{accounts.map((account, index) => (
<Option key={index} value={account.address}>
{account.address.split(':').slice(1).join(':')}
</Option>
))}
</Select>
</div>
)}
</div>
);
};
export default ConnectWallet;

View File

@ -1,5 +1,7 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import axios from 'axios';
import { Deployment } from 'gql-client';
import { DeployStep, DeployStatus } from './DeployStep'; import { DeployStep, DeployStatus } from './DeployStep';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
@ -7,13 +9,37 @@ import { Heading } from '../../shared/Heading';
import { Button } from '../../shared/Button'; import { Button } from '../../shared/Button';
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon'; import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog'; import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
import { useGQLClient } from 'context/GQLClientContext';
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
type RequestState =
| 'SUBMITTED'
| 'DEPLOYING'
| 'DEPLOYED'
| 'REMOVED'
| 'CANCELLED'
| 'ERROR';
type Record = {
id: string;
createTime: string;
app: string;
lastState: RequestState;
lastUpdate: string;
logAvailable: boolean;
};
const TIMEOUT_DURATION = 5000;
const Deploy = () => { const Deploy = () => {
const client = useGQLClient();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const projectId = searchParams.get('projectId'); const projectId = searchParams.get('projectId');
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [deployment, setDeployment] = useState<Deployment>();
const [record, setRecord] = useState<Record>();
const handleOpen = () => setOpen(!open); const handleOpen = () => setOpen(!open);
const navigate = useNavigate(); const navigate = useNavigate();
@ -23,13 +49,67 @@ const Deploy = () => {
navigate(`/${orgSlug}/projects/create`); navigate(`/${orgSlug}/projects/create`);
}, []); }, []);
useEffect(() => { const isDeploymentFailed = useMemo(() => {
const timerID = setTimeout(() => { if (!record) {
navigate(`/${orgSlug}/projects/create/success/${projectId}`); return false;
}, TIMEOUT_DURATION); }
return () => clearInterval(timerID); // Not checking for `REMOVED` status as this status is received for a brief period before receiving `DEPLOYED` status
}, []); if (record.lastState === 'CANCELLED' || record.lastState === 'ERROR') {
return true;
} else {
return false;
}
}, [record]);
const fetchDeploymentRecords = useCallback(async () => {
if (!deployment) {
return;
}
try {
const response = await axios.get(
`${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`,
);
const record: Record = response.data;
setRecord(record);
} catch (err: any) {
console.log('Error fetching data from deployer', err);
}
}, [deployment]);
const fetchDeployment = useCallback(async () => {
if (!projectId) {
return;
}
const { deployments } = await client.getDeployments(projectId);
setDeployment(deployments[0]);
}, [client, projectId]);
useEffect(() => {
fetchDeployment();
fetchDeploymentRecords();
const interval = setInterval(() => {
fetchDeploymentRecords();
}, FETCH_DEPLOYMENTS_INTERVAL);
return () => {
clearInterval(interval);
};
}, [fetchDeployment, fetchDeploymentRecords]);
useEffect(() => {
if (!record) {
return;
}
if (record.lastState === 'DEPLOYED') {
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
}
}, [record]);
return ( return (
<div className="space-y-7"> <div className="space-y-7">
@ -42,6 +122,7 @@ const Deploy = () => {
<ClockOutlineIcon size={16} className="text-elements-mid-em" /> <ClockOutlineIcon size={16} className="text-elements-mid-em" />
<Stopwatch <Stopwatch
offsetTimestamp={setStopWatchOffset(Date.now().toString())} offsetTimestamp={setStopWatchOffset(Date.now().toString())}
isPaused={isDeploymentFailed}
/> />
</div> </div>
</div> </div>
@ -60,30 +141,36 @@ const Deploy = () => {
/> />
</div> </div>
<div> {!isDeploymentFailed ? (
<DeployStep <div>
title="Building" <DeployStep
status={DeployStatus.COMPLETE} title={record ? 'Submitted' : 'Submitting'}
step="1" status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
processTime="72000" step="1"
/> />
<DeployStep
title="Deployment summary" <DeployStep
status={DeployStatus.PROCESSING} title={
step="2" record && record.lastState === 'DEPLOYED'
startTime={Date.now().toString()} ? 'Deployed'
/> : 'Deploying'
<DeployStep }
title="Running checks" status={
status={DeployStatus.NOT_STARTED} !record
step="3" ? DeployStatus.NOT_STARTED
/> : record.lastState === 'DEPLOYED'
<DeployStep ? DeployStatus.COMPLETE
title="Assigning domains" : DeployStatus.PROCESSING
status={DeployStatus.NOT_STARTED} }
step="4" step="2"
/> startTime={Date.now().toString()}
</div> />
</div>
) : (
<div>
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,27 +1,16 @@
import { useState } from 'react';
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
import { Stopwatch, setStopWatchOffset } from '../../StopWatch'; import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
import FormatMillisecond from '../../FormatMilliSecond';
import processLogs from '../../../assets/process-logs.json';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import { import {
CheckRoundFilledIcon, CheckRoundFilledIcon,
ClockOutlineIcon, ClockOutlineIcon,
CopyIcon,
LoaderIcon, LoaderIcon,
MinusCircleIcon,
PlusIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { useIntersectionObserver } from 'usehooks-ts';
enum DeployStatus { enum DeployStatus {
PROCESSING = 'progress', PROCESSING = 'progress',
COMPLETE = 'complete', COMPLETE = 'complete',
NOT_STARTED = 'notStarted', NOT_STARTED = 'notStarted',
ERROR = 'error',
} }
interface DeployStepsProps { interface DeployStepsProps {
@ -32,35 +21,11 @@ interface DeployStepsProps {
processTime?: string; processTime?: string;
} }
const DeployStep = ({ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
step,
status,
title,
startTime,
processTime,
}: DeployStepsProps) => {
const [isOpen, setIsOpen] = useState(false);
const { toast, dismiss } = useToast();
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
threshold: 1,
});
const disableCollapse = status !== DeployStatus.COMPLETE;
return ( return (
<div className="border-b border-border-separator"> <div className="border-b border-border-separator">
{/* Collapisble trigger */}
<button <button
className={cn( className={cn('flex justify-between w-full py-5 gap-2', 'cursor-auto')}
'flex justify-between w-full py-5 gap-2',
disableCollapse && 'cursor-auto',
)}
tabIndex={disableCollapse ? -1 : undefined}
onClick={() => {
if (!disableCollapse) {
setIsOpen((val) => !val);
}
}}
> >
<div className={cn('grow flex items-center gap-3')}> <div className={cn('grow flex items-center gap-3')}>
{/* Icon */} {/* Icon */}
@ -73,12 +38,6 @@ const DeployStep = ({
{status === DeployStatus.PROCESSING && ( {status === DeployStatus.PROCESSING && (
<LoaderIcon className="animate-spin text-elements-link" /> <LoaderIcon className="animate-spin text-elements-link" />
)} )}
{status === DeployStatus.COMPLETE && (
<div className="text-controls-primary">
{!isOpen && <PlusIcon size={24} />}
{isOpen && <MinusCircleIcon size={24} />}
</div>
)}
</div> </div>
{/* Title */} {/* Title */}
@ -96,7 +55,10 @@ const DeployStep = ({
{status === DeployStatus.PROCESSING && ( {status === DeployStatus.PROCESSING && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ClockOutlineIcon size={16} className="text-elements-low-em" /> <ClockOutlineIcon size={16} className="text-elements-low-em" />
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} /> <Stopwatch
offsetTimestamp={setStopWatchOffset(startTime!)}
isPaused={false}
/>
</div> </div>
)} )}
{status === DeployStatus.COMPLETE && ( {status === DeployStatus.COMPLETE && (
@ -107,51 +69,9 @@ const DeployStep = ({
size={15} size={15}
/> />
</div> </div>
<FormatMillisecond time={Number(processTime)} />{' '}
</div> </div>
)} )}
</button> </button>
{/* Collapsible */}
<Collapse open={isOpen}>
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
{/* Logs */}
{processLogs.map((log, key) => {
return (
<p className="font-mono" key={key}>
{log}
</p>
);
})}
{/* End of logs ref used for hiding gradient overlay */}
<div ref={ref} />
{/* Overflow gradient overlay */}
{!hideGradientOverlay && (
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
)}
{/* Copy log button */}
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
<Button
size="xs"
onClick={() => {
navigator.clipboard.writeText(processLogs.join('\n'));
toast({
title: 'Logs copied',
variant: 'success',
id: 'logs',
onDismiss: dismiss,
});
}}
leftIcon={<CopyIcon size={16} />}
>
Copy log
</Button>
</div>
</div>
</Collapse>
</div> </div>
); );
}; };

View File

@ -94,7 +94,7 @@ export const MockConnectGitCard = () => {
}, [segmentedControlsValue]); }, [segmentedControlsValue]);
return ( return (
<div className="relative bg-base-bg shadow-card rounded-2xl px-2 py-2 w-full max-w-[560px] flex flex-col gap-2"> <div className="relative dark:bg-overlay bg-base-bg shadow-card dark:shadow-background rounded-2xl px-2 py-2 w-full max-w-[560px] flex flex-col gap-2">
{/* Content */} {/* Content */}
<SegmentedControls <SegmentedControls
value={segmentedControlsValue} value={segmentedControlsValue}
@ -106,7 +106,7 @@ export const MockConnectGitCard = () => {
{renderContent} {renderContent}
{/* Shade */} {/* Shade */}
<div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white to-transparent" /> <div className="pointer-events-none z-99 absolute inset-0 rounded-2xl bg-gradient-to-t from-white dark:from-overlay to-transparent" />
</div> </div>
); );
}; };
@ -121,18 +121,18 @@ const MockProjectCard = ({
visibility?: string; visibility?: string;
}) => { }) => {
return ( return (
<div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"> <div className="group flex items-start sm:items-center gap-3 pl-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized dark:hover:bg-background relative">
{/* Icon container */} {/* Icon container */}
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex"> <div className="w-10 h-10 bg-base-bg dark:bg-background rounded-md justify-center items-center flex">
<GithubIcon /> <GithubIcon />
</div> </div>
{/* Content */} {/* Content */}
<div className="flex flex-1 gap-3 flex-wrap"> <div className="flex flex-1 gap-3 flex-wrap">
<div className="flex flex-col items-start gap-1"> <div className="flex flex-col items-start gap-1">
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]"> <p className="text-elements-high-em text-sm dark:text-foreground font-medium tracking-[-0.006em]">
{full_name} {full_name}
</p> </p>
<p className="text-elements-low-em text-xs"> <p className="text-elements-low-em text-xs dark:text-foreground-secondary">
{updated_at && relativeTimeISO(updated_at)} {updated_at && relativeTimeISO(updated_at)}
</p> </p>
</div> </div>
@ -149,13 +149,13 @@ const MockProjectCard = ({
const MockTemplateCard = ({ icon, name }: { icon: string; name: string }) => { const MockTemplateCard = ({ icon, name }: { icon: string; name: string }) => {
return ( return (
<div className="flex items-center gap-3 px-3 py-3 hover:bg-base-bg-emphasized rounded-2xl group relative cursor-default"> <div className="flex items-center gap-3 px-3 py-3 hover:bg-base-bg-emphasized dark:hover:bg-background relative rounded-2xl group relative cursor-default">
{/* Icon */} {/* Icon */}
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm"> <div className="px-1 py-1 rounded-xl bg-base-bg dark:bg-background border border-border-interactive/10 shadow-card-sm">
<TemplateIcon type={icon as TemplateIconType} /> <TemplateIcon type={icon as TemplateIconType} />
</div> </div>
{/* Name */} {/* Name */}
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em"> <p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em dark:text-foreground">
{name} {name}
</p> </p>
</div> </div>

View File

@ -12,6 +12,7 @@ import {
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogActions, DialogActions,
Tooltip,
} from '@mui/material'; } from '@mui/material';
import { Avatar } from 'components/shared/Avatar'; import { Avatar } from 'components/shared/Avatar';
@ -92,28 +93,38 @@ const DeploymentDetailsCard = ({
}; };
const fetchDeploymentLogs = async () => { const fetchDeploymentLogs = async () => {
let url = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`; setDeploymentLogs('Loading logs...');
const res = await fetch(url, { cache: 'no-store' });
handleOpenDialog(); handleOpenDialog();
if (res.ok) { const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
const logs = await res.text(); const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
setDeploymentLogs(logs); (res) => res.json(),
);
if (!statusRes.logAvailable) {
setDeploymentLogs(statusRes.lastState);
} else {
const logsUrl = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
const logsRes = await fetch(logsUrl, { cache: 'no-store' }).then((res) =>
res.text(),
);
setDeploymentLogs(logsRes);
} }
}; };
const renderDeploymentStatus = useCallback( const renderDeploymentStatus = useCallback(
(className?: string) => { (className?: string) => {
return ( return (
<div className={className}> <Tooltip title="Click to view build logs">
<Tag <div className={className} style={{ cursor: 'pointer' }}>
leftIcon={getIconByDeploymentStatus(deployment.status)} <Tag
size="xs" leftIcon={getIconByDeploymentStatus(deployment.status)}
type={STATUS_COLORS[deployment.status] ?? 'neutral'} size="xs"
onClick={fetchDeploymentLogs} type={STATUS_COLORS[deployment.status] ?? 'neutral'}
> onClick={fetchDeploymentLogs}
{deployment.status} >
</Tag> {deployment.status}
</div> </Tag>
</div>
</Tooltip>
); );
}, },
[deployment.status, deployment.commitHash], [deployment.status, deployment.commitHash],
@ -185,8 +196,8 @@ const DeploymentDetailsCard = ({
type="orange" type="orange"
initials={getInitials(deployment.createdBy.name ?? '')} initials={getInitials(deployment.createdBy.name ?? '')}
className="lg:size-5 2xl:size-6" className="lg:size-5 2xl:size-6"
// TODO: Add avatarUrl // TODO: Add avatarUrl
// imageSrc={deployment.createdBy.avatarUrl} // imageSrc={deployment.createdBy.avatarUrl}
></Avatar> ></Avatar>
</div> </div>
<OverflownText <OverflownText

View File

@ -56,7 +56,7 @@ export const DeploymentMenu = ({
const updateDeployment = async () => { const updateDeployment = async () => {
const isUpdated = await client.updateDeploymentToProd(deployment.id); const isUpdated = await client.updateDeploymentToProd(deployment.id);
if (isUpdated) { if (isUpdated.updateDeploymentToProd) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'deployment_changed_to_production', id: 'deployment_changed_to_production',
@ -77,7 +77,7 @@ export const DeploymentMenu = ({
const redeployToProd = async () => { const redeployToProd = async () => {
const isRedeployed = await client.redeployToProd(deployment.id); const isRedeployed = await client.redeployToProd(deployment.id);
setConfirmButtonLoadingLoading(false); setConfirmButtonLoadingLoading(false);
if (isRedeployed) { if (isRedeployed.redeployToProd) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'redeployed_to_production', id: 'redeployed_to_production',
@ -100,7 +100,7 @@ export const DeploymentMenu = ({
project.id, project.id,
deployment.id, deployment.id,
); );
if (isRollbacked) { if (isRollbacked.rollbackDeployment) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'deployment_rolled_back', id: 'deployment_rolled_back',
@ -124,7 +124,7 @@ export const DeploymentMenu = ({
setIsConfirmDeleteLoading(false); setIsConfirmDeleteLoading(false);
setDeleteDeploymentDialog((preVal) => !preVal); setDeleteDeploymentDialog((preVal) => !preVal);
if (isDeleted) { if (isDeleted.deleteDeployment) {
await onUpdate(); await onUpdate();
toast({ toast({
id: 'deployment_removal_requested', id: 'deployment_removal_requested',

View File

@ -16,7 +16,7 @@ export const Activity = ({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Heading className="text-lg leading-6 font-medium">Activity</Heading> <Heading className="text-lg leading-6 font-medium">Activity</Heading>
<Button variant="tertiary" size="sm"> <Button variant="tertiary" size="sm">
See all SEE ALL
</Button> </Button>
</div> </div>
<div className="mt-5"> <div className="mt-5">

View File

@ -18,7 +18,6 @@ import { Button, Heading, Tag } from 'components/shared';
const WAIT_DURATION = 5000; const WAIT_DURATION = 5000;
const DIALOG_STYLE = { const DIALOG_STYLE = {
backgroundColor: 'rgba(0,0,0, .9)',
padding: '2em', padding: '2em',
borderRadius: '0.5em', borderRadius: '0.5em',
marginLeft: '0.5em', marginLeft: '0.5em',
@ -46,27 +45,27 @@ export const AuctionCard = ({ project }: { project: Project }) => {
const result = await client.getAuctionData(project.auctionId); const result = await client.getAuctionData(project.auctionId);
setAuctionStatus(result.status); setAuctionStatus(result.status);
setAuctionDetails(result); setAuctionDetails(result);
setDeployers(project.deployers); }, [project.auctionId, project.deployers, project.fundsReleased]);
setFundsStatus(project.fundsReleased);
}, []); const fetchUpdatedProject = useCallback(async () => {
const updatedProject = await client.getProject(project.id);
setDeployers(updatedProject.project!.deployers!);
setFundsStatus(updatedProject.project!.fundsReleased!);
}, [project.id]);
const fetchData = useCallback(async () => {
await Promise.all([checkAuctionStatus(), fetchUpdatedProject()]);
}, [checkAuctionStatus, fetchUpdatedProject]);
useEffect(() => { useEffect(() => {
if (auctionStatus !== 'completed') { fetchData();
checkAuctionStatus();
const intervalId = setInterval(checkAuctionStatus, WAIT_DURATION);
return () => clearInterval(intervalId);
}
if (auctionStatus === 'completed') { const timerId = setInterval(() => {
const fetchUpdatedProject = async () => { fetchData();
// Wait for 5 secs since the project is not immediately updated with deployer LRNs }, WAIT_DURATION);
await new Promise((resolve) => setTimeout(resolve, WAIT_DURATION));
const updatedProject = await client.getProject(project.id); return () => clearInterval(timerId);
setDeployers(updatedProject.project?.deployers || []); }, [fetchData]);
};
fetchUpdatedProject();
}
}, [auctionStatus, client]);
const renderAuctionStatus = useCallback( const renderAuctionStatus = useCallback(
() => ( () => (
@ -86,28 +85,27 @@ export const AuctionCard = ({ project }: { project: Project }) => {
return ( return (
<> <>
<div className="p-3 gap-2 rounded-xl border border-gray-200 transition-colors hover:bg-base-bg-alternate flex flex-col mt-8"> <div className="p-3 gap-2 rounded-xl border dark:border-overlay3 border-gray-200 transition-colors hover:bg-base-bg-alternate dark:hover:bg-overlay3 flex flex-col mt-8">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Heading className="text-lg leading-6 font-medium"> <Heading className="text-lg leading-6 font-medium">
Auction details Auction details
</Heading> </Heading>
<Button onClick={handleOpenDialog} variant="tertiary" size="sm"> <Button onClick={handleOpenDialog} variant="tertiary" size="sm">
View details VIEW DETAILS
</Button> </Button>
</div> </div>
<div className="flex justify-between items-center mt-2"> <div className="flex justify-between items-center mt-2">
<span className="text-elements-high-em text-sm font-medium tracking-tight"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Id Auction Id
</span> </span>
<span className="text-elements-mid-em text-sm text-right"> <span className="text-elements-mid-em dark:text-foreground text-sm text-right">
{project.auctionId} {project.auctionId}
</span> </span>
</div> </div>
<div className="flex justify-between items-center mt-1"> <div className="flex justify-between items-center mt-1">
<span className="text-elements-high-em text-sm font-medium tracking-tight"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Auction Status Auction Status
</span> </span>
<div className="ml-2">{renderAuctionStatus()}</div> <div className="ml-2">{renderAuctionStatus()}</div>
@ -117,29 +115,35 @@ export const AuctionCard = ({ project }: { project: Project }) => {
<> <>
{deployers?.length > 0 ? ( {deployers?.length > 0 ? (
<div> <div>
<span className="text-elements-high-em text-sm font-medium tracking-tight"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Deployer LRNs Deployer LRNs
</span> </span>
{deployers.map((deployer, index) => ( {deployers.map((deployer, index) => (
<p key={index} className="text-elements-mid-em text-sm"> <p
key={index}
className="text-elements-mid-em dark:text-foreground text-sm"
>
{'\u2022'} {deployer.deployerLrn} {'\u2022'} {deployer.deployerLrn}
</p> </p>
))} ))}
<div className="flex justify-between items-center mt-1"> <div className="flex justify-between items-center mt-1">
<span className="text-elements-high-em text-sm font-medium tracking-tight"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
Deployer Funds Status Deployer Funds Status
</span> </span>
<div className="ml-2"> <div className="ml-2">
<Tag size="xs" type={fundsStatus ? 'positive' : 'emphasized'}> <Tag
{fundsStatus ? 'RELEASED' : 'LOCKED'} size="xs"
type={fundsStatus ? 'positive' : 'emphasized'}
>
{fundsStatus ? 'RELEASED' : 'WAITING'}
</Tag> </Tag>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="mt-3"> <div className="mt-3">
<span className="text-elements-high-em text-sm font-medium tracking-tight"> <span className="text-elements-high-em dark:text-foreground-secondary text-sm font-medium tracking-tight">
No winning deployers No winning deployers
</span> </span>
</div> </div>
@ -153,15 +157,24 @@ export const AuctionCard = ({ project }: { project: Project }) => {
onClose={handleCloseDialog} onClose={handleCloseDialog}
fullWidth fullWidth
maxWidth="md" maxWidth="md"
PaperProps={{
className: 'dark:bg-overlay2',
}}
> >
<DialogTitle>Auction Details</DialogTitle> <DialogTitle className="dark:text-foreground">
Auction Details
</DialogTitle>
<DialogContent style={DIALOG_STYLE}> <DialogContent style={DIALOG_STYLE}>
{auctionDetails && ( {auctionDetails && (
<pre>{JSON.stringify(auctionDetails, null, 2)}</pre> <pre className="dark:text-foreground-secondary">
{JSON.stringify(auctionDetails, null, 2)}
</pre>
)} )}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleCloseDialog}>Close</Button> <Button onClick={handleCloseDialog} shape="default">
CLOSE
</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</> </>

View File

@ -18,7 +18,7 @@ export const OverviewInfo = ({
return ( return (
<div className="flex justify-between gap-2 py-3 text-sm items-center"> <div className="flex justify-between gap-2 py-3 text-sm items-center">
<div className="flex gap-2 items-center text-elements-high-em"> <div className="flex gap-2 items-center text-elements-high-em dark:text-foreground-secondary">
{styledIcon} {styledIcon}
{label} {label}
</div> </div>

View File

@ -93,11 +93,11 @@ const AddMemberDialog = ({
/> />
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button onClick={handleOpen} variant="secondary"> <Button onClick={handleOpen} variant="danger" shape="default">
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={!isValid}> <Button type="submit" disabled={!isValid} shape="default">
Send invite SEND INVITE
</Button> </Button>
</Modal.Footer> </Modal.Footer>
</form> </form>

View File

@ -24,7 +24,7 @@ const DisplayEnvironmentVariables = ({
return ( return (
<> <>
<div <div
className="flex gap-4 p-2" className="flex gap-4 p-2 dark:text-foreground"
onClick={() => setOpenCollapse((cur) => !cur)} onClick={() => setOpenCollapse((cur) => !cur)}
> >
{openCollapse ? <ChevronUpSmallIcon /> : <ChevronDownSmallIcon />} {openCollapse ? <ChevronUpSmallIcon /> : <ChevronDownSmallIcon />}
@ -33,7 +33,7 @@ const DisplayEnvironmentVariables = ({
</div> </div>
<Collapse open={openCollapse}> <Collapse open={openCollapse}>
{variables.length === 0 ? ( {variables.length === 0 ? (
<div className="bg-slate-100 rounded-xl flex-col p-4"> <div className="bg-slate-100 dark:bg-overlay2 dark:text-foreground rounded-xl flex-col p-4">
No environment variables added yet. Once you add them, they'll show No environment variables added yet. Once you add them, they'll show
up here. up here.
</div> </div>

View File

@ -80,7 +80,7 @@ const MemberCard = ({
return ( return (
<div <div
className={`flex py-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'}`} className={`flex py-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'} dark:text-foreground`}
> >
<div className="basis-1/2"> <div className="basis-1/2">
{member.name && ( {member.name && (

View File

@ -102,8 +102,8 @@ const SetupDomain = () => {
)} )}
<div className="self-stretch"> <div className="self-stretch">
<Button disabled={!isValid} type="submit"> <Button disabled={!isValid} type="submit" shape="default">
Next NEXT
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -30,7 +30,12 @@ export const avatarTheme = tv(
fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'], fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'],
}, },
blue: { blue: {
fallback: ['text-elements-info', 'bg-base-bg-emphasized-info'], fallback: [
'text-elements-info',
'bg-base-bg-emphasized-info',
'dark:text-foreground',
'dark:bg-primary',
],
}, },
}, },
size: { size: {

View File

@ -16,6 +16,7 @@ export const buttonTheme = tv(
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
'transition-colors', 'transition-colors',
'duration-150', 'duration-150',
'font-mono',
], ],
variants: { variants: {
size: { size: {
@ -28,7 +29,7 @@ export const buttonTheme = tv(
true: 'w-full', true: 'w-full',
}, },
shape: { shape: {
default: 'rounded-lg', default: 'rounded',
rounded: 'rounded-full', rounded: 'rounded-full',
}, },
iconOnly: { iconOnly: {
@ -39,21 +40,21 @@ export const buttonTheme = tv(
'text-elements-on-primary', 'text-elements-on-primary',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-controls-primary', 'bg-primary',
'shadow-button', 'shadow-button',
'hover:bg-controls-primary-hovered', 'hover:bg-primary-hovered',
'focus-visible:bg-controls-primary-hovered', 'focus-visible:bg-primary-hovered',
'disabled:text-elements-on-disabled', 'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled', 'disabled:bg-controls-disabled',
'disabled:border-transparent', 'disabled:border-transparent',
'disabled:shadow-none', 'disabled:shadow-none',
], ],
secondary: [ secondary: [
'text-elements-on-secondary', 'text-primary',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-controls-secondary', 'bg-secondary',
'hover:bg-controls-secondary-hovered', 'hover:bg-overlay2',
'focus-visible:bg-controls-secondary-hovered', 'focus-visible:bg-controls-secondary-hovered',
'disabled:text-elements-on-disabled', 'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled', 'disabled:bg-controls-disabled',
@ -77,10 +78,12 @@ export const buttonTheme = tv(
], ],
ghost: [ ghost: [
'text-elements-on-tertiary', 'text-elements-on-tertiary',
'dark:text-foreground-secondary',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-transparent', 'bg-transparent',
'hover:bg-controls-tertiary-hovered', 'hover:bg-controls-tertiary-hovered',
'dark:hover:bg-overlay2',
'hover:border-border-interactive-hovered', 'hover:border-border-interactive-hovered',
'focus-visible:bg-controls-tertiary-hovered', 'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered', 'focus-visible:border-border-interactive-hovered',
@ -93,7 +96,7 @@ export const buttonTheme = tv(
'text-elements-on-danger', 'text-elements-on-danger',
'border', 'border',
'border-transparent', 'border-transparent',
'bg-border-danger', 'bg-error',
'hover:bg-controls-danger-hovered', 'hover:bg-controls-danger-hovered',
'focus-visible:bg-controls-danger-hovered', 'focus-visible:bg-controls-danger-hovered',
'disabled:text-elements-on-disabled', 'disabled:text-elements-on-disabled',

View File

@ -40,11 +40,11 @@ abbr[title] {
} }
.react-calendar__tile { .react-calendar__tile {
@apply h-12 w-12 text-elements-high-em; @apply h-12 w-12 text-elements-high-em dark:text-foreground;
} }
.react-calendar__tile:hover { .react-calendar__tile:hover {
@apply bg-base-bg-emphasized rounded-lg; @apply bg-base-bg-emphasized dark:bg-overlay3 rounded-lg;
} }
.react-calendar__tile:focus-visible { .react-calendar__tile:focus-visible {
@ -52,7 +52,7 @@ abbr[title] {
} }
.react-calendar__tile--now { .react-calendar__tile--now {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg; @apply bg-base-bg-emphasized dark:bg-overlay3 text-elements-high-em rounded-lg;
} }
.react-calendar__tile--now:hover { .react-calendar__tile--now:hover {
@ -77,7 +77,7 @@ abbr[title] {
/* Range -- START */ /* Range -- START */
.react-calendar__tile--range { .react-calendar__tile--range {
@apply bg-controls-secondary text-elements-on-secondary rounded-none; @apply bg-controls-secondary dark:bg-overlay3 text-elements-on-secondary rounded-none;
} }
.react-calendar__tile--range:hover { .react-calendar__tile--range:hover {
@ -89,7 +89,7 @@ abbr[title] {
} }
.react-calendar__tile--rangeStart { .react-calendar__tile--rangeStart {
@apply bg-controls-primary text-elements-on-primary rounded-lg; @apply bg-controls-primary dark:bg-primary text-elements-on-primary rounded-lg;
} }
.react-calendar__tile--rangeStart:hover { .react-calendar__tile--rangeStart:hover {
@ -101,7 +101,7 @@ abbr[title] {
} }
.react-calendar__tile--rangeEnd { .react-calendar__tile--rangeEnd {
@apply bg-controls-primary text-elements-on-primary rounded-lg; @apply bg-controls-primary dark:bg-primary text-elements-on-primary rounded-lg;
} }
.react-calendar__tile--rangeEnd:hover { .react-calendar__tile--rangeEnd:hover {

View File

@ -5,7 +5,9 @@ export const calendarTheme = tv({
wrapper: [ wrapper: [
'max-w-[352px]', 'max-w-[352px]',
'bg-surface-floating', 'bg-surface-floating',
'dark:bg-overlay2',
'shadow-dropdown', 'shadow-dropdown',
'dark:shadow-background',
'rounded-xl', 'rounded-xl',
], ],
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'], calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
@ -28,9 +30,12 @@ export const calendarTheme = tv({
'border', 'border',
'border-border-interactive', 'border-border-interactive',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground',
'shadow-field', 'shadow-field',
'bg-white', 'bg-white',
'dark:bg-overlay3',
'hover:bg-base-bg-alternate', 'hover:bg-base-bg-alternate',
'dark:hover:bg-foreground-secondary',
'focus-visible:bg-base-bg-alternate', 'focus-visible:bg-base-bg-alternate',
], ],
footer: [ footer: [

View File

@ -280,6 +280,7 @@ export const Calendar = ({
showNavigation={false} showNavigation={false}
selectRange={selectRange} selectRange={selectRange}
onChange={handleChange} onChange={handleChange}
// tileClassName="dark:text-foreground-secondary dark:hover:bg-overlay3"
onClickMonth={(date) => handleChangeNavigation('month', date)} onClickMonth={(date) => handleChangeNavigation('month', date)}
onClickYear={(date) => handleChangeNavigation('year', date)} onClickYear={(date) => handleChangeNavigation('year', date)}
/> />
@ -297,19 +298,20 @@ export const Calendar = ({
) : ( ) : (
<> <>
{value && ( {value && (
<Button variant="danger" onClick={handleReset}> <Button variant="danger" onClick={handleReset} shape="default">
Reset RESET
</Button> </Button>
)} )}
<div className="space-x-3"> <div className="space-x-3">
<Button variant="tertiary" onClick={onCancel}> <Button variant="tertiary" onClick={onCancel} shape="default">
Cancel CANCEL
</Button> </Button>
<Button <Button
disabled={!value} disabled={!value}
onClick={() => (value ? onSelect?.(value) : null)} onClick={() => (value ? onSelect?.(value) : null)}
shape="default"
> >
Select SELECT
</Button> </Button>
</div> </div>
</> </>

View File

@ -11,7 +11,9 @@ export const getCheckboxVariant = tv({
'focus-visible:text-controls-disabled', 'focus-visible:text-controls-disabled',
'group-focus-visible:text-controls-disabled', 'group-focus-visible:text-controls-disabled',
'data-[state=checked]:text-elements-on-primary', 'data-[state=checked]:text-elements-on-primary',
'dark:data-[state=checked]:text-foreground',
'data-[state=checked]:group-focus-visible:text-elements-on-primary', 'data-[state=checked]:group-focus-visible:text-elements-on-primary',
'dark:data-[state=checked]:group-focus-visible:text-foreground',
'data-[state=indeterminate]:text-elements-on-primary', 'data-[state=indeterminate]:text-elements-on-primary',
'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active', 'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active',
], ],
@ -23,6 +25,7 @@ export const getCheckboxVariant = tv({
'border', 'border',
'border-border-interactive/10', 'border-border-interactive/10',
'bg-controls-tertiary', 'bg-controls-tertiary',
'dark:bg-background',
'rounded-md', 'rounded-md',
'transition-all', 'transition-all',
'duration-150', 'duration-150',
@ -30,9 +33,13 @@ export const getCheckboxVariant = tv({
'shadow-button', 'shadow-button',
'group-hover:border-border-interactive/[0.14]', 'group-hover:border-border-interactive/[0.14]',
'group-hover:bg-controls-tertiary', 'group-hover:bg-controls-tertiary',
'dark:group-hover:bg-overlay',
'data-[state=checked]:bg-controls-primary', 'data-[state=checked]:bg-controls-primary',
'dark:data-[state=checked]:bg-primary',
'data-[state=checked]:hover:bg-controls-primary-hovered', 'data-[state=checked]:hover:bg-controls-primary-hovered',
'dark:data-[state=checked]:hover:bg-primary-hovered',
'data-[state=checked]:focus-visible:bg-controls-primary-hovered', 'data-[state=checked]:focus-visible:bg-controls-primary-hovered',
'dark:data-[state=checked]:focus-visible:bg-primary-hovered',
'data-[disabled]:bg-controls-disabled', 'data-[disabled]:bg-controls-disabled',
'data-[disabled]:shadow-none', 'data-[disabled]:shadow-none',
'data-[disabled]:hover:border-border-interactive/10', 'data-[disabled]:hover:border-border-interactive/10',
@ -43,12 +50,17 @@ export const getCheckboxVariant = tv({
'text-sm', 'text-sm',
'tracking-[-0.006em]', 'tracking-[-0.006em]',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground',
'flex', 'flex',
'flex-col', 'flex-col',
'gap-1', 'gap-1',
'px-1', 'px-1',
], ],
description: ['text-xs', 'text-elements-low-em'], description: [
'text-xs',
'text-elements-low-em',
'dark:text-foreground-secondary',
],
}, },
variants: { variants: {
disabled: { disabled: {

View File

@ -11,7 +11,7 @@ export const GitIcon: React.FC<CustomIconProps> = (props) => {
> >
<path <path
d="M35.7782 16.4219L20.0791 0.723956C19.864 0.508762 19.6087 0.338053 19.3276 0.221583C19.0466 0.105114 18.7453 0.045166 18.4411 0.045166C18.1368 0.045166 17.8356 0.105114 17.5545 0.221583C17.2735 0.338053 17.0181 0.508762 16.8031 0.723956L13.5443 3.9843L17.6788 8.11882C18.1649 7.95374 18.6875 7.92797 19.1875 8.04442C19.6874 8.16088 20.1448 8.41491 20.5079 8.77778C20.8731 9.14329 21.128 9.60418 21.2435 10.1077C21.359 10.6113 21.3304 11.1372 21.161 11.6253L25.1473 15.6103C25.6355 15.4408 26.1616 15.4122 26.6653 15.5279C27.169 15.6437 27.6299 15.899 27.9952 16.2646C28.251 16.5204 28.454 16.8241 28.5925 17.1584C28.7309 17.4926 28.8022 17.8509 28.8022 18.2127C28.8022 18.5745 28.7309 18.9328 28.5925 19.267C28.454 19.6013 28.251 19.905 27.9952 20.1608C27.4779 20.6776 26.7766 20.9678 26.0455 20.9678C25.3143 20.9678 24.6131 20.6776 24.0958 20.1608C23.7116 19.7759 23.4497 19.286 23.3434 18.7526C23.237 18.2192 23.2907 17.6663 23.4979 17.1634L19.7805 13.4472V23.2287C20.1729 23.4225 20.5134 23.707 20.7739 24.0586C21.0345 24.4103 21.2075 24.8189 21.2786 25.2507C21.3497 25.6825 21.317 26.1251 21.183 26.5417C21.0491 26.9583 20.8178 27.337 20.5083 27.6465C20.2525 27.9023 19.9488 28.1053 19.6146 28.2438C19.2803 28.3822 18.922 28.4535 18.5602 28.4535C18.1984 28.4535 17.8402 28.3822 17.5059 28.2438C17.1716 28.1053 16.8679 27.9023 16.6121 27.6465C16.3562 27.3907 16.1532 27.0869 16.0147 26.7526C15.8762 26.4183 15.8049 26.06 15.8049 25.6982C15.8049 25.3363 15.8762 24.978 16.0147 24.6437C16.1532 24.3094 16.3562 24.0057 16.6121 23.7499C16.8699 23.4916 17.1763 23.2869 17.5137 23.1477V13.2762C17.1777 13.1378 16.8724 12.9344 16.6153 12.6777C16.3582 12.421 16.1543 12.116 16.0154 11.7802C15.8765 11.4445 15.8053 11.0846 15.8059 10.7212C15.8065 10.3579 15.8789 9.9982 16.0189 9.66291L11.9423 5.58552L1.17673 16.3483C0.742912 16.783 0.499268 17.372 0.499268 17.9862C0.499268 18.6003 0.742912 19.1893 1.17673 19.624L16.8766 35.3239C17.3113 35.7576 17.9002 36.0011 18.5143 36.0011C19.1283 36.0011 19.7172 35.7576 20.1519 35.3239L35.7782 19.6975C36.212 19.2629 36.4557 18.6738 36.4557 18.0597C36.4557 17.4456 36.212 16.8566 35.7782 16.4219Z" d="M35.7782 16.4219L20.0791 0.723956C19.864 0.508762 19.6087 0.338053 19.3276 0.221583C19.0466 0.105114 18.7453 0.045166 18.4411 0.045166C18.1368 0.045166 17.8356 0.105114 17.5545 0.221583C17.2735 0.338053 17.0181 0.508762 16.8031 0.723956L13.5443 3.9843L17.6788 8.11882C18.1649 7.95374 18.6875 7.92797 19.1875 8.04442C19.6874 8.16088 20.1448 8.41491 20.5079 8.77778C20.8731 9.14329 21.128 9.60418 21.2435 10.1077C21.359 10.6113 21.3304 11.1372 21.161 11.6253L25.1473 15.6103C25.6355 15.4408 26.1616 15.4122 26.6653 15.5279C27.169 15.6437 27.6299 15.899 27.9952 16.2646C28.251 16.5204 28.454 16.8241 28.5925 17.1584C28.7309 17.4926 28.8022 17.8509 28.8022 18.2127C28.8022 18.5745 28.7309 18.9328 28.5925 19.267C28.454 19.6013 28.251 19.905 27.9952 20.1608C27.4779 20.6776 26.7766 20.9678 26.0455 20.9678C25.3143 20.9678 24.6131 20.6776 24.0958 20.1608C23.7116 19.7759 23.4497 19.286 23.3434 18.7526C23.237 18.2192 23.2907 17.6663 23.4979 17.1634L19.7805 13.4472V23.2287C20.1729 23.4225 20.5134 23.707 20.7739 24.0586C21.0345 24.4103 21.2075 24.8189 21.2786 25.2507C21.3497 25.6825 21.317 26.1251 21.183 26.5417C21.0491 26.9583 20.8178 27.337 20.5083 27.6465C20.2525 27.9023 19.9488 28.1053 19.6146 28.2438C19.2803 28.3822 18.922 28.4535 18.5602 28.4535C18.1984 28.4535 17.8402 28.3822 17.5059 28.2438C17.1716 28.1053 16.8679 27.9023 16.6121 27.6465C16.3562 27.3907 16.1532 27.0869 16.0147 26.7526C15.8762 26.4183 15.8049 26.06 15.8049 25.6982C15.8049 25.3363 15.8762 24.978 16.0147 24.6437C16.1532 24.3094 16.3562 24.0057 16.6121 23.7499C16.8699 23.4916 17.1763 23.2869 17.5137 23.1477V13.2762C17.1777 13.1378 16.8724 12.9344 16.6153 12.6777C16.3582 12.421 16.1543 12.116 16.0154 11.7802C15.8765 11.4445 15.8053 11.0846 15.8059 10.7212C15.8065 10.3579 15.8789 9.9982 16.0189 9.66291L11.9423 5.58552L1.17673 16.3483C0.742912 16.783 0.499268 17.372 0.499268 17.9862C0.499268 18.6003 0.742912 19.1893 1.17673 19.624L16.8766 35.3239C17.3113 35.7576 17.9002 36.0011 18.5143 36.0011C19.1283 36.0011 19.7172 35.7576 20.1519 35.3239L35.7782 19.6975C36.212 19.2629 36.4557 18.6738 36.4557 18.0597C36.4557 17.4456 36.212 16.8566 35.7782 16.4219Z"
fill="#158FFF" fill="#0000F4"
/> />
</CustomIcon> </CustomIcon>
); );

View File

@ -13,7 +13,7 @@ export const GithubIcon: React.FC<CustomIconProps> = (props) => {
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"
d="M9.9702 0.206024C4.45694 0.206024 0 4.69582 0 10.2503C0 14.6903 2.85571 18.4487 6.81735 19.7789C7.31265 19.8789 7.49408 19.5628 7.49408 19.2968C7.49408 19.064 7.47776 18.2658 7.47776 17.4342C4.70429 18.033 4.12674 16.2368 4.12674 16.2368C3.68102 15.0728 3.02061 14.7736 3.02061 14.7736C2.11286 14.1583 3.08673 14.1583 3.08673 14.1583C4.09367 14.2248 4.62204 15.1893 4.62204 15.1893C5.51327 16.7191 6.94939 16.2868 7.52714 16.0207C7.60959 15.3721 7.87388 14.9232 8.15449 14.6738C5.94245 14.4409 3.6151 13.5762 3.6151 9.71807C3.6151 8.62051 4.01102 7.72256 4.63837 7.02419C4.53939 6.7748 4.19265 5.74358 4.73755 4.36337C4.73755 4.36337 5.57939 4.09725 7.47755 5.39439C8.29022 5.17453 9.12832 5.06268 9.9702 5.06174C10.812 5.06174 11.6702 5.17827 12.4627 5.39439C14.361 4.09725 15.2029 4.36337 15.2029 4.36337C15.7478 5.74358 15.4008 6.7748 15.3018 7.02419C15.9457 7.72256 16.3253 8.62051 16.3253 9.71807C16.3253 13.5762 13.998 14.4242 11.7694 14.6738C12.1327 14.9897 12.4461 15.5883 12.4461 16.5362C12.4461 17.8832 12.4298 18.9642 12.4298 19.2966C12.4298 19.5628 12.6114 19.8789 13.1065 19.7791C17.0682 18.4485 19.9239 14.6903 19.9239 10.2503C19.9402 4.69582 15.4669 0.206024 9.9702 0.206024Z" d="M9.9702 0.206024C4.45694 0.206024 0 4.69582 0 10.2503C0 14.6903 2.85571 18.4487 6.81735 19.7789C7.31265 19.8789 7.49408 19.5628 7.49408 19.2968C7.49408 19.064 7.47776 18.2658 7.47776 17.4342C4.70429 18.033 4.12674 16.2368 4.12674 16.2368C3.68102 15.0728 3.02061 14.7736 3.02061 14.7736C2.11286 14.1583 3.08673 14.1583 3.08673 14.1583C4.09367 14.2248 4.62204 15.1893 4.62204 15.1893C5.51327 16.7191 6.94939 16.2868 7.52714 16.0207C7.60959 15.3721 7.87388 14.9232 8.15449 14.6738C5.94245 14.4409 3.6151 13.5762 3.6151 9.71807C3.6151 8.62051 4.01102 7.72256 4.63837 7.02419C4.53939 6.7748 4.19265 5.74358 4.73755 4.36337C4.73755 4.36337 5.57939 4.09725 7.47755 5.39439C8.29022 5.17453 9.12832 5.06268 9.9702 5.06174C10.812 5.06174 11.6702 5.17827 12.4627 5.39439C14.361 4.09725 15.2029 4.36337 15.2029 4.36337C15.7478 5.74358 15.4008 6.7748 15.3018 7.02419C15.9457 7.72256 16.3253 8.62051 16.3253 9.71807C16.3253 13.5762 13.998 14.4242 11.7694 14.6738C12.1327 14.9897 12.4461 15.5883 12.4461 16.5362C12.4461 17.8832 12.4298 18.9642 12.4298 19.2966C12.4298 19.5628 12.6114 19.8789 13.1065 19.7791C17.0682 18.4485 19.9239 14.6903 19.9239 10.2503C19.9402 4.69582 15.4669 0.206024 9.9702 0.206024Z"
fill="#0B1D2E" fill="#FBFBFB"
/> />
</CustomIcon> </CustomIcon>
); );

View File

@ -0,0 +1,21 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const LaconicIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
{...props}
>
<rect width="48" height="48" rx="4" fill="#29292E" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.0494 24.6233C18.8425 21.8302 20.5713 17.973 20.5706 13.7142C20.5717 13.1361 20.5396 12.5645 20.4762 12L12 12.0008L12.0003 28.2867C11.9996 30.2608 12.7522 32.2356 14.2578 33.7411C15.7633 35.2466 17.7395 36.0001 19.7139 35.9991L19.7134 35.9996L36 36L35.9995 27.5227C35.4362 27.4605 34.8645 27.4285 34.2852 27.4284C30.0275 27.4289 26.1701 29.1577 23.377 31.9507C21.3446 33.9321 18.0858 33.9325 16.0785 31.9252C14.0722 29.9191 14.0715 26.6593 16.0494 24.6233ZM34.2419 13.7624C31.9012 11.4217 28.0982 11.4208 25.7566 13.7624C23.4151 16.1038 23.4159 19.9067 25.7566 22.2473C28.0986 24.5892 31.9004 24.5889 34.2419 22.2473C36.5835 19.9059 36.5839 16.1042 34.2419 13.7624Z"
fill="#FBFBFB"
/>
</CustomIcon>
);
};

View File

@ -1,7 +1,12 @@
import { tv, type VariantProps } from 'tailwind-variants'; import { tv, type VariantProps } from 'tailwind-variants';
export const headingTheme = tv({ export const headingTheme = tv({
base: ['text-elements-high-em', 'font-display', 'font-normal'], base: [
'text-elements-high-em',
'dark:text-foreground',
'font-display',
'font-normal',
],
}); });
export type HeadingVariants = VariantProps<typeof headingTheme>; export type HeadingVariants = VariantProps<typeof headingTheme>;

View File

@ -18,7 +18,7 @@ export const IconWithFrame = ({
'relative justify-center items-center gap-2.5 inline-flex', 'relative justify-center items-center gap-2.5 inline-flex',
'w-16 h-16 rounded-2xl shadow-inner', 'w-16 h-16 rounded-2xl shadow-inner',
'border border-b-[3px] border-border-interactive border-opacity-10', 'border border-b-[3px] border-border-interactive border-opacity-10',
'bg-controls-secondary', 'bg-background',
className, className,
)} )}
{...props} {...props}

View File

@ -14,22 +14,29 @@ export const inputTheme = tv(
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
'disabled:bg-controls-disabled', 'disabled:bg-controls-disabled',
], ],
label: ['text-sm', 'text-elements-high-em'], label: [
'text-sm',
'text-elements-high-em',
'dark:text-foreground-secondary',
],
description: ['text-xs', 'text-elements-low-em'], description: ['text-xs', 'text-elements-low-em'],
input: [ input: [
'focus-ring', 'focus-ring',
'dark:focus:ring-0',
'block', 'block',
'w-full', 'w-full',
'h-full', 'h-full',
'shadow-sm', 'shadow-sm',
'border', 'border',
'rounded-lg', 'rounded-lg',
'dark:bg-overlay2',
'dark:text-foreground',
'text-elements-mid-em', 'text-elements-mid-em',
'border-border-interactive', 'border-border-interactive',
'disabled:shadow-none', 'disabled:shadow-none',
'disabled:border-none', 'disabled:border-none',
], ],
icon: ['text-elements-low-em'], icon: ['text-elements-low-em dark:text-foreground-secondary'],
iconContainer: [ iconContainer: [
'absolute', 'absolute',
'inset-y-0', 'inset-y-0',
@ -39,7 +46,13 @@ export const inputTheme = tv(
'cursor-pointer', 'cursor-pointer',
], ],
helperIcon: [], helperIcon: [],
helperText: ['flex', 'gap-2', 'items-center', 'text-elements-danger'], helperText: [
'flex',
'gap-2',
'items-center',
'text-elements-danger',
'dark:text-primary',
],
}, },
variants: { variants: {
state: { state: {
@ -54,7 +67,7 @@ export const inputTheme = tv(
'shadow-none', 'shadow-none',
'focus:outline-border-danger', 'focus:outline-border-danger',
], ],
helperText: 'text-elements-danger', helperText: 'text-error',
}, },
}, },
size: { size: {

View File

@ -31,6 +31,7 @@ export const modalTheme = tv({
'sm:px-6', 'sm:px-6',
'sm:py-5', 'sm:py-5',
'bg-base-bg-alternate', 'bg-base-bg-alternate',
'dark:bg-overlay2',
], ],
headerTitle: [ headerTitle: [
'text-base', 'text-base',
@ -39,7 +40,11 @@ export const modalTheme = tv({
'sm:tracking-normal', 'sm:tracking-normal',
'text-elements-high-em', 'text-elements-high-em',
], ],
headerDescription: ['text-sm', 'text-elements-low-em'], headerDescription: [
'text-sm',
'text-elements-low-em',
'dark:text-foreground-secondary',
],
footer: ['flex', 'gap-3', 'px-4', 'pb-4', 'pt-7', 'sm:pb-6', 'sm:px-6'], footer: ['flex', 'gap-3', 'px-4', 'pb-4', 'pt-7', 'sm:pb-6', 'sm:px-6'],
content: [ content: [
'h-fit', 'h-fit',
@ -53,8 +58,11 @@ export const modalTheme = tv({
'sm:max-w-[562px]', 'sm:max-w-[562px]',
'rounded-2xl', 'rounded-2xl',
'bg-base-bg', 'bg-base-bg',
'dark:bg-overlay',
'shadow-card', 'shadow-card',
'dark:shadow-background',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground-secondary',
], ],
body: ['flex-1', 'px-4', 'pt-4', 'sm:pt-6', 'sm:px-6'], body: ['flex-1', 'px-4', 'pt-4', 'sm:pt-6', 'sm:px-6'],
}, },

View File

@ -4,7 +4,12 @@ export const radioTheme = tv({
slots: { slots: {
root: ['flex', 'gap-3'], root: ['flex', 'gap-3'],
wrapper: ['flex', 'items-center', 'gap-2', 'group'], wrapper: ['flex', 'items-center', 'gap-2', 'group'],
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'], label: [
'text-sm',
'tracking-[-0.006em]',
'text-elements-high-em',
'dark:text-foreground',
],
radio: [ radio: [
'w-5', 'w-5',
'h-5', 'h-5',
@ -17,6 +22,7 @@ export const radioTheme = tv({
'focus-ring', 'focus-ring',
// Checked // Checked
'data-[state=checked]:bg-controls-primary', 'data-[state=checked]:bg-controls-primary',
'data-[state=checked]:bg-controls-primary',
'data-[state=checked]:group-hover:bg-controls-primary-hovered', 'data-[state=checked]:group-hover:bg-controls-primary-hovered',
], ],
indicator: [ indicator: [
@ -36,6 +42,7 @@ export const radioTheme = tv({
'after:group-focus-visible:bg-controls-disabled', 'after:group-focus-visible:bg-controls-disabled',
// Checked // Checked
'after:data-[state=checked]:bg-elements-on-primary', 'after:data-[state=checked]:bg-elements-on-primary',
'dark:after:data-[state=checked]:bg-primary-hovered',
'after:data-[state=checked]:group-hover:bg-elements-on-primary', 'after:data-[state=checked]:group-hover:bg-elements-on-primary',
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary', 'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
], ],

View File

@ -9,6 +9,7 @@ export const segmentedControlsTheme = tv({
'flex', 'flex',
'items-center', 'items-center',
'bg-base-bg-emphasized', 'bg-base-bg-emphasized',
'dark:bg-background',
'gap-0.5', 'gap-0.5',
'rounded-lg', 'rounded-lg',
], ],
@ -18,6 +19,7 @@ export const segmentedControlsTheme = tv({
'justify-center', 'justify-center',
'gap-2', 'gap-2',
'text-elements-mid-em', 'text-elements-mid-em',
'dark:text-foreground',
'bg-transparent', 'bg-transparent',
'border', 'border',
'border-transparent', 'border-transparent',
@ -26,6 +28,7 @@ export const segmentedControlsTheme = tv({
'rounded-lg', 'rounded-lg',
'focus-ring', 'focus-ring',
'hover:bg-controls-tertiary-hovered', 'hover:bg-controls-tertiary-hovered',
'dark:hover:bg-overlay2',
'focus-visible:z-20', 'focus-visible:z-20',
'focus-visible:bg-controls-tertiary-hovered', 'focus-visible:bg-controls-tertiary-hovered',
'disabled:text-controls-disabled', 'disabled:text-controls-disabled',
@ -33,6 +36,7 @@ export const segmentedControlsTheme = tv({
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
'disabled:border-transparent', 'disabled:border-transparent',
'data-[active=true]:bg-controls-tertiary', 'data-[active=true]:bg-controls-tertiary',
'dark:data-[active=true]:bg-overlay2',
'data-[active=true]:text-elements-high-em', 'data-[active=true]:text-elements-high-em',
'data-[active=true]:border-border-interactive/10', 'data-[active=true]:border-border-interactive/10',
'data-[active=true]:shadow-field', 'data-[active=true]:shadow-field',

View File

@ -3,8 +3,16 @@ import { VariantProps, tv } from 'tailwind-variants';
export const selectTheme = tv({ export const selectTheme = tv({
slots: { slots: {
container: ['flex', 'flex-col', 'relative', 'gap-2', 'w-full'], container: ['flex', 'flex-col', 'relative', 'gap-2', 'w-full'],
label: ['text-sm', 'text-elements-high-em'], label: [
description: ['text-xs', 'text-elements-low-em'], 'text-sm',
'text-elements-high-em',
'dark:text-foreground-secondary',
],
description: [
'text-xs',
'text-elements-low-em',
'dark:text-foreground-secondary',
],
inputWrapper: [ inputWrapper: [
'relative', 'relative',
'flex', 'flex',
@ -14,6 +22,7 @@ export const selectTheme = tv({
'w-full', 'w-full',
'rounded-lg', 'rounded-lg',
'bg-transparent', 'bg-transparent',
'dark:bg-overlay2',
'text-elements-mid-em', 'text-elements-mid-em',
'shadow-sm', 'shadow-sm',
'border', 'border',
@ -22,7 +31,7 @@ export const selectTheme = tv({
'disabled:shadow-none', 'disabled:shadow-none',
'disabled:border-none', 'disabled:border-none',
], ],
input: ['outline-none'], input: ['outline-none', 'dark:bg-overlay2', 'dark:text-foreground'],
iconContainer: [ iconContainer: [
'absolute', 'absolute',
'inset-y-0', 'inset-y-0',
@ -32,9 +41,15 @@ export const selectTheme = tv({
'z-10', 'z-10',
'cursor-pointer', 'cursor-pointer',
], ],
icon: ['text-elements-mid-em'], icon: ['text-elements-mid-em', 'dark:text-foreground-secondary'],
helperIcon: [], helperIcon: [],
helperText: ['flex', 'gap-2', 'items-center', 'text-elements-low-em'], helperText: [
'flex',
'gap-2',
'items-center',
'text-elements-low-em',
'dark:text-foreground-secondary',
],
popover: [ popover: [
'z-20', 'z-20',
'absolute', 'absolute',
@ -44,12 +59,14 @@ export const selectTheme = tv({
'gap-0.5', 'gap-0.5',
'min-w-full', 'min-w-full',
'bg-surface-floating', 'bg-surface-floating',
'dark:bg-overlay2',
'shadow-dropdown', 'shadow-dropdown',
'w-auto', 'w-auto',
'max-h-60', 'max-h-60',
'overflow-auto', 'overflow-auto',
'border', 'border',
'border-gray-200', 'border-gray-200',
'dark:border-overlay',
'rounded-xl', 'rounded-xl',
], ],
}, },
@ -79,7 +96,7 @@ export const selectTheme = tv({
'shadow-none', 'shadow-none',
'focus:outline-border-danger', 'focus:outline-border-danger',
], ],
helperText: ['text-elements-danger'], helperText: ['text-error'],
}, },
}, },
size: { size: {

View File

@ -12,11 +12,12 @@ export const selectItemTheme = tv({
'group', 'group',
'data-[disabled]:cursor-not-allowed', 'data-[disabled]:cursor-not-allowed',
], ],
icon: ['h-4.5', 'w-4.5', 'text-elements-high-em'], icon: ['h-4.5', 'w-4.5', 'text-elements-high-em', 'dark:text-foreground'],
content: ['flex', 'flex-1', 'whitespace-nowrap'], content: ['flex', 'flex-1', 'whitespace-nowrap'],
label: [ label: [
'text-sm', 'text-sm',
'text-elements-high-em', 'text-elements-high-em',
'dark:text-foreground',
'tracking-[-0.006em]', 'tracking-[-0.006em]',
'data-[disabled]:text-elements-disabled', 'data-[disabled]:text-elements-disabled',
], ],
@ -47,7 +48,11 @@ export const selectItemTheme = tv({
}, },
active: { active: {
true: { true: {
wrapper: ['bg-base-bg-emphasized', 'data-[disabled]:bg-transparent'], wrapper: [
'bg-base-bg-emphasized',
'dark:bg-overlay3',
'data-[disabled]:bg-transparent',
],
}, },
}, },
}, },

View File

@ -87,15 +87,15 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
value="" value=""
className="hidden lg:flex" className="hidden lg:flex"
> >
<a className="cursor-pointer" onClick={handleLogOut}> <a className="cursor-pointer font-mono" onClick={handleLogOut}>
Log Out LOG OUT
</a> </a>
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger icon={<QuestionMarkRoundIcon />} value=""> <Tabs.Trigger icon={<QuestionMarkRoundIcon />} value="">
<a className="cursor-pointer">Documentation</a> <a className="cursor-pointer font-mono">DOCUMENTATION</a>
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger icon={<LifeBuoyIcon />} value=""> <Tabs.Trigger icon={<LifeBuoyIcon />} value="">
<a className="cursor-pointer">Support</a> <a className="cursor-pointer font-mono">SUPPORT</a>
</Tabs.Trigger> </Tabs.Trigger>
</Tabs.List> </Tabs.List>
</Tabs> </Tabs>

View File

@ -45,6 +45,7 @@ export const switchTheme = tv({
true: { true: {
switch: [ switch: [
'bg-controls-primary', 'bg-controls-primary',
'dark:bg-primary',
'hover:bg-controls-primary-hovered', 'hover:bg-controls-primary-hovered',
'focus-visible:bg-controls-primary-hovered', 'focus-visible:bg-controls-primary-hovered',
], ],

View File

@ -8,6 +8,7 @@ export const tableTheme = tv({
'border-b', 'border-b',
'border-sky-950/opacity-5', 'border-sky-950/opacity-5',
'text-sky-950', 'text-sky-950',
'dark:text-foreground-secondary',
'text-sm', 'text-sm',
'font-medium', 'font-medium',
'leading-tight', 'leading-tight',
@ -17,6 +18,7 @@ export const tableTheme = tv({
columnHeaderCell: [ columnHeaderCell: [
'p-4', 'p-4',
'text-sky-950', 'text-sky-950',
'dark:text-foreground-secondary',
'text-sm', 'text-sm',
'font-medium', 'font-medium',
'uppercase', 'uppercase',
@ -26,6 +28,7 @@ export const tableTheme = tv({
rowHeaderCell: [ rowHeaderCell: [
'p-4', 'p-4',
'text-slate-600', 'text-slate-600',
'dark:text-foreground',
'text-sm', 'text-sm',
'font-normal', 'font-normal',
'leading-tight', 'leading-tight',
@ -36,6 +39,7 @@ export const tableTheme = tv({
'whitespace-nowrap', 'whitespace-nowrap',
'text-sm', 'text-sm',
'text-slate-600', 'text-slate-600',
'dark:text-foreground',
'font-normal', 'font-normal',
'text-left', 'text-left',
], ],

View File

@ -11,14 +11,17 @@ export const tabsTheme = tv({
'cursor-default', 'cursor-default',
'select-none', 'select-none',
'text-elements-low-em', 'text-elements-low-em',
'dark:text-foreground',
'border-b-2', 'border-b-2',
'border-transparent', 'border-transparent',
'hover:border-border-interactive/10', 'hover:border-border-interactive/10',
'hover:text-elements-mid-em', 'hover:text-elements-mid-em',
'dark:hover:text-foreground-secondary',
'focus-within:border-border-interactive/10', 'focus-within:border-border-interactive/10',
'data-[state=active]:font-medium', 'data-[state=active]:font-medium',
'data-[state=active]:text-elements-high-em', 'data-[state=active]:text-elements-high-em',
'data-[state=active]:border-elements-high-em', 'data-[state=active]:border-elements-high-em',
'data-[state=active]:border-primary',
// Vertical // Vertical
'data-[orientation=vertical]:px-3', 'data-[orientation=vertical]:px-3',
'data-[orientation=vertical]:py-3', 'data-[orientation=vertical]:py-3',
@ -27,6 +30,7 @@ export const tabsTheme = tv({
'data-[orientation=vertical]:rounded-xl', 'data-[orientation=vertical]:rounded-xl',
'data-[orientation=vertical]:border-transparent', 'data-[orientation=vertical]:border-transparent',
'data-[orientation=vertical]:hover:bg-base-bg-emphasized', 'data-[orientation=vertical]:hover:bg-base-bg-emphasized',
'data-[orientation=vertical]:dark:hover:bg-overlay2',
'data-[orientation=vertical]:hover:text-elements-mid-em', 'data-[orientation=vertical]:hover:text-elements-mid-em',
'data-[orientation=vertical]:hover:border-transparent', 'data-[orientation=vertical]:hover:border-transparent',
'data-[orientation=vertical]:focus-visible:border-transparent', 'data-[orientation=vertical]:focus-visible:border-transparent',
@ -34,13 +38,17 @@ export const tabsTheme = tv({
'data-[orientation=vertical]:focus-visible:text-elements-mid-em', 'data-[orientation=vertical]:focus-visible:text-elements-mid-em',
'data-[orientation=vertical]:data-[state=active]:font-normal', 'data-[orientation=vertical]:data-[state=active]:font-normal',
'data-[orientation=vertical]:data-[state=active]:bg-base-bg-emphasized', 'data-[orientation=vertical]:data-[state=active]:bg-base-bg-emphasized',
'data-[orientation=vertical]:data-[state=active]:dark:bg-overlay',
'data-[orientation=vertical]:data-[state=active]:border-transparent', '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]:hover:text-elements-high-em',
'data-[orientation=vertical]:data-[state=active]:focus-visible:text-elements-high-em', 'data-[orientation=vertical]:data-[state=active]:focus-visible:text-elements-high-em',
// TODO: demo additions // TODO: demo additions
'data-[orientation=vertical]:data-[state=active]:bg-snowball-200', 'data-[orientation=vertical]:data-[state=active]:bg-snowball-200',
'data-[orientation=vertical]:data-[state=active]:dark:bg-overlay',
'data-[orientation=vertical]:data-[state=active]:hover:bg-snowball-200', 'data-[orientation=vertical]:data-[state=active]:hover:bg-snowball-200',
'data-[orientation=vertical]:data-[state=active]:dark:hover:bg-overlay2',
'data-[orientation=vertical]:data-[state=active]:text-snowball-800', 'data-[orientation=vertical]:data-[state=active]:text-snowball-800',
'data-[orientation=vertical]:data-[state=active]:dark:text-foreground',
'data-[orientation=vertical]:data-[state=active]:hover:text-snowball-800', 'data-[orientation=vertical]:data-[state=active]:hover:text-snowball-800',
'data-[orientation=vertical]:data-[state=active]:shadow-[0px_1px_0px_0px_rgba(8,47,86,0.06)_inset]', 'data-[orientation=vertical]:data-[state=active]:shadow-[0px_1px_0px_0px_rgba(8,47,86,0.06)_inset]',
], ],

View File

@ -6,8 +6,10 @@ export const tooltipTheme = tv({
'z-tooltip', 'z-tooltip',
'rounded-md', 'rounded-md',
'bg-surface-high-contrast', 'bg-surface-high-contrast',
'dark:bg-overlay3',
'p-2', 'p-2',
'text-elements-on-high-contrast', 'text-elements-on-high-contrast',
'dark:text-foreground-secondary',
], ],
arrow: ['fill-surface-high-contrast'], arrow: ['fill-surface-high-contrast'],
}, },

View File

@ -0,0 +1,210 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import SignClient from '@walletconnect/sign-client';
import { getSdkError } from '@walletconnect/utils';
import { SessionTypes } from '@walletconnect/types';
import { walletConnectModal } from '../utils/web3modal';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_CONNECT_ID,
} from 'utils/constants';
interface ClientInterface {
signClient: SignClient | undefined;
session: SessionTypes.Struct | undefined;
loadingSession: boolean;
onConnect: () => Promise<void>;
onDisconnect: () => Promise<void>;
onSessionDelete: () => void;
accounts: { address: string }[] | undefined;
}
const ClientContext = createContext({} as ClientInterface);
export const useWalletConnectClient = () => {
return useContext(ClientContext);
};
export const WalletConnectClientProvider = ({
children,
}: {
children: JSX.Element;
}) => {
const [signClient, setSignClient] = useState<SignClient>();
const [session, setSession] = useState<SessionTypes.Struct>();
const [loadingSession, setLoadingSession] = useState(true);
const [accounts, setAccounts] = useState<{ address: string }[]>();
const isSignClientInitializing = useRef<boolean>(false);
const onSessionConnect = useCallback(async (session: SessionTypes.Struct) => {
setSession(session);
}, []);
const subscribeToEvents = useCallback(
async (client: SignClient) => {
client.on('session_update', ({ topic, params }) => {
const { namespaces } = params;
const currentSession = client.session.get(topic);
const updatedSession = { ...currentSession, namespaces };
setSession(updatedSession);
});
},
[setSession],
);
const onConnect = async () => {
const proposalNamespace = {
cosmos: {
methods: ['cosmos_sendTokens'],
chains: [`cosmos:${VITE_LACONICD_CHAIN_ID}`],
events: [],
},
};
try {
const { uri, approval } = await signClient!.connect({
requiredNamespaces: proposalNamespace,
});
if (uri) {
walletConnectModal.openModal({ uri });
const session = await approval();
onSessionConnect(session);
walletConnectModal.closeModal();
}
} catch (e) {
console.error(e);
}
};
const onDisconnect = useCallback(async () => {
if (typeof signClient === 'undefined') {
throw new Error('WalletConnect is not initialized');
}
if (typeof session === 'undefined') {
throw new Error('Session is not connected');
}
await signClient.disconnect({
topic: session.topic,
reason: getSdkError('USER_DISCONNECTED'),
});
onSessionDelete();
}, [signClient, session]);
const onSessionDelete = () => {
setAccounts(undefined);
setSession(undefined);
};
const checkPersistedState = useCallback(
async (signClient: SignClient) => {
if (typeof signClient === 'undefined') {
throw new Error('WalletConnect is not initialized');
}
if (typeof session !== 'undefined') return;
if (signClient.session.length) {
const lastKeyIndex = signClient.session.keys.length - 1;
const previousSsession = signClient.session.get(
signClient.session.keys[lastKeyIndex],
);
await onSessionConnect(previousSsession);
return previousSsession;
}
},
[session, onSessionConnect],
);
const createClient = useCallback(async () => {
isSignClientInitializing.current = true;
try {
const signClient = await SignClient.init({
projectId: VITE_WALLET_CONNECT_ID,
metadata: {
name: 'Deploy App',
description: '',
url: window.location.href,
icons: ['https://avatars.githubusercontent.com/u/92608123'],
},
});
setSignClient(signClient);
await checkPersistedState(signClient);
await subscribeToEvents(signClient);
setLoadingSession(false);
} catch (e) {
console.error('error in createClient', e);
}
isSignClientInitializing.current = false;
}, [setSignClient, checkPersistedState, subscribeToEvents]);
useEffect(() => {
if (!signClient && !isSignClientInitializing.current) {
createClient();
}
}, [signClient, createClient]);
useEffect(() => {
const populateAccounts = async () => {
if (!session) {
return;
}
if (!session.namespaces['cosmos']) {
console.log('Accounts for cosmos namespace not found');
return;
}
const cosmosAddresses = session.namespaces['cosmos'].accounts;
const cosmosAccounts = cosmosAddresses.map((address) => ({
address,
}));
const allAccounts = cosmosAccounts;
setAccounts(allAccounts);
};
populateAccounts();
}, [session]);
useEffect(() => {
if (!signClient) {
return;
}
signClient.on('session_delete', onSessionDelete);
return () => {
signClient.off('session_delete', onSessionDelete);
};
});
return (
<ClientContext.Provider
value={{
signClient,
onConnect,
onDisconnect,
onSessionDelete,
loadingSession,
session,
accounts,
}}
>
{children}
</ClientContext.Provider>
);
};

View File

@ -2,7 +2,7 @@ import { ReactNode } from 'react';
import assert from 'assert'; import assert from 'assert';
import { SiweMessage, generateNonce } from 'siwe'; import { SiweMessage, generateNonce } from 'siwe';
import { WagmiProvider } from 'wagmi'; import { WagmiProvider } from 'wagmi';
import { arbitrum, mainnet } from 'wagmi/chains'; import { mainnet } from 'wagmi/chains';
import axios from 'axios'; import axios from 'axios';
import { createWeb3Modal } from '@web3modal/wagmi/react'; import { createWeb3Modal } from '@web3modal/wagmi/react';
@ -31,12 +31,12 @@ const axiosInstance = axios.create({
withCredentials: true, withCredentials: true,
}); });
const metadata = { const metadata = {
name: 'Web3Modal', name: 'Deploy App Auth',
description: 'Snowball Web3Modal', description: '',
url: window.location.origin, url: window.location.origin,
icons: ['https://avatars.githubusercontent.com/u/37784886'], icons: ['https://avatars.githubusercontent.com/u/37784886'],
}; };
const chains = [mainnet, arbitrum] as const; const chains = [mainnet] as const;
const config = defaultWagmiConfig({ const config = defaultWagmiConfig({
chains, chains,
projectId: VITE_WALLET_CONNECT_ID, projectId: VITE_WALLET_CONNECT_ID,

View File

@ -144,6 +144,7 @@
@layer utilities { @layer utilities {
.focus-ring { .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; @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;
@apply focus-visible:ring-[3px] dark:focus-visible:ring-primary focus-visible:ring-offset-1 dark:focus-visible:ring-offset-primary focus-visible:outline-none;
} }
@keyframes dialog-overlay-show { @keyframes dialog-overlay-show {

View File

@ -16,6 +16,7 @@ import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error'; import { LogErrorBoundary } from 'utils/log-error';
import { BASE_URL } from 'utils/constants'; import { BASE_URL } from 'utils/constants';
import Web3ModalProvider from './context/Web3Provider'; import Web3ModalProvider from './context/Web3Provider';
import { WalletConnectClientProvider } from 'context/WalletConnectContext';
console.log(`v-0.0.9`); console.log(`v-0.0.9`);
@ -31,14 +32,16 @@ const gqlClient = new GQLClient({ gqlEndpoint });
root.render( root.render(
<LogErrorBoundary> <LogErrorBoundary>
<React.StrictMode> <React.StrictMode>
<ThemeProvider> <WalletConnectClientProvider>
<Web3ModalProvider> <ThemeProvider>
<GQLClientProvider client={gqlClient}> <Web3ModalProvider>
<App /> <GQLClientProvider client={gqlClient}>
<Toaster /> <App />
</GQLClientProvider> <Toaster />
</Web3ModalProvider> </GQLClientProvider>
</ThemeProvider> </Web3ModalProvider>
</ThemeProvider>
</WalletConnectClientProvider>
</React.StrictMode> </React.StrictMode>
</LogErrorBoundary>, </LogErrorBoundary>,
); );

View File

@ -32,7 +32,7 @@ const ProjectSearch = () => {
return ( return (
<section className="h-full flex flex-col"> <section className="h-full flex flex-col">
{/* Header */} {/* Header */}
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] hover:z-30"> <div className="sticky hidden lg:block top-0 border-b dark:bg-overlay bg-base-bg border-border-separator/[0.06] hover:z-30">
<div className="flex pr-6 pl-2 py-2 items-center"> <div className="flex pr-6 pl-2 py-2 items-center">
<div className="flex-1"> <div className="flex-1">
<ProjectSearchBar <ProjectSearchBar
@ -69,7 +69,7 @@ const ProjectSearch = () => {
</div> </div>
{/* Content */} {/* Content */}
<section className="h-full z-0 overflow-y-auto"> <section className="h-full z-0 overflow-y-auto dark:bg-overlay">
<Outlet /> <Outlet />
</section> </section>
</section> </section>

View File

@ -1 +0,0 @@
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,9 +1,8 @@
import { CloudyFlow } from 'components/CloudyFlow';
import { Login } from './auth/Login'; import { Login } from './auth/Login';
const AuthPage = () => { const AuthPage = () => {
return ( return (
<CloudyFlow className="flex flex-col min-h-screen"> <div className="dark:bg-background flex flex-col min-h-screen">
<div className="py-8 relative z-10"> <div className="py-8 relative z-10">
<div className="flex justify-center items-center gap-2.5"> <div className="flex justify-center items-center gap-2.5">
<img <img
@ -11,17 +10,14 @@ const AuthPage = () => {
alt="snowball logo" alt="snowball logo"
className="h-10 rounded-xl" className="h-10 rounded-xl"
/> />
<div className="text-sky-950 text-2xl font-semibold font-display leading-loose">
Snowball
</div>
</div> </div>
</div> </div>
<div className="pb-12 relative z-10 flex-1 flex-center"> <div className="pb-12 relative z-10 flex-1 flex-center">
<div className="max-w-[520px] w-full bg-white rounded-xl shadow"> <div className="max-w-[520px] w-full dark:bg-overlay bg-white rounded-xl shadow">
<Login /> <Login />
</div> </div>
</div> </div>
</CloudyFlow> </div>
); );
}; };

View File

@ -1,83 +0,0 @@
import { Button } from 'components/shared/Button';
import { LoaderIcon } from 'components/shared/CustomIcon';
import { KeyIcon } from 'components/shared/CustomIcon/KeyIcon';
import { InlineNotification } from 'components/shared/InlineNotification';
import { Input } from 'components/shared/Input';
import { WavyBorder } from 'components/shared/WavyBorder';
import { useState } from 'react';
import { IconRight } from 'react-day-picker';
import { useSnowball } from 'utils/use-snowball';
type Props = {
onDone: () => void;
};
export const CreatePasskey = ({}: Props) => {
const snowball = useSnowball();
const [name, setName] = useState('');
const auth = snowball.auth.passkey;
const loading = !!auth.state.loading;
async function createPasskey() {
await auth.register(name);
}
return (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
<div className="w-16 h-16 p-2 bg-sky-100 rounded-[800px] justify-center items-center gap-2 inline-flex">
<KeyIcon />
</div>
<div>
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-loose">
Create a passkey
</div>
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
Passkeys allow you to sign in securely without using passwords.
</div>
</div>
</div>
<WavyBorder className="self-stretch" variant="stroke" />
<div className="p-6 flex-col justify-center items-center gap-8 inline-flex">
<div className="self-stretch h-36 flex-col justify-center items-center gap-2 flex">
<div className="self-stretch h-[72px] flex-col justify-start items-start gap-2 flex">
<div className="self-stretch h-5 px-1 flex-col justify-start items-start gap-1 flex">
<div className="self-stretch text-sky-950 text-sm font-normal font-['Inter'] leading-tight">
Give it a name
</div>
</div>
<Input
value={name}
onInput={(e: any) => {
setName(e.target.value);
}}
/>
</div>
{auth.state.error ? (
<InlineNotification
title={auth.state.error.message}
variant="danger"
/>
) : (
<InlineNotification
title={`Once you press the "Create passkeys" button, you'll receive a prompt to create the passkey.`}
variant="info"
/>
)}
</div>
<Button
rightIcon={
loading ? <LoaderIcon className="animate-spin" /> : <IconRight />
}
className="self-stretch"
disabled={!name || loading}
onClick={createPasskey}
>
Create Passkey
</Button>
</div>
</div>
);
};

View File

@ -10,16 +10,16 @@ type Props = {
}; };
export const Done = ({ continueTo }: Props) => { export const Done = ({ continueTo }: Props) => {
return ( return (
<div> <div className="dark: bg-background">
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex"> <div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
<div className="w-16 h-16 p-2 bg-sky-100 rounded-[800px] justify-center items-center gap-2 inline-flex"> <div className="w-16 h-16 p-2 dark:bg-overlay bg-sky-100 rounded-[800px] justify-center items-center gap-2 inline-flex">
<CheckRoundFilledIcon /> <CheckRoundFilledIcon />
</div> </div>
<div> <div>
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-loose"> <div className="self-stretch text-center dark:text-foreground text-sky-950 text-2xl font-medium font-display leading-loose">
You&apos;re in! You&apos;re in!
</div> </div>
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight"> <div className="text-center dark:text-foreground-secondary text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
It's time to get your project rolling 😎 It's time to get your project rolling 😎
</div> </div>
</div> </div>
@ -33,7 +33,7 @@ export const Done = ({ continueTo }: Props) => {
href={continueTo} href={continueTo}
variant={'primary'} variant={'primary'}
> >
Enter Snowball Enter Laconic
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -4,8 +4,8 @@ export const Login = () => {
return ( return (
<div> <div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex"> <div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight"> <div className="self-stretch text-center text-foreground text-2xl font-medium font-display leading-tight">
Sign in to Snowball Sign in to Laconic
</div> </div>
</div> </div>
<WavyBorder className="self-stretch" variant="stroke" /> <WavyBorder className="self-stretch" variant="stroke" />

View File

@ -40,6 +40,8 @@ const deployment: Deployment = {
deployerApiUrl: 'https://webapp-deployer-api.example.com', deployerApiUrl: 'https://webapp-deployer-api.example.com',
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu', deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
deployerLrn: 'lrn://example/deployers/webapp-deployer-api.example.com', deployerLrn: 'lrn://example/deployers/webapp-deployer-api.example.com',
minimumPayment: '1000alnt',
baseDomain: 'pwa.example.com',
}, },
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
createdBy: { createdBy: {

View File

@ -6,11 +6,12 @@ import { Heading, Badge, Button } from 'components/shared';
import { PlusIcon } from 'components/shared/CustomIcon'; import { PlusIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
import { project as mockProject } from 'pages/index';
const Projects = () => { const Projects = () => {
const client = useGQLClient(); const client = useGQLClient();
const { orgSlug } = useParams(); const { orgSlug } = useParams();
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([mockProject]);
const fetchProjects = useCallback(async () => { const fetchProjects = useCallback(async () => {
const { projectsInOrganization } = await client.getProjectsInOrganization( const { projectsInOrganization } = await client.getProjectsInOrganization(
@ -30,7 +31,7 @@ const Projects = () => {
<div className="flex items-center"> <div className="flex items-center">
<div className="grow"> <div className="grow">
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<Heading as="h2" className="text-[24px]"> <Heading as="h2" className="text-[24px] dark:text-foreground">
Projects Projects
</Heading> </Heading>
<Badge className="bg-base-bg-alternate text-elements-mid-em h-7 w-7"> <Badge className="bg-base-bg-alternate text-elements-mid-em h-7 w-7">
@ -39,7 +40,7 @@ const Projects = () => {
</div> </div>
</div> </div>
<Link to="projects/create"> <Link to="projects/create">
<Button leftIcon={<PlusIcon />}>Create project</Button> <Button leftIcon={<PlusIcon />}>CREATE PROJECT</Button>
</Link> </Link>
</div> </div>
{/* List of projects */} {/* List of projects */}

View File

@ -39,7 +39,7 @@ export const DashboardLayout = ({
<section <section
{...props} {...props}
className={cn( className={cn(
'flex flex-col lg:flex-row h-screen bg-snowball-50', 'flex flex-col lg:flex-row h-screen bg-background',
className, className,
)} )}
> >
@ -112,7 +112,7 @@ export const DashboardLayout = ({
}} }}
transition={{ ease: 'easeInOut', duration: 0.3 }} transition={{ ease: 'easeInOut', duration: 0.3 }}
> >
<div className="rounded-t-3xl lg:rounded-3xl bg-base-bg h-full shadow-card overflow-y-auto relative"> <div className="rounded-t-3xl lg:rounded-3xl dark:bg-background bg-base-bg h-full shadow-card dark:shadow-background overflow-y-auto relative">
<OctokitProvider> <OctokitProvider>
<Outlet /> <Outlet />
</OctokitProvider> </OctokitProvider>

View File

@ -78,7 +78,7 @@ const Id = () => {
leftIcon={<ChevronLeft />} leftIcon={<ChevronLeft />}
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
/> />
<Heading className="text-2xl font-medium truncate"> <Heading className="text-2xl font-medium truncate dark:text-foreground">
{project?.name} {project?.name}
</Heading> </Heading>
</div> </div>
@ -88,13 +88,24 @@ const Id = () => {
{...buttonSize} {...buttonSize}
className="h-11 transition-colors" className="h-11 transition-colors"
variant="tertiary" variant="tertiary"
shape="default"
> >
Open repo OPEN REPO
</Button> </Button>
</Link> </Link>
<Button {...buttonSize} className="h-11 transition-colors"> {project.deployments.length > 0 && (
Go to app <Link
</Button> to={`https://${project.name.toLowerCase()}.${project.deployments[0].deployer.baseDomain}`}
>
<Button
{...buttonSize}
className="h-11 transition-colors"
shape="default"
>
GO TO APP
</Button>
</Link>
)}
</div> </div>
</div> </div>
<WavyBorder /> <WavyBorder />

View File

@ -20,7 +20,10 @@ export const CreateProjectLayout = ({
const closeBtnLink = `/${orgSlug}`; const closeBtnLink = `/${orgSlug}`;
const heading = ( const heading = (
<Heading as="h2" className="flex-1 text-xl md:text-2xl font-medium"> <Heading
as="h2"
className="flex-1 text-xl md:text-2xl font-medium dark:text-foreground"
>
Create new project Create new project
</Heading> </Heading>
); );
@ -30,17 +33,20 @@ export const CreateProjectLayout = ({
{/* Desktop */} {/* Desktop */}
<section <section
{...props} {...props}
className={cn('h-full flex-col hidden md:flex', className)} className={cn(
'dark:bg-background h-full flex-col hidden md:flex',
className,
)}
> >
<div className="sticky top-0"> <div className="sticky top-0">
<div className="flex px-6 py-4 bg-base-bg items-center gap-4"> <div className="flex px-6 py-4 dark:bg-overlay bg-base-bg items-center gap-4">
{heading} {heading}
{/* Cannot save btn as variable since responsive variant don't work with compoundVariant */} {/* Cannot save btn as variable since responsive variant don't work with compoundVariant */}
<Link to={closeBtnLink}> <Link to={closeBtnLink}>
<Button <Button
iconOnly iconOnly
variant="tertiary" variant="primary"
leftIcon={<CrossIcon />} leftIcon={<CrossIcon />}
aria-label="close" aria-label="close"
/> />

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