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:
parent
9df1791c72
commit
ac618f9a32
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.', []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"version": "0.5.4",
|
||||
"timestamp": 1679061942846
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,3 +18,12 @@ query totalTokens {
|
|||
}
|
||||
}
|
||||
|
||||
# query to get the ens name of an address
|
||||
query getENSNames($address: ID!) {
|
||||
account(id: $address) {
|
||||
domains {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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' }}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './color-picker';
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@ export * from './avatar';
|
|||
export * from './separator.styles';
|
||||
export * from './text';
|
||||
export * from './switch';
|
||||
export * from './color-picker';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
|
@ -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]);
|
||||
};
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from './form';
|
||||
export * from './form.utils';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
declare global {
|
||||
type ReactState<S> = [S, Dispatch<SetStateAction<S>>];
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ export * from './object';
|
|||
export * from './context';
|
||||
export * from './toast';
|
||||
export * from './log';
|
||||
export * from './string-validators';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './repo-configuration-body';
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
@ -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('');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './mint-form';
|
||||
export * from './mint-form.context';
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue