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 {
Address,
Bytes,
log,
store,
ethereum,
BigInt,
Address,
Bytes,
log,
store,
ethereum,
BigInt,
} from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config]
import {
ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent,
ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent,
NewAccessPoint as NewAccessPointEvent,
ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent,
ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent,
ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent,
ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent,
NewAccessPoint as NewAccessPointEvent,
ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent,
ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent,
} from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema]
import {
AccessPoint,
Owner,
} from '../generated/schema';
import { AccessPoint, Owner } from '../generated/schema';
/**
* 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?
*/
export function handleNewAccessPoint(event: NewAccessPointEvent): void {
// Create an AccessPoint entity
let accessPointEntity = new AccessPoint(event.params.apName);
accessPointEntity.score = BigInt.fromU32(0);
accessPointEntity.contentVerified = 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.owner = event.params.owner;
accessPointEntity.token = Bytes.fromByteArray(
Bytes.fromBigInt(event.params.tokenId)
);
// Create an AccessPoint entity
let accessPointEntity = new AccessPoint(event.params.apName);
accessPointEntity.score = BigInt.fromU32(0);
accessPointEntity.contentVerified = 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.owner = event.params.owner;
accessPointEntity.token = Bytes.fromByteArray(
Bytes.fromBigInt(event.params.tokenId)
);
// Load / Create an Owner entity
let ownerEntity = Owner.load(event.params.owner);
// Load / Create an Owner entity
let ownerEntity = Owner.load(event.params.owner);
if (!ownerEntity) {
// Create a new owner entity
ownerEntity = new Owner(event.params.owner);
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
ownerEntity.collection = false;
}
if (!ownerEntity) {
// Create a new owner entity
ownerEntity = new Owner(event.params.owner);
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
ownerEntity.collection = false;
}
// Save entities.
accessPointEntity.save();
ownerEntity.save();
// Save entities.
accessPointEntity.save();
ownerEntity.save();
}
/**
* This handler will update the status of an access point entity.
*/
export function handleChangeAccessPointCreationStatus(
event: ChangeAccessPointCreationStatusEvent
event: ChangeAccessPointCreationStatusEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
let status = event.params.status;
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
let status = event.params.status;
if (accessPointEntity) {
switch (status) {
case 0:
accessPointEntity.creationStatus = 'DRAFT';
break;
case 1:
accessPointEntity.creationStatus = 'APPROVED';
break;
case 2:
accessPointEntity.creationStatus = 'REJECTED';
break;
case 3:
accessPointEntity.creationStatus = 'REMOVED';
break;
default:
// Unknown status
log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName]
);
}
if (accessPointEntity) {
switch (status) {
case 0:
accessPointEntity.creationStatus = 'DRAFT';
break;
case 1:
accessPointEntity.creationStatus = 'APPROVED';
break;
case 2:
accessPointEntity.creationStatus = 'REJECTED';
break;
case 3:
accessPointEntity.creationStatus = 'REMOVED';
break;
default:
// Unknown status
log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName]
);
}
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName]
);
}
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}',
[status.toString(), event.params.apName]
);
}
}
/**
* This handler will update the score of an access point entity.
*/
export function handleChangeAccessPointScore(
event: ChangeAccessPointCreationScoreEvent
event: ChangeAccessPointCreationScoreEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
if (accessPointEntity) {
accessPointEntity.score = event.params.score;
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}',
[event.params.score.toString(), event.params.apName]
);
}
if (accessPointEntity) {
accessPointEntity.score = event.params.score;
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}',
[event.params.score.toString(), event.params.apName]
);
}
}
/**
* This handler will update the nameVerified field of an access point entity.
*/
export function handleChangeAccessPointNameVerify(
event: ChangeAccessPointNameVerifyEvent
event: ChangeAccessPointNameVerifyEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
if (accessPointEntity) {
accessPointEntity.nameVerified = event.params.verified;
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[event.params.verified.toString(), event.params.apName]
);
}
if (accessPointEntity) {
accessPointEntity.nameVerified = event.params.verified;
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[event.params.verified.toString(), event.params.apName]
);
}
}
/**
* This handler will update the contentVerified field of an access point entity.
*/
export function handleChangeAccessPointContentVerify(
event: ChangeAccessPointContentVerifyEvent
event: ChangeAccessPointContentVerifyEvent
): void {
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
// Load the AccessPoint entity
let accessPointEntity = AccessPoint.load(event.params.apName);
if (accessPointEntity) {
accessPointEntity.contentVerified = event.params.verified;
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[event.params.verified.toString(), event.params.apName]
);
}
if (accessPointEntity) {
accessPointEntity.contentVerified = event.params.verified;
accessPointEntity.save();
} else {
// Unknown access point
log.error(
'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}',
[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]
import {
MetadataUpdate as MetadataUpdateEvent,
MetadataUpdate1 as MetadataUpdateEvent1,
MetadataUpdate2 as MetadataUpdateEvent2,
MetadataUpdate3 as MetadataUpdateEvent3,
MetadataUpdate4 as MetadataUpdateEvent4,
MetadataUpdate as MetadataUpdateEvent,
MetadataUpdate1 as MetadataUpdateEvent1,
MetadataUpdate2 as MetadataUpdateEvent2,
MetadataUpdate3 as MetadataUpdateEvent3,
MetadataUpdate4 as MetadataUpdateEvent4,
} from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema]
import {
GitRepository as GitRepositoryEntity,
MetadataUpdate,
Token,
GitRepository as GitRepositoryEntity,
MetadataUpdate,
Token,
} from '../generated/schema';
export function handleMetadataUpdateWithStringValue(
event: MetadataUpdateEvent1
event: MetadataUpdateEvent1
): void {
/**
* Metadata handled here:
* setTokenExternalURL
* setTokenENS
* setTokenName
* setTokenDescription
* setTokenLogo
* */
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
/**
* Metadata handled here:
* setTokenExternalURL
* setTokenENS
* setTokenName
* setTokenDescription
* setTokenLogo
* */
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.tokenId = event.params._tokenId;
entity.key = event.params.key;
entity.stringValue = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.tokenId = event.params._tokenId;
entity.key = event.params.key;
entity.stringValue = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
entity.save();
// UPDATE TOKEN
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
// UPDATE TOKEN
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
if (token) {
if (event.params.key == 'externalURL') {
token.externalURL = event.params.value;
} else if (event.params.key == 'ENS') {
token.ENS = event.params.value;
} else if (event.params.key == 'name') {
token.name = event.params.value;
} else if (event.params.key == 'description') {
token.description = event.params.value;
} else {
// logo
token.logo = event.params.value;
}
token.save();
if (token) {
if (event.params.key == 'externalURL') {
token.externalURL = event.params.value;
} else if (event.params.key == 'ENS') {
token.ENS = event.params.value;
} else if (event.params.key == 'name') {
token.name = event.params.value;
} else if (event.params.key == 'description') {
token.description = event.params.value;
} else {
// logo
token.logo = event.params.value;
}
token.save();
}
}
export function handleMetadataUpdateWithDoubleStringValue(
event: MetadataUpdateEvent3
event: MetadataUpdateEvent3
): void {
/**
* setTokenBuild
*/
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
/**
* setTokenBuild
*/
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.key = event.params.key;
entity.tokenId = event.params._tokenId;
entity.doubleStringValue = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.key = event.params.key;
entity.tokenId = event.params._tokenId;
entity.doubleStringValue = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
entity.save();
// UPDATE TOKEN
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
// UPDATE TOKEN
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
if (token) {
if (event.params.key == 'build') {
let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]);
if (!gitRepositoryEntity) {
// Create a new gitRepository entity
gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]);
}
token.commitHash = event.params.value[0];
token.gitRepository = event.params.value[1];
token.save();
gitRepositoryEntity.save();
}
if (token) {
if (event.params.key == 'build') {
let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]);
if (!gitRepositoryEntity) {
// Create a new gitRepository entity
gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]);
}
token.commitHash = event.params.value[0];
token.gitRepository = event.params.value[1];
token.save();
gitRepositoryEntity.save();
}
}
}
export function handleMetadataUpdateWithIntValue(
event: MetadataUpdateEvent2
event: MetadataUpdateEvent2
): void {
/**
* setTokenColor
*/
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
/**
* setTokenColor
*/
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.key = event.params.key;
entity.tokenId = event.params._tokenId;
entity.uint24Value = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.key = event.params.key;
entity.tokenId = event.params._tokenId;
entity.uint24Value = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
entity.save();
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
if (token) {
if (event.params.key == 'color') {
token.color = event.params.value;
}
token.save();
if (token) {
if (event.params.key == 'color') {
token.color = event.params.value;
}
token.save();
}
}
export function handleMetadataUpdateWithBooleanValue(
event: MetadataUpdateEvent4
event: MetadataUpdateEvent4
): void {
/**
* accessPointAutoApproval
*/
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
/**
* accessPointAutoApproval
*/
let entity = new MetadataUpdate(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.key = event.params.key;
entity.tokenId = event.params._tokenId;
entity.booleanValue = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.key = event.params.key;
entity.tokenId = event.params._tokenId;
entity.booleanValue = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
entity.save();
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
let token = Token.load(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
if (token) {
if (event.params.key == 'accessPointAutoApproval') {
token.accessPointAutoApproval = event.params.value;
}
token.save();
if (token) {
if (event.params.key == 'accessPointAutoApproval') {
token.accessPointAutoApproval = event.params.value;
}
}
token.save();
}
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
{
"version": "0.5.4",
"timestamp": 1679061942846
}
}

View File

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

View File

@ -2,10 +2,19 @@ import { Button, Card, Flex, Icon } from '@/components';
import { useRef } from 'react';
// @ts-ignore
import ColorThief from 'colorthief';
import { Mint } from '../../../../mint.context';
export const ColorPicker = () => {
const { appLogo, logoColor, setLogoColor } = Mint.useContext();
export type ColorPickerProps = {
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 imageRef = useRef<HTMLImageElement>(null);
@ -17,6 +26,10 @@ export const ColorPicker = () => {
setLogoColor(hexColor);
};
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLogoColor(e.target.value);
};
const handleColorPickerClick = () => {
inputColorRef.current?.click();
};
@ -43,6 +56,7 @@ export const ColorPicker = () => {
borderRadius: '$md',
color: '$slate12',
zIndex: '$dropdown',
minWidth: '$28',
}}
onClick={handleColorPickerClick}
>
@ -53,14 +67,14 @@ export const ColorPicker = () => {
className="absolute right-16"
type="color"
value={logoColor}
onChange={(e) => setLogoColor(e.target.value)}
onChange={handleColorChange}
/>
</Flex>
</div>
<img
className="hidden"
src={appLogo}
src={logo}
ref={imageRef}
onLoad={handleLogoLoad}
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 { Combobox as ComboboxLib, Transition } from '@headlessui/react';
import 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 { Flex } from '@/components/layout';
import { useDebounce } from '@/hooks/use-debounce';
@ -16,20 +26,16 @@ type ComboboxInputProps = {
*/
leftIcon: IconName;
/**
* Function to handle the input change
* Value to indicate it's invalid
*/
handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/**
* Function to handle the input click. When the user clicks on the input, the list of options will be displayed
*/
handleInputClick: () => void;
};
error?: boolean;
} & ComboboxLibInputProps<'input', ComboboxItem>;
const ComboboxInput = ({
open,
leftIcon,
handleInputChange,
handleInputClick,
error,
...props
}: ComboboxInputProps) => (
<div className="relative w-full">
<Icon
@ -45,14 +51,15 @@ const ComboboxInput = ({
/>
<ComboboxLib.Input
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
? '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}
onChange={handleInputChange}
onClick={handleInputClick}
{...props}
/>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
<Icon name="chevron-down" css={{ fontSize: '$xs' }} />
@ -134,131 +141,151 @@ export type ComboboxProps = {
/**
* 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> = ({
items,
selectedValue = { value: '', label: '' },
withAutocomplete = false,
leftIcon = 'search',
onChange,
}) => {
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
[]
);
export const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
(
{
items,
selectedValue = { value: '', label: '' },
withAutocomplete = false,
leftIcon = 'search',
onChange,
onBlur,
error = false,
},
ref
) => {
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
[]
);
useEffect(() => {
// If the selected value doesn't exist in the list of items, we add it
if (
items.filter((item) => item === selectedValue).length === 0 &&
selectedValue.value !== undefined &&
autocompleteItems.length === 0 &&
withAutocomplete
) {
setAutocompleteItems([selectedValue]);
}
}, [selectedValue]);
useEffect(() => {
// If the selected value doesn't exist in the list of items, we add it
if (
items.filter((item) => item === selectedValue).length === 0 &&
selectedValue.value !== undefined &&
autocompleteItems.length === 0 &&
withAutocomplete
) {
setAutocompleteItems([selectedValue]);
}
}, [selectedValue]);
useEffect(() => {
setFilteredItems(items);
}, [items]);
const buttonRef = useRef<HTMLButtonElement>(null);
const handleSearch = useDebounce((searchValue: string) => {
if (searchValue === '') {
useEffect(() => {
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([]);
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);
return (
<ComboboxLib
value={selectedValue}
by="value"
onChange={handleComboboxChange}
>
{({ 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>) => {
event.stopPropagation();
handleSearch(event.target.value);
};
const handleInputClick = () => {
buttonRef.current?.click();
};
const handleComboboxChange = (option: ComboboxItem) => {
onChange(option);
};
const handleLeaveTransition = () => {
setFilteredItems(items);
if (selectedValue.value === undefined && withAutocomplete) {
setAutocompleteItems([]);
handleComboboxChange({} as ComboboxItem);
}
};
return (
<ComboboxLib
value={selectedValue}
by="value"
onChange={handleComboboxChange}
>
{({ open }) => (
<div className="relative">
<ComboboxInput
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
open={open}
leftIcon={leftIcon}
/>
<ComboboxLib.Button ref={buttonRef} className="hidden" />
<Transition
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>
);
};
<Transition
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 './text';
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 { Icon } from '../icon';
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',
},
});
import { InputFileStyles as S } from './input-file.styles';
type InputFileProps = {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
} & React.ComponentProps<typeof Flex>;
} & React.ComponentProps<typeof S.Border>;
export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
({ value: file, onChange, css, ...props }, ref) => {
@ -40,24 +22,13 @@ export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
return (
<>
<Flex
css={{
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
...(css || {}),
}}
ref={ref}
{...props}
onClick={handleInputClick}
>
<S.Container onClick={handleInputClick}>
{file !== '' ? (
<img className="absolute w-14 h-14" src={file} alt="logo" />
) : (
<Icon name="upload" size="md" css={{ position: 'absolute' }} />
)}
<BorderInput />
<S.Border {...props} ref={ref} />
<input
type="file"
className="hidden"
@ -65,7 +36,7 @@ export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
ref={inputFileRef}
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',
});
static readonly Overline = styled('div', {
display: 'flex',
justifyContent: 'space-between',
});
static readonly OverlineErrors = styled(Flex, {
flexDirection: 'column',
});
static readonly ErrorMessage = styled('span', {
color: '$red11',
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 {
FormFieldContext,
FormFieldProvider,
useFormFieldContext,
} from './form-field.context';
import {
FormProvider,
useFormContext,
useFormFieldValidatorValue,
} from './form.context';
import { FormStyles } from './form.styles';
export abstract class Form {
static readonly Root = FormProvider;
static readonly Field = forwardRef<HTMLDivElement, Form.FieldProps>(
({ children, ...props }, ref) => {
({ children, context, ...props }, ref) => {
const {
value: [value],
} = context;
const validationEnabled = useState(Boolean(value));
return (
<FormStyles.Field ref={ref} {...props}>
{children}
</FormStyles.Field>
<FormFieldProvider value={{ ...context, validationEnabled }}>
<FormStyles.Field ref={ref} {...props}>
{children}
</FormStyles.Field>
</FormFieldProvider>
);
}
);
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) => {
const { validators } = useFormFieldContext();
const isRequired = useMemo(
() => hasValidator(validators, 'required'),
[validators]
);
return (
<FormStyles.MaxLength ref={ref} {...props}>
<FormStyles.Label ref={ref} {...props}>
{children}
</FormStyles.MaxLength>
{isRequired && <FormStyles.RequiredLabel>*</FormStyles.RequiredLabel>}
</FormStyles.Label>
);
}
);
static readonly Error = forwardRef<HTMLDivElement, Form.ErrorProps>(
({ children, ...props }, ref) => (
<FormStyles.ErrorMessage ref={ref} {...props}>
{children}
</FormStyles.ErrorMessage>
)
);
static readonly Overline = forwardRef<HTMLDivElement>((props, ref) => {
const {
validations: [validations],
} = useFormContext();
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>(
(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<
HTMLTextAreaElement,
Form.TextareaProps
>((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<
HTMLInputElement,
Form.LogoFileInputProps
>((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 type FieldProps = {
children: React.ReactNode;
context: Omit<FormFieldContext, 'validationEnabled'>;
} & React.ComponentProps<typeof FormStyles.Field>;
export type LabelProps = { isRequired?: boolean } & React.ComponentProps<
typeof FormStyles.Label
>;
export type LabelProps = React.ComponentProps<typeof FormStyles.Label>;
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.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 = [
{
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) => {
setTimeout(() => {
resolved({
listSites,
});
resolved(listBranches);
}, 2500);
});
};

View File

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

View File

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

View File

@ -4,3 +4,4 @@ export * from './object';
export * from './context';
export * from './toast';
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 = () => {
const [selectedValue, setSelectedValue] = useState({} as ComboboxItem);
const [selectedValueAutocomplete, setSelectedValueAutocomplete] = useState(
{} as ComboboxItem
);
const [selectedValue, setSelectedValue] = useState('');
const [selectedValueAutocomplete, setSelectedValueAutocomplete] =
useState('');
const handleComboboxChange = (item: ComboboxItem) => {
setSelectedValue(item);
const handleComboboxChange = (value: string) => {
setSelectedValue(value);
};
const handleComboboxChangeAutocomplete = (item: ComboboxItem) => {
setSelectedValueAutocomplete(item);
const handleComboboxChangeAutocomplete = (value: string) => {
setSelectedValueAutocomplete(value);
};
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 { 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';
export const RepoConfigurationBody = () => {
const { repositoryName, branchName, commitHash } = Mint.useContext();
const {
form: {
isValid: [isValid],
},
} = useMintFormContext();
const { repositoryName } = Mint.useContext();
const { nextStep } = Stepper.useContext();
@ -31,7 +38,7 @@ export const RepoConfigurationBody = () => {
/>
<RepoBranchCommitFields />
<Button
disabled={!branchName.value || !commitHash}
disabled={!isValid}
colorScheme="blue"
variant="solid"
onClick={handleContinueClick}

View File

@ -1,13 +1,24 @@
import { DropdownItem } from '@/components';
import { MintCardHeader } from '@/views/mint/mint-card';
import { Mint } from '@/views/mint/mint.context';
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
export const RepoConfigurationHeader = () => {
const { setGithubStep, setBranchName, setCommitHash } = Mint.useContext();
const { setGithubStep } = Mint.useContext();
const {
form: {
gitBranch: {
value: [, setBranchName],
},
gitCommit: {
value: [, setCommitHash],
},
},
} = useMintFormContext();
const handlePrevStepClick = () => {
setGithubStep(2);
setBranchName({} as DropdownItem);
setBranchName('');
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 { useDebounce } from '@/hooks/use-debounce';
import { useGithubStore } from '@/store';
import { MintCardHeader } from '@/views/mint/mint-card';
import { Mint } from '@/views/mint/mint.context';
import React, { forwardRef, useState } from 'react';
import React, { useState } from 'react';
import { RepositoriesList } from './repositories-list';
import { UserOrgsCombobox } from './users-orgs-combobox';
@ -44,9 +52,27 @@ export const GithubRepositoryConnection: React.FC = () => {
return (
<Card.Container css={{ maxWidth: '$107h', maxHeight: '$95h', pr: '$3h' }}>
<MintCardHeader
<Card.Heading
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' }}>
<Grid css={{ rowGap: '$2' }}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,35 +1,58 @@
import { Combobox, ComboboxItem, Form } from '@/components';
import { ensActions, useAppDispatch, useEnsStore } from '@/store';
import { Mint } from '@/views/mint/mint.context';
import { getENSNamesDocument } from '@/../.graphclient';
import { ComboboxItem, Form } from '@/components';
import { AppLog } from '@/utils';
import { useQuery } from '@apollo/client';
import { useCallback, useEffect, useMemo } from 'react';
import { useAccount } from 'wagmi';
import { useMintFormContext } from '../../mint-form.context';
export const EnsField = () => {
const { ens, ensError, setEns } = Mint.useContext();
const { state, ensNames } = useEnsStore();
const dispatch = useAppDispatch();
const { address } = useAccount();
const { data, error } = useQuery(getENSNamesDocument, {
variables: {
address: address?.toString() || '', //should skip if undefined
skip: address === undefined,
},
});
if (state === 'idle' && address) {
dispatch(ensActions.fetchEnsNamesThunk(address));
}
const {
form: { ens },
} = useMintFormContext();
const handleEnsChange = (item: ComboboxItem) => {
setEns(item);
};
const showError = useCallback(() => {
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 (
<Form.Field css={{ flex: 1 }}>
<Form.Field context={ens} css={{ flex: 1 }}>
<Form.Label>ENS</Form.Label>
<Combobox
items={ensNames.map((ens) => ({
label: ens,
value: ens,
}))}
selectedValue={ens}
onChange={handleEnsChange}
withAutocomplete
/>
{ensError && <Form.Error>{ensError}</Form.Error>}
<Form.Combobox items={ensNames} />
<Form.Overline />
</Form.Field>
);
};

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { Button, Card, Grid } from '@/components';
import { Mint } from '../mint.context';
import { useMintFormContext } from '../nfa-step/form-step';
import { SVGPreview } from './svg-preview';
type NftCardProps = {
@ -24,7 +24,22 @@ export const NftCard: React.FC<NftCardProps> = ({
isLoading,
}) => {
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 (
<Card.Container css={{ width: '$107h', p: '$0' }}>
@ -32,7 +47,7 @@ export const NftCard: React.FC<NftCardProps> = ({
color={logoColor}
logo={appLogo}
name={appName}
ens={ens.label}
ens={ens}
size={size}
css="rounded-t-xhl"
/>
@ -43,10 +58,7 @@ export const NftCard: React.FC<NftCardProps> = ({
leftIcon={leftIcon}
rightIcon={rightIcon}
/>
{/* TODO replace for real price when integrate with wallet */}
<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
colorScheme="blue"
variant="solid"

View File

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