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 {
|
import {
|
||||||
Address,
|
Address,
|
||||||
Bytes,
|
Bytes,
|
||||||
log,
|
log,
|
||||||
store,
|
store,
|
||||||
ethereum,
|
ethereum,
|
||||||
BigInt,
|
BigInt,
|
||||||
} from '@graphprotocol/graph-ts';
|
} from '@graphprotocol/graph-ts';
|
||||||
|
|
||||||
// Event Imports [based on the yaml config]
|
// Event Imports [based on the yaml config]
|
||||||
import {
|
import {
|
||||||
ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent,
|
ChangeAccessPointCreationStatus as ChangeAccessPointCreationStatusEvent,
|
||||||
ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent,
|
ChangeAccessPointScore as ChangeAccessPointCreationScoreEvent,
|
||||||
NewAccessPoint as NewAccessPointEvent,
|
NewAccessPoint as NewAccessPointEvent,
|
||||||
ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent,
|
ChangeAccessPointNameVerify as ChangeAccessPointNameVerifyEvent,
|
||||||
ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent,
|
ChangeAccessPointContentVerify as ChangeAccessPointContentVerifyEvent,
|
||||||
} from '../generated/FleekNFA/FleekNFA';
|
} from '../generated/FleekNFA/FleekNFA';
|
||||||
|
|
||||||
// Entity Imports [based on the schema]
|
// Entity Imports [based on the schema]
|
||||||
import {
|
import { AccessPoint, Owner } from '../generated/schema';
|
||||||
AccessPoint,
|
|
||||||
Owner,
|
|
||||||
} from '../generated/schema';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handler will create and load entities in the following order:
|
* This handler will create and load entities in the following order:
|
||||||
|
|
@ -29,133 +26,133 @@ import {
|
||||||
* Note to discuss later: Should a `NewAccessPoint` entity be also created and defined?
|
* Note to discuss later: Should a `NewAccessPoint` entity be also created and defined?
|
||||||
*/
|
*/
|
||||||
export function handleNewAccessPoint(event: NewAccessPointEvent): void {
|
export function handleNewAccessPoint(event: NewAccessPointEvent): void {
|
||||||
// Create an AccessPoint entity
|
// Create an AccessPoint entity
|
||||||
let accessPointEntity = new AccessPoint(event.params.apName);
|
let accessPointEntity = new AccessPoint(event.params.apName);
|
||||||
accessPointEntity.score = BigInt.fromU32(0);
|
accessPointEntity.score = BigInt.fromU32(0);
|
||||||
accessPointEntity.contentVerified = false;
|
accessPointEntity.contentVerified = false;
|
||||||
accessPointEntity.nameVerified = false;
|
accessPointEntity.nameVerified = false;
|
||||||
accessPointEntity.creationStatus = 'DRAFT'; // Since a `ChangeAccessPointCreationStatus` event is emitted instantly after `NewAccessPoint`, the status will be updated in that handler.
|
accessPointEntity.creationStatus = 'DRAFT'; // Since a `ChangeAccessPointCreationStatus` event is emitted instantly after `NewAccessPoint`, the status will be updated in that handler.
|
||||||
accessPointEntity.owner = event.params.owner;
|
accessPointEntity.owner = event.params.owner;
|
||||||
accessPointEntity.token = Bytes.fromByteArray(
|
accessPointEntity.token = Bytes.fromByteArray(
|
||||||
Bytes.fromBigInt(event.params.tokenId)
|
Bytes.fromBigInt(event.params.tokenId)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load / Create an Owner entity
|
// Load / Create an Owner entity
|
||||||
let ownerEntity = Owner.load(event.params.owner);
|
let ownerEntity = Owner.load(event.params.owner);
|
||||||
|
|
||||||
if (!ownerEntity) {
|
if (!ownerEntity) {
|
||||||
// Create a new owner entity
|
// Create a new owner entity
|
||||||
ownerEntity = new Owner(event.params.owner);
|
ownerEntity = new Owner(event.params.owner);
|
||||||
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
|
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
|
||||||
ownerEntity.collection = false;
|
ownerEntity.collection = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save entities.
|
// Save entities.
|
||||||
accessPointEntity.save();
|
accessPointEntity.save();
|
||||||
ownerEntity.save();
|
ownerEntity.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handler will update the status of an access point entity.
|
* This handler will update the status of an access point entity.
|
||||||
*/
|
*/
|
||||||
export function handleChangeAccessPointCreationStatus(
|
export function handleChangeAccessPointCreationStatus(
|
||||||
event: ChangeAccessPointCreationStatusEvent
|
event: ChangeAccessPointCreationStatusEvent
|
||||||
): void {
|
): void {
|
||||||
// Load the AccessPoint entity
|
// Load the AccessPoint entity
|
||||||
let accessPointEntity = AccessPoint.load(event.params.apName);
|
let accessPointEntity = AccessPoint.load(event.params.apName);
|
||||||
let status = event.params.status;
|
let status = event.params.status;
|
||||||
|
|
||||||
if (accessPointEntity) {
|
if (accessPointEntity) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0:
|
case 0:
|
||||||
accessPointEntity.creationStatus = 'DRAFT';
|
accessPointEntity.creationStatus = 'DRAFT';
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
accessPointEntity.creationStatus = 'APPROVED';
|
accessPointEntity.creationStatus = 'APPROVED';
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
accessPointEntity.creationStatus = 'REJECTED';
|
accessPointEntity.creationStatus = 'REJECTED';
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
accessPointEntity.creationStatus = 'REMOVED';
|
accessPointEntity.creationStatus = 'REMOVED';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Unknown status
|
// Unknown status
|
||||||
log.error(
|
log.error(
|
||||||
'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}',
|
'Unable to handle ChangeAccessPointCreationStatus. Unknown status. Status: {}, AccessPoint: {}',
|
||||||
[status.toString(), event.params.apName]
|
[status.toString(), event.params.apName]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
accessPointEntity.save();
|
accessPointEntity.save();
|
||||||
} else {
|
} else {
|
||||||
// Unknown access point
|
// Unknown access point
|
||||||
log.error(
|
log.error(
|
||||||
'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}',
|
'Unable to handle ChangeAccessPointCreationStatus. Unknown access point. Status: {}, AccessPoint: {}',
|
||||||
[status.toString(), event.params.apName]
|
[status.toString(), event.params.apName]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handler will update the score of an access point entity.
|
* This handler will update the score of an access point entity.
|
||||||
*/
|
*/
|
||||||
export function handleChangeAccessPointScore(
|
export function handleChangeAccessPointScore(
|
||||||
event: ChangeAccessPointCreationScoreEvent
|
event: ChangeAccessPointCreationScoreEvent
|
||||||
): void {
|
): void {
|
||||||
// Load the AccessPoint entity
|
// Load the AccessPoint entity
|
||||||
let accessPointEntity = AccessPoint.load(event.params.apName);
|
let accessPointEntity = AccessPoint.load(event.params.apName);
|
||||||
|
|
||||||
if (accessPointEntity) {
|
if (accessPointEntity) {
|
||||||
accessPointEntity.score = event.params.score;
|
accessPointEntity.score = event.params.score;
|
||||||
accessPointEntity.save();
|
accessPointEntity.save();
|
||||||
} else {
|
} else {
|
||||||
// Unknown access point
|
// Unknown access point
|
||||||
log.error(
|
log.error(
|
||||||
'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}',
|
'Unable to handle ChangeAccessPointScore. Unknown access point. Score: {}, AccessPoint: {}',
|
||||||
[event.params.score.toString(), event.params.apName]
|
[event.params.score.toString(), event.params.apName]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handler will update the nameVerified field of an access point entity.
|
* This handler will update the nameVerified field of an access point entity.
|
||||||
*/
|
*/
|
||||||
export function handleChangeAccessPointNameVerify(
|
export function handleChangeAccessPointNameVerify(
|
||||||
event: ChangeAccessPointNameVerifyEvent
|
event: ChangeAccessPointNameVerifyEvent
|
||||||
): void {
|
): void {
|
||||||
// Load the AccessPoint entity
|
// Load the AccessPoint entity
|
||||||
let accessPointEntity = AccessPoint.load(event.params.apName);
|
let accessPointEntity = AccessPoint.load(event.params.apName);
|
||||||
|
|
||||||
if (accessPointEntity) {
|
if (accessPointEntity) {
|
||||||
accessPointEntity.nameVerified = event.params.verified;
|
accessPointEntity.nameVerified = event.params.verified;
|
||||||
accessPointEntity.save();
|
accessPointEntity.save();
|
||||||
} else {
|
} else {
|
||||||
// Unknown access point
|
// Unknown access point
|
||||||
log.error(
|
log.error(
|
||||||
'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}',
|
'Unable to handle ChangeAccessPointNameVerify. Unknown access point. Verified: {}, AccessPoint: {}',
|
||||||
[event.params.verified.toString(), event.params.apName]
|
[event.params.verified.toString(), event.params.apName]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handler will update the contentVerified field of an access point entity.
|
* This handler will update the contentVerified field of an access point entity.
|
||||||
*/
|
*/
|
||||||
export function handleChangeAccessPointContentVerify(
|
export function handleChangeAccessPointContentVerify(
|
||||||
event: ChangeAccessPointContentVerifyEvent
|
event: ChangeAccessPointContentVerifyEvent
|
||||||
): void {
|
): void {
|
||||||
// Load the AccessPoint entity
|
// Load the AccessPoint entity
|
||||||
let accessPointEntity = AccessPoint.load(event.params.apName);
|
let accessPointEntity = AccessPoint.load(event.params.apName);
|
||||||
|
|
||||||
if (accessPointEntity) {
|
if (accessPointEntity) {
|
||||||
accessPointEntity.contentVerified = event.params.verified;
|
accessPointEntity.contentVerified = event.params.verified;
|
||||||
accessPointEntity.save();
|
accessPointEntity.save();
|
||||||
} else {
|
} else {
|
||||||
// Unknown access point
|
// Unknown access point
|
||||||
log.error(
|
log.error(
|
||||||
'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}',
|
'Unable to handle ChangeAccessPointContentVerify. Unknown access point. Verified: {}, AccessPoint: {}',
|
||||||
[event.params.verified.toString(), event.params.apName]
|
[event.params.verified.toString(), event.params.apName]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,163 +2,163 @@ import { Bytes } from '@graphprotocol/graph-ts';
|
||||||
|
|
||||||
// Event Imports [based on the yaml config]
|
// Event Imports [based on the yaml config]
|
||||||
import {
|
import {
|
||||||
MetadataUpdate as MetadataUpdateEvent,
|
MetadataUpdate as MetadataUpdateEvent,
|
||||||
MetadataUpdate1 as MetadataUpdateEvent1,
|
MetadataUpdate1 as MetadataUpdateEvent1,
|
||||||
MetadataUpdate2 as MetadataUpdateEvent2,
|
MetadataUpdate2 as MetadataUpdateEvent2,
|
||||||
MetadataUpdate3 as MetadataUpdateEvent3,
|
MetadataUpdate3 as MetadataUpdateEvent3,
|
||||||
MetadataUpdate4 as MetadataUpdateEvent4,
|
MetadataUpdate4 as MetadataUpdateEvent4,
|
||||||
} from '../generated/FleekNFA/FleekNFA';
|
} from '../generated/FleekNFA/FleekNFA';
|
||||||
|
|
||||||
// Entity Imports [based on the schema]
|
// Entity Imports [based on the schema]
|
||||||
import {
|
import {
|
||||||
GitRepository as GitRepositoryEntity,
|
GitRepository as GitRepositoryEntity,
|
||||||
MetadataUpdate,
|
MetadataUpdate,
|
||||||
Token,
|
Token,
|
||||||
} from '../generated/schema';
|
} from '../generated/schema';
|
||||||
|
|
||||||
export function handleMetadataUpdateWithStringValue(
|
export function handleMetadataUpdateWithStringValue(
|
||||||
event: MetadataUpdateEvent1
|
event: MetadataUpdateEvent1
|
||||||
): void {
|
): void {
|
||||||
/**
|
/**
|
||||||
* Metadata handled here:
|
* Metadata handled here:
|
||||||
* setTokenExternalURL
|
* setTokenExternalURL
|
||||||
* setTokenENS
|
* setTokenENS
|
||||||
* setTokenName
|
* setTokenName
|
||||||
* setTokenDescription
|
* setTokenDescription
|
||||||
* setTokenLogo
|
* setTokenLogo
|
||||||
* */
|
* */
|
||||||
let entity = new MetadataUpdate(
|
let entity = new MetadataUpdate(
|
||||||
event.transaction.hash.concatI32(event.logIndex.toI32())
|
event.transaction.hash.concatI32(event.logIndex.toI32())
|
||||||
);
|
);
|
||||||
|
|
||||||
entity.tokenId = event.params._tokenId;
|
entity.tokenId = event.params._tokenId;
|
||||||
entity.key = event.params.key;
|
entity.key = event.params.key;
|
||||||
entity.stringValue = event.params.value;
|
entity.stringValue = event.params.value;
|
||||||
entity.blockNumber = event.block.number;
|
entity.blockNumber = event.block.number;
|
||||||
entity.blockTimestamp = event.block.timestamp;
|
entity.blockTimestamp = event.block.timestamp;
|
||||||
entity.transactionHash = event.transaction.hash;
|
entity.transactionHash = event.transaction.hash;
|
||||||
|
|
||||||
entity.save();
|
entity.save();
|
||||||
|
|
||||||
// UPDATE TOKEN
|
// UPDATE TOKEN
|
||||||
let token = Token.load(
|
let token = Token.load(
|
||||||
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (event.params.key == 'externalURL') {
|
if (event.params.key == 'externalURL') {
|
||||||
token.externalURL = event.params.value;
|
token.externalURL = event.params.value;
|
||||||
} else if (event.params.key == 'ENS') {
|
} else if (event.params.key == 'ENS') {
|
||||||
token.ENS = event.params.value;
|
token.ENS = event.params.value;
|
||||||
} else if (event.params.key == 'name') {
|
} else if (event.params.key == 'name') {
|
||||||
token.name = event.params.value;
|
token.name = event.params.value;
|
||||||
} else if (event.params.key == 'description') {
|
} else if (event.params.key == 'description') {
|
||||||
token.description = event.params.value;
|
token.description = event.params.value;
|
||||||
} else {
|
} else {
|
||||||
// logo
|
// logo
|
||||||
token.logo = event.params.value;
|
token.logo = event.params.value;
|
||||||
}
|
|
||||||
token.save();
|
|
||||||
}
|
}
|
||||||
|
token.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleMetadataUpdateWithDoubleStringValue(
|
export function handleMetadataUpdateWithDoubleStringValue(
|
||||||
event: MetadataUpdateEvent3
|
event: MetadataUpdateEvent3
|
||||||
): void {
|
): void {
|
||||||
/**
|
/**
|
||||||
* setTokenBuild
|
* setTokenBuild
|
||||||
*/
|
*/
|
||||||
let entity = new MetadataUpdate(
|
let entity = new MetadataUpdate(
|
||||||
event.transaction.hash.concatI32(event.logIndex.toI32())
|
event.transaction.hash.concatI32(event.logIndex.toI32())
|
||||||
);
|
);
|
||||||
|
|
||||||
entity.key = event.params.key;
|
entity.key = event.params.key;
|
||||||
entity.tokenId = event.params._tokenId;
|
entity.tokenId = event.params._tokenId;
|
||||||
entity.doubleStringValue = event.params.value;
|
entity.doubleStringValue = event.params.value;
|
||||||
entity.blockNumber = event.block.number;
|
entity.blockNumber = event.block.number;
|
||||||
entity.blockTimestamp = event.block.timestamp;
|
entity.blockTimestamp = event.block.timestamp;
|
||||||
entity.transactionHash = event.transaction.hash;
|
entity.transactionHash = event.transaction.hash;
|
||||||
|
|
||||||
entity.save();
|
entity.save();
|
||||||
|
|
||||||
// UPDATE TOKEN
|
// UPDATE TOKEN
|
||||||
let token = Token.load(
|
let token = Token.load(
|
||||||
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (event.params.key == 'build') {
|
if (event.params.key == 'build') {
|
||||||
let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]);
|
let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]);
|
||||||
if (!gitRepositoryEntity) {
|
if (!gitRepositoryEntity) {
|
||||||
// Create a new gitRepository entity
|
// Create a new gitRepository entity
|
||||||
gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]);
|
gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]);
|
||||||
}
|
}
|
||||||
token.commitHash = event.params.value[0];
|
token.commitHash = event.params.value[0];
|
||||||
token.gitRepository = event.params.value[1];
|
token.gitRepository = event.params.value[1];
|
||||||
token.save();
|
token.save();
|
||||||
gitRepositoryEntity.save();
|
gitRepositoryEntity.save();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleMetadataUpdateWithIntValue(
|
export function handleMetadataUpdateWithIntValue(
|
||||||
event: MetadataUpdateEvent2
|
event: MetadataUpdateEvent2
|
||||||
): void {
|
): void {
|
||||||
/**
|
/**
|
||||||
* setTokenColor
|
* setTokenColor
|
||||||
*/
|
*/
|
||||||
let entity = new MetadataUpdate(
|
let entity = new MetadataUpdate(
|
||||||
event.transaction.hash.concatI32(event.logIndex.toI32())
|
event.transaction.hash.concatI32(event.logIndex.toI32())
|
||||||
);
|
);
|
||||||
|
|
||||||
entity.key = event.params.key;
|
entity.key = event.params.key;
|
||||||
entity.tokenId = event.params._tokenId;
|
entity.tokenId = event.params._tokenId;
|
||||||
entity.uint24Value = event.params.value;
|
entity.uint24Value = event.params.value;
|
||||||
entity.blockNumber = event.block.number;
|
entity.blockNumber = event.block.number;
|
||||||
entity.blockTimestamp = event.block.timestamp;
|
entity.blockTimestamp = event.block.timestamp;
|
||||||
entity.transactionHash = event.transaction.hash;
|
entity.transactionHash = event.transaction.hash;
|
||||||
|
|
||||||
entity.save();
|
entity.save();
|
||||||
|
|
||||||
let token = Token.load(
|
let token = Token.load(
|
||||||
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (event.params.key == 'color') {
|
if (event.params.key == 'color') {
|
||||||
token.color = event.params.value;
|
token.color = event.params.value;
|
||||||
}
|
|
||||||
token.save();
|
|
||||||
}
|
}
|
||||||
|
token.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleMetadataUpdateWithBooleanValue(
|
export function handleMetadataUpdateWithBooleanValue(
|
||||||
event: MetadataUpdateEvent4
|
event: MetadataUpdateEvent4
|
||||||
): void {
|
): void {
|
||||||
/**
|
/**
|
||||||
* accessPointAutoApproval
|
* accessPointAutoApproval
|
||||||
*/
|
*/
|
||||||
let entity = new MetadataUpdate(
|
let entity = new MetadataUpdate(
|
||||||
event.transaction.hash.concatI32(event.logIndex.toI32())
|
event.transaction.hash.concatI32(event.logIndex.toI32())
|
||||||
);
|
);
|
||||||
|
|
||||||
entity.key = event.params.key;
|
entity.key = event.params.key;
|
||||||
entity.tokenId = event.params._tokenId;
|
entity.tokenId = event.params._tokenId;
|
||||||
entity.booleanValue = event.params.value;
|
entity.booleanValue = event.params.value;
|
||||||
entity.blockNumber = event.block.number;
|
entity.blockNumber = event.block.number;
|
||||||
entity.blockTimestamp = event.block.timestamp;
|
entity.blockTimestamp = event.block.timestamp;
|
||||||
entity.transactionHash = event.transaction.hash;
|
entity.transactionHash = event.transaction.hash;
|
||||||
|
|
||||||
entity.save();
|
entity.save();
|
||||||
|
|
||||||
let token = Token.load(
|
let token = Token.load(
|
||||||
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (event.params.key == 'accessPointAutoApproval') {
|
if (event.params.key == 'accessPointAutoApproval') {
|
||||||
token.accessPointAutoApproval = event.params.value;
|
token.accessPointAutoApproval = event.params.value;
|
||||||
}
|
|
||||||
token.save();
|
|
||||||
}
|
}
|
||||||
}
|
token.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,85 @@
|
||||||
import {
|
import { Bytes, log } from '@graphprotocol/graph-ts';
|
||||||
Bytes,
|
|
||||||
log,
|
|
||||||
} from '@graphprotocol/graph-ts';
|
|
||||||
|
|
||||||
// Event Imports [based on the yaml config]
|
// Event Imports [based on the yaml config]
|
||||||
import {
|
import { NewMint as NewMintEvent } from '../generated/FleekNFA/FleekNFA';
|
||||||
NewMint as NewMintEvent,
|
|
||||||
} from '../generated/FleekNFA/FleekNFA';
|
|
||||||
|
|
||||||
// Entity Imports [based on the schema]
|
// Entity Imports [based on the schema]
|
||||||
import {
|
import {
|
||||||
Owner,
|
Owner,
|
||||||
GitRepository as GitRepositoryEntity,
|
GitRepository as GitRepositoryEntity,
|
||||||
NewMint,
|
NewMint,
|
||||||
Token,
|
Token,
|
||||||
} from '../generated/schema';
|
} from '../generated/schema';
|
||||||
|
|
||||||
export function handleNewMint(event: NewMintEvent): void {
|
export function handleNewMint(event: NewMintEvent): void {
|
||||||
let newMintEntity = new NewMint(
|
let newMintEntity = new NewMint(
|
||||||
event.transaction.hash.concatI32(event.logIndex.toI32())
|
event.transaction.hash.concatI32(event.logIndex.toI32())
|
||||||
);
|
);
|
||||||
|
|
||||||
let name = event.params.name;
|
let name = event.params.name;
|
||||||
let description = event.params.description;
|
let description = event.params.description;
|
||||||
let externalURL = event.params.externalURL;
|
let externalURL = event.params.externalURL;
|
||||||
let ENS = event.params.ENS;
|
let ENS = event.params.ENS;
|
||||||
let gitRepository = event.params.gitRepository;
|
let gitRepository = event.params.gitRepository;
|
||||||
let commitHash = event.params.commitHash;
|
let commitHash = event.params.commitHash;
|
||||||
let logo = event.params.logo;
|
let logo = event.params.logo;
|
||||||
let color = event.params.color;
|
let color = event.params.color;
|
||||||
let accessPointAutoApproval = event.params.accessPointAutoApproval;
|
let accessPointAutoApproval = event.params.accessPointAutoApproval;
|
||||||
let tokenId = event.params.tokenId;
|
let tokenId = event.params.tokenId;
|
||||||
let ownerAddress = event.params.owner;
|
let ownerAddress = event.params.owner;
|
||||||
let verifierAddress = event.params.verifier;
|
let verifierAddress = event.params.verifier;
|
||||||
|
|
||||||
newMintEntity.tokenId = tokenId;
|
newMintEntity.tokenId = tokenId;
|
||||||
newMintEntity.name = name;
|
newMintEntity.name = name;
|
||||||
newMintEntity.description = description;
|
newMintEntity.description = description;
|
||||||
newMintEntity.externalURL = externalURL;
|
newMintEntity.externalURL = externalURL;
|
||||||
newMintEntity.ENS = ENS;
|
newMintEntity.ENS = ENS;
|
||||||
newMintEntity.commitHash = commitHash;
|
newMintEntity.commitHash = commitHash;
|
||||||
newMintEntity.gitRepository = gitRepository;
|
newMintEntity.gitRepository = gitRepository;
|
||||||
newMintEntity.logo = logo;
|
newMintEntity.logo = logo;
|
||||||
newMintEntity.color = color;
|
newMintEntity.color = color;
|
||||||
newMintEntity.accessPointAutoApproval = accessPointAutoApproval;
|
newMintEntity.accessPointAutoApproval = accessPointAutoApproval;
|
||||||
newMintEntity.triggeredBy = event.params.minter;
|
newMintEntity.triggeredBy = event.params.minter;
|
||||||
newMintEntity.owner = ownerAddress;
|
newMintEntity.owner = ownerAddress;
|
||||||
newMintEntity.verifier = verifierAddress;
|
newMintEntity.verifier = verifierAddress;
|
||||||
newMintEntity.blockNumber = event.block.number;
|
newMintEntity.blockNumber = event.block.number;
|
||||||
newMintEntity.blockTimestamp = event.block.timestamp;
|
newMintEntity.blockTimestamp = event.block.timestamp;
|
||||||
newMintEntity.transactionHash = event.transaction.hash;
|
newMintEntity.transactionHash = event.transaction.hash;
|
||||||
newMintEntity.save();
|
newMintEntity.save();
|
||||||
log.error('{}', [tokenId.toString()]);
|
log.error('{}', [tokenId.toString()]);
|
||||||
|
|
||||||
// Create Token, Owner, and Controller entities
|
// Create Token, Owner, and Controller entities
|
||||||
|
|
||||||
let owner = Owner.load(ownerAddress);
|
let owner = Owner.load(ownerAddress);
|
||||||
let token = new Token(Bytes.fromByteArray(Bytes.fromBigInt(tokenId)));
|
let token = new Token(Bytes.fromByteArray(Bytes.fromBigInt(tokenId)));
|
||||||
|
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
// Create a new owner entity
|
// Create a new owner entity
|
||||||
owner = new Owner(ownerAddress);
|
owner = new Owner(ownerAddress);
|
||||||
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
|
// Since no CollectionRoleChanged event was emitted before for this address, we can set `collection` to false.
|
||||||
owner.collection = false;
|
owner.collection = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate Token with data from the event
|
// Populate Token with data from the event
|
||||||
token.tokenId = tokenId;
|
token.tokenId = tokenId;
|
||||||
token.name = name;
|
token.name = name;
|
||||||
token.description = description;
|
token.description = description;
|
||||||
token.externalURL = externalURL;
|
token.externalURL = externalURL;
|
||||||
token.ENS = ENS;
|
token.ENS = ENS;
|
||||||
token.gitRepository = gitRepository;
|
token.gitRepository = gitRepository;
|
||||||
token.commitHash = commitHash;
|
token.commitHash = commitHash;
|
||||||
token.logo = logo;
|
token.logo = logo;
|
||||||
token.color = color;
|
token.color = color;
|
||||||
token.accessPointAutoApproval = accessPointAutoApproval;
|
token.accessPointAutoApproval = accessPointAutoApproval;
|
||||||
token.owner = ownerAddress;
|
token.owner = ownerAddress;
|
||||||
token.verifier = verifierAddress;
|
token.verifier = verifierAddress;
|
||||||
token.mintTransaction = event.transaction.hash.concatI32(
|
token.mintTransaction = event.transaction.hash.concatI32(
|
||||||
event.logIndex.toI32()
|
event.logIndex.toI32()
|
||||||
);
|
);
|
||||||
token.mintedBy = event.params.minter;
|
token.mintedBy = event.params.minter;
|
||||||
token.controllers = [ownerAddress];
|
token.controllers = [ownerAddress];
|
||||||
|
|
||||||
// Save entities
|
// Save entities
|
||||||
owner.save();
|
owner.save();
|
||||||
token.save();
|
token.save();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,60 @@
|
||||||
import {
|
import { Bytes, log, store } from '@graphprotocol/graph-ts';
|
||||||
Bytes,
|
|
||||||
log,
|
|
||||||
store
|
|
||||||
} from '@graphprotocol/graph-ts';
|
|
||||||
|
|
||||||
// Event Imports [based on the yaml config]
|
// Event Imports [based on the yaml config]
|
||||||
import {
|
import { Transfer as TransferEvent } from '../generated/FleekNFA/FleekNFA';
|
||||||
Transfer as TransferEvent,
|
|
||||||
} from '../generated/FleekNFA/FleekNFA';
|
|
||||||
|
|
||||||
// Entity Imports [based on the schema]
|
// Entity Imports [based on the schema]
|
||||||
import {
|
import { Owner, Token, Transfer } from '../generated/schema';
|
||||||
Owner,
|
|
||||||
Token,
|
|
||||||
Transfer,
|
|
||||||
} from '../generated/schema';
|
|
||||||
|
|
||||||
export function handleTransfer(event: TransferEvent): void {
|
export function handleTransfer(event: TransferEvent): void {
|
||||||
let transfer = new Transfer(
|
let transfer = new Transfer(
|
||||||
event.transaction.hash.concatI32(event.logIndex.toI32())
|
event.transaction.hash.concatI32(event.logIndex.toI32())
|
||||||
);
|
);
|
||||||
|
|
||||||
const TokenId = event.params.tokenId;
|
const TokenId = event.params.tokenId;
|
||||||
|
|
||||||
transfer.from = event.params.from;
|
transfer.from = event.params.from;
|
||||||
transfer.to = event.params.to;
|
transfer.to = event.params.to;
|
||||||
transfer.tokenId = TokenId;
|
transfer.tokenId = TokenId;
|
||||||
|
|
||||||
transfer.blockNumber = event.block.number;
|
transfer.blockNumber = event.block.number;
|
||||||
transfer.blockTimestamp = event.block.timestamp;
|
transfer.blockTimestamp = event.block.timestamp;
|
||||||
transfer.transactionHash = event.transaction.hash;
|
transfer.transactionHash = event.transaction.hash;
|
||||||
|
|
||||||
transfer.save();
|
transfer.save();
|
||||||
|
|
||||||
let token: Token | null;
|
let token: Token | null;
|
||||||
|
|
||||||
let owner_address = event.params.to;
|
let owner_address = event.params.to;
|
||||||
let owner = Owner.load(owner_address);
|
let owner = Owner.load(owner_address);
|
||||||
|
|
||||||
if (!owner) {
|
if (!owner) {
|
||||||
// Create a new owner entity
|
// Create a new owner entity
|
||||||
owner = new Owner(owner_address);
|
owner = new Owner(owner_address);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseInt(event.params.from.toHexString()) !== 0) {
|
if (parseInt(event.params.from.toHexString()) !== 0) {
|
||||||
if (parseInt(event.params.to.toHexString()) === 0) {
|
if (parseInt(event.params.to.toHexString()) === 0) {
|
||||||
// Burn
|
// Burn
|
||||||
// Remove the entity from storage
|
// Remove the entity from storage
|
||||||
// Its controllers and owner will be affected.
|
// Its controllers and owner will be affected.
|
||||||
store.remove('Token', TokenId.toString());
|
store.remove('Token', TokenId.toString());
|
||||||
|
} else {
|
||||||
|
// Transfer
|
||||||
|
// Load the Token by using its TokenId
|
||||||
|
token = Token.load(Bytes.fromByteArray(Bytes.fromBigInt(TokenId)));
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// Entity exists
|
||||||
|
token.owner = owner_address;
|
||||||
|
|
||||||
|
// Save both entities
|
||||||
|
owner.save();
|
||||||
|
token.save();
|
||||||
} else {
|
} else {
|
||||||
// Transfer
|
// Entity does not exist
|
||||||
// Load the Token by using its TokenId
|
log.error('Unknown token was transferred.', []);
|
||||||
token = Token.load(
|
|
||||||
Bytes.fromByteArray(Bytes.fromBigInt(TokenId))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
// Entity exists
|
|
||||||
token.owner = owner_address;
|
|
||||||
|
|
||||||
// Save both entities
|
|
||||||
owner.save();
|
|
||||||
token.save();
|
|
||||||
} else {
|
|
||||||
// Entity does not exist
|
|
||||||
log.error('Unknown token was transferred.', []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"timestamp": 1679061942846
|
"timestamp": 1679061942846
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ sources:
|
||||||
handler:
|
handler:
|
||||||
graphql:
|
graphql:
|
||||||
endpoint: https://api.thegraph.com/subgraphs/name/emperororokusaki/flk-test-subgraph #replace for nfa subgraph
|
endpoint: https://api.thegraph.com/subgraphs/name/emperororokusaki/flk-test-subgraph #replace for nfa subgraph
|
||||||
|
- name: ENS
|
||||||
|
handler:
|
||||||
|
graphql:
|
||||||
|
endpoint: https://api.thegraph.com/subgraphs/name/ensdomains/ens
|
||||||
|
|
||||||
documents:
|
documents:
|
||||||
- ./graphql/*.graphql
|
- ./graphql/*.graphql
|
||||||
|
|
|
||||||
|
|
@ -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 React, { forwardRef } from 'react';
|
||||||
import { Flex } from '../layout';
|
import { Flex } from '../layout';
|
||||||
import { CardStyles } from './card.styles';
|
import { CardStyles } from './card.styles';
|
||||||
|
|
@ -15,9 +14,9 @@ export abstract class Card {
|
||||||
);
|
);
|
||||||
|
|
||||||
static readonly Heading = forwardRef<HTMLHeadingElement, Card.HeadingProps>(
|
static readonly Heading = forwardRef<HTMLHeadingElement, Card.HeadingProps>(
|
||||||
({ title, leftIcon, rightIcon, ...props }, ref) => {
|
({ title, leftIcon, rightIcon, css, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Flex css={{ justifyContent: 'space-between' }}>
|
<Flex css={{ justifyContent: 'space-between', ...css }}>
|
||||||
<Flex>
|
<Flex>
|
||||||
{leftIcon}
|
{leftIcon}
|
||||||
<CardStyles.Heading ref={ref} {...props}>
|
<CardStyles.Heading ref={ref} {...props}>
|
||||||
|
|
@ -58,6 +57,7 @@ export namespace Card {
|
||||||
|
|
||||||
export type HeadingProps = {
|
export type HeadingProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
css?: React.CSSProperties;
|
||||||
leftIcon?: React.ReactNode;
|
leftIcon?: React.ReactNode;
|
||||||
rightIcon?: React.ReactNode;
|
rightIcon?: React.ReactNode;
|
||||||
} & React.ComponentProps<typeof CardStyles.Heading>;
|
} & React.ComponentProps<typeof CardStyles.Heading>;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,19 @@ import { Button, Card, Flex, Icon } from '@/components';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import ColorThief from 'colorthief';
|
import ColorThief from 'colorthief';
|
||||||
import { Mint } from '../../../../mint.context';
|
|
||||||
|
|
||||||
export const ColorPicker = () => {
|
export type ColorPickerProps = {
|
||||||
const { appLogo, logoColor, setLogoColor } = Mint.useContext();
|
logoColor: string;
|
||||||
|
setLogoColor: (color: string) => void;
|
||||||
|
logo: string;
|
||||||
|
} & React.HTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
|
export const ColorPicker: React.FC<ColorPickerProps> = ({
|
||||||
|
logoColor,
|
||||||
|
logo,
|
||||||
|
setLogoColor,
|
||||||
|
onBlur,
|
||||||
|
}) => {
|
||||||
const inputColorRef = useRef<HTMLInputElement>(null);
|
const inputColorRef = useRef<HTMLInputElement>(null);
|
||||||
const imageRef = useRef<HTMLImageElement>(null);
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
|
@ -17,6 +26,10 @@ export const ColorPicker = () => {
|
||||||
setLogoColor(hexColor);
|
setLogoColor(hexColor);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setLogoColor(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleColorPickerClick = () => {
|
const handleColorPickerClick = () => {
|
||||||
inputColorRef.current?.click();
|
inputColorRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
@ -43,6 +56,7 @@ export const ColorPicker = () => {
|
||||||
borderRadius: '$md',
|
borderRadius: '$md',
|
||||||
color: '$slate12',
|
color: '$slate12',
|
||||||
zIndex: '$dropdown',
|
zIndex: '$dropdown',
|
||||||
|
minWidth: '$28',
|
||||||
}}
|
}}
|
||||||
onClick={handleColorPickerClick}
|
onClick={handleColorPickerClick}
|
||||||
>
|
>
|
||||||
|
|
@ -53,14 +67,14 @@ export const ColorPicker = () => {
|
||||||
className="absolute right-16"
|
className="absolute right-16"
|
||||||
type="color"
|
type="color"
|
||||||
value={logoColor}
|
value={logoColor}
|
||||||
onChange={(e) => setLogoColor(e.target.value)}
|
onChange={handleColorChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
className="hidden"
|
className="hidden"
|
||||||
src={appLogo}
|
src={logo}
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
onLoad={handleLogoLoad}
|
onLoad={handleLogoLoad}
|
||||||
style={{ width: '50px', height: '50px' }}
|
style={{ width: '50px', height: '50px' }}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './color-picker';
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
import React, {
|
||||||
import { Combobox as ComboboxLib, Transition } from '@headlessui/react';
|
forwardRef,
|
||||||
|
Fragment,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Combobox as ComboboxLib,
|
||||||
|
ComboboxInputProps as ComboboxLibInputProps,
|
||||||
|
Transition,
|
||||||
|
} from '@headlessui/react';
|
||||||
import { Icon, IconName } from '@/components/core/icon';
|
import { Icon, IconName } from '@/components/core/icon';
|
||||||
import { Flex } from '@/components/layout';
|
import { Flex } from '@/components/layout';
|
||||||
import { useDebounce } from '@/hooks/use-debounce';
|
import { useDebounce } from '@/hooks/use-debounce';
|
||||||
|
|
@ -16,20 +26,16 @@ type ComboboxInputProps = {
|
||||||
*/
|
*/
|
||||||
leftIcon: IconName;
|
leftIcon: IconName;
|
||||||
/**
|
/**
|
||||||
* Function to handle the input change
|
* Value to indicate it's invalid
|
||||||
*/
|
*/
|
||||||
handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
error?: boolean;
|
||||||
/**
|
} & ComboboxLibInputProps<'input', ComboboxItem>;
|
||||||
* Function to handle the input click. When the user clicks on the input, the list of options will be displayed
|
|
||||||
*/
|
|
||||||
handleInputClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ComboboxInput = ({
|
const ComboboxInput = ({
|
||||||
open,
|
open,
|
||||||
leftIcon,
|
leftIcon,
|
||||||
handleInputChange,
|
error,
|
||||||
handleInputClick,
|
...props
|
||||||
}: ComboboxInputProps) => (
|
}: ComboboxInputProps) => (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<Icon
|
<Icon
|
||||||
|
|
@ -45,14 +51,15 @@ const ComboboxInput = ({
|
||||||
/>
|
/>
|
||||||
<ComboboxLib.Input
|
<ComboboxLib.Input
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
className={`w-full border-solid border border-slate7 h-11 py-3 px-10 text-sm leading-5 text-slate11 outline-none ${
|
className={`w-full border-solid border h-11 py-3 px-10 text-sm leading-5 text-slate11 outline-none ${
|
||||||
open
|
open
|
||||||
? 'border-b-0 rounded-t-xl bg-black border-slate6'
|
? 'border-b-0 rounded-t-xl bg-black border-slate6'
|
||||||
: 'rounded-xl bg-transparent'
|
: `rounded-xl bg-transparent cursor-pointer ${
|
||||||
|
error ? 'border-red9' : 'border-slate7'
|
||||||
|
}`
|
||||||
}`}
|
}`}
|
||||||
displayValue={(selectedValue: ComboboxItem) => selectedValue.label}
|
displayValue={(selectedValue: ComboboxItem) => selectedValue.label}
|
||||||
onChange={handleInputChange}
|
{...props}
|
||||||
onClick={handleInputClick}
|
|
||||||
/>
|
/>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<Icon name="chevron-down" css={{ fontSize: '$xs' }} />
|
<Icon name="chevron-down" css={{ fontSize: '$xs' }} />
|
||||||
|
|
@ -134,131 +141,151 @@ export type ComboboxProps = {
|
||||||
/**
|
/**
|
||||||
* Callback when the selected value changes.
|
* Callback when the selected value changes.
|
||||||
*/
|
*/
|
||||||
onChange(option: ComboboxItem): void;
|
onChange: (option: ComboboxItem) => void;
|
||||||
|
/**
|
||||||
|
* Function to handle the input blur
|
||||||
|
*/
|
||||||
|
onBlur?: () => void;
|
||||||
|
/**
|
||||||
|
* Value to indicate it's invalid
|
||||||
|
*/
|
||||||
|
error?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Combobox: React.FC<ComboboxProps> = ({
|
export const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
|
||||||
items,
|
(
|
||||||
selectedValue = { value: '', label: '' },
|
{
|
||||||
withAutocomplete = false,
|
items,
|
||||||
leftIcon = 'search',
|
selectedValue = { value: '', label: '' },
|
||||||
onChange,
|
withAutocomplete = false,
|
||||||
}) => {
|
leftIcon = 'search',
|
||||||
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
|
onChange,
|
||||||
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
|
onBlur,
|
||||||
[]
|
error = false,
|
||||||
);
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
|
||||||
|
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the selected value doesn't exist in the list of items, we add it
|
// If the selected value doesn't exist in the list of items, we add it
|
||||||
if (
|
if (
|
||||||
items.filter((item) => item === selectedValue).length === 0 &&
|
items.filter((item) => item === selectedValue).length === 0 &&
|
||||||
selectedValue.value !== undefined &&
|
selectedValue.value !== undefined &&
|
||||||
autocompleteItems.length === 0 &&
|
autocompleteItems.length === 0 &&
|
||||||
withAutocomplete
|
withAutocomplete
|
||||||
) {
|
) {
|
||||||
setAutocompleteItems([selectedValue]);
|
setAutocompleteItems([selectedValue]);
|
||||||
}
|
}
|
||||||
}, [selectedValue]);
|
}, [selectedValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredItems(items);
|
|
||||||
}, [items]);
|
|
||||||
|
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const handleSearch = useDebounce((searchValue: string) => {
|
|
||||||
if (searchValue === '') {
|
|
||||||
setFilteredItems(items);
|
setFilteredItems(items);
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
if (withAutocomplete) {
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const handleSearch = useDebounce((searchValue: string) => {
|
||||||
|
if (searchValue === '') {
|
||||||
|
setFilteredItems(items);
|
||||||
|
|
||||||
|
if (withAutocomplete) {
|
||||||
|
setAutocompleteItems([]);
|
||||||
|
handleComboboxChange({} as ComboboxItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const filteredValues = items.filter((item) =>
|
||||||
|
cleanString(item.label).startsWith(cleanString(searchValue))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (withAutocomplete && filteredValues.length === 0) {
|
||||||
|
// If the search value doesn't exist in the list of items, we add it
|
||||||
|
setAutocompleteItems([{ value: searchValue, label: searchValue }]);
|
||||||
|
}
|
||||||
|
setFilteredItems(filteredValues);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleSearch(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputClick = () => {
|
||||||
|
buttonRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComboboxChange = (optionSelected: ComboboxItem) => {
|
||||||
|
onChange(optionSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeaveTransition = () => {
|
||||||
|
setFilteredItems(items);
|
||||||
|
if (selectedValue.value === undefined && withAutocomplete) {
|
||||||
setAutocompleteItems([]);
|
setAutocompleteItems([]);
|
||||||
handleComboboxChange({} as ComboboxItem);
|
handleComboboxChange({} as ComboboxItem);
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
const filteredValues = items.filter((item) =>
|
|
||||||
cleanString(item.label).startsWith(cleanString(searchValue))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (withAutocomplete && filteredValues.length === 0) {
|
return (
|
||||||
// If the search value doesn't exist in the list of items, we add it
|
<ComboboxLib
|
||||||
setAutocompleteItems([{ value: searchValue, label: searchValue }]);
|
value={selectedValue}
|
||||||
}
|
by="value"
|
||||||
setFilteredItems(filteredValues);
|
onChange={handleComboboxChange}
|
||||||
}
|
>
|
||||||
}, 200);
|
{({ open }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<ComboboxInput
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
open={open}
|
||||||
|
leftIcon={leftIcon}
|
||||||
|
onBlur={onBlur}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
<ComboboxLib.Button ref={buttonRef} className="hidden" />
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
<Transition
|
||||||
event.stopPropagation();
|
show={open}
|
||||||
handleSearch(event.target.value);
|
as={Fragment}
|
||||||
};
|
enter="transition duration-400 ease-out"
|
||||||
|
leave="transition ease-out duration-100"
|
||||||
const handleInputClick = () => {
|
leaveFrom="opacity-100"
|
||||||
buttonRef.current?.click();
|
leaveTo="opacity-0"
|
||||||
};
|
afterLeave={handleLeaveTransition}
|
||||||
|
>
|
||||||
const handleComboboxChange = (option: ComboboxItem) => {
|
<ComboboxLib.Options className="absolute max-h-60 w-full z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
|
||||||
onChange(option);
|
{[...autocompleteItems, ...filteredItems].length === 0 ||
|
||||||
};
|
filteredItems === undefined ? (
|
||||||
|
<NoResults />
|
||||||
const handleLeaveTransition = () => {
|
) : (
|
||||||
setFilteredItems(items);
|
<>
|
||||||
if (selectedValue.value === undefined && withAutocomplete) {
|
{autocompleteItems.length > 0 && <span>Create new</span>}
|
||||||
setAutocompleteItems([]);
|
{autocompleteItems.map(
|
||||||
handleComboboxChange({} as ComboboxItem);
|
(autocompleteOption: ComboboxItem) => (
|
||||||
}
|
<ComboboxOption
|
||||||
};
|
key={autocompleteOption.value}
|
||||||
|
option={autocompleteOption}
|
||||||
return (
|
/>
|
||||||
<ComboboxLib
|
)
|
||||||
value={selectedValue}
|
)}
|
||||||
by="value"
|
{autocompleteItems.length > 0 &&
|
||||||
onChange={handleComboboxChange}
|
filteredItems.length > 0 && (
|
||||||
>
|
<Separator css={{ mb: '$2' }} />
|
||||||
{({ open }) => (
|
)}
|
||||||
<div className="relative">
|
{filteredItems.map((option: ComboboxItem) => (
|
||||||
<ComboboxInput
|
<ComboboxOption key={option.value} option={option} />
|
||||||
handleInputChange={handleInputChange}
|
))}
|
||||||
handleInputClick={handleInputClick}
|
</>
|
||||||
open={open}
|
)}
|
||||||
leftIcon={leftIcon}
|
</ComboboxLib.Options>
|
||||||
/>
|
</Transition>
|
||||||
<ComboboxLib.Button ref={buttonRef} className="hidden" />
|
</div>
|
||||||
|
)}
|
||||||
<Transition
|
</ComboboxLib>
|
||||||
show={open}
|
);
|
||||||
as={Fragment}
|
}
|
||||||
enter="transition duration-400 ease-out"
|
);
|
||||||
leave="transition ease-out duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
afterLeave={handleLeaveTransition}
|
|
||||||
>
|
|
||||||
<ComboboxLib.Options className="absolute max-h-60 w-full z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
|
|
||||||
{[...autocompleteItems, ...filteredItems].length === 0 ||
|
|
||||||
filteredItems === undefined ? (
|
|
||||||
<NoResults />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{autocompleteItems.length > 0 && <span>Create new</span>}
|
|
||||||
{autocompleteItems.map((autocompleteOption: ComboboxItem) => (
|
|
||||||
<ComboboxOption
|
|
||||||
key={autocompleteOption.value}
|
|
||||||
option={autocompleteOption}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{autocompleteItems.length > 0 && filteredItems.length > 0 && (
|
|
||||||
<Separator css={{ mb: '$2' }} />
|
|
||||||
)}
|
|
||||||
{filteredItems.map((option: ComboboxItem) => (
|
|
||||||
<ComboboxOption key={option.value} option={option} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ComboboxLib.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ComboboxLib>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ export * from './avatar';
|
||||||
export * from './separator.styles';
|
export * from './separator.styles';
|
||||||
export * from './text';
|
export * from './text';
|
||||||
export * from './switch';
|
export * from './switch';
|
||||||
|
export * from './color-picker';
|
||||||
|
|
|
||||||
|
|
@ -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 { forwardRef, useRef } from 'react';
|
||||||
import { Icon } from '../icon';
|
import { Icon } from '../icon';
|
||||||
|
import { InputFileStyles as S } from './input-file.styles';
|
||||||
const { styled } = dripStitches;
|
|
||||||
|
|
||||||
const BorderInput = styled('div', {
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderColor: '$gray7',
|
|
||||||
width: '$22',
|
|
||||||
height: '$22',
|
|
||||||
transition: 'border-color 0.2s ease-in-out',
|
|
||||||
borderWidth: '$default',
|
|
||||||
borderRadius: '$lg',
|
|
||||||
zIndex: '$docked',
|
|
||||||
|
|
||||||
'&:hover': {
|
|
||||||
borderColor: '$gray8',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
type InputFileProps = {
|
type InputFileProps = {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
} & React.ComponentProps<typeof Flex>;
|
} & React.ComponentProps<typeof S.Border>;
|
||||||
|
|
||||||
export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
|
export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
|
||||||
({ value: file, onChange, css, ...props }, ref) => {
|
({ value: file, onChange, css, ...props }, ref) => {
|
||||||
|
|
@ -40,24 +22,13 @@ export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<S.Container onClick={handleInputClick}>
|
||||||
css={{
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
...(css || {}),
|
|
||||||
}}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
onClick={handleInputClick}
|
|
||||||
>
|
|
||||||
{file !== '' ? (
|
{file !== '' ? (
|
||||||
<img className="absolute w-14 h-14" src={file} alt="logo" />
|
<img className="absolute w-14 h-14" src={file} alt="logo" />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="upload" size="md" css={{ position: 'absolute' }} />
|
<Icon name="upload" size="md" css={{ position: 'absolute' }} />
|
||||||
)}
|
)}
|
||||||
<BorderInput />
|
<S.Border {...props} ref={ref} />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
|
|
@ -65,7 +36,7 @@ export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
|
||||||
ref={inputFileRef}
|
ref={inputFileRef}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</S.Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
mt: '$1h',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static readonly Overline = styled('div', {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
});
|
||||||
|
|
||||||
|
static readonly OverlineErrors = styled(Flex, {
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
|
|
||||||
static readonly ErrorMessage = styled('span', {
|
static readonly ErrorMessage = styled('span', {
|
||||||
color: '$red11',
|
color: '$red11',
|
||||||
fontSize: '0.625rem',
|
fontSize: '0.625rem',
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,313 @@
|
||||||
import React, { forwardRef } from 'react';
|
import { hasValidator } from '@/utils';
|
||||||
|
import { fileToBase64 } from '@/views/mint/nfa-step/form-step/form.utils';
|
||||||
|
import React, { forwardRef, useMemo, useState } from 'react';
|
||||||
|
import { ColorPicker, Combobox, ComboboxItem } from '../core';
|
||||||
import { Input, LogoFileInput, Textarea } from '../core/input';
|
import { Input, LogoFileInput, Textarea } from '../core/input';
|
||||||
|
import {
|
||||||
|
FormFieldContext,
|
||||||
|
FormFieldProvider,
|
||||||
|
useFormFieldContext,
|
||||||
|
} from './form-field.context';
|
||||||
|
import {
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormFieldValidatorValue,
|
||||||
|
} from './form.context';
|
||||||
import { FormStyles } from './form.styles';
|
import { FormStyles } from './form.styles';
|
||||||
|
|
||||||
export abstract class Form {
|
export abstract class Form {
|
||||||
|
static readonly Root = FormProvider;
|
||||||
|
|
||||||
static readonly Field = forwardRef<HTMLDivElement, Form.FieldProps>(
|
static readonly Field = forwardRef<HTMLDivElement, Form.FieldProps>(
|
||||||
({ children, ...props }, ref) => {
|
({ children, context, ...props }, ref) => {
|
||||||
|
const {
|
||||||
|
value: [value],
|
||||||
|
} = context;
|
||||||
|
const validationEnabled = useState(Boolean(value));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormStyles.Field ref={ref} {...props}>
|
<FormFieldProvider value={{ ...context, validationEnabled }}>
|
||||||
{children}
|
<FormStyles.Field ref={ref} {...props}>
|
||||||
</FormStyles.Field>
|
{children}
|
||||||
|
</FormStyles.Field>
|
||||||
|
</FormFieldProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
static readonly Label = forwardRef<HTMLLabelElement, Form.LabelProps>(
|
static readonly Label = forwardRef<HTMLLabelElement, Form.LabelProps>(
|
||||||
({ children, isRequired, ...props }, ref) => (
|
|
||||||
<FormStyles.Label ref={ref} {...props}>
|
|
||||||
{children}{' '}
|
|
||||||
{isRequired && <FormStyles.RequiredLabel>*</FormStyles.RequiredLabel>}
|
|
||||||
</FormStyles.Label>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
static readonly MaxLength = forwardRef<HTMLLabelElement, Form.LabelProps>(
|
|
||||||
({ children, ...props }, ref) => {
|
({ children, ...props }, ref) => {
|
||||||
|
const { validators } = useFormFieldContext();
|
||||||
|
|
||||||
|
const isRequired = useMemo(
|
||||||
|
() => hasValidator(validators, 'required'),
|
||||||
|
[validators]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormStyles.MaxLength ref={ref} {...props}>
|
<FormStyles.Label ref={ref} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</FormStyles.MaxLength>
|
{isRequired && <FormStyles.RequiredLabel>*</FormStyles.RequiredLabel>}
|
||||||
|
</FormStyles.Label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
static readonly Error = forwardRef<HTMLDivElement, Form.ErrorProps>(
|
static readonly Overline = forwardRef<HTMLDivElement>((props, ref) => {
|
||||||
({ children, ...props }, ref) => (
|
const {
|
||||||
<FormStyles.ErrorMessage ref={ref} {...props}>
|
validations: [validations],
|
||||||
{children}
|
} = useFormContext();
|
||||||
</FormStyles.ErrorMessage>
|
const {
|
||||||
)
|
id,
|
||||||
);
|
value: [value],
|
||||||
|
validationEnabled: [validationEnabled],
|
||||||
|
validators,
|
||||||
|
} = useFormFieldContext();
|
||||||
|
|
||||||
|
const errors = useMemo(() => {
|
||||||
|
if (!validationEnabled) return [];
|
||||||
|
if (!validations[id]) return [];
|
||||||
|
return validations[id].map((validator) => validator.message);
|
||||||
|
}, [validations, id, validationEnabled]);
|
||||||
|
|
||||||
|
const counter = useMemo(
|
||||||
|
() => hasValidator(validators, 'maxLength')?.args || 0,
|
||||||
|
[validators]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormStyles.Overline ref={ref} {...props}>
|
||||||
|
<FormStyles.OverlineErrors>
|
||||||
|
{errors.map((error) => (
|
||||||
|
<FormStyles.ErrorMessage key={error}>
|
||||||
|
{error}
|
||||||
|
</FormStyles.ErrorMessage>
|
||||||
|
))}
|
||||||
|
</FormStyles.OverlineErrors>
|
||||||
|
|
||||||
|
{Boolean(counter) && (
|
||||||
|
<FormStyles.MaxLength>
|
||||||
|
{`${value.length}/${counter}`}
|
||||||
|
</FormStyles.MaxLength>
|
||||||
|
)}
|
||||||
|
</FormStyles.Overline>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
static readonly Input = forwardRef<HTMLInputElement, Form.InputProps>(
|
static readonly Input = forwardRef<HTMLInputElement, Form.InputProps>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
return <Input ref={ref} {...props} />;
|
const {
|
||||||
|
id,
|
||||||
|
validators,
|
||||||
|
value: [value, setValue],
|
||||||
|
validationEnabled: [validationEnabled, setValidationEnabled],
|
||||||
|
} = useFormFieldContext();
|
||||||
|
const isValid = useFormFieldValidatorValue(id, validators, value);
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (props.onChange) props.onChange(e);
|
||||||
|
setValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = (
|
||||||
|
e: React.FocusEvent<HTMLInputElement, Element>
|
||||||
|
) => {
|
||||||
|
if (props.onBlur) props.onBlur(e);
|
||||||
|
setValidationEnabled(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
aria-invalid={validationEnabled && !isValid}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static readonly Combobox = forwardRef<HTMLInputElement, Form.ComboboxProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
validators,
|
||||||
|
value: [value, setValue],
|
||||||
|
validationEnabled: [validationEnabled, setValidationEnabled],
|
||||||
|
} = useFormFieldContext();
|
||||||
|
|
||||||
|
const comboboxValue = useMemo(() => {
|
||||||
|
// if it's with autocomplete maybe won't be on the items list
|
||||||
|
const item = props.items.find((item) => item.label === value);
|
||||||
|
if (props.withAutocomplete && !item && value !== '') {
|
||||||
|
//return the selected value if the item doesn't exist
|
||||||
|
return { label: value, value: value };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const isValid = useFormFieldValidatorValue(id, validators, value);
|
||||||
|
|
||||||
|
const handleComboboxChange = (option: ComboboxItem) => {
|
||||||
|
if (props.onChange) props.onChange(option);
|
||||||
|
setValue(option.label);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComboboxBlur = () => {
|
||||||
|
setValidationEnabled(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
onChange={handleComboboxChange}
|
||||||
|
selectedValue={comboboxValue || ({} as ComboboxItem)}
|
||||||
|
onBlur={handleComboboxBlur}
|
||||||
|
error={validationEnabled && !isValid}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
static readonly ColorPicker = ({
|
||||||
|
logo,
|
||||||
|
setLogoColor,
|
||||||
|
}: Form.ColorPickerProps) => {
|
||||||
|
const {
|
||||||
|
value: [value, setValue],
|
||||||
|
validationEnabled: [, setValidationEnabled],
|
||||||
|
} = useFormFieldContext();
|
||||||
|
|
||||||
|
const handleColorChange = (color: string) => {
|
||||||
|
if (setLogoColor) setLogoColor(color);
|
||||||
|
setValue(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
setValidationEnabled(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
logo={logo}
|
||||||
|
logoColor={value}
|
||||||
|
setLogoColor={handleColorChange}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
static readonly Textarea = forwardRef<
|
static readonly Textarea = forwardRef<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
Form.TextareaProps
|
Form.TextareaProps
|
||||||
>((props, ref) => {
|
>((props, ref) => {
|
||||||
return <Textarea ref={ref} {...props} />;
|
const {
|
||||||
|
id,
|
||||||
|
validators,
|
||||||
|
value: [value, setValue],
|
||||||
|
validationEnabled: [validationEnabled, setValidationEnabled],
|
||||||
|
} = useFormFieldContext();
|
||||||
|
const isValid = useFormFieldValidatorValue(id, validators, value);
|
||||||
|
|
||||||
|
const handleTextareaChange = (
|
||||||
|
e: React.ChangeEvent<HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
if (props.onChange) props.onChange(e);
|
||||||
|
setValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextareaBlur = (
|
||||||
|
e: React.FocusEvent<HTMLTextAreaElement, Element>
|
||||||
|
) => {
|
||||||
|
if (props.onBlur) props.onBlur(e);
|
||||||
|
setValidationEnabled(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={handleTextareaChange}
|
||||||
|
aria-invalid={validationEnabled && !isValid}
|
||||||
|
onBlur={handleTextareaBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
static readonly LogoFileInput = forwardRef<
|
static readonly LogoFileInput = forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
Form.LogoFileInputProps
|
Form.LogoFileInputProps
|
||||||
>((props, ref) => {
|
>((props, ref) => {
|
||||||
return <LogoFileInput ref={ref} {...props} />;
|
const {
|
||||||
|
id,
|
||||||
|
validators,
|
||||||
|
value: [value, setValue],
|
||||||
|
validationEnabled: [, setValidationEnabled],
|
||||||
|
} = useFormFieldContext();
|
||||||
|
|
||||||
|
const isValid = useFormFieldValidatorValue(id, validators, value);
|
||||||
|
|
||||||
|
const handleFileInputChange = async (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
//Convert to string base64 to send to contract
|
||||||
|
setValidationEnabled(true);
|
||||||
|
const fileBase64 = await fileToBase64(file);
|
||||||
|
setValue(fileBase64);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LogoFileInput
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
aria-invalid={value !== '' && !isValid}
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Form {
|
export namespace Form {
|
||||||
export type FieldProps = {
|
export type FieldProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
context: Omit<FormFieldContext, 'validationEnabled'>;
|
||||||
} & React.ComponentProps<typeof FormStyles.Field>;
|
} & React.ComponentProps<typeof FormStyles.Field>;
|
||||||
|
|
||||||
export type LabelProps = { isRequired?: boolean } & React.ComponentProps<
|
export type LabelProps = React.ComponentProps<typeof FormStyles.Label>;
|
||||||
typeof FormStyles.Label
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type ErrorProps = React.ComponentProps<typeof FormStyles.ErrorMessage>;
|
export type ErrorProps = React.ComponentProps<typeof FormStyles.ErrorMessage>;
|
||||||
|
|
||||||
export type InputProps = React.ComponentProps<typeof Input>;
|
export type InputProps = Omit<React.ComponentProps<typeof Input>, 'error'>;
|
||||||
|
|
||||||
export type TextareaProps = React.ComponentProps<typeof Textarea>;
|
export type TextareaProps = Omit<
|
||||||
|
React.ComponentProps<typeof Textarea>,
|
||||||
|
'value' | 'error'
|
||||||
|
>;
|
||||||
|
|
||||||
export type LogoFileInputProps = React.ComponentProps<typeof LogoFileInput>;
|
export type ComboboxProps = {
|
||||||
|
onChange?: (option: ComboboxItem) => void;
|
||||||
|
} & Omit<
|
||||||
|
React.ComponentProps<typeof Combobox>,
|
||||||
|
'error' | 'selectedValue' | 'onChange'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type LogoFileInputProps = Omit<
|
||||||
|
React.ComponentProps<typeof LogoFileInput>,
|
||||||
|
'value' | 'onChange'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ColorPickerProps = {
|
||||||
|
setLogoColor?: (color: string) => void;
|
||||||
|
} & Omit<
|
||||||
|
React.ComponentProps<typeof ColorPicker>,
|
||||||
|
'setLogoColor' | 'logoColor'
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
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 = [
|
const listSites = [
|
||||||
{
|
{
|
||||||
tokenId: 1,
|
tokenId: 1,
|
||||||
|
|
@ -26,12 +29,16 @@ const listSites = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const fetchMintedSites = async () => {
|
const listBranches: ComboboxItem[] = [
|
||||||
|
{ value: '4573495837458934', label: 'main' },
|
||||||
|
{ value: '293857439857348', label: 'develop' },
|
||||||
|
{ value: '12344', label: 'feat/nabvar' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const fetchMintedSites = async (): Promise<ComboboxItem[]> => {
|
||||||
return new Promise((resolved) => {
|
return new Promise((resolved) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolved({
|
resolved(listBranches);
|
||||||
listSites,
|
|
||||||
});
|
|
||||||
}, 2500);
|
}, 2500);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { RootState } from '@/store';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { ensActions } from '../ens-slice';
|
import { ensActions } from '../ens-slice';
|
||||||
|
|
||||||
|
//TODO maybe deprecate cause we uss the ENS graph
|
||||||
export const fetchEnsNamesThunk = createAsyncThunk<void, `0x${string}`>(
|
export const fetchEnsNamesThunk = createAsyncThunk<void, `0x${string}`>(
|
||||||
'ens/fetchEnsNames',
|
'ens/fetchEnsNames',
|
||||||
async (address, { dispatch, getState }) => {
|
async (address, { dispatch, getState }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { githubActions, RootState } from '@/store';
|
import { githubActions, RootState } from '@/store';
|
||||||
|
import { AppLog } from '@/utils';
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { GithubClient } from '../github-client';
|
import { GithubClient } from '../github-client';
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ export const fetchRepositoriesThunk = createAsyncThunk(
|
||||||
|
|
||||||
dispatch(githubActions.setRepositories(repositories));
|
dispatch(githubActions.setRepositories(repositories));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
AppLog.errorToast('Failed to fetch repositories. Please try again.');
|
||||||
dispatch(githubActions.setQueryState('failed'));
|
dispatch(githubActions.setQueryState('failed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ export * from './object';
|
||||||
export * from './context';
|
export * from './context';
|
||||||
export * from './toast';
|
export * from './toast';
|
||||||
export * from './log';
|
export * from './log';
|
||||||
|
export * from './string-validators';
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
export const ComboboxTest = () => {
|
||||||
const [selectedValue, setSelectedValue] = useState({} as ComboboxItem);
|
const [selectedValue, setSelectedValue] = useState('');
|
||||||
const [selectedValueAutocomplete, setSelectedValueAutocomplete] = useState(
|
const [selectedValueAutocomplete, setSelectedValueAutocomplete] =
|
||||||
{} as ComboboxItem
|
useState('');
|
||||||
);
|
|
||||||
|
|
||||||
const handleComboboxChange = (item: ComboboxItem) => {
|
const handleComboboxChange = (value: string) => {
|
||||||
setSelectedValue(item);
|
setSelectedValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleComboboxChangeAutocomplete = (item: ComboboxItem) => {
|
const handleComboboxChangeAutocomplete = (value: string) => {
|
||||||
setSelectedValueAutocomplete(item);
|
setSelectedValueAutocomplete(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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 { Mint } from '@/views/mint/mint.context';
|
||||||
import { RepoRow } from '../repository-row';
|
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
|
||||||
|
import { RepoRow } from '../../repository-row';
|
||||||
import { RepoBranchCommitFields } from './repo-branch-commit-fields';
|
import { RepoBranchCommitFields } from './repo-branch-commit-fields';
|
||||||
|
|
||||||
export const RepoConfigurationBody = () => {
|
export const RepoConfigurationBody = () => {
|
||||||
const { repositoryName, branchName, commitHash } = Mint.useContext();
|
const {
|
||||||
|
form: {
|
||||||
|
isValid: [isValid],
|
||||||
|
},
|
||||||
|
} = useMintFormContext();
|
||||||
|
|
||||||
|
const { repositoryName } = Mint.useContext();
|
||||||
|
|
||||||
const { nextStep } = Stepper.useContext();
|
const { nextStep } = Stepper.useContext();
|
||||||
|
|
||||||
|
|
@ -31,7 +38,7 @@ export const RepoConfigurationBody = () => {
|
||||||
/>
|
/>
|
||||||
<RepoBranchCommitFields />
|
<RepoBranchCommitFields />
|
||||||
<Button
|
<Button
|
||||||
disabled={!branchName.value || !commitHash}
|
disabled={!isValid}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={handleContinueClick}
|
onClick={handleContinueClick}
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
import { DropdownItem } from '@/components';
|
import { DropdownItem } from '@/components';
|
||||||
import { MintCardHeader } from '@/views/mint/mint-card';
|
import { MintCardHeader } from '@/views/mint/mint-card';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
|
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
|
||||||
|
|
||||||
export const RepoConfigurationHeader = () => {
|
export const RepoConfigurationHeader = () => {
|
||||||
const { setGithubStep, setBranchName, setCommitHash } = Mint.useContext();
|
const { setGithubStep } = Mint.useContext();
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
gitBranch: {
|
||||||
|
value: [, setBranchName],
|
||||||
|
},
|
||||||
|
gitCommit: {
|
||||||
|
value: [, setCommitHash],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = useMintFormContext();
|
||||||
|
|
||||||
const handlePrevStepClick = () => {
|
const handlePrevStepClick = () => {
|
||||||
setGithubStep(2);
|
setGithubStep(2);
|
||||||
setBranchName({} as DropdownItem);
|
setBranchName('');
|
||||||
setCommitHash('');
|
setCommitHash('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { Card, ComboboxItem, Flex, Grid, Icon, Spinner } from '@/components';
|
import {
|
||||||
|
Card,
|
||||||
|
ComboboxItem,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Spinner,
|
||||||
|
} from '@/components';
|
||||||
import { Input } from '@/components/core/input';
|
import { Input } from '@/components/core/input';
|
||||||
import { useDebounce } from '@/hooks/use-debounce';
|
import { useDebounce } from '@/hooks/use-debounce';
|
||||||
import { useGithubStore } from '@/store';
|
import { useGithubStore } from '@/store';
|
||||||
import { MintCardHeader } from '@/views/mint/mint-card';
|
import { MintCardHeader } from '@/views/mint/mint-card';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
import React, { forwardRef, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { RepositoriesList } from './repositories-list';
|
import { RepositoriesList } from './repositories-list';
|
||||||
import { UserOrgsCombobox } from './users-orgs-combobox';
|
import { UserOrgsCombobox } from './users-orgs-combobox';
|
||||||
|
|
||||||
|
|
@ -44,9 +52,27 @@ export const GithubRepositoryConnection: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card.Container css={{ maxWidth: '$107h', maxHeight: '$95h', pr: '$3h' }}>
|
<Card.Container css={{ maxWidth: '$107h', maxHeight: '$95h', pr: '$3h' }}>
|
||||||
<MintCardHeader
|
<Card.Heading
|
||||||
title="Select Repository"
|
title="Select Repository"
|
||||||
onClickBack={handlePrevStepClick}
|
css={{ pr: '$3h' }}
|
||||||
|
leftIcon={
|
||||||
|
<IconButton
|
||||||
|
aria-label="back"
|
||||||
|
colorScheme="gray"
|
||||||
|
variant="link"
|
||||||
|
icon={<Icon name="back" />}
|
||||||
|
css={{ mr: '$2' }}
|
||||||
|
onClick={handlePrevStepClick}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightIcon={
|
||||||
|
<IconButton
|
||||||
|
aria-label="info"
|
||||||
|
colorScheme="gray"
|
||||||
|
variant="link"
|
||||||
|
icon={<Icon name="info" />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Card.Body css={{ pt: '$4' }}>
|
<Card.Body css={{ pt: '$4' }}>
|
||||||
<Grid css={{ rowGap: '$2' }}>
|
<Grid css={{ rowGap: '$2' }}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Button, Separator } from '@/components';
|
import { Button, Separator } from '@/components';
|
||||||
import { githubActions, GithubState, useAppDispatch } from '@/store';
|
import { githubActions, GithubState, useAppDispatch } from '@/store';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { RepoRow } from '../repository-row';
|
import { RepoRow } from '../repository-row';
|
||||||
|
|
||||||
type RepositoryProps = {
|
type RepositoryProps = {
|
||||||
|
|
@ -13,11 +14,12 @@ export const Repository = ({ repository, index, length }: RepositoryProps) => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleSelectRepo = () => {
|
const handleSelectRepo = useCallback(() => {
|
||||||
setRepositoryName(repository);
|
setRepositoryName(repository);
|
||||||
setGithubStep(3);
|
setGithubStep(3);
|
||||||
dispatch(githubActions.setQueryState('idle'));
|
dispatch(githubActions.setQueryState('idle'));
|
||||||
};
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RepoRow
|
<RepoRow
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Avatar, Combobox, ComboboxItem } from '@/components';
|
import { Avatar, Combobox, ComboboxItem } from '@/components';
|
||||||
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
|
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
|
||||||
|
import { AppLog } from '@/utils';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
|
@ -16,8 +17,12 @@ export const UserOrgsCombobox = () => {
|
||||||
}, [dispatch, queryUserAndOrganizations]);
|
}, [dispatch, queryUserAndOrganizations]);
|
||||||
|
|
||||||
const handleUserOrgChange = (item: ComboboxItem) => {
|
const handleUserOrgChange = (item: ComboboxItem) => {
|
||||||
dispatch(githubActions.fetchRepositoriesThunk(item.value));
|
if (item) {
|
||||||
setSelectedUserOrg(item);
|
dispatch(githubActions.fetchRepositoriesThunk(item.value));
|
||||||
|
setSelectedUserOrg(item);
|
||||||
|
} else {
|
||||||
|
AppLog.errorToast('Error selecting user/org. Try again');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -29,7 +34,7 @@ export const UserOrgsCombobox = () => {
|
||||||
//SET first user
|
//SET first user
|
||||||
setSelectedUserOrg(userAndOrganizations[0]);
|
setSelectedUserOrg(userAndOrganizations[0]);
|
||||||
}
|
}
|
||||||
}, [queryUserAndOrganizations]);
|
}, [queryUserAndOrganizations, selectedUserOrg, userAndOrganizations]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Stepper } from '@/components';
|
import { Form, Stepper } from '@/components';
|
||||||
import { MintPreview } from './preview-step/mint-preview';
|
import { MintPreview } from './preview-step/mint-preview';
|
||||||
import { GithubStep } from './github-step';
|
import { GithubStep } from './github-step';
|
||||||
import { MintStep } from './mint-step';
|
import { MintStep } from './mint-step';
|
||||||
|
|
@ -6,40 +6,48 @@ import { WalletStep } from './wallet-step';
|
||||||
import { NFAStep } from './nfa-step';
|
import { NFAStep } from './nfa-step';
|
||||||
import { Mint } from './mint.context';
|
import { Mint } from './mint.context';
|
||||||
import { NftMinted } from './nft-minted';
|
import { NftMinted } from './nft-minted';
|
||||||
|
import { useMintFormContext } from './nfa-step/form-step';
|
||||||
|
|
||||||
export const MintStepper = () => {
|
export const MintStepper = () => {
|
||||||
const {
|
const {
|
||||||
transaction: { isSuccess },
|
transaction: { isSuccess },
|
||||||
} = Mint.useTransactionContext();
|
} = Mint.useTransactionContext();
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
isValid: [, setIsValid],
|
||||||
|
},
|
||||||
|
} = useMintFormContext();
|
||||||
|
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
return (
|
return (
|
||||||
<Stepper.Root initialStep={1}>
|
<Stepper.Root initialStep={1}>
|
||||||
<Stepper.Container>
|
<Form.Root onValidationChange={setIsValid}>
|
||||||
<Stepper.Step>
|
<Stepper.Container>
|
||||||
<MintStep header="Connect your Ethereum Wallet to mint an NFA">
|
<Stepper.Step>
|
||||||
<WalletStep />
|
<MintStep header="Connect your Ethereum Wallet to mint an NFA">
|
||||||
</MintStep>
|
<WalletStep />
|
||||||
</Stepper.Step>
|
</MintStep>
|
||||||
|
</Stepper.Step>
|
||||||
|
|
||||||
<Stepper.Step>
|
<Stepper.Step>
|
||||||
<MintStep header="Connect GitHub and select repository">
|
<MintStep header="Connect GitHub and select repository">
|
||||||
<GithubStep />
|
<GithubStep />
|
||||||
</MintStep>
|
</MintStep>
|
||||||
</Stepper.Step>
|
</Stepper.Step>
|
||||||
|
|
||||||
<Stepper.Step>
|
<Stepper.Step>
|
||||||
<MintStep header="Finalize a few key things for your NFA">
|
<MintStep header="Finalize a few key things for your NFA">
|
||||||
<NFAStep />
|
<NFAStep />
|
||||||
</MintStep>
|
</MintStep>
|
||||||
</Stepper.Step>
|
</Stepper.Step>
|
||||||
|
|
||||||
<Stepper.Step>
|
<Stepper.Step>
|
||||||
<MintStep header="Review your NFA and mint it on Polygon">
|
<MintStep header="Review your NFA and mint it on Polygon">
|
||||||
<MintPreview />
|
<MintPreview />
|
||||||
</MintStep>
|
</MintStep>
|
||||||
</Stepper.Step>
|
</Stepper.Step>
|
||||||
</Stepper.Container>
|
</Stepper.Container>
|
||||||
|
</Form.Root>
|
||||||
</Stepper.Root>
|
</Stepper.Root>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { ComboboxItem, DropdownItem } from '@/components';
|
import { ComboboxItem } from '@/components';
|
||||||
import { EthereumHooks } from '@/integrations';
|
import { EthereumHooks } from '@/integrations';
|
||||||
import { AppLog, createContext } from '@/utils';
|
import { AppLog, createContext } from '@/utils';
|
||||||
import { GithubState, useFleekERC721Billing } from '@/store';
|
import { GithubState, useFleekERC721Billing } from '@/store';
|
||||||
|
|
@ -9,32 +9,14 @@ export type MintContext = {
|
||||||
billing: string | undefined;
|
billing: string | undefined;
|
||||||
selectedUserOrg: ComboboxItem;
|
selectedUserOrg: ComboboxItem;
|
||||||
repositoryName: GithubState.Repository;
|
repositoryName: GithubState.Repository;
|
||||||
branchName: DropdownItem; //get value from DropdownItem to mint
|
|
||||||
commitHash: string;
|
|
||||||
githubStep: number;
|
githubStep: number;
|
||||||
nfaStep: number;
|
nfaStep: number;
|
||||||
appName: string;
|
|
||||||
appDescription: string;
|
|
||||||
appLogo: string;
|
|
||||||
logoColor: string;
|
|
||||||
ens: ComboboxItem;
|
|
||||||
domain: string;
|
|
||||||
verifyNFA: boolean;
|
verifyNFA: boolean;
|
||||||
ensError: string;
|
|
||||||
setGithubStep: (step: number) => void;
|
setGithubStep: (step: number) => void;
|
||||||
setNfaStep: (step: number) => void;
|
setNfaStep: (step: number) => void;
|
||||||
setSelectedUserOrg: (userOrg: ComboboxItem) => void;
|
setSelectedUserOrg: (userOrgValue: ComboboxItem) => void;
|
||||||
setRepositoryName: (repo: GithubState.Repository) => void;
|
setRepositoryName: (repo: GithubState.Repository) => void;
|
||||||
setBranchName: (branch: DropdownItem) => void;
|
|
||||||
setCommitHash: (hash: string) => void;
|
|
||||||
setAppName: (name: string) => void;
|
|
||||||
setAppDescription: (description: string) => void;
|
|
||||||
setAppLogo: (logo: string) => void;
|
|
||||||
setLogoColor: (color: string) => void;
|
|
||||||
setEns: (ens: ComboboxItem) => void;
|
|
||||||
setDomain: (domain: string) => void;
|
|
||||||
setVerifyNFA: (verify: boolean) => void;
|
setVerifyNFA: (verify: boolean) => void;
|
||||||
setEnsError: (error: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [MintProvider, useContext] = createContext<MintContext>({
|
const [MintProvider, useContext] = createContext<MintContext>({
|
||||||
|
|
@ -56,24 +38,13 @@ export abstract class Mint {
|
||||||
const [selectedUserOrg, setSelectedUserOrg] = useState({} as ComboboxItem);
|
const [selectedUserOrg, setSelectedUserOrg] = useState({} as ComboboxItem);
|
||||||
const [repositoryName, setRepositoryName] =
|
const [repositoryName, setRepositoryName] =
|
||||||
useState<GithubState.Repository>({} as GithubState.Repository);
|
useState<GithubState.Repository>({} as GithubState.Repository);
|
||||||
const [branchName, setBranchName] = useState({} as DropdownItem);
|
|
||||||
const [commitHash, setCommitHash] = useState('');
|
|
||||||
const [githubStep, setGithubStepContext] = useState(1);
|
const [githubStep, setGithubStepContext] = useState(1);
|
||||||
|
|
||||||
//NFA Details
|
//NFA Details
|
||||||
const [nfaStep, setNfaStep] = useState(1);
|
const [nfaStep, setNfaStep] = useState(1);
|
||||||
const [appName, setAppName] = useState('');
|
|
||||||
const [appDescription, setAppDescription] = useState('');
|
|
||||||
const [appLogo, setAppLogo] = useState('');
|
|
||||||
const [logoColor, setLogoColor] = useState('');
|
|
||||||
const [ens, setEns] = useState({} as ComboboxItem);
|
|
||||||
const [domain, setDomain] = useState('');
|
|
||||||
const [verifyNFA, setVerifyNFA] = useState(true);
|
const [verifyNFA, setVerifyNFA] = useState(true);
|
||||||
const [billing] = useFleekERC721Billing('Mint');
|
const [billing] = useFleekERC721Billing('Mint');
|
||||||
|
|
||||||
//Field validations
|
|
||||||
const [ensError, setEnsError] = useState<string>('');
|
|
||||||
|
|
||||||
const setGithubStep = (step: number): void => {
|
const setGithubStep = (step: number): void => {
|
||||||
if (step > 0 && step <= 3) {
|
if (step > 0 && step <= 3) {
|
||||||
setGithubStepContext(step);
|
setGithubStepContext(step);
|
||||||
|
|
@ -86,32 +57,14 @@ export abstract class Mint {
|
||||||
billing,
|
billing,
|
||||||
selectedUserOrg,
|
selectedUserOrg,
|
||||||
repositoryName,
|
repositoryName,
|
||||||
branchName,
|
|
||||||
commitHash,
|
|
||||||
githubStep,
|
githubStep,
|
||||||
nfaStep,
|
nfaStep,
|
||||||
appName,
|
|
||||||
appDescription,
|
|
||||||
appLogo,
|
|
||||||
logoColor,
|
|
||||||
ens,
|
|
||||||
domain,
|
|
||||||
verifyNFA,
|
verifyNFA,
|
||||||
ensError,
|
|
||||||
setSelectedUserOrg,
|
setSelectedUserOrg,
|
||||||
setGithubStep,
|
setGithubStep,
|
||||||
setNfaStep,
|
setNfaStep,
|
||||||
setRepositoryName,
|
setRepositoryName,
|
||||||
setBranchName,
|
|
||||||
setCommitHash,
|
|
||||||
setAppName,
|
|
||||||
setAppDescription,
|
|
||||||
setAppLogo,
|
|
||||||
setLogoColor,
|
|
||||||
setEns,
|
|
||||||
setDomain,
|
|
||||||
setVerifyNFA,
|
setVerifyNFA,
|
||||||
setEnsError,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TransactionProvider
|
<TransactionProvider
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
import { Flex } from '@/components';
|
import { Flex } from '@/components';
|
||||||
import { MintStepper } from './mint-stepper';
|
import { MintStepper } from './mint-stepper';
|
||||||
import { Mint as MintContext } from './mint.context';
|
import { Mint as MintContext } from './mint.context';
|
||||||
|
import { MintFormProvider, useMintFormContextInit } from './nfa-step/form-step';
|
||||||
|
|
||||||
export const Mint = () => (
|
export const Mint = () => {
|
||||||
<Flex
|
const context = useMintFormContextInit();
|
||||||
css={{
|
return (
|
||||||
flexGrow: 1,
|
<Flex
|
||||||
justifyContent: 'center',
|
css={{
|
||||||
alignItems: 'center',
|
height: '100%',
|
||||||
}}
|
flexDirection: 'column',
|
||||||
>
|
justifyContent: 'center',
|
||||||
<MintContext.Provider>
|
alignItems: 'center',
|
||||||
<MintStepper />
|
}}
|
||||||
</MintContext.Provider>
|
>
|
||||||
</Flex>
|
<MintContext.Provider>
|
||||||
);
|
<MintFormProvider value={context}>
|
||||||
|
<MintStepper />
|
||||||
|
</MintFormProvider>
|
||||||
|
</MintContext.Provider>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,19 @@
|
||||||
import { Form } from '@/components';
|
import { Form } from '@/components';
|
||||||
import { Mint } from '../../../mint.context';
|
import { useMintFormContext } from '../mint-form.context';
|
||||||
|
|
||||||
const maxCharacters = 250;
|
|
||||||
|
|
||||||
export const AppDescriptionField = () => {
|
export const AppDescriptionField = () => {
|
||||||
const { appDescription, setAppDescription } = Mint.useContext();
|
const {
|
||||||
|
form: { appDescription },
|
||||||
const handleAppDescriptionChange = (
|
} = useMintFormContext();
|
||||||
e: React.ChangeEvent<HTMLTextAreaElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.value.length > maxCharacters) return;
|
|
||||||
setAppDescription(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Field>
|
<Form.Field context={appDescription}>
|
||||||
<Form.Label isRequired>Description</Form.Label>
|
<Form.Label>Description</Form.Label>
|
||||||
<Form.Textarea
|
<Form.Textarea
|
||||||
placeholder="Add information about your project here."
|
placeholder="Add information about your project here."
|
||||||
css={{ height: 'auto' }}
|
css={{ height: 'auto' }}
|
||||||
value={appDescription}
|
|
||||||
onChange={handleAppDescriptionChange}
|
|
||||||
/>
|
/>
|
||||||
<Form.MaxLength>
|
<Form.Overline />
|
||||||
{appDescription.length}/{maxCharacters}
|
|
||||||
</Form.MaxLength>
|
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,16 @@
|
||||||
import { Form } from '@/components';
|
import { Form } from '@/components';
|
||||||
import { Mint } from '../../../mint.context';
|
import { useMintFormContext } from '../mint-form.context';
|
||||||
|
|
||||||
const maxCharacters = 100;
|
|
||||||
|
|
||||||
export const AppNameField = () => {
|
export const AppNameField = () => {
|
||||||
const { appName, setAppName } = Mint.useContext();
|
const {
|
||||||
|
form: { appName },
|
||||||
|
} = useMintFormContext();
|
||||||
|
|
||||||
const handleAppNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setAppName(e.target.value);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Form.Field>
|
<Form.Field context={appName}>
|
||||||
<Form.Label isRequired>Name</Form.Label>
|
<Form.Label>Name</Form.Label>
|
||||||
<Form.Input
|
<Form.Input placeholder="Your app name" />
|
||||||
placeholder="Your app name"
|
<Form.Overline />
|
||||||
value={appName}
|
|
||||||
onChange={handleAppNameChange}
|
|
||||||
/>
|
|
||||||
<Form.MaxLength>
|
|
||||||
{appName.length}/{maxCharacters}
|
|
||||||
</Form.MaxLength>
|
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
import { Form } from '@/components';
|
import { Form } from '@/components';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
|
import { useMintFormContext } from '../../mint-form.context';
|
||||||
|
|
||||||
export const DomainField = () => {
|
export const DomainField = () => {
|
||||||
const { domain, setDomain } = Mint.useContext();
|
const {
|
||||||
|
form: { domainURL },
|
||||||
const handleDomainChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
} = useMintFormContext();
|
||||||
setDomain(e.target.value);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Form.Field css={{ flex: 1 }}>
|
<Form.Field context={domainURL} css={{ flex: 1 }}>
|
||||||
<Form.Label isRequired>Domain</Form.Label>
|
<Form.Label>Domain</Form.Label>
|
||||||
<Form.Input
|
<Form.Input placeholder="mydomain.com" />
|
||||||
placeholder="mydomain.com"
|
<Form.Overline />
|
||||||
value={domain}
|
|
||||||
onChange={handleDomainChange}
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,58 @@
|
||||||
import { Combobox, ComboboxItem, Form } from '@/components';
|
import { getENSNamesDocument } from '@/../.graphclient';
|
||||||
import { ensActions, useAppDispatch, useEnsStore } from '@/store';
|
import { ComboboxItem, Form } from '@/components';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { AppLog } from '@/utils';
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { useAccount } from 'wagmi';
|
import { useAccount } from 'wagmi';
|
||||||
|
import { useMintFormContext } from '../../mint-form.context';
|
||||||
|
|
||||||
export const EnsField = () => {
|
export const EnsField = () => {
|
||||||
const { ens, ensError, setEns } = Mint.useContext();
|
|
||||||
const { state, ensNames } = useEnsStore();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { address } = useAccount();
|
const { address } = useAccount();
|
||||||
|
const { data, error } = useQuery(getENSNamesDocument, {
|
||||||
|
variables: {
|
||||||
|
address: address?.toString() || '', //should skip if undefined
|
||||||
|
skip: address === undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (state === 'idle' && address) {
|
const {
|
||||||
dispatch(ensActions.fetchEnsNamesThunk(address));
|
form: { ens },
|
||||||
}
|
} = useMintFormContext();
|
||||||
|
|
||||||
const handleEnsChange = (item: ComboboxItem) => {
|
const showError = useCallback(() => {
|
||||||
setEns(item);
|
AppLog.errorToast(
|
||||||
};
|
'There was an error trying to get your ENS names. Please try again later.'
|
||||||
|
);
|
||||||
|
}, [AppLog]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
showError();
|
||||||
|
}
|
||||||
|
}, [error, showError]);
|
||||||
|
|
||||||
|
const ensNames = useMemo(() => {
|
||||||
|
const ensList: ComboboxItem[] = [];
|
||||||
|
if (data && data.account && data.account.domains) {
|
||||||
|
data.account.domains.forEach((ens) => {
|
||||||
|
const { name } = ens;
|
||||||
|
if (name) {
|
||||||
|
ensList.push({
|
||||||
|
label: name,
|
||||||
|
value: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ensList;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Field css={{ flex: 1 }}>
|
<Form.Field context={ens} css={{ flex: 1 }}>
|
||||||
<Form.Label>ENS</Form.Label>
|
<Form.Label>ENS</Form.Label>
|
||||||
<Combobox
|
<Form.Combobox items={ensNames} />
|
||||||
items={ensNames.map((ens) => ({
|
<Form.Overline />
|
||||||
label: ens,
|
|
||||||
value: ens,
|
|
||||||
}))}
|
|
||||||
selectedValue={ens}
|
|
||||||
onChange={handleEnsChange}
|
|
||||||
withAutocomplete
|
|
||||||
/>
|
|
||||||
{ensError && <Form.Error>{ensError}</Form.Error>}
|
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,26 @@
|
||||||
import { Flex, Form } from '@/components';
|
import { Flex, Form } from '@/components';
|
||||||
import { AppLog } from '@/utils';
|
import { useMintFormContext } from '../../mint-form.context';
|
||||||
import { useState } from 'react';
|
|
||||||
import { Mint } from '../../../../mint.context';
|
|
||||||
import { fileToBase64, validateFileSize } from '../../form.utils';
|
|
||||||
import { ColorPicker } from './color-picker';
|
|
||||||
|
|
||||||
export const LogoField = () => {
|
export const LogoField = () => {
|
||||||
const { appLogo, setAppLogo, setLogoColor } = Mint.useContext();
|
const {
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
form: { appLogo: appLogoContext, logoColor: logoColorContext },
|
||||||
|
} = useMintFormContext();
|
||||||
|
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const {
|
||||||
const file = e.target.files?.[0];
|
value: [appLogo],
|
||||||
|
} = appLogoContext;
|
||||||
|
|
||||||
if (file) {
|
|
||||||
if (validateFileSize(file)) {
|
|
||||||
const fileBase64 = await fileToBase64(file);
|
|
||||||
setAppLogo(fileBase64);
|
|
||||||
setErrorMessage(null);
|
|
||||||
} else {
|
|
||||||
setAppLogo('');
|
|
||||||
setLogoColor('');
|
|
||||||
setErrorMessage('File size is too big');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Flex css={{ width: '$full', gap: '$4h', alignItems: 'flex-start' }}>
|
<Flex css={{ width: '$full', gap: '$4h', alignItems: 'flex-start' }}>
|
||||||
<Form.Field>
|
<Form.Field context={appLogoContext}>
|
||||||
<Form.Label isRequired>Logo</Form.Label>
|
<Form.Label>Logo</Form.Label>
|
||||||
<Form.LogoFileInput value={appLogo} onChange={handleFileChange} />
|
<Form.LogoFileInput />
|
||||||
{errorMessage && <Form.Error>{errorMessage}</Form.Error>}
|
<Form.Overline />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field context={logoColorContext} css={{ flexGrow: 1 }}>
|
||||||
|
<Form.ColorPicker logo={appLogo} />
|
||||||
|
<Form.Overline />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<ColorPicker />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,3 @@
|
||||||
//TODO create env variable
|
|
||||||
const DEFAULT_MAX_FILE_SIZE = 10; // in KB
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The file size must be capped to a size that the contract can handle
|
|
||||||
*/
|
|
||||||
export const validateFileSize = (
|
|
||||||
file: File,
|
|
||||||
maxSize = DEFAULT_MAX_FILE_SIZE
|
|
||||||
): boolean => {
|
|
||||||
return file.size <= 1024 * maxSize;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the File from the input to a base64 string.
|
* Converts the File from the input to a base64 string.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './mint-form';
|
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 { Mint } from '../../mint.context';
|
||||||
import {
|
import {
|
||||||
LogoField,
|
LogoField,
|
||||||
|
|
@ -10,24 +10,41 @@ import { MintCardHeader } from '../../mint-card';
|
||||||
import { useAccount } from 'wagmi';
|
import { useAccount } from 'wagmi';
|
||||||
import { parseColorToNumber } from './form.utils';
|
import { parseColorToNumber } from './form.utils';
|
||||||
import { AppLog } from '@/utils';
|
import { AppLog } from '@/utils';
|
||||||
|
import { useMintFormContext } from './mint-form.context';
|
||||||
|
|
||||||
export const MintFormStep = () => {
|
export const MintFormStep = () => {
|
||||||
|
const {
|
||||||
|
form: {
|
||||||
|
appName: {
|
||||||
|
value: [appName],
|
||||||
|
},
|
||||||
|
appDescription: {
|
||||||
|
value: [appDescription],
|
||||||
|
},
|
||||||
|
appLogo: {
|
||||||
|
value: [appLogo],
|
||||||
|
},
|
||||||
|
ens: {
|
||||||
|
value: [ens],
|
||||||
|
},
|
||||||
|
domainURL: {
|
||||||
|
value: [domainURL],
|
||||||
|
},
|
||||||
|
gitCommit: {
|
||||||
|
value: [gitCommit],
|
||||||
|
},
|
||||||
|
gitBranch: {
|
||||||
|
value: [gitBranch],
|
||||||
|
},
|
||||||
|
logoColor: {
|
||||||
|
value: [logoColor],
|
||||||
|
},
|
||||||
|
isValid: [isValid],
|
||||||
|
},
|
||||||
|
} = useMintFormContext();
|
||||||
const { address } = useAccount();
|
const { address } = useAccount();
|
||||||
const { nextStep } = Stepper.useContext();
|
const { nextStep } = Stepper.useContext();
|
||||||
const {
|
const { billing, repositoryName, verifyNFA, setNfaStep } = Mint.useContext();
|
||||||
billing,
|
|
||||||
appName,
|
|
||||||
appDescription,
|
|
||||||
domain,
|
|
||||||
appLogo,
|
|
||||||
branchName,
|
|
||||||
commitHash,
|
|
||||||
ens,
|
|
||||||
logoColor,
|
|
||||||
repositoryName,
|
|
||||||
verifyNFA,
|
|
||||||
setNfaStep,
|
|
||||||
} = Mint.useContext();
|
|
||||||
const { setArgs } = Mint.useTransactionContext();
|
const { setArgs } = Mint.useTransactionContext();
|
||||||
|
|
||||||
const handleNextStep = () => {
|
const handleNextStep = () => {
|
||||||
|
|
@ -35,16 +52,15 @@ export const MintFormStep = () => {
|
||||||
AppLog.errorToast('No address found. Please connect your wallet.');
|
AppLog.errorToast('No address found. Please connect your wallet.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: we need to make sure all values are correct before
|
|
||||||
// setting the args otherwise mint may fail
|
// setting the args otherwise mint may fail
|
||||||
setArgs([
|
setArgs([
|
||||||
address,
|
address,
|
||||||
appName,
|
appName,
|
||||||
appDescription,
|
appDescription,
|
||||||
domain,
|
domainURL,
|
||||||
ens.value,
|
ens,
|
||||||
commitHash,
|
gitCommit,
|
||||||
`${repositoryName.url}/tree/${branchName.label}`,
|
`${repositoryName.url}/tree/${gitBranch}`,
|
||||||
appLogo,
|
appLogo,
|
||||||
parseColorToNumber(logoColor),
|
parseColorToNumber(logoColor),
|
||||||
verifyNFA,
|
verifyNFA,
|
||||||
|
|
@ -74,7 +90,7 @@ export const MintFormStep = () => {
|
||||||
<EnsDomainField />
|
<EnsDomainField />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Button
|
<Button
|
||||||
disabled={!appName || !appDescription || !domain}
|
disabled={!isValid}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={handleNextStep}
|
onClick={handleNextStep}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Button, Card, Grid } from '@/components';
|
import { Button, Card, Grid } from '@/components';
|
||||||
import { Mint } from '../mint.context';
|
import { useMintFormContext } from '../nfa-step/form-step';
|
||||||
import { SVGPreview } from './svg-preview';
|
import { SVGPreview } from './svg-preview';
|
||||||
|
|
||||||
type NftCardProps = {
|
type NftCardProps = {
|
||||||
|
|
@ -24,7 +24,22 @@ export const NftCard: React.FC<NftCardProps> = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
}) => {
|
}) => {
|
||||||
const size = '26.5rem';
|
const size = '26.5rem';
|
||||||
const { appLogo, logoColor, appName, ens } = Mint.useContext();
|
const {
|
||||||
|
form: {
|
||||||
|
appName: {
|
||||||
|
value: [appName],
|
||||||
|
},
|
||||||
|
appLogo: {
|
||||||
|
value: [appLogo],
|
||||||
|
},
|
||||||
|
logoColor: {
|
||||||
|
value: [logoColor],
|
||||||
|
},
|
||||||
|
ens: {
|
||||||
|
value: [ens],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = useMintFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card.Container css={{ width: '$107h', p: '$0' }}>
|
<Card.Container css={{ width: '$107h', p: '$0' }}>
|
||||||
|
|
@ -32,7 +47,7 @@ export const NftCard: React.FC<NftCardProps> = ({
|
||||||
color={logoColor}
|
color={logoColor}
|
||||||
logo={appLogo}
|
logo={appLogo}
|
||||||
name={appName}
|
name={appName}
|
||||||
ens={ens.label}
|
ens={ens}
|
||||||
size={size}
|
size={size}
|
||||||
css="rounded-t-xhl"
|
css="rounded-t-xhl"
|
||||||
/>
|
/>
|
||||||
|
|
@ -43,10 +58,7 @@ export const NftCard: React.FC<NftCardProps> = ({
|
||||||
leftIcon={leftIcon}
|
leftIcon={leftIcon}
|
||||||
rightIcon={rightIcon}
|
rightIcon={rightIcon}
|
||||||
/>
|
/>
|
||||||
{/* TODO replace for real price when integrate with wallet */}
|
|
||||||
<span className="text-slate11 text-sm">{message}</span>
|
<span className="text-slate11 text-sm">{message}</span>
|
||||||
{/* TODO add desabled when user doesnt have enough MATIC */}
|
|
||||||
{/* TODO repalce for app name when connect with context */}
|
|
||||||
<Button
|
<Button
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ module.exports = {
|
||||||
green4: 'rgba(17, 49, 35, 1)',
|
green4: 'rgba(17, 49, 35, 1)',
|
||||||
green11: 'rgba(76, 195, 138, 1)',
|
green11: 'rgba(76, 195, 138, 1)',
|
||||||
red4: 'rgba(72, 26, 29, 1)',
|
red4: 'rgba(72, 26, 29, 1)',
|
||||||
|
red9: 'rgba(229, 72, 77, 1)',
|
||||||
red11: 'rgba(255, 99, 105, 1)',
|
red11: 'rgba(255, 99, 105, 1)',
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue