feat: form field validations (#190)

* feat: add form field validation

* chore: add form on repo configuration

* wip: setting default branch

* chore: set default branch

* chore: form field validation workig with some fix needed

* chore: fix change first github step

* feat: set default branch

* feat: validation for textarea. fix styles on select repository

* chore: PR comments changes

* chore: remove constant

* chore: added comments

* chore: change combobox input props

* chore: remove ens validation since we dont allow custom ens

* chore: remove isEns

* refactor: fetch ens list from ens graph
This commit is contained in:
Camila Sosa Morales 2023-03-29 18:13:22 -03:00 committed by GitHub
parent 9df1791c72
commit ac618f9a32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1478 additions and 910 deletions

View File

@ -1,26 +1,23 @@
import { import {
Address, Address,
Bytes, Bytes,
log, log,
store, store,
ethereum, ethereum,
BigInt, BigInt,
} from '@graphprotocol/graph-ts'; } from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config] // Event Imports [based on the yaml config]
import { import {
ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent, ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent,
ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent, ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent,
NewAccessPoint as NewAccessPointEvent, NewAccessPoint as NewAccessPointEvent,
ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent, ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent,
ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent, ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent,
} from '../generated/FleekNFA/FleekNFA'; } from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema] // Entity Imports [based on the schema]
import { import { AccessPoint, Owner } from '../generated/schema';
AccessPoint,
Owner,
} from '../generated/schema';
/** /**
* This handler will create and load entities in the following order: * This handler will create and load entities in the following order:
@ -29,133 +26,133 @@ import {
* Note to discuss later: Should a `NewAccessPoint` entity be also created and defined? * Note to discuss later: Should a `NewAccessPoint` entity be also created and defined?
*/ */
export function handleNewAccessPoint(event: NewAccessPointEvent): void { export function handleNewAccessPoint(event: NewAccessPointEvent): void {
// Create an AccessPoint entity // Create an AccessPoint entity
let accessPointEntity = new AccessPoint(event.params.apName); let accessPointEntity = new AccessPoint(event.params.apName);
accessPointEntity.score = BigInt.fromU32(0); accessPointEntity.score = BigInt.fromU32(0);
accessPointEntity.contentVerified = false; accessPointEntity.contentVerified = false;
accessPointEntity.nameVerified = false; accessPointEntity.nameVerified = false;
accessPointEntity.creationStatus = 'DRAFT'; // Since a `ChangeAccessPointCreationStatus` event is emitted instantly after `NewAccessPoint`, the status will be updated in that handler. accessPointEntity.creationStatus = 'DRAFT'; // Since a `ChangeAccessPointCreationStatus` event is emitted instantly after `NewAccessPoint`, the status will be updated in that handler.
accessPointEntity.owner = event.params.owner; accessPointEntity.owner = event.params.owner;
accessPointEntity.token = Bytes.fromByteArray( accessPointEntity.token = Bytes.fromByteArray(
Bytes.fromBigInt(event.params.tokenId) Bytes.fromBigInt(event.params.tokenId)
); );
// Load / Create an Owner entity // Load / Create an Owner entity
let ownerEntity = Owner.load(event.params.owner); let ownerEntity = Owner.load(event.params.owner);
if (!ownerEntity) { if (!ownerEntity) {
// Create a new owner entity // Create a new owner entity
ownerEntity = new Owner(event.params.owner); ownerEntity = new Owner(event.params.owner);
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false. // Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
ownerEntity.collection = false; ownerEntity.collection = false;
} }
// Save entities. // Save entities.
accessPointEntity.save(); accessPointEntity.save();
ownerEntity.save(); ownerEntity.save();
} }
/** /**
* This handler will update the status of an access point entity. * This handler will update the status of an access point entity.
*/ */
export function handleChangeAccessPointCreationStatus( export function handleChangeAccessPointCreationStatus(
event: ChangeAccessPointCreationStatusEvent event: ChangeAccessPointCreationStatusEvent
): void { ): void {
// Load the AccessPoint entity // Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName); let accessPointEntity = AccessPoint.load(event.params.apName);
let status = event.params.status; let status = event.params.status;
if (accessPointEntity) { if (accessPointEntity) {
switch (status) { switch (status) {
case 0: case 0:
accessPointEntity.creationStatus = 'DRAFT'; accessPointEntity.creationStatus = 'DRAFT';
break; break;
case 1: case 1:
accessPointEntity.creationStatus = 'APPROVED'; accessPointEntity.creationStatus = 'APPROVED';
break; break;
case 2: case 2:
accessPointEntity.creationStatus = 'REJECTED'; accessPointEntity.creationStatus = 'REJECTED';
break; break;
case 3: case 3:
accessPointEntity.creationStatus = 'REMOVED'; accessPointEntity.creationStatus = 'REMOVED';
break; break;
default: default:
// Unknown status // Unknown status
log.error( log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}', 'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName] [status.toString(), event.params.apName]
); );
} }
accessPointEntity.save(); accessPointEntity.save();
} else { } else {
// Unknown access point // Unknown access point
log.error( log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}', 'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName] [status.toString(), event.params.apName]
); );
} }
} }
/** /**
* This handler will update the score of an access point entity. * This handler will update the score of an access point entity.
*/ */
export function handleChangeAccessPointScore( export function handleChangeAccessPointScore(
event: ChangeAccessPointCreationScoreEvent event: ChangeAccessPointCreationScoreEvent
): void { ): void {
// Load the AccessPoint entity // Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName); let accessPointEntity = AccessPoint.load(event.params.apName);
if (accessPointEntity) { if (accessPointEntity) {
accessPointEntity.score = event.params.score; accessPointEntity.score = event.params.score;
accessPointEntity.save(); accessPointEntity.save();
} else { } else {
// Unknown access point // Unknown access point
log.error( log.error(
'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}', 'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}',
[event.params.score.toString(), event.params.apName] [event.params.score.toString(), event.params.apName]
); );
} }
} }
/** /**
* This handler will update the nameVerified field of an access point entity. * This handler will update the nameVerified field of an access point entity.
*/ */
export function handleChangeAccessPointNameVerify( export function handleChangeAccessPointNameVerify(
event: ChangeAccessPointNameVerifyEvent event: ChangeAccessPointNameVerifyEvent
): void { ): void {
// Load the AccessPoint entity // Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName); let accessPointEntity = AccessPoint.load(event.params.apName);
if (accessPointEntity) { if (accessPointEntity) {
accessPointEntity.nameVerified = event.params.verified; accessPointEntity.nameVerified = event.params.verified;
accessPointEntity.save(); accessPointEntity.save();
} else { } else {
// Unknown access point // Unknown access point
log.error( log.error(
'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}', 'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[event.params.verified.toString(), event.params.apName] [event.params.verified.toString(), event.params.apName]
); );
} }
} }
/** /**
* This handler will update the contentVerified field of an access point entity. * This handler will update the contentVerified field of an access point entity.
*/ */
export function handleChangeAccessPointContentVerify( export function handleChangeAccessPointContentVerify(
event: ChangeAccessPointContentVerifyEvent event: ChangeAccessPointContentVerifyEvent
): void { ): void {
// Load the AccessPoint entity // Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName); let accessPointEntity = AccessPoint.load(event.params.apName);
if (accessPointEntity) { if (accessPointEntity) {
accessPointEntity.contentVerified = event.params.verified; accessPointEntity.contentVerified = event.params.verified;
accessPointEntity.save(); accessPointEntity.save();
} else { } else {
// Unknown access point // Unknown access point
log.error( log.error(
'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}', 'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[event.params.verified.toString(), event.params.apName] [event.params.verified.toString(), event.params.apName]
); );
} }
} }

View File

@ -2,163 +2,163 @@ import { Bytes } from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config] // Event Imports [based on the yaml config]
import { import {
MetadataUpdate as MetadataUpdateEvent, MetadataUpdate as MetadataUpdateEvent,
MetadataUpdate1 as MetadataUpdateEvent1, MetadataUpdate1 as MetadataUpdateEvent1,
MetadataUpdate2 as MetadataUpdateEvent2, MetadataUpdate2 as MetadataUpdateEvent2,
MetadataUpdate3 as MetadataUpdateEvent3, MetadataUpdate3 as MetadataUpdateEvent3,
MetadataUpdate4 as MetadataUpdateEvent4, MetadataUpdate4 as MetadataUpdateEvent4,
} from '../generated/FleekNFA/FleekNFA'; } from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema] // Entity Imports [based on the schema]
import { import {
GitRepository as GitRepositoryEntity, GitRepository as GitRepositoryEntity,
MetadataUpdate, MetadataUpdate,
Token, Token,
} from '../generated/schema'; } from '../generated/schema';
export function handleMetadataUpdateWithStringValue( export function handleMetadataUpdateWithStringValue(
event: MetadataUpdateEvent1 event: MetadataUpdateEvent1
): void { ): void {
/** /**
* Metadata handled here: * Metadata handled here:
* setTokenExternalURL * setTokenExternalURL
* setTokenENS * setTokenENS
* setTokenName * setTokenName
* setTokenDescription * setTokenDescription
* setTokenLogo * setTokenLogo
* */ * */
let entity = new MetadataUpdate( let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32()) event.transaction.hash.concatI32(event.logIndex.toI32())
); );
entity.tokenId = event.params._tokenId; entity.tokenId = event.params._tokenId;
entity.key = event.params.key; entity.key = event.params.key;
entity.stringValue = event.params.value; entity.stringValue = event.params.value;
entity.blockNumber = event.block.number; entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp; entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash; entity.transactionHash = event.transaction.hash;
entity.save(); entity.save();
// UPDATE TOKEN // UPDATE TOKEN
let token = Token.load( let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
); );
if (token) { if (token) {
if (event.params.key == 'externalURL') { if (event.params.key == 'externalURL') {
token.externalURL = event.params.value; token.externalURL = event.params.value;
} else if (event.params.key == 'ENS') { } else if (event.params.key == 'ENS') {
token.ENS = event.params.value; token.ENS = event.params.value;
} else if (event.params.key == 'name') { } else if (event.params.key == 'name') {
token.name = event.params.value; token.name = event.params.value;
} else if (event.params.key == 'description') { } else if (event.params.key == 'description') {
token.description = event.params.value; token.description = event.params.value;
} else { } else {
// logo // logo
token.logo = event.params.value; token.logo = event.params.value;
}
token.save();
} }
token.save();
}
} }
export function handleMetadataUpdateWithDoubleStringValue( export function handleMetadataUpdateWithDoubleStringValue(
event: MetadataUpdateEvent3 event: MetadataUpdateEvent3
): void { ): void {
/** /**
* setTokenBuild * setTokenBuild
*/ */
let entity = new MetadataUpdate( let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32()) event.transaction.hash.concatI32(event.logIndex.toI32())
); );
entity.key = event.params.key; entity.key = event.params.key;
entity.tokenId = event.params._tokenId; entity.tokenId = event.params._tokenId;
entity.doubleStringValue = event.params.value; entity.doubleStringValue = event.params.value;
entity.blockNumber = event.block.number; entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp; entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash; entity.transactionHash = event.transaction.hash;
entity.save(); entity.save();
// UPDATE TOKEN // UPDATE TOKEN
let token = Token.load( let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
); );
if (token) { if (token) {
if (event.params.key == 'build') { if (event.params.key == 'build') {
let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]); let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]);
if (!gitRepositoryEntity) { if (!gitRepositoryEntity) {
// Create a new gitRepository entity // Create a new gitRepository entity
gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]); gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]);
} }
token.commitHash = event.params.value[0]; token.commitHash = event.params.value[0];
token.gitRepository = event.params.value[1]; token.gitRepository = event.params.value[1];
token.save(); token.save();
gitRepositoryEntity.save(); gitRepositoryEntity.save();
}
} }
}
} }
export function handleMetadataUpdateWithIntValue( export function handleMetadataUpdateWithIntValue(
event: MetadataUpdateEvent2 event: MetadataUpdateEvent2
): void { ): void {
/** /**
* setTokenColor * setTokenColor
*/ */
let entity = new MetadataUpdate( let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32()) event.transaction.hash.concatI32(event.logIndex.toI32())
); );
entity.key = event.params.key; entity.key = event.params.key;
entity.tokenId = event.params._tokenId; entity.tokenId = event.params._tokenId;
entity.uint24Value = event.params.value; entity.uint24Value = event.params.value;
entity.blockNumber = event.block.number; entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp; entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash; entity.transactionHash = event.transaction.hash;
entity.save(); entity.save();
let token = Token.load( let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
); );
if (token) { if (token) {
if (event.params.key == 'color') { if (event.params.key == 'color') {
token.color = event.params.value; token.color = event.params.value;
}
token.save();
} }
token.save();
}
} }
export function handleMetadataUpdateWithBooleanValue( export function handleMetadataUpdateWithBooleanValue(
event: MetadataUpdateEvent4 event: MetadataUpdateEvent4
): void { ): void {
/** /**
* accessPointAutoApproval * accessPointAutoApproval
*/ */
let entity = new MetadataUpdate( let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32()) event.transaction.hash.concatI32(event.logIndex.toI32())
); );
entity.key = event.params.key; entity.key = event.params.key;
entity.tokenId = event.params._tokenId; entity.tokenId = event.params._tokenId;
entity.booleanValue = event.params.value; entity.booleanValue = event.params.value;
entity.blockNumber = event.block.number; entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp; entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash; entity.transactionHash = event.transaction.hash;
entity.save(); entity.save();
let token = Token.load( let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId)) Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
); );
if (token) { if (token) {
if (event.params.key == 'accessPointAutoApproval') { if (event.params.key == 'accessPointAutoApproval') {
token.accessPointAutoApproval = event.params.value; token.accessPointAutoApproval = event.params.value;
}
token.save();
} }
token.save();
}
} }

View File

@ -1,90 +1,85 @@
import { import { Bytes, log } from '@graphprotocol/graph-ts';
Bytes,
log,
} from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config] // Event Imports [based on the yaml config]
import { import { NewMint as NewMintEvent } from '../generated/FleekNFA/FleekNFA';
NewMint as NewMintEvent,
} from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema] // Entity Imports [based on the schema]
import { import {
Owner, Owner,
GitRepository as GitRepositoryEntity, GitRepository as GitRepositoryEntity,
NewMint, NewMint,
Token, Token,
} from '../generated/schema'; } from '../generated/schema';
export function handleNewMint(event: NewMintEvent): void { export function handleNewMint(event: NewMintEvent): void {
let newMintEntity = new NewMint( let newMintEntity = new NewMint(
event.transaction.hash.concatI32(event.logIndex.toI32()) event.transaction.hash.concatI32(event.logIndex.toI32())
); );
let name = event.params.name; let name = event.params.name;
let description = event.params.description; let description = event.params.description;
let externalURL = event.params.externalURL; let externalURL = event.params.externalURL;
let ENS = event.params.ENS; let ENS = event.params.ENS;
let gitRepository = event.params.gitRepository; let gitRepository = event.params.gitRepository;
let commitHash = event.params.commitHash; let commitHash = event.params.commitHash;
let logo = event.params.logo; let logo = event.params.logo;
let color = event.params.color; let color = event.params.color;
let accessPointAutoApproval = event.params.accessPointAutoApproval; let accessPointAutoApproval = event.params.accessPointAutoApproval;
let tokenId = event.params.tokenId; let tokenId = event.params.tokenId;
let ownerAddress = event.params.owner; let ownerAddress = event.params.owner;
let verifierAddress = event.params.verifier; let verifierAddress = event.params.verifier;
newMintEntity.tokenId = tokenId; newMintEntity.tokenId = tokenId;
newMintEntity.name = name; newMintEntity.name = name;
newMintEntity.description = description; newMintEntity.description = description;
newMintEntity.externalURL = externalURL; newMintEntity.externalURL = externalURL;
newMintEntity.ENS = ENS; newMintEntity.ENS = ENS;
newMintEntity.commitHash = commitHash; newMintEntity.commitHash = commitHash;
newMintEntity.gitRepository = gitRepository; newMintEntity.gitRepository = gitRepository;
newMintEntity.logo = logo; newMintEntity.logo = logo;
newMintEntity.color = color; newMintEntity.color = color;
newMintEntity.accessPointAutoApproval = accessPointAutoApproval; newMintEntity.accessPointAutoApproval = accessPointAutoApproval;
newMintEntity.triggeredBy = event.params.minter; newMintEntity.triggeredBy = event.params.minter;
newMintEntity.owner = ownerAddress; newMintEntity.owner = ownerAddress;
newMintEntity.verifier = verifierAddress; newMintEntity.verifier = verifierAddress;
newMintEntity.blockNumber = event.block.number; newMintEntity.blockNumber = event.block.number;
newMintEntity.blockTimestamp = event.block.timestamp; newMintEntity.blockTimestamp = event.block.timestamp;
newMintEntity.transactionHash = event.transaction.hash; newMintEntity.transactionHash = event.transaction.hash;
newMintEntity.save(); newMintEntity.save();
log.error('{}', [tokenId.toString()]); log.error('{}', [tokenId.toString()]);
// Create Token, Owner, and Controller entities // Create Token, Owner, and Controller entities
let owner = Owner.load(ownerAddress); let owner = Owner.load(ownerAddress);
let token = new Token(Bytes.fromByteArray(Bytes.fromBigInt(tokenId))); let token = new Token(Bytes.fromByteArray(Bytes.fromBigInt(tokenId)));
if (!owner) { if (!owner) {
// Create a new owner entity // Create a new owner entity
owner = new Owner(ownerAddress); owner = new Owner(ownerAddress);
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false. // Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
owner.collection = false; owner.collection = false;
} }
// Populate Token with data from the event // Populate Token with data from the event
token.tokenId = tokenId; token.tokenId = tokenId;
token.name = name; token.name = name;
token.description = description; token.description = description;
token.externalURL = externalURL; token.externalURL = externalURL;
token.ENS = ENS; token.ENS = ENS;
token.gitRepository = gitRepository; token.gitRepository = gitRepository;
token.commitHash = commitHash; token.commitHash = commitHash;
token.logo = logo; token.logo = logo;
token.color = color; token.color = color;
token.accessPointAutoApproval = accessPointAutoApproval; token.accessPointAutoApproval = accessPointAutoApproval;
token.owner = ownerAddress; token.owner = ownerAddress;
token.verifier = verifierAddress; token.verifier = verifierAddress;
token.mintTransaction = event.transaction.hash.concatI32( token.mintTransaction = event.transaction.hash.concatI32(
event.logIndex.toI32() event.logIndex.toI32()
); );
token.mintedBy = event.params.minter; token.mintedBy = event.params.minter;
token.controllers = [ownerAddress]; token.controllers = [ownerAddress];
// Save entities // Save entities
owner.save(); owner.save();
token.save(); token.save();
} }

View File

@ -1,73 +1,60 @@
import { import { Bytes, log, store } from '@graphprotocol/graph-ts';
Bytes,
log,
store
} from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config] // Event Imports [based on the yaml config]
import { import { Transfer as TransferEvent } from '../generated/FleekNFA/FleekNFA';
Transfer as TransferEvent,
} from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema] // Entity Imports [based on the schema]
import { import { Owner, Token, Transfer } from '../generated/schema';
Owner,
Token,
Transfer,
} from '../generated/schema';
export function handleTransfer(event: TransferEvent): void { export function handleTransfer(event: TransferEvent): void {
let transfer = new Transfer( let transfer = new Transfer(
event.transaction.hash.concatI32(event.logIndex.toI32()) event.transaction.hash.concatI32(event.logIndex.toI32())
); );
const TokenId = event.params.tokenId; const TokenId = event.params.tokenId;
transfer.from = event.params.from; transfer.from = event.params.from;
transfer.to = event.params.to; transfer.to = event.params.to;
transfer.tokenId = TokenId; transfer.tokenId = TokenId;
transfer.blockNumber = event.block.number; transfer.blockNumber = event.block.number;
transfer.blockTimestamp = event.block.timestamp; transfer.blockTimestamp = event.block.timestamp;
transfer.transactionHash = event.transaction.hash; transfer.transactionHash = event.transaction.hash;
transfer.save(); transfer.save();
let token: Token | null; let token: Token | null;
let owner_address = event.params.to; let owner_address = event.params.to;
let owner = Owner.load(owner_address); let owner = Owner.load(owner_address);
if (!owner) { if (!owner) {
// Create a new owner entity // Create a new owner entity
owner = new Owner(owner_address); owner = new Owner(owner_address);
} }
if (parseInt(event.params.from.toHexString()) !== 0) { if (parseInt(event.params.from.toHexString()) !== 0) {
if (parseInt(event.params.to.toHexString()) === 0) { if (parseInt(event.params.to.toHexString()) === 0) {
// Burn // Burn
// Remove the entity from storage // Remove the entity from storage
// Its controllers and owner will be affected. // Its controllers and owner will be affected.
store.remove('Token', TokenId.toString()); store.remove('Token', TokenId.toString());
} else {
// Transfer
// Load the Token by using its TokenId
token = Token.load(Bytes.fromByteArray(Bytes.fromBigInt(TokenId)));
if (token) {
// Entity exists
token.owner = owner_address;
// Save both entities
owner.save();
token.save();
} else { } else {
// Transfer // Entity does not exist
// Load the Token by using its TokenId log.error('Unknown token was transferred.', []);
token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(TokenId))
);
if (token) {
// Entity exists
token.owner = owner_address;
// Save both entities
owner.save();
token.save();
} else {
// Entity does not exist
log.error('Unknown token was transferred.', []);
}
} }
} }
} }
}

View File

@ -4,6 +4,10 @@ sources:
handler: handler:
graphql: graphql:
endpoint: https://api.thegraph.com/subgraphs/name/emperororokusaki/flk-test-subgraph #replace for nfa subgraph endpoint: https://api.thegraph.com/subgraphs/name/emperororokusaki/flk-test-subgraph #replace for nfa subgraph
- name: ENS
handler:
graphql:
endpoint: https://api.thegraph.com/subgraphs/name/ensdomains/ens
documents: documents:
- ./graphql/*.graphql - ./graphql/*.graphql

View File

@ -18,3 +18,12 @@ query totalTokens {
} }
} }
# query to get the ens name of an address
query getENSNames($address: ID!) {
account(id: $address) {
domains {
name
}
}
}

View File

@ -1,4 +1,3 @@
import { Octokit } from 'octokit';
import React, { forwardRef } from 'react'; import React, { forwardRef } from 'react';
import { Flex } from '../layout'; import { Flex } from '../layout';
import { CardStyles } from './card.styles'; import { CardStyles } from './card.styles';
@ -15,9 +14,9 @@ export abstract class Card {
); );
static readonly Heading = forwardRef<HTMLHeadingElement, Card.HeadingProps>( static readonly Heading = forwardRef<HTMLHeadingElement, Card.HeadingProps>(
({ title, leftIcon, rightIcon, ...props }, ref) => { ({ title, leftIcon, rightIcon, css, ...props }, ref) => {
return ( return (
<Flex css={{ justifyContent: 'space-between' }}> <Flex css={{ justifyContent: 'space-between', ...css }}>
<Flex> <Flex>
{leftIcon} {leftIcon}
<CardStyles.Heading ref={ref} {...props}> <CardStyles.Heading ref={ref} {...props}>
@ -58,6 +57,7 @@ export namespace Card {
export type HeadingProps = { export type HeadingProps = {
title: string; title: string;
css?: React.CSSProperties;
leftIcon?: React.ReactNode; leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode; rightIcon?: React.ReactNode;
} & React.ComponentProps<typeof CardStyles.Heading>; } & React.ComponentProps<typeof CardStyles.Heading>;

View File

@ -2,10 +2,19 @@ import { Button, Card, Flex, Icon } from '@/components';
import { useRef } from 'react'; import { useRef } from 'react';
// @ts-ignore // @ts-ignore
import ColorThief from 'colorthief'; import ColorThief from 'colorthief';
import { Mint } from '../../../../mint.context';
export const ColorPicker = () => { export type ColorPickerProps = {
const { appLogo, logoColor, setLogoColor } = Mint.useContext(); logoColor: string;
setLogoColor: (color: string) => void;
logo: string;
} & React.HTMLAttributes<HTMLInputElement>;
export const ColorPicker: React.FC<ColorPickerProps> = ({
logoColor,
logo,
setLogoColor,
onBlur,
}) => {
const inputColorRef = useRef<HTMLInputElement>(null); const inputColorRef = useRef<HTMLInputElement>(null);
const imageRef = useRef<HTMLImageElement>(null); const imageRef = useRef<HTMLImageElement>(null);
@ -17,6 +26,10 @@ export const ColorPicker = () => {
setLogoColor(hexColor); setLogoColor(hexColor);
}; };
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLogoColor(e.target.value);
};
const handleColorPickerClick = () => { const handleColorPickerClick = () => {
inputColorRef.current?.click(); inputColorRef.current?.click();
}; };
@ -43,6 +56,7 @@ export const ColorPicker = () => {
borderRadius: '$md', borderRadius: '$md',
color: '$slate12', color: '$slate12',
zIndex: '$dropdown', zIndex: '$dropdown',
minWidth: '$28',
}} }}
onClick={handleColorPickerClick} onClick={handleColorPickerClick}
> >
@ -53,14 +67,14 @@ export const ColorPicker = () => {
className="absolute right-16" className="absolute right-16"
type="color" type="color"
value={logoColor} value={logoColor}
onChange={(e) => setLogoColor(e.target.value)} onChange={handleColorChange}
/> />
</Flex> </Flex>
</div> </div>
<img <img
className="hidden" className="hidden"
src={appLogo} src={logo}
ref={imageRef} ref={imageRef}
onLoad={handleLogoLoad} onLoad={handleLogoLoad}
style={{ width: '50px', height: '50px' }} style={{ width: '50px', height: '50px' }}

View File

@ -0,0 +1 @@
export * from './color-picker';

View File

@ -1,5 +1,15 @@
import React, { Fragment, useEffect, useRef, useState } from 'react'; import React, {
import { Combobox as ComboboxLib, Transition } from '@headlessui/react'; forwardRef,
Fragment,
useEffect,
useRef,
useState,
} from 'react';
import {
Combobox as ComboboxLib,
ComboboxInputProps as ComboboxLibInputProps,
Transition,
} from '@headlessui/react';
import { Icon, IconName } from '@/components/core/icon'; import { Icon, IconName } from '@/components/core/icon';
import { Flex } from '@/components/layout'; import { Flex } from '@/components/layout';
import { useDebounce } from '@/hooks/use-debounce'; import { useDebounce } from '@/hooks/use-debounce';
@ -16,20 +26,16 @@ type ComboboxInputProps = {
*/ */
leftIcon: IconName; leftIcon: IconName;
/** /**
* Function to handle the input change * Value to indicate it's invalid
*/ */
handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void; error?: boolean;
/** } & ComboboxLibInputProps<'input', ComboboxItem>;
* Function to handle the input click. When the user clicks on the input, the list of options will be displayed
*/
handleInputClick: () => void;
};
const ComboboxInput = ({ const ComboboxInput = ({
open, open,
leftIcon, leftIcon,
handleInputChange, error,
handleInputClick, ...props
}: ComboboxInputProps) => ( }: ComboboxInputProps) => (
<div className="relative w-full"> <div className="relative w-full">
<Icon <Icon
@ -45,14 +51,15 @@ const ComboboxInput = ({
/> />
<ComboboxLib.Input <ComboboxLib.Input
placeholder="Search" placeholder="Search"
className={`w-full border-solid border border-slate7 h-11 py-3 px-10 text-sm leading-5 text-slate11 outline-none ${ className={`w-full border-solid border h-11 py-3 px-10 text-sm leading-5 text-slate11 outline-none ${
open open
? 'border-b-0 rounded-t-xl bg-black border-slate6' ? 'border-b-0 rounded-t-xl bg-black border-slate6'
: 'rounded-xl bg-transparent' : `rounded-xl bg-transparent cursor-pointer ${
error ? 'border-red9' : 'border-slate7'
}`
}`} }`}
displayValue={(selectedValue: ComboboxItem) => selectedValue.label} displayValue={(selectedValue: ComboboxItem) => selectedValue.label}
onChange={handleInputChange} {...props}
onClick={handleInputClick}
/> />
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
<Icon name="chevron-down" css={{ fontSize: '$xs' }} /> <Icon name="chevron-down" css={{ fontSize: '$xs' }} />
@ -134,131 +141,151 @@ export type ComboboxProps = {
/** /**
* Callback when the selected value changes. * Callback when the selected value changes.
*/ */
onChange(option: ComboboxItem): void; onChange: (option: ComboboxItem) => void;
/**
* Function to handle the input blur
*/
onBlur?: () => void;
/**
* Value to indicate it's invalid
*/
error?: boolean;
}; };
export const Combobox: React.FC<ComboboxProps> = ({ export const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
items, (
selectedValue = { value: '', label: '' }, {
withAutocomplete = false, items,
leftIcon = 'search', selectedValue = { value: '', label: '' },
onChange, withAutocomplete = false,
}) => { leftIcon = 'search',
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]); onChange,
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>( onBlur,
[] error = false,
); },
ref
) => {
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
[]
);
useEffect(() => { useEffect(() => {
// If the selected value doesn't exist in the list of items, we add it // If the selected value doesn't exist in the list of items, we add it
if ( if (
items.filter((item) => item === selectedValue).length === 0 && items.filter((item) => item === selectedValue).length === 0 &&
selectedValue.value !== undefined && selectedValue.value !== undefined &&
autocompleteItems.length === 0 && autocompleteItems.length === 0 &&
withAutocomplete withAutocomplete
) { ) {
setAutocompleteItems([selectedValue]); setAutocompleteItems([selectedValue]);
} }
}, [selectedValue]); }, [selectedValue]);
useEffect(() => { useEffect(() => {
setFilteredItems(items);
}, [items]);
const buttonRef = useRef<HTMLButtonElement>(null);
const handleSearch = useDebounce((searchValue: string) => {
if (searchValue === '') {
setFilteredItems(items); setFilteredItems(items);
}, [items]);
if (withAutocomplete) { const buttonRef = useRef<HTMLButtonElement>(null);
const handleSearch = useDebounce((searchValue: string) => {
if (searchValue === '') {
setFilteredItems(items);
if (withAutocomplete) {
setAutocompleteItems([]);
handleComboboxChange({} as ComboboxItem);
}
} else {
const filteredValues = items.filter((item) =>
cleanString(item.label).startsWith(cleanString(searchValue))
);
if (withAutocomplete && filteredValues.length === 0) {
// If the search value doesn't exist in the list of items, we add it
setAutocompleteItems([{ value: searchValue, label: searchValue }]);
}
setFilteredItems(filteredValues);
}
}, 200);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
handleSearch(event.target.value);
};
const handleInputClick = () => {
buttonRef.current?.click();
};
const handleComboboxChange = (optionSelected: ComboboxItem) => {
onChange(optionSelected);
};
const handleLeaveTransition = () => {
setFilteredItems(items);
if (selectedValue.value === undefined && withAutocomplete) {
setAutocompleteItems([]); setAutocompleteItems([]);
handleComboboxChange({} as ComboboxItem); handleComboboxChange({} as ComboboxItem);
} }
} else { };
const filteredValues = items.filter((item) =>
cleanString(item.label).startsWith(cleanString(searchValue))
);
if (withAutocomplete && filteredValues.length === 0) { return (
// If the search value doesn't exist in the list of items, we add it <ComboboxLib
setAutocompleteItems([{ value: searchValue, label: searchValue }]); value={selectedValue}
} by="value"
setFilteredItems(filteredValues); onChange={handleComboboxChange}
} >
}, 200); {({ open }) => (
<div className="relative">
<ComboboxInput
onChange={handleInputChange}
onClick={handleInputClick}
open={open}
leftIcon={leftIcon}
onBlur={onBlur}
error={error}
/>
<ComboboxLib.Button ref={buttonRef} className="hidden" />
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { <Transition
event.stopPropagation(); show={open}
handleSearch(event.target.value); as={Fragment}
}; enter="transition duration-400 ease-out"
leave="transition ease-out duration-100"
const handleInputClick = () => { leaveFrom="opacity-100"
buttonRef.current?.click(); leaveTo="opacity-0"
}; afterLeave={handleLeaveTransition}
>
const handleComboboxChange = (option: ComboboxItem) => { <ComboboxLib.Options className="absolute max-h-60 w-full z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
onChange(option); {[...autocompleteItems, ...filteredItems].length === 0 ||
}; filteredItems === undefined ? (
<NoResults />
const handleLeaveTransition = () => { ) : (
setFilteredItems(items); <>
if (selectedValue.value === undefined && withAutocomplete) { {autocompleteItems.length > 0 && <span>Create new</span>}
setAutocompleteItems([]); {autocompleteItems.map(
handleComboboxChange({} as ComboboxItem); (autocompleteOption: ComboboxItem) => (
} <ComboboxOption
}; key={autocompleteOption.value}
option={autocompleteOption}
return ( />
<ComboboxLib )
value={selectedValue} )}
by="value" {autocompleteItems.length > 0 &&
onChange={handleComboboxChange} filteredItems.length > 0 && (
> <Separator css={{ mb: '$2' }} />
{({ open }) => ( )}
<div className="relative"> {filteredItems.map((option: ComboboxItem) => (
<ComboboxInput <ComboboxOption key={option.value} option={option} />
handleInputChange={handleInputChange} ))}
handleInputClick={handleInputClick} </>
open={open} )}
leftIcon={leftIcon} </ComboboxLib.Options>
/> </Transition>
<ComboboxLib.Button ref={buttonRef} className="hidden" /> </div>
)}
<Transition </ComboboxLib>
show={open} );
as={Fragment} }
enter="transition duration-400 ease-out" );
leave="transition ease-out duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={handleLeaveTransition}
>
<ComboboxLib.Options className="absolute max-h-60 w-full z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
{[...autocompleteItems, ...filteredItems].length === 0 ||
filteredItems === undefined ? (
<NoResults />
) : (
<>
{autocompleteItems.length > 0 && <span>Create new</span>}
{autocompleteItems.map((autocompleteOption: ComboboxItem) => (
<ComboboxOption
key={autocompleteOption.value}
option={autocompleteOption}
/>
))}
{autocompleteItems.length > 0 && filteredItems.length > 0 && (
<Separator css={{ mb: '$2' }} />
)}
{filteredItems.map((option: ComboboxItem) => (
<ComboboxOption key={option.value} option={option} />
))}
</>
)}
</ComboboxLib.Options>
</Transition>
</div>
)}
</ComboboxLib>
);
};

View File

@ -6,3 +6,4 @@ export * from './avatar';
export * from './separator.styles'; export * from './separator.styles';
export * from './text'; export * from './text';
export * from './switch'; export * from './switch';
export * from './color-picker';

View File

@ -0,0 +1,32 @@
import { Flex } from '@/components/layout';
import { dripStitches } from '@/theme';
const { styled } = dripStitches;
export abstract class InputFileStyles {
static readonly Container = styled(Flex, {
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
});
static readonly Border = styled('div', {
borderStyle: 'solid',
borderColor: '$gray7',
width: '$22',
height: '$22',
transition: 'border-color 0.2s ease-in-out',
borderWidth: '$default',
borderRadius: '$lg',
zIndex: '$docked',
'&:hover': {
borderColor: '$gray8',
},
'&[aria-invalid=true], &[data-invalid]': {
borderColor: '$red9',
},
//TODO add error state
});
}

View File

@ -1,29 +1,11 @@
import { Flex } from '../../layout';
import { dripStitches } from '../../../theme';
import { forwardRef, useRef } from 'react'; import { forwardRef, useRef } from 'react';
import { Icon } from '../icon'; import { Icon } from '../icon';
import { InputFileStyles as S } from './input-file.styles';
const { styled } = dripStitches;
const BorderInput = styled('div', {
borderStyle: 'solid',
borderColor: '$gray7',
width: '$22',
height: '$22',
transition: 'border-color 0.2s ease-in-out',
borderWidth: '$default',
borderRadius: '$lg',
zIndex: '$docked',
'&:hover': {
borderColor: '$gray8',
},
});
type InputFileProps = { type InputFileProps = {
value: string; value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
} & React.ComponentProps<typeof Flex>; } & React.ComponentProps<typeof S.Border>;
export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>( export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
({ value: file, onChange, css, ...props }, ref) => { ({ value: file, onChange, css, ...props }, ref) => {
@ -40,24 +22,13 @@ export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
return ( return (
<> <>
<Flex <S.Container onClick={handleInputClick}>
css={{
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
...(css || {}),
}}
ref={ref}
{...props}
onClick={handleInputClick}
>
{file !== '' ? ( {file !== '' ? (
<img className="absolute w-14 h-14" src={file} alt="logo" /> <img className="absolute w-14 h-14" src={file} alt="logo" />
) : ( ) : (
<Icon name="upload" size="md" css={{ position: 'absolute' }} /> <Icon name="upload" size="md" css={{ position: 'absolute' }} />
)} )}
<BorderInput /> <S.Border {...props} ref={ref} />
<input <input
type="file" type="file"
className="hidden" className="hidden"
@ -65,7 +36,7 @@ export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
ref={inputFileRef} ref={inputFileRef}
onChange={handleFileChange} onChange={handleFileChange}
/> />
</Flex> </S.Container>
</> </>
); );
} }

View File

@ -0,0 +1,15 @@
import { createContext, StringValidator } from '@/utils';
export type FormFieldContext = {
id: string;
validators: StringValidator[];
value: ReactState<string>;
validationEnabled: ReactState<boolean>;
};
export const [FormFieldProvider, useFormFieldContext] =
createContext<FormFieldContext>({
name: 'FormFieldContext',
hookName: 'useFormFieldContext',
providerName: 'FormFieldProvider',
});

View File

@ -0,0 +1,79 @@
import { createContext, StringValidator } from '@/utils';
import { useCallback, useEffect, useMemo, useState } from 'react';
export type FormValidations = { [key: string]: StringValidator[] };
export type FormContext = {
onValidationChange: (isValid: boolean) => void;
validations: ReactState<FormValidations>;
};
const [FormProviderCore, useFormContext] = createContext<FormContext>({
name: 'FormContext',
hookName: 'useFormContext',
providerName: 'FormProvider',
});
export { useFormContext };
export const FormProvider = ({
children,
onValidationChange,
}: React.PropsWithChildren<
Pick<FormContext, 'onValidationChange'>
>): JSX.Element => {
const validations = useState<FormValidations>({});
useEffect(() => {
onValidationChange(Object.values(validations[0]).length === 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [validations]);
return (
<FormProviderCore value={{ onValidationChange, validations }}>
{children}
</FormProviderCore>
);
};
export const useFormFieldValidator = (
id: string,
validators: StringValidator[]
): ((value: string) => boolean) => {
const {
validations: [, setValidations],
} = useFormContext();
return useCallback(
(value: string) => {
const fieldValidations = validators.reduce<StringValidator[]>(
(acc, validator) =>
validator.validate(value) ? acc : [...acc, validator],
[]
);
if (fieldValidations.length > 0) {
setValidations((prev) => ({ ...prev, [id]: fieldValidations }));
return false;
}
setValidations((prev) => {
const { [id]: toBeRemoved, ...rest } = prev;
return rest;
});
return true;
},
[id, validators, setValidations]
);
};
export const useFormFieldValidatorValue = (
id: string,
validators: StringValidator[],
value: string
): boolean => {
const validatorHandler = useFormFieldValidator(id, validators);
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => validatorHandler(value), [value]);
};

View File

@ -42,6 +42,15 @@ export abstract class FormStyles {
mt: '$1h', mt: '$1h',
}); });
static readonly Overline = styled('div', {
display: 'flex',
justifyContent: 'space-between',
});
static readonly OverlineErrors = styled(Flex, {
flexDirection: 'column',
});
static readonly ErrorMessage = styled('span', { static readonly ErrorMessage = styled('span', {
color: '$red11', color: '$red11',
fontSize: '0.625rem', fontSize: '0.625rem',

View File

@ -1,80 +1,313 @@
import React, { forwardRef } from 'react'; import { hasValidator } from '@/utils';
import { fileToBase64 } from '@/views/mint/nfa-step/form-step/form.utils';
import React, { forwardRef, useMemo, useState } from 'react';
import { ColorPicker, Combobox, ComboboxItem } from '../core';
import { Input, LogoFileInput, Textarea } from '../core/input'; import { Input, LogoFileInput, Textarea } from '../core/input';
import {
FormFieldContext,
FormFieldProvider,
useFormFieldContext,
} from './form-field.context';
import {
FormProvider,
useFormContext,
useFormFieldValidatorValue,
} from './form.context';
import { FormStyles } from './form.styles'; import { FormStyles } from './form.styles';
export abstract class Form { export abstract class Form {
static readonly Root = FormProvider;
static readonly Field = forwardRef<HTMLDivElement, Form.FieldProps>( static readonly Field = forwardRef<HTMLDivElement, Form.FieldProps>(
({ children, ...props }, ref) => { ({ children, context, ...props }, ref) => {
const {
value: [value],
} = context;
const validationEnabled = useState(Boolean(value));
return ( return (
<FormStyles.Field ref={ref} {...props}> <FormFieldProvider value={{ ...context, validationEnabled }}>
{children} <FormStyles.Field ref={ref} {...props}>
</FormStyles.Field> {children}
</FormStyles.Field>
</FormFieldProvider>
); );
} }
); );
static readonly Label = forwardRef<HTMLLabelElement, Form.LabelProps>( static readonly Label = forwardRef<HTMLLabelElement, Form.LabelProps>(
({ children, isRequired, ...props }, ref) => (
<FormStyles.Label ref={ref} {...props}>
{children}{' '}
{isRequired && <FormStyles.RequiredLabel>*</FormStyles.RequiredLabel>}
</FormStyles.Label>
)
);
static readonly MaxLength = forwardRef<HTMLLabelElement, Form.LabelProps>(
({ children, ...props }, ref) => { ({ children, ...props }, ref) => {
const { validators } = useFormFieldContext();
const isRequired = useMemo(
() => hasValidator(validators, 'required'),
[validators]
);
return ( return (
<FormStyles.MaxLength ref={ref} {...props}> <FormStyles.Label ref={ref} {...props}>
{children} {children}
</FormStyles.MaxLength> {isRequired && <FormStyles.RequiredLabel>*</FormStyles.RequiredLabel>}
</FormStyles.Label>
); );
} }
); );
static readonly Error = forwardRef<HTMLDivElement, Form.ErrorProps>( static readonly Overline = forwardRef<HTMLDivElement>((props, ref) => {
({ children, ...props }, ref) => ( const {
<FormStyles.ErrorMessage ref={ref} {...props}> validations: [validations],
{children} } = useFormContext();
</FormStyles.ErrorMessage> const {
) id,
); value: [value],
validationEnabled: [validationEnabled],
validators,
} = useFormFieldContext();
const errors = useMemo(() => {
if (!validationEnabled) return [];
if (!validations[id]) return [];
return validations[id].map((validator) => validator.message);
}, [validations, id, validationEnabled]);
const counter = useMemo(
() => hasValidator(validators, 'maxLength')?.args || 0,
[validators]
);
return (
<FormStyles.Overline ref={ref} {...props}>
<FormStyles.OverlineErrors>
{errors.map((error) => (
<FormStyles.ErrorMessage key={error}>
{error}
</FormStyles.ErrorMessage>
))}
</FormStyles.OverlineErrors>
{Boolean(counter) && (
<FormStyles.MaxLength>
{`${value.length}/${counter}`}
</FormStyles.MaxLength>
)}
</FormStyles.Overline>
);
});
static readonly Input = forwardRef<HTMLInputElement, Form.InputProps>( static readonly Input = forwardRef<HTMLInputElement, Form.InputProps>(
(props, ref) => { (props, ref) => {
return <Input ref={ref} {...props} />; const {
id,
validators,
value: [value, setValue],
validationEnabled: [validationEnabled, setValidationEnabled],
} = useFormFieldContext();
const isValid = useFormFieldValidatorValue(id, validators, value);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (props.onChange) props.onChange(e);
setValue(e.target.value);
};
const handleInputBlur = (
e: React.FocusEvent<HTMLInputElement, Element>
) => {
if (props.onBlur) props.onBlur(e);
setValidationEnabled(true);
};
return (
<Input
ref={ref}
{...props}
value={value}
onChange={handleInputChange}
aria-invalid={validationEnabled && !isValid}
onBlur={handleInputBlur}
/>
);
} }
); );
static readonly Combobox = forwardRef<HTMLInputElement, Form.ComboboxProps>(
(props, ref) => {
const {
id,
validators,
value: [value, setValue],
validationEnabled: [validationEnabled, setValidationEnabled],
} = useFormFieldContext();
const comboboxValue = useMemo(() => {
// if it's with autocomplete maybe won't be on the items list
const item = props.items.find((item) => item.label === value);
if (props.withAutocomplete && !item && value !== '') {
//return the selected value if the item doesn't exist
return { label: value, value: value };
}
return item;
}, [value]);
const isValid = useFormFieldValidatorValue(id, validators, value);
const handleComboboxChange = (option: ComboboxItem) => {
if (props.onChange) props.onChange(option);
setValue(option.label);
};
const handleComboboxBlur = () => {
setValidationEnabled(true);
};
return (
<Combobox
ref={ref}
{...props}
onChange={handleComboboxChange}
selectedValue={comboboxValue || ({} as ComboboxItem)}
onBlur={handleComboboxBlur}
error={validationEnabled && !isValid}
/>
);
}
);
static readonly ColorPicker = ({
logo,
setLogoColor,
}: Form.ColorPickerProps) => {
const {
value: [value, setValue],
validationEnabled: [, setValidationEnabled],
} = useFormFieldContext();
const handleColorChange = (color: string) => {
if (setLogoColor) setLogoColor(color);
setValue(color);
};
const handleInputBlur = () => {
setValidationEnabled(true);
};
return (
<ColorPicker
logo={logo}
logoColor={value}
setLogoColor={handleColorChange}
onBlur={handleInputBlur}
/>
);
};
static readonly Textarea = forwardRef< static readonly Textarea = forwardRef<
HTMLTextAreaElement, HTMLTextAreaElement,
Form.TextareaProps Form.TextareaProps
>((props, ref) => { >((props, ref) => {
return <Textarea ref={ref} {...props} />; const {
id,
validators,
value: [value, setValue],
validationEnabled: [validationEnabled, setValidationEnabled],
} = useFormFieldContext();
const isValid = useFormFieldValidatorValue(id, validators, value);
const handleTextareaChange = (
e: React.ChangeEvent<HTMLTextAreaElement>
) => {
if (props.onChange) props.onChange(e);
setValue(e.target.value);
};
const handleTextareaBlur = (
e: React.FocusEvent<HTMLTextAreaElement, Element>
) => {
if (props.onBlur) props.onBlur(e);
setValidationEnabled(true);
};
return (
<Textarea
ref={ref}
{...props}
value={value}
onChange={handleTextareaChange}
aria-invalid={validationEnabled && !isValid}
onBlur={handleTextareaBlur}
/>
);
}); });
static readonly LogoFileInput = forwardRef< static readonly LogoFileInput = forwardRef<
HTMLInputElement, HTMLInputElement,
Form.LogoFileInputProps Form.LogoFileInputProps
>((props, ref) => { >((props, ref) => {
return <LogoFileInput ref={ref} {...props} />; const {
id,
validators,
value: [value, setValue],
validationEnabled: [, setValidationEnabled],
} = useFormFieldContext();
const isValid = useFormFieldValidatorValue(id, validators, value);
const handleFileInputChange = async (
e: React.ChangeEvent<HTMLInputElement>
) => {
const file = e.target.files?.[0];
if (file) {
//Convert to string base64 to send to contract
setValidationEnabled(true);
const fileBase64 = await fileToBase64(file);
setValue(fileBase64);
}
};
return (
<LogoFileInput
ref={ref}
{...props}
value={value}
aria-invalid={value !== '' && !isValid}
onChange={handleFileInputChange}
/>
);
}); });
} }
export namespace Form { export namespace Form {
export type FieldProps = { export type FieldProps = {
children: React.ReactNode; children: React.ReactNode;
context: Omit<FormFieldContext, 'validationEnabled'>;
} & React.ComponentProps<typeof FormStyles.Field>; } & React.ComponentProps<typeof FormStyles.Field>;
export type LabelProps = { isRequired?: boolean } & React.ComponentProps< export type LabelProps = React.ComponentProps<typeof FormStyles.Label>;
typeof FormStyles.Label
>;
export type ErrorProps = React.ComponentProps<typeof FormStyles.ErrorMessage>; export type ErrorProps = React.ComponentProps<typeof FormStyles.ErrorMessage>;
export type InputProps = React.ComponentProps<typeof Input>; export type InputProps = Omit<React.ComponentProps<typeof Input>, 'error'>;
export type TextareaProps = React.ComponentProps<typeof Textarea>; export type TextareaProps = Omit<
React.ComponentProps<typeof Textarea>,
'value' | 'error'
>;
export type LogoFileInputProps = React.ComponentProps<typeof LogoFileInput>; export type ComboboxProps = {
onChange?: (option: ComboboxItem) => void;
} & Omit<
React.ComponentProps<typeof Combobox>,
'error' | 'selectedValue' | 'onChange'
>;
export type LogoFileInputProps = Omit<
React.ComponentProps<typeof LogoFileInput>,
'value' | 'onChange'
>;
export type ColorPickerProps = {
setLogoColor?: (color: string) => void;
} & Omit<
React.ComponentProps<typeof ColorPicker>,
'setLogoColor' | 'logoColor'
>;
} }

View File

@ -0,0 +1,19 @@
import { useState } from 'react';
import { StringValidator } from '@/utils';
import { FormFieldContext } from './form-field.context';
export type FormField = Omit<FormFieldContext, 'validationEnabled'>;
export const useFormField = (
id: string,
validators: StringValidator[],
initialValue = ''
): FormField => {
const value = useState(initialValue);
return {
id,
validators,
value,
};
};

View File

@ -1 +1,2 @@
export * from './form'; export * from './form';
export * from './form.utils';

5
ui/src/declarations/index.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import { Dispatch, SetStateAction } from 'react';
declare global {
type ReactState<S> = [S, Dispatch<SetStateAction<S>>];
}

View File

@ -1,3 +1,6 @@
import { ComboboxItem } from '@/components';
import { GithubState } from '@/store';
const listSites = [ const listSites = [
{ {
tokenId: 1, tokenId: 1,
@ -26,12 +29,16 @@ const listSites = [
}, },
]; ];
export const fetchMintedSites = async () => { const listBranches: ComboboxItem[] = [
{ value: '4573495837458934', label: 'main' },
{ value: '293857439857348', label: 'develop' },
{ value: '12344', label: 'feat/nabvar' },
];
export const fetchMintedSites = async (): Promise<ComboboxItem[]> => {
return new Promise((resolved) => { return new Promise((resolved) => {
setTimeout(() => { setTimeout(() => {
resolved({ resolved(listBranches);
listSites,
});
}, 2500); }, 2500);
}); });
}; };

View File

@ -3,6 +3,7 @@ import { RootState } from '@/store';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { ensActions } from '../ens-slice'; import { ensActions } from '../ens-slice';
//TODO maybe deprecate cause we uss the ENS graph
export const fetchEnsNamesThunk = createAsyncThunk<void, `0x${string}`>( export const fetchEnsNamesThunk = createAsyncThunk<void, `0x${string}`>(
'ens/fetchEnsNames', 'ens/fetchEnsNames',
async (address, { dispatch, getState }) => { async (address, { dispatch, getState }) => {

View File

@ -1,4 +1,5 @@
import { githubActions, RootState } from '@/store'; import { githubActions, RootState } from '@/store';
import { AppLog } from '@/utils';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { GithubClient } from '../github-client'; import { GithubClient } from '../github-client';
@ -18,7 +19,7 @@ export const fetchRepositoriesThunk = createAsyncThunk(
dispatch(githubActions.setRepositories(repositories)); dispatch(githubActions.setRepositories(repositories));
} catch (error) { } catch (error) {
console.log(error); AppLog.errorToast('Failed to fetch repositories. Please try again.');
dispatch(githubActions.setQueryState('failed')); dispatch(githubActions.setQueryState('failed'));
} }
} }

View File

@ -4,3 +4,4 @@ export * from './object';
export * from './context'; export * from './context';
export * from './toast'; export * from './toast';
export * from './log'; export * from './log';
export * from './string-validators';

View File

@ -0,0 +1,73 @@
export type StringValidator = {
name: string;
message: string;
validate: (value?: string) => boolean;
};
type StringValidatorWithParams<T> = (args: T) => StringValidator & { args: T };
const required: StringValidator = {
name: 'required',
validate: (value = '') => {
return value.length > 0;
},
message: 'This field is required',
};
const maxLength: StringValidatorWithParams<number> = (length: number) => ({
name: 'maxLength',
validate: (value = '') => value.length <= length,
message: 'This field is too long',
args: length,
});
const isUrl: StringValidator = {
name: 'isUrl',
validate: (value = '') => {
const regex =
/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
return regex.test(value);
},
message: 'Is not a valid URL',
};
const maxFileSize: StringValidatorWithParams<number> = (maxSize: number) => ({
name: 'maxFileSize',
validate: (value = '') => {
const file: File = new File([value], 'file');
return file.size <= 1024 * maxSize;
},
message: 'File is too large',
args: maxSize,
});
const hasSpecialCharacters: StringValidator = {
name: 'specialCharacters',
validate: (value = '') => {
if (value !== '') {
const regex = /[!@#$%^&*()?":{}|<>`/]/;
return !regex.test(value);
}
return true;
},
message: 'This field has special characters',
};
export const StringValidators = {
required,
maxLength,
isUrl,
maxFileSize,
hasSpecialCharacters,
};
export const hasValidator = <
Name extends keyof typeof StringValidators,
Type = (typeof StringValidators)[Name]
>(
validators: StringValidator[],
name: Name
): Type extends StringValidatorWithParams<any>
? ReturnType<Type> | undefined
: StringValidator | undefined =>
validators.find((validator) => validator.name === name) as any;

View File

@ -8,17 +8,16 @@ const itemsCombobox = [
]; ];
export const ComboboxTest = () => { export const ComboboxTest = () => {
const [selectedValue, setSelectedValue] = useState({} as ComboboxItem); const [selectedValue, setSelectedValue] = useState('');
const [selectedValueAutocomplete, setSelectedValueAutocomplete] = useState( const [selectedValueAutocomplete, setSelectedValueAutocomplete] =
{} as ComboboxItem useState('');
);
const handleComboboxChange = (item: ComboboxItem) => { const handleComboboxChange = (value: string) => {
setSelectedValue(item); setSelectedValue(value);
}; };
const handleComboboxChangeAutocomplete = (item: ComboboxItem) => { const handleComboboxChangeAutocomplete = (value: string) => {
setSelectedValueAutocomplete(item); setSelectedValueAutocomplete(value);
}; };
return ( return (

View File

@ -1,88 +0,0 @@
import { useEffect } from 'react';
import { Combobox, ComboboxItem, Flex, Form, Spinner } from '@/components';
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
import { Mint } from '@/views/mint/mint.context';
export const RepoBranchCommitFields = () => {
const { queryLoading, branches } = useGithubStore();
const dispatch = useAppDispatch();
const {
repositoryName,
selectedUserOrg,
branchName,
commitHash,
setBranchName,
setCommitHash,
} = Mint.useContext();
useEffect(() => {
if (queryLoading === 'idle') {
dispatch(
githubActions.fetchBranchesThunk({
owner: selectedUserOrg.label,
repository: repositoryName.name,
})
);
}
}, [queryLoading, dispatch]);
useEffect(() => {
if (queryLoading === 'success' && branches.length > 0) {
const defaultBranch = branches.find(
(branch) =>
branch.label.toLowerCase() ===
repositoryName.defaultBranch.toLowerCase()
);
if (defaultBranch) {
setBranchName(defaultBranch);
setCommitHash(defaultBranch.value);
}
}
}, [queryLoading, branches]);
const handleBranchChange = (dropdownOption: ComboboxItem) => {
setBranchName(dropdownOption);
setCommitHash(dropdownOption.value);
};
const handleCommitHashChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCommitHash(e.target.value);
};
if (queryLoading === 'loading') {
return (
<Flex
css={{
height: '9.75rem',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Spinner />
</Flex>
);
}
return (
<>
<Form.Field>
<Form.Label>Git Branch</Form.Label>
<Combobox
leftIcon="branch"
items={branches}
selectedValue={branchName}
onChange={handleBranchChange}
/>
</Form.Field>
<Form.Field>
<Form.Label>Git Commit</Form.Label>
<Form.Input
placeholder="Select branch to get last commit"
value={commitHash}
onChange={handleCommitHashChange}
/>
</Form.Field>
</>
);
};

View File

@ -0,0 +1 @@
export * from './repo-configuration-body';

View File

@ -0,0 +1,95 @@
import { useEffect } from 'react';
import { ComboboxItem, Flex, Form, Spinner } from '@/components';
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
import { Mint } from '@/views/mint/mint.context';
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
import { AppLog } from '@/utils';
export const RepoBranchCommitFields = () => {
const { queryLoading, branches } = useGithubStore();
const dispatch = useAppDispatch();
const {
form: { gitBranch: gitBranchContext, gitCommit: gitCommitContext },
} = useMintFormContext();
const {
value: [gitBranch, setGitBranch],
} = gitBranchContext;
const {
value: [, setGitCommit],
} = gitCommitContext;
const { repositoryName, selectedUserOrg } = Mint.useContext();
useEffect(() => {
if (queryLoading === 'idle') {
dispatch(
githubActions.fetchBranchesThunk({
owner: selectedUserOrg.label,
repository: repositoryName.name,
})
);
}
}, [queryLoading, dispatch]);
useEffect(() => {
try {
if (
queryLoading === 'success' &&
branches.length > 0 &&
repositoryName.defaultBranch !== undefined &&
gitBranch === '' //we only set the default branch the first time
) {
const defaultBranch = branches.find(
(branch) =>
branch.label.toLowerCase() ===
repositoryName.defaultBranch.toLowerCase()
);
if (defaultBranch) {
setGitBranch(defaultBranch.label);
setGitCommit(defaultBranch.value);
}
}
} catch (error) {
AppLog.errorToast('We had a problem. Try again');
}
}, [queryLoading, branches]);
if (queryLoading === 'loading') {
return (
<Flex
css={{
height: '9.75rem',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Spinner />
</Flex>
);
}
const handleBranchChange = (branch: ComboboxItem) => {
setGitBranch(branch.label);
setGitCommit(branch.value);
};
return (
<>
<Form.Field context={gitBranchContext}>
<Form.Label>Git Branch</Form.Label>
<Form.Combobox
leftIcon="branch"
items={branches}
onChange={handleBranchChange}
/>
</Form.Field>
<Form.Field context={gitCommitContext}>
<Form.Label>Git Commit</Form.Label>
<Form.Input placeholder="Select branch to get last commit" />
<Form.Overline />
</Form.Field>
</>
);
};

View File

@ -1,10 +1,17 @@
import { Button, Card, Flex, Stepper } from '@/components'; import { Button, Card, Flex, Form, Stepper } from '@/components';
import { Mint } from '@/views/mint/mint.context'; import { Mint } from '@/views/mint/mint.context';
import { RepoRow } from '../repository-row'; import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
import { RepoRow } from '../../repository-row';
import { RepoBranchCommitFields } from './repo-branch-commit-fields'; import { RepoBranchCommitFields } from './repo-branch-commit-fields';
export const RepoConfigurationBody = () => { export const RepoConfigurationBody = () => {
const { repositoryName, branchName, commitHash } = Mint.useContext(); const {
form: {
isValid: [isValid],
},
} = useMintFormContext();
const { repositoryName } = Mint.useContext();
const { nextStep } = Stepper.useContext(); const { nextStep } = Stepper.useContext();
@ -31,7 +38,7 @@ export const RepoConfigurationBody = () => {
/> />
<RepoBranchCommitFields /> <RepoBranchCommitFields />
<Button <Button
disabled={!branchName.value || !commitHash} disabled={!isValid}
colorScheme="blue" colorScheme="blue"
variant="solid" variant="solid"
onClick={handleContinueClick} onClick={handleContinueClick}

View File

@ -1,13 +1,24 @@
import { DropdownItem } from '@/components'; import { DropdownItem } from '@/components';
import { MintCardHeader } from '@/views/mint/mint-card'; import { MintCardHeader } from '@/views/mint/mint-card';
import { Mint } from '@/views/mint/mint.context'; import { Mint } from '@/views/mint/mint.context';
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
export const RepoConfigurationHeader = () => { export const RepoConfigurationHeader = () => {
const { setGithubStep, setBranchName, setCommitHash } = Mint.useContext(); const { setGithubStep } = Mint.useContext();
const {
form: {
gitBranch: {
value: [, setBranchName],
},
gitCommit: {
value: [, setCommitHash],
},
},
} = useMintFormContext();
const handlePrevStepClick = () => { const handlePrevStepClick = () => {
setGithubStep(2); setGithubStep(2);
setBranchName({} as DropdownItem); setBranchName('');
setCommitHash(''); setCommitHash('');
}; };

View File

@ -1,10 +1,18 @@
import { Card, ComboboxItem, Flex, Grid, Icon, Spinner } from '@/components'; import {
Card,
ComboboxItem,
Flex,
Grid,
Icon,
IconButton,
Spinner,
} from '@/components';
import { Input } from '@/components/core/input'; import { Input } from '@/components/core/input';
import { useDebounce } from '@/hooks/use-debounce'; import { useDebounce } from '@/hooks/use-debounce';
import { useGithubStore } from '@/store'; import { useGithubStore } from '@/store';
import { MintCardHeader } from '@/views/mint/mint-card'; import { MintCardHeader } from '@/views/mint/mint-card';
import { Mint } from '@/views/mint/mint.context'; import { Mint } from '@/views/mint/mint.context';
import React, { forwardRef, useState } from 'react'; import React, { useState } from 'react';
import { RepositoriesList } from './repositories-list'; import { RepositoriesList } from './repositories-list';
import { UserOrgsCombobox } from './users-orgs-combobox'; import { UserOrgsCombobox } from './users-orgs-combobox';
@ -44,9 +52,27 @@ export const GithubRepositoryConnection: React.FC = () => {
return ( return (
<Card.Container css={{ maxWidth: '$107h', maxHeight: '$95h', pr: '$3h' }}> <Card.Container css={{ maxWidth: '$107h', maxHeight: '$95h', pr: '$3h' }}>
<MintCardHeader <Card.Heading
title="Select Repository" title="Select Repository"
onClickBack={handlePrevStepClick} css={{ pr: '$3h' }}
leftIcon={
<IconButton
aria-label="back"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
css={{ mr: '$2' }}
onClick={handlePrevStepClick}
/>
}
rightIcon={
<IconButton
aria-label="info"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/> />
<Card.Body css={{ pt: '$4' }}> <Card.Body css={{ pt: '$4' }}>
<Grid css={{ rowGap: '$2' }}> <Grid css={{ rowGap: '$2' }}>

View File

@ -1,6 +1,7 @@
import { Button, Separator } from '@/components'; import { Button, Separator } from '@/components';
import { githubActions, GithubState, useAppDispatch } from '@/store'; import { githubActions, GithubState, useAppDispatch } from '@/store';
import { Mint } from '@/views/mint/mint.context'; import { Mint } from '@/views/mint/mint.context';
import { useCallback } from 'react';
import { RepoRow } from '../repository-row'; import { RepoRow } from '../repository-row';
type RepositoryProps = { type RepositoryProps = {
@ -13,11 +14,12 @@ export const Repository = ({ repository, index, length }: RepositoryProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleSelectRepo = () => { const handleSelectRepo = useCallback(() => {
setRepositoryName(repository); setRepositoryName(repository);
setGithubStep(3); setGithubStep(3);
dispatch(githubActions.setQueryState('idle')); dispatch(githubActions.setQueryState('idle'));
}; }, [dispatch]);
return ( return (
<> <>
<RepoRow <RepoRow

View File

@ -1,5 +1,6 @@
import { Avatar, Combobox, ComboboxItem } from '@/components'; import { Avatar, Combobox, ComboboxItem } from '@/components';
import { githubActions, useAppDispatch, useGithubStore } from '@/store'; import { githubActions, useAppDispatch, useGithubStore } from '@/store';
import { AppLog } from '@/utils';
import { Mint } from '@/views/mint/mint.context'; import { Mint } from '@/views/mint/mint.context';
import { useEffect } from 'react'; import { useEffect } from 'react';
@ -16,8 +17,12 @@ export const UserOrgsCombobox = () => {
}, [dispatch, queryUserAndOrganizations]); }, [dispatch, queryUserAndOrganizations]);
const handleUserOrgChange = (item: ComboboxItem) => { const handleUserOrgChange = (item: ComboboxItem) => {
dispatch(githubActions.fetchRepositoriesThunk(item.value)); if (item) {
setSelectedUserOrg(item); dispatch(githubActions.fetchRepositoriesThunk(item.value));
setSelectedUserOrg(item);
} else {
AppLog.errorToast('Error selecting user/org. Try again');
}
}; };
useEffect(() => { useEffect(() => {
@ -29,7 +34,7 @@ export const UserOrgsCombobox = () => {
//SET first user //SET first user
setSelectedUserOrg(userAndOrganizations[0]); setSelectedUserOrg(userAndOrganizations[0]);
} }
}, [queryUserAndOrganizations]); }, [queryUserAndOrganizations, selectedUserOrg, userAndOrganizations]);
return ( return (
<Combobox <Combobox

View File

@ -1,4 +1,4 @@
import { Stepper } from '@/components'; import { Form, Stepper } from '@/components';
import { MintPreview } from './preview-step/mint-preview'; import { MintPreview } from './preview-step/mint-preview';
import { GithubStep } from './github-step'; import { GithubStep } from './github-step';
import { MintStep } from './mint-step'; import { MintStep } from './mint-step';
@ -6,40 +6,48 @@ import { WalletStep } from './wallet-step';
import { NFAStep } from './nfa-step'; import { NFAStep } from './nfa-step';
import { Mint } from './mint.context'; import { Mint } from './mint.context';
import { NftMinted } from './nft-minted'; import { NftMinted } from './nft-minted';
import { useMintFormContext } from './nfa-step/form-step';
export const MintStepper = () => { export const MintStepper = () => {
const { const {
transaction: { isSuccess }, transaction: { isSuccess },
} = Mint.useTransactionContext(); } = Mint.useTransactionContext();
const {
form: {
isValid: [, setIsValid],
},
} = useMintFormContext();
if (!isSuccess) { if (!isSuccess) {
return ( return (
<Stepper.Root initialStep={1}> <Stepper.Root initialStep={1}>
<Stepper.Container> <Form.Root onValidationChange={setIsValid}>
<Stepper.Step> <Stepper.Container>
<MintStep header="Connect your Ethereum Wallet to mint an NFA"> <Stepper.Step>
<WalletStep /> <MintStep header="Connect your Ethereum Wallet to mint an NFA">
</MintStep> <WalletStep />
</Stepper.Step> </MintStep>
</Stepper.Step>
<Stepper.Step> <Stepper.Step>
<MintStep header="Connect GitHub and select repository"> <MintStep header="Connect GitHub and select repository">
<GithubStep /> <GithubStep />
</MintStep> </MintStep>
</Stepper.Step> </Stepper.Step>
<Stepper.Step> <Stepper.Step>
<MintStep header="Finalize a few key things for your NFA"> <MintStep header="Finalize a few key things for your NFA">
<NFAStep /> <NFAStep />
</MintStep> </MintStep>
</Stepper.Step> </Stepper.Step>
<Stepper.Step> <Stepper.Step>
<MintStep header="Review your NFA and mint it on Polygon"> <MintStep header="Review your NFA and mint it on Polygon">
<MintPreview /> <MintPreview />
</MintStep> </MintStep>
</Stepper.Step> </Stepper.Step>
</Stepper.Container> </Stepper.Container>
</Form.Root>
</Stepper.Root> </Stepper.Root>
); );
} else { } else {

View File

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { ComboboxItem, DropdownItem } from '@/components'; import { ComboboxItem } from '@/components';
import { EthereumHooks } from '@/integrations'; import { EthereumHooks } from '@/integrations';
import { AppLog, createContext } from '@/utils'; import { AppLog, createContext } from '@/utils';
import { GithubState, useFleekERC721Billing } from '@/store'; import { GithubState, useFleekERC721Billing } from '@/store';
@ -9,32 +9,14 @@ export type MintContext = {
billing: string | undefined; billing: string | undefined;
selectedUserOrg: ComboboxItem; selectedUserOrg: ComboboxItem;
repositoryName: GithubState.Repository; repositoryName: GithubState.Repository;
branchName: DropdownItem; //get value from DropdownItem to mint
commitHash: string;
githubStep: number; githubStep: number;
nfaStep: number; nfaStep: number;
appName: string;
appDescription: string;
appLogo: string;
logoColor: string;
ens: ComboboxItem;
domain: string;
verifyNFA: boolean; verifyNFA: boolean;
ensError: string;
setGithubStep: (step: number) => void; setGithubStep: (step: number) => void;
setNfaStep: (step: number) => void; setNfaStep: (step: number) => void;
setSelectedUserOrg: (userOrg: ComboboxItem) => void; setSelectedUserOrg: (userOrgValue: ComboboxItem) => void;
setRepositoryName: (repo: GithubState.Repository) => void; setRepositoryName: (repo: GithubState.Repository) => void;
setBranchName: (branch: DropdownItem) => void;
setCommitHash: (hash: string) => void;
setAppName: (name: string) => void;
setAppDescription: (description: string) => void;
setAppLogo: (logo: string) => void;
setLogoColor: (color: string) => void;
setEns: (ens: ComboboxItem) => void;
setDomain: (domain: string) => void;
setVerifyNFA: (verify: boolean) => void; setVerifyNFA: (verify: boolean) => void;
setEnsError: (error: string) => void;
}; };
const [MintProvider, useContext] = createContext<MintContext>({ const [MintProvider, useContext] = createContext<MintContext>({
@ -56,24 +38,13 @@ export abstract class Mint {
const [selectedUserOrg, setSelectedUserOrg] = useState({} as ComboboxItem); const [selectedUserOrg, setSelectedUserOrg] = useState({} as ComboboxItem);
const [repositoryName, setRepositoryName] = const [repositoryName, setRepositoryName] =
useState<GithubState.Repository>({} as GithubState.Repository); useState<GithubState.Repository>({} as GithubState.Repository);
const [branchName, setBranchName] = useState({} as DropdownItem);
const [commitHash, setCommitHash] = useState('');
const [githubStep, setGithubStepContext] = useState(1); const [githubStep, setGithubStepContext] = useState(1);
//NFA Details //NFA Details
const [nfaStep, setNfaStep] = useState(1); const [nfaStep, setNfaStep] = useState(1);
const [appName, setAppName] = useState('');
const [appDescription, setAppDescription] = useState('');
const [appLogo, setAppLogo] = useState('');
const [logoColor, setLogoColor] = useState('');
const [ens, setEns] = useState({} as ComboboxItem);
const [domain, setDomain] = useState('');
const [verifyNFA, setVerifyNFA] = useState(true); const [verifyNFA, setVerifyNFA] = useState(true);
const [billing] = useFleekERC721Billing('Mint'); const [billing] = useFleekERC721Billing('Mint');
//Field validations
const [ensError, setEnsError] = useState<string>('');
const setGithubStep = (step: number): void => { const setGithubStep = (step: number): void => {
if (step > 0 && step <= 3) { if (step > 0 && step <= 3) {
setGithubStepContext(step); setGithubStepContext(step);
@ -86,32 +57,14 @@ export abstract class Mint {
billing, billing,
selectedUserOrg, selectedUserOrg,
repositoryName, repositoryName,
branchName,
commitHash,
githubStep, githubStep,
nfaStep, nfaStep,
appName,
appDescription,
appLogo,
logoColor,
ens,
domain,
verifyNFA, verifyNFA,
ensError,
setSelectedUserOrg, setSelectedUserOrg,
setGithubStep, setGithubStep,
setNfaStep, setNfaStep,
setRepositoryName, setRepositoryName,
setBranchName,
setCommitHash,
setAppName,
setAppDescription,
setAppLogo,
setLogoColor,
setEns,
setDomain,
setVerifyNFA, setVerifyNFA,
setEnsError,
}} }}
> >
<TransactionProvider <TransactionProvider

View File

@ -1,17 +1,24 @@
import { Flex } from '@/components'; import { Flex } from '@/components';
import { MintStepper } from './mint-stepper'; import { MintStepper } from './mint-stepper';
import { Mint as MintContext } from './mint.context'; import { Mint as MintContext } from './mint.context';
import { MintFormProvider, useMintFormContextInit } from './nfa-step/form-step';
export const Mint = () => ( export const Mint = () => {
<Flex const context = useMintFormContextInit();
css={{ return (
flexGrow: 1, <Flex
justifyContent: 'center', css={{
alignItems: 'center', height: '100%',
}} flexDirection: 'column',
> justifyContent: 'center',
<MintContext.Provider> alignItems: 'center',
<MintStepper /> }}
</MintContext.Provider> >
</Flex> <MintContext.Provider>
); <MintFormProvider value={context}>
<MintStepper />
</MintFormProvider>
</MintContext.Provider>
</Flex>
);
};

View File

@ -1,30 +1,19 @@
import { Form } from '@/components'; import { Form } from '@/components';
import { Mint } from '../../../mint.context'; import { useMintFormContext } from '../mint-form.context';
const maxCharacters = 250;
export const AppDescriptionField = () => { export const AppDescriptionField = () => {
const { appDescription, setAppDescription } = Mint.useContext(); const {
form: { appDescription },
const handleAppDescriptionChange = ( } = useMintFormContext();
e: React.ChangeEvent<HTMLTextAreaElement>
) => {
if (e.target.value.length > maxCharacters) return;
setAppDescription(e.target.value);
};
return ( return (
<Form.Field> <Form.Field context={appDescription}>
<Form.Label isRequired>Description</Form.Label> <Form.Label>Description</Form.Label>
<Form.Textarea <Form.Textarea
placeholder="Add information about your project here." placeholder="Add information about your project here."
css={{ height: 'auto' }} css={{ height: 'auto' }}
value={appDescription}
onChange={handleAppDescriptionChange}
/> />
<Form.MaxLength> <Form.Overline />
{appDescription.length}/{maxCharacters}
</Form.MaxLength>
</Form.Field> </Form.Field>
); );
}; };

View File

@ -1,25 +1,16 @@
import { Form } from '@/components'; import { Form } from '@/components';
import { Mint } from '../../../mint.context'; import { useMintFormContext } from '../mint-form.context';
const maxCharacters = 100;
export const AppNameField = () => { export const AppNameField = () => {
const { appName, setAppName } = Mint.useContext(); const {
form: { appName },
} = useMintFormContext();
const handleAppNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAppName(e.target.value);
};
return ( return (
<Form.Field> <Form.Field context={appName}>
<Form.Label isRequired>Name</Form.Label> <Form.Label>Name</Form.Label>
<Form.Input <Form.Input placeholder="Your app name" />
placeholder="Your app name" <Form.Overline />
value={appName}
onChange={handleAppNameChange}
/>
<Form.MaxLength>
{appName.length}/{maxCharacters}
</Form.MaxLength>
</Form.Field> </Form.Field>
); );
}; };

View File

@ -1,20 +1,16 @@
import { Form } from '@/components'; import { Form } from '@/components';
import { Mint } from '@/views/mint/mint.context'; import { Mint } from '@/views/mint/mint.context';
import { useMintFormContext } from '../../mint-form.context';
export const DomainField = () => { export const DomainField = () => {
const { domain, setDomain } = Mint.useContext(); const {
form: { domainURL },
const handleDomainChange = (e: React.ChangeEvent<HTMLInputElement>) => { } = useMintFormContext();
setDomain(e.target.value);
};
return ( return (
<Form.Field css={{ flex: 1 }}> <Form.Field context={domainURL} css={{ flex: 1 }}>
<Form.Label isRequired>Domain</Form.Label> <Form.Label>Domain</Form.Label>
<Form.Input <Form.Input placeholder="mydomain.com" />
placeholder="mydomain.com" <Form.Overline />
value={domain}
onChange={handleDomainChange}
/>
</Form.Field> </Form.Field>
); );
}; };

View File

@ -1,35 +1,58 @@
import { Combobox, ComboboxItem, Form } from '@/components'; import { getENSNamesDocument } from '@/../.graphclient';
import { ensActions, useAppDispatch, useEnsStore } from '@/store'; import { ComboboxItem, Form } from '@/components';
import { Mint } from '@/views/mint/mint.context'; import { AppLog } from '@/utils';
import { useQuery } from '@apollo/client';
import { useCallback, useEffect, useMemo } from 'react';
import { useAccount } from 'wagmi'; import { useAccount } from 'wagmi';
import { useMintFormContext } from '../../mint-form.context';
export const EnsField = () => { export const EnsField = () => {
const { ens, ensError, setEns } = Mint.useContext();
const { state, ensNames } = useEnsStore();
const dispatch = useAppDispatch();
const { address } = useAccount(); const { address } = useAccount();
const { data, error } = useQuery(getENSNamesDocument, {
variables: {
address: address?.toString() || '', //should skip if undefined
skip: address === undefined,
},
});
if (state === 'idle' && address) { const {
dispatch(ensActions.fetchEnsNamesThunk(address)); form: { ens },
} } = useMintFormContext();
const handleEnsChange = (item: ComboboxItem) => { const showError = useCallback(() => {
setEns(item); AppLog.errorToast(
}; 'There was an error trying to get your ENS names. Please try again later.'
);
}, [AppLog]);
useEffect(() => {
if (error) {
showError();
}
}, [error, showError]);
const ensNames = useMemo(() => {
const ensList: ComboboxItem[] = [];
if (data && data.account && data.account.domains) {
data.account.domains.forEach((ens) => {
const { name } = ens;
if (name) {
ensList.push({
label: name,
value: name,
});
}
});
}
return ensList;
}, [data]);
return ( return (
<Form.Field css={{ flex: 1 }}> <Form.Field context={ens} css={{ flex: 1 }}>
<Form.Label>ENS</Form.Label> <Form.Label>ENS</Form.Label>
<Combobox <Form.Combobox items={ensNames} />
items={ensNames.map((ens) => ({ <Form.Overline />
label: ens,
value: ens,
}))}
selectedValue={ens}
onChange={handleEnsChange}
withAutocomplete
/>
{ensError && <Form.Error>{ensError}</Form.Error>}
</Form.Field> </Form.Field>
); );
}; };

View File

@ -1,37 +1,26 @@
import { Flex, Form } from '@/components'; import { Flex, Form } from '@/components';
import { AppLog } from '@/utils'; import { useMintFormContext } from '../../mint-form.context';
import { useState } from 'react';
import { Mint } from '../../../../mint.context';
import { fileToBase64, validateFileSize } from '../../form.utils';
import { ColorPicker } from './color-picker';
export const LogoField = () => { export const LogoField = () => {
const { appLogo, setAppLogo, setLogoColor } = Mint.useContext(); const {
const [errorMessage, setErrorMessage] = useState<string | null>(null); form: { appLogo: appLogoContext, logoColor: logoColorContext },
} = useMintFormContext();
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const {
const file = e.target.files?.[0]; value: [appLogo],
} = appLogoContext;
if (file) {
if (validateFileSize(file)) {
const fileBase64 = await fileToBase64(file);
setAppLogo(fileBase64);
setErrorMessage(null);
} else {
setAppLogo('');
setLogoColor('');
setErrorMessage('File size is too big');
}
}
};
return ( return (
<Flex css={{ width: '$full', gap: '$4h', alignItems: 'flex-start' }}> <Flex css={{ width: '$full', gap: '$4h', alignItems: 'flex-start' }}>
<Form.Field> <Form.Field context={appLogoContext}>
<Form.Label isRequired>Logo</Form.Label> <Form.Label>Logo</Form.Label>
<Form.LogoFileInput value={appLogo} onChange={handleFileChange} /> <Form.LogoFileInput />
{errorMessage && <Form.Error>{errorMessage}</Form.Error>} <Form.Overline />
</Form.Field>
<Form.Field context={logoColorContext} css={{ flexGrow: 1 }}>
<Form.ColorPicker logo={appLogo} />
<Form.Overline />
</Form.Field> </Form.Field>
<ColorPicker />
</Flex> </Flex>
); );
}; };

View File

@ -1,16 +1,3 @@
//TODO create env variable
const DEFAULT_MAX_FILE_SIZE = 10; // in KB
/**
* The file size must be capped to a size that the contract can handle
*/
export const validateFileSize = (
file: File,
maxSize = DEFAULT_MAX_FILE_SIZE
): boolean => {
return file.size <= 1024 * maxSize;
};
/** /**
* Converts the File from the input to a base64 string. * Converts the File from the input to a base64 string.
*/ */

View File

@ -1 +1,2 @@
export * from './mint-form'; export * from './mint-form';
export * from './mint-form.context';

View File

@ -0,0 +1,55 @@
import { FormField, useFormField } from '@/components';
import { createContext, StringValidators } from '@/utils';
import { useState } from 'react';
export type MintFormContext = {
form: {
gitBranch: FormField;
gitCommit: FormField;
appName: FormField;
appDescription: FormField;
appLogo: FormField;
logoColor: FormField;
ens: FormField;
domainURL: FormField;
isValid: ReactState<boolean>;
};
};
export const [MintFormProvider, useMintFormContext] =
createContext<MintFormContext>({
name: 'MintFormContext',
hookName: 'useMintFormContext',
providerName: 'MintFormProvider',
});
export const useMintFormContextInit = (): MintFormContext => ({
form: {
gitBranch: useFormField('gitBranch', [StringValidators.required]),
gitCommit: useFormField('gitCommit', [StringValidators.required]),
appName: useFormField('appName', [
StringValidators.required,
StringValidators.maxLength(50),
]),
appDescription: useFormField('appDescription', [
StringValidators.required,
StringValidators.maxLength(250),
StringValidators.hasSpecialCharacters,
]),
appLogo: useFormField('appLogo', [
StringValidators.maxFileSize(10), // in KB
StringValidators.required,
]),
logoColor: useFormField(
'logoColor',
[StringValidators.required],
'#000000'
),
domainURL: useFormField('domainURL', [
StringValidators.required,
StringValidators.isUrl,
]),
ens: useFormField('ens', [], ''),
isValid: useState(false),
},
});

View File

@ -1,4 +1,4 @@
import { Button, Card, Grid, Stepper } from '@/components'; import { Button, Card, Form, Grid, Stepper } from '@/components';
import { Mint } from '../../mint.context'; import { Mint } from '../../mint.context';
import { import {
LogoField, LogoField,
@ -10,24 +10,41 @@ import { MintCardHeader } from '../../mint-card';
import { useAccount } from 'wagmi'; import { useAccount } from 'wagmi';
import { parseColorToNumber } from './form.utils'; import { parseColorToNumber } from './form.utils';
import { AppLog } from '@/utils'; import { AppLog } from '@/utils';
import { useMintFormContext } from './mint-form.context';
export const MintFormStep = () => { export const MintFormStep = () => {
const {
form: {
appName: {
value: [appName],
},
appDescription: {
value: [appDescription],
},
appLogo: {
value: [appLogo],
},
ens: {
value: [ens],
},
domainURL: {
value: [domainURL],
},
gitCommit: {
value: [gitCommit],
},
gitBranch: {
value: [gitBranch],
},
logoColor: {
value: [logoColor],
},
isValid: [isValid],
},
} = useMintFormContext();
const { address } = useAccount(); const { address } = useAccount();
const { nextStep } = Stepper.useContext(); const { nextStep } = Stepper.useContext();
const { const { billing, repositoryName, verifyNFA, setNfaStep } = Mint.useContext();
billing,
appName,
appDescription,
domain,
appLogo,
branchName,
commitHash,
ens,
logoColor,
repositoryName,
verifyNFA,
setNfaStep,
} = Mint.useContext();
const { setArgs } = Mint.useTransactionContext(); const { setArgs } = Mint.useTransactionContext();
const handleNextStep = () => { const handleNextStep = () => {
@ -35,16 +52,15 @@ export const MintFormStep = () => {
AppLog.errorToast('No address found. Please connect your wallet.'); AppLog.errorToast('No address found. Please connect your wallet.');
return; return;
} }
// TODO: we need to make sure all values are correct before
// setting the args otherwise mint may fail // setting the args otherwise mint may fail
setArgs([ setArgs([
address, address,
appName, appName,
appDescription, appDescription,
domain, domainURL,
ens.value, ens,
commitHash, gitCommit,
`${repositoryName.url}/tree/${branchName.label}`, `${repositoryName.url}/tree/${gitBranch}`,
appLogo, appLogo,
parseColorToNumber(logoColor), parseColorToNumber(logoColor),
verifyNFA, verifyNFA,
@ -74,7 +90,7 @@ export const MintFormStep = () => {
<EnsDomainField /> <EnsDomainField />
</Grid> </Grid>
<Button <Button
disabled={!appName || !appDescription || !domain} disabled={!isValid}
colorScheme="blue" colorScheme="blue"
variant="solid" variant="solid"
onClick={handleNextStep} onClick={handleNextStep}

View File

@ -1,5 +1,5 @@
import { Button, Card, Grid } from '@/components'; import { Button, Card, Grid } from '@/components';
import { Mint } from '../mint.context'; import { useMintFormContext } from '../nfa-step/form-step';
import { SVGPreview } from './svg-preview'; import { SVGPreview } from './svg-preview';
type NftCardProps = { type NftCardProps = {
@ -24,7 +24,22 @@ export const NftCard: React.FC<NftCardProps> = ({
isLoading, isLoading,
}) => { }) => {
const size = '26.5rem'; const size = '26.5rem';
const { appLogo, logoColor, appName, ens } = Mint.useContext(); const {
form: {
appName: {
value: [appName],
},
appLogo: {
value: [appLogo],
},
logoColor: {
value: [logoColor],
},
ens: {
value: [ens],
},
},
} = useMintFormContext();
return ( return (
<Card.Container css={{ width: '$107h', p: '$0' }}> <Card.Container css={{ width: '$107h', p: '$0' }}>
@ -32,7 +47,7 @@ export const NftCard: React.FC<NftCardProps> = ({
color={logoColor} color={logoColor}
logo={appLogo} logo={appLogo}
name={appName} name={appName}
ens={ens.label} ens={ens}
size={size} size={size}
css="rounded-t-xhl" css="rounded-t-xhl"
/> />
@ -43,10 +58,7 @@ export const NftCard: React.FC<NftCardProps> = ({
leftIcon={leftIcon} leftIcon={leftIcon}
rightIcon={rightIcon} rightIcon={rightIcon}
/> />
{/* TODO replace for real price when integrate with wallet */}
<span className="text-slate11 text-sm">{message}</span> <span className="text-slate11 text-sm">{message}</span>
{/* TODO add desabled when user doesnt have enough MATIC */}
{/* TODO repalce for app name when connect with context */}
<Button <Button
colorScheme="blue" colorScheme="blue"
variant="solid" variant="solid"

View File

@ -15,6 +15,7 @@ module.exports = {
green4: 'rgba(17, 49, 35, 1)', green4: 'rgba(17, 49, 35, 1)',
green11: 'rgba(76, 195, 138, 1)', green11: 'rgba(76, 195, 138, 1)',
red4: 'rgba(72, 26, 29, 1)', red4: 'rgba(72, 26, 29, 1)',
red9: 'rgba(229, 72, 77, 1)',
red11: 'rgba(255, 99, 105, 1)', red11: 'rgba(255, 99, 105, 1)',
}, },
borderRadius: { borderRadius: {