Merge pull request #260 from fleekxyz/release/release-v0.0.7

Release v0.0.7 to main
This commit is contained in:
Camila Sosa Morales 2023-05-11 18:43:13 -03:00 committed by GitHub
commit ebffbaff0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 2064 additions and 469 deletions

View File

@ -113,7 +113,9 @@ contract FleekERC721 is
bool accessPointAutoApproval,
address verifier
) public payable requirePayment(Billing.Mint) returns (uint256) {
FleekENS.requireENSOwner(ens);
if (!hasCollectionRole(CollectionRoles.Verifier, verifier))
revert MustHaveCollectionRole(uint8(CollectionRoles.Verifier));
if (bytes(ens).length > 0) FleekENS.requireENSOwner(ens);
uint256 tokenId = _appIds;
_mint(to, tokenId);

View File

@ -45,7 +45,7 @@
"colorthief": "^2.3.2",
"dotenv": "^16.0.2",
"ethers": "^5.7.2",
"hardhat": "^2.11.2",
"hardhat": "^2.14.0",
"hardhat-contract-sizer": "^2.6.1",
"hardhat-gas-reporter": "^1.0.9",
"lint-staged": "^13.0.4",

View File

@ -103,4 +103,39 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
assertEq(tokenId, 0);
assertEq(CuT.ownerOf(tokenId), to);
}
function testFuzz_shouldNotAllowMintWithInvalidVerifier(address verifier) public {
vm.assume(!CuT.hasCollectionRole(FleekAccessControl.CollectionRoles.Verifier, verifier));
expectRevertWithCollectionRole(FleekAccessControl.CollectionRoles.Verifier);
CuT.mint(
deployer,
TestConstants.APP_NAME,
TestConstants.APP_DESCRIPTION,
TestConstants.APP_EXTERNAL_URL,
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
false,
verifier
);
}
function test_shouldAllowMintWithEmptyENS() public {
uint256 tokenId = CuT.mint(
deployer,
TestConstants.APP_NAME,
TestConstants.APP_DESCRIPTION,
TestConstants.APP_EXTERNAL_URL,
"",
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
false,
deployer
);
assertEq(tokenId, 0);
}
}

View File

@ -1,9 +1,9 @@
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { expect } from 'chai';
import { TestConstants, Fixtures } from './helpers';
import { TestConstants, Fixtures, Errors } from './helpers';
import { ethers } from 'hardhat';
const { MintParams } = TestConstants;
const { MintParams, CollectionRoles } = TestConstants;
describe('FleekERC721.Minting', () => {
it('should be able to mint a new token', async () => {
@ -51,4 +51,49 @@ describe('FleekERC721.Minting', () => {
expect(await contract.ownerOf(tokenId)).to.equal(owner.address);
expect(await contract.ownerOf(tokenId)).not.to.equal(otherAccount.address);
});
it('should not allow minting with non verifier account param', async () => {
const { owner, otherAccount, contract } = await loadFixture(
Fixtures.default
);
await expect(
contract.mint(
owner.address,
MintParams.name,
MintParams.description,
MintParams.externalUrl,
MintParams.ens,
MintParams.commitHash,
MintParams.gitRepository,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
otherAccount.address
)
)
.to.be.revertedWithCustomError(contract, Errors.MustHaveCollectionRole)
.withArgs(CollectionRoles.Verifier);
});
it('should allow minting with empty ens', async () => {
const { owner, contract } = await loadFixture(Fixtures.default);
const response = await contract.mint(
owner.address,
MintParams.name,
MintParams.description,
MintParams.externalUrl,
'',
MintParams.commitHash,
MintParams.gitRepository,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
owner.address
);
expect(response.value).to.be.instanceOf(ethers.BigNumber);
expect(response.value.toNumber()).to.equal(0);
});
});

View File

@ -104,6 +104,6 @@ describe('Deploy', () => {
TestConstants.MintParams.accessPointAutoApprovalSettings,
owner.address
)
).to.be.revertedWithCustomError(implementation, Errors.ContractIsPaused);
).to.be.reverted;
});
});

View File

@ -2,6 +2,42 @@
# yarn lockfile v1
"@chainsafe/as-sha256@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz#3639df0e1435cab03f4d9870cc3ac079e57a6fc9"
integrity sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==
"@chainsafe/persistent-merkle-tree@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz#4c9ee80cc57cd3be7208d98c40014ad38f36f7ff"
integrity sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==
dependencies:
"@chainsafe/as-sha256" "^0.3.1"
"@chainsafe/persistent-merkle-tree@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz#2b4a62c9489a5739dedd197250d8d2f5427e9f63"
integrity sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw==
dependencies:
"@chainsafe/as-sha256" "^0.3.1"
"@chainsafe/ssz@^0.10.0":
version "0.10.2"
resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.10.2.tgz#c782929e1bb25fec66ba72e75934b31fd087579e"
integrity sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg==
dependencies:
"@chainsafe/as-sha256" "^0.3.1"
"@chainsafe/persistent-merkle-tree" "^0.5.0"
"@chainsafe/ssz@^0.9.2":
version "0.9.4"
resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.4.tgz#696a8db46d6975b600f8309ad3a12f7c0e310497"
integrity sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ==
dependencies:
"@chainsafe/as-sha256" "^0.3.1"
"@chainsafe/persistent-merkle-tree" "^0.4.2"
case "^1.6.3"
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@ -256,7 +292,7 @@
dependencies:
"@ethersproject/logger" "^5.7.0"
"@ethersproject/providers@5.7.2":
"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2":
version "5.7.2"
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb"
integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==
@ -477,29 +513,31 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@nomicfoundation/ethereumjs-block@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz#fdd5c045e7baa5169abeed0e1202bf94e4481c49"
integrity sha512-bk8uP8VuexLgyIZAHExH1QEovqx0Lzhc9Ntm63nCRKLHXIZkobaFaeCVwTESV7YkPKUk7NiK11s8ryed4CS9yA==
"@nomicfoundation/ethereumjs-block@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.1.tgz#6f89664f55febbd723195b6d0974773d29ee133d"
integrity sha512-u1Yioemi6Ckj3xspygu/SfFvm8vZEO8/Yx5a1QLzi6nVU0jz3Pg2OmHKJ5w+D9Ogk1vhwRiqEBAqcb0GVhCyHw==
dependencies:
"@nomicfoundation/ethereumjs-common" "^3.0.0"
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-trie" "^5.0.0"
"@nomicfoundation/ethereumjs-tx" "^4.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@nomicfoundation/ethereumjs-common" "4.0.1"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
"@nomicfoundation/ethereumjs-trie" "6.0.1"
"@nomicfoundation/ethereumjs-tx" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
ethereum-cryptography "0.1.3"
ethers "^5.7.1"
"@nomicfoundation/ethereumjs-blockchain@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0.tgz#1a8c243a46d4d3691631f139bfb3a4a157187b0c"
integrity sha512-pLFEoea6MWd81QQYSReLlLfH7N9v7lH66JC/NMPN848ySPPQA5renWnE7wPByfQFzNrPBuDDRFFULMDmj1C0xw==
"@nomicfoundation/ethereumjs-blockchain@7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.1.tgz#80e0bd3535bfeb9baa29836b6f25123dab06a726"
integrity sha512-NhzndlGg829XXbqJEYrF1VeZhAwSPgsK/OB7TVrdzft3y918hW5KNd7gIZ85sn6peDZOdjBsAXIpXZ38oBYE5A==
dependencies:
"@nomicfoundation/ethereumjs-block" "^4.0.0"
"@nomicfoundation/ethereumjs-common" "^3.0.0"
"@nomicfoundation/ethereumjs-ethash" "^2.0.0"
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-trie" "^5.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@nomicfoundation/ethereumjs-block" "5.0.1"
"@nomicfoundation/ethereumjs-common" "4.0.1"
"@nomicfoundation/ethereumjs-ethash" "3.0.1"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
"@nomicfoundation/ethereumjs-trie" "6.0.1"
"@nomicfoundation/ethereumjs-tx" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
abstract-level "^1.0.3"
debug "^4.3.3"
ethereum-cryptography "0.1.3"
@ -507,105 +545,105 @@
lru-cache "^5.1.1"
memory-level "^1.0.0"
"@nomicfoundation/ethereumjs-common@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz#f6bcc7753994555e49ab3aa517fc8bcf89c280b9"
integrity sha512-WS7qSshQfxoZOpHG/XqlHEGRG1zmyjYrvmATvc4c62+gZXgre1ymYP8ZNgx/3FyZY0TWe9OjFlKOfLqmgOeYwA==
"@nomicfoundation/ethereumjs-common@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.1.tgz#4702d82df35b07b5407583b54a45bf728e46a2f0"
integrity sha512-OBErlkfp54GpeiE06brBW/TTbtbuBJV5YI5Nz/aB2evTDo+KawyEzPjBlSr84z/8MFfj8wS2wxzQX1o32cev5g==
dependencies:
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@nomicfoundation/ethereumjs-util" "9.0.1"
crc-32 "^1.2.0"
"@nomicfoundation/ethereumjs-ethash@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0.tgz#11539c32fe0990e1122ff987d1b84cfa34774e81"
integrity sha512-WpDvnRncfDUuXdsAXlI4lXbqUDOA+adYRQaEezIkxqDkc+LDyYDbd/xairmY98GnQzo1zIqsIL6GB5MoMSJDew==
"@nomicfoundation/ethereumjs-ethash@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.1.tgz#65ca494d53e71e8415c9a49ef48bc921c538fc41"
integrity sha512-KDjGIB5igzWOp8Ik5I6QiRH5DH+XgILlplsHR7TEuWANZA759G6krQ6o8bvj+tRUz08YygMQu/sGd9mJ1DYT8w==
dependencies:
"@nomicfoundation/ethereumjs-block" "^4.0.0"
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@nomicfoundation/ethereumjs-block" "5.0.1"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
abstract-level "^1.0.3"
bigint-crypto-utils "^3.0.23"
ethereum-cryptography "0.1.3"
"@nomicfoundation/ethereumjs-evm@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0.tgz#99cd173c03b59107c156a69c5e215409098a370b"
integrity sha512-hVS6qRo3V1PLKCO210UfcEQHvlG7GqR8iFzp0yyjTg2TmJQizcChKgWo8KFsdMw6AyoLgLhHGHw4HdlP8a4i+Q==
"@nomicfoundation/ethereumjs-evm@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.1.tgz#f35681e203363f69ce2b3d3bf9f44d4e883ca1f1"
integrity sha512-oL8vJcnk0Bx/onl+TgQOQ1t/534GKFaEG17fZmwtPFeH8S5soiBYPCLUrvANOl4sCp9elYxIMzIiTtMtNNN8EQ==
dependencies:
"@nomicfoundation/ethereumjs-common" "^3.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@types/async-eventemitter" "^0.2.1"
async-eventemitter "^0.2.4"
"@ethersproject/providers" "^5.7.1"
"@nomicfoundation/ethereumjs-common" "4.0.1"
"@nomicfoundation/ethereumjs-tx" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
debug "^4.3.3"
ethereum-cryptography "0.1.3"
mcl-wasm "^0.7.1"
rustbn.js "~0.2.0"
"@nomicfoundation/ethereumjs-rlp@^4.0.0", "@nomicfoundation/ethereumjs-rlp@^4.0.0-beta.2":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz#d9a9c5f0f10310c8849b6525101de455a53e771d"
integrity sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw==
"@nomicfoundation/ethereumjs-rlp@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.1.tgz#0b30c1cf77d125d390408e391c4bb5291ef43c28"
integrity sha512-xtxrMGa8kP4zF5ApBQBtjlSbN5E2HI8m8FYgVSYAnO6ssUoY5pVPGy2H8+xdf/bmMa22Ce8nWMH3aEW8CcqMeQ==
"@nomicfoundation/ethereumjs-statemanager@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0.tgz#14a9d4e1c828230368f7ab520c144c34d8721e4b"
integrity sha512-jCtqFjcd2QejtuAMjQzbil/4NHf5aAWxUc+CvS0JclQpl+7M0bxMofR2AJdtz+P3u0ke2euhYREDiE7iSO31vQ==
"@nomicfoundation/ethereumjs-statemanager@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.1.tgz#8824a97938db4471911e2d2f140f79195def5935"
integrity sha512-B5ApMOnlruVOR7gisBaYwFX+L/AP7i/2oAahatssjPIBVDF6wTX1K7Qpa39E/nzsH8iYuL3krkYeUFIdO3EMUQ==
dependencies:
"@nomicfoundation/ethereumjs-common" "^3.0.0"
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-trie" "^5.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@nomicfoundation/ethereumjs-common" "4.0.1"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
debug "^4.3.3"
ethereum-cryptography "0.1.3"
functional-red-black-tree "^1.0.1"
ethers "^5.7.1"
js-sdsl "^4.1.4"
"@nomicfoundation/ethereumjs-trie@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0.tgz#dcfbe3be53a94bc061c9767a396c16702bc2f5b7"
integrity sha512-LIj5XdE+s+t6WSuq/ttegJzZ1vliwg6wlb+Y9f4RlBpuK35B9K02bO7xU+E6Rgg9RGptkWd6TVLdedTI4eNc2A==
"@nomicfoundation/ethereumjs-trie@6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.1.tgz#662c55f6b50659fd4b22ea9f806a7401cafb7717"
integrity sha512-A64It/IMpDVODzCgxDgAAla8jNjNtsoQZIzZUfIV5AY6Coi4nvn7+VReBn5itlxMiL2yaTlQr9TRWp3CSI6VoA==
dependencies:
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
"@types/readable-stream" "^2.3.13"
ethereum-cryptography "0.1.3"
readable-stream "^3.6.0"
"@nomicfoundation/ethereumjs-tx@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz#59dc7452b0862b30342966f7052ab9a1f7802f52"
integrity sha512-Gg3Lir2lNUck43Kp/3x6TfBNwcWC9Z1wYue9Nz3v4xjdcv6oDW9QSMJxqsKw9QEGoBBZ+gqwpW7+F05/rs/g1w==
"@nomicfoundation/ethereumjs-tx@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.1.tgz#7629dc2036b4a33c34e9f0a592b43227ef4f0c7d"
integrity sha512-0HwxUF2u2hrsIM1fsasjXvlbDOq1ZHFV2dd1yGq8CA+MEYhaxZr8OTScpVkkxqMwBcc5y83FyPl0J9MZn3kY0w==
dependencies:
"@nomicfoundation/ethereumjs-common" "^3.0.0"
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@chainsafe/ssz" "^0.9.2"
"@ethersproject/providers" "^5.7.2"
"@nomicfoundation/ethereumjs-common" "4.0.1"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
ethereum-cryptography "0.1.3"
"@nomicfoundation/ethereumjs-util@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz#deb2b15d2c308a731e82977aefc4e61ca0ece6c5"
integrity sha512-2emi0NJ/HmTG+CGY58fa+DQuAoroFeSH9gKu9O6JnwTtlzJtgfTixuoOqLEgyyzZVvwfIpRueuePb8TonL1y+A==
"@nomicfoundation/ethereumjs-util@9.0.1":
version "9.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.1.tgz#530cda8bae33f8b5020a8f199ed1d0a2ce48ec89"
integrity sha512-TwbhOWQ8QoSCFhV/DDfSmyfFIHjPjFBj957219+V3jTZYZ2rf9PmDtNOeZWAE3p3vlp8xb02XGpd0v6nTUPbsA==
dependencies:
"@nomicfoundation/ethereumjs-rlp" "^4.0.0-beta.2"
"@chainsafe/ssz" "^0.10.0"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
ethereum-cryptography "0.1.3"
"@nomicfoundation/ethereumjs-vm@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz#2bb50d332bf41790b01a3767ffec3987585d1de6"
integrity sha512-JMPxvPQ3fzD063Sg3Tp+UdwUkVxMoo1uML6KSzFhMH3hoQi/LMuXBoEHAoW83/vyNS9BxEe6jm6LmT5xdeEJ6w==
"@nomicfoundation/ethereumjs-vm@7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.1.tgz#7d035e0993bcad10716c8b36e61dfb87fa3ca05f"
integrity sha512-rArhyn0jPsS/D+ApFsz3yVJMQ29+pVzNZ0VJgkzAZ+7FqXSRtThl1C1prhmlVr3YNUlfpZ69Ak+RUT4g7VoOuQ==
dependencies:
"@nomicfoundation/ethereumjs-block" "^4.0.0"
"@nomicfoundation/ethereumjs-blockchain" "^6.0.0"
"@nomicfoundation/ethereumjs-common" "^3.0.0"
"@nomicfoundation/ethereumjs-evm" "^1.0.0"
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-statemanager" "^1.0.0"
"@nomicfoundation/ethereumjs-trie" "^5.0.0"
"@nomicfoundation/ethereumjs-tx" "^4.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@types/async-eventemitter" "^0.2.1"
async-eventemitter "^0.2.4"
"@nomicfoundation/ethereumjs-block" "5.0.1"
"@nomicfoundation/ethereumjs-blockchain" "7.0.1"
"@nomicfoundation/ethereumjs-common" "4.0.1"
"@nomicfoundation/ethereumjs-evm" "2.0.1"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
"@nomicfoundation/ethereumjs-statemanager" "2.0.1"
"@nomicfoundation/ethereumjs-trie" "6.0.1"
"@nomicfoundation/ethereumjs-tx" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
debug "^4.3.3"
ethereum-cryptography "0.1.3"
functional-red-black-tree "^1.0.1"
mcl-wasm "^0.7.1"
rustbn.js "~0.2.0"
@ -901,11 +939,6 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@types/async-eventemitter@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@types/async-eventemitter/-/async-eventemitter-0.2.1.tgz#f8e6280e87e8c60b2b938624b0a3530fb3e24712"
integrity sha512-M2P4Ng26QbAeITiH7w1d7OxtldgfAe0wobpyJzVK/XOb0cUGKU2R4pfAhqcJBXAe2ife5ZOhSv4wk7p+ffURtg==
"@types/bignumber.js@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/bignumber.js/-/bignumber.js-5.0.0.tgz#d9f1a378509f3010a3255e9cc822ad0eeb4ab969"
@ -1030,6 +1063,14 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/readable-stream@^2.3.13":
version "2.3.15"
resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.15.tgz#3d79c9ceb1b6a57d5f6e6976f489b9b5384321ae"
integrity sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==
dependencies:
"@types/node" "*"
safe-buffer "~5.1.1"
"@types/responselike@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
@ -1296,13 +1337,6 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
async-eventemitter@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.4.tgz#f5e7c8ca7d3e46aab9ec40a292baf686a0bafaca"
integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==
dependencies:
async "^2.4.0"
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
@ -1313,13 +1347,6 @@ async@1.x:
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==
async@^2.4.0:
version "2.6.4"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
dependencies:
lodash "^4.17.14"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -1604,6 +1631,11 @@ camelcase@^6.0.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
case@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/case/-/case-1.6.3.tgz#0a4386e3e9825351ca2e6216c60467ff5f1ea1c9"
integrity sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==
caseless@^0.12.0, caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@ -2564,7 +2596,7 @@ ethers@^4.0.40:
uuid "2.0.1"
xmlhttprequest "1.8.0"
ethers@^5.7.2:
ethers@^5.7.1, ethers@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e"
integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==
@ -3230,23 +3262,23 @@ hardhat-gas-reporter@^1.0.9:
eth-gas-reporter "^0.2.25"
sha1 "^1.1.1"
hardhat@^2.11.2:
version "2.12.6"
resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.12.6.tgz#ea3c058bbd81850867389d10f76037cfa52a0019"
integrity sha512-0Ent1O5DsPgvaVb5sxEgsQ3bJRt/Ex92tsoO+xjoNH2Qc4bFmhI5/CHVlFikulalxOPjNmw5XQ2vJFuVQFESAA==
hardhat@^2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.14.0.tgz#b60c74861494aeb1b50803cf04cc47865a42b87a"
integrity sha512-73jsInY4zZahMSVFurSK+5TNCJTXMv+vemvGia0Ac34Mm19fYp6vEPVGF3sucbumszsYxiTT2TbS8Ii2dsDSoQ==
dependencies:
"@ethersproject/abi" "^5.1.2"
"@metamask/eth-sig-util" "^4.0.0"
"@nomicfoundation/ethereumjs-block" "^4.0.0"
"@nomicfoundation/ethereumjs-blockchain" "^6.0.0"
"@nomicfoundation/ethereumjs-common" "^3.0.0"
"@nomicfoundation/ethereumjs-evm" "^1.0.0"
"@nomicfoundation/ethereumjs-rlp" "^4.0.0"
"@nomicfoundation/ethereumjs-statemanager" "^1.0.0"
"@nomicfoundation/ethereumjs-trie" "^5.0.0"
"@nomicfoundation/ethereumjs-tx" "^4.0.0"
"@nomicfoundation/ethereumjs-util" "^8.0.0"
"@nomicfoundation/ethereumjs-vm" "^6.0.0"
"@nomicfoundation/ethereumjs-block" "5.0.1"
"@nomicfoundation/ethereumjs-blockchain" "7.0.1"
"@nomicfoundation/ethereumjs-common" "4.0.1"
"@nomicfoundation/ethereumjs-evm" "2.0.1"
"@nomicfoundation/ethereumjs-rlp" "5.0.1"
"@nomicfoundation/ethereumjs-statemanager" "2.0.1"
"@nomicfoundation/ethereumjs-trie" "6.0.1"
"@nomicfoundation/ethereumjs-tx" "5.0.1"
"@nomicfoundation/ethereumjs-util" "9.0.1"
"@nomicfoundation/ethereumjs-vm" "7.0.1"
"@nomicfoundation/solidity-analyzer" "^0.1.0"
"@sentry/node" "^5.18.1"
"@types/bn.js" "^5.1.0"
@ -3766,6 +3798,11 @@ jpeg-js@^0.4.1:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
js-sdsl@^4.1.4:
version "4.4.0"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==
js-sha3@0.5.7, js-sha3@^0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7"

View File

@ -5,6 +5,8 @@ jspm_packages
# Serverless directories
.serverless
yarn-error.log
# esbuild directories
.esbuild

View File

@ -36,4 +36,12 @@ functions:
- http:
path: build
method: post
cors: true
submitMintInfo:
handler: dist/functions/mints/handler.submitMintInfo
events:
- http:
path: mint
method: post
cors: true

View File

@ -0,0 +1,45 @@
import {
APIGatewayProxyResult,
APIGatewayEvent,
///APIGatewayEventRequestContext,
} from 'aws-lambda';
import { formatJSONResponse } from '@libs/api-gateway';
import { v4 } from 'uuid';
export const submitMintInfo = async (
event: APIGatewayEvent,
///context: APIGatewayEventRequestContext
): Promise<APIGatewayProxyResult> => {
try {
const id = v4();
/**if (!verifyAlchemySig(event.headers.xalchemywork)) {
throw new Error('Invalid sig');
}**/
if (event.body == undefined) {
throw new Error('Undefined data');
}
const mintInfo = {
buildId: id,
createdAt: new Date().toISOString(),
body: JSON.parse(event.body),
};
// check if we have it in mongo
// if so, trigger verification call
// if not, add to mongo
return formatJSONResponse({
mintInfo,
});
} catch (e) {
return formatJSONResponse({
status: 500,
message: e,
});
}
};

View File

@ -0,0 +1,13 @@
import { handlerPath } from '@libs/handler-resolver';
export const newMint = {
handler: `${handlerPath(__dirname)}/handler.newMint`,
events: [
{
http: {
method: 'post',
path: 'mint',
},
},
],
};

View File

@ -41,7 +41,15 @@ query getLatestNFAs {
query getNFA($id: ID!) {
token(id: $id) {
tokenId
owner {
id
}
name
description
ENS
externalURL
logo
color
}
}

View File

@ -3,7 +3,13 @@ import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
import { themeGlobals } from '@/theme/globals';
import { AppPage, ToastProvider } from './components';
import { ComponentsTest, CreateAP, Explore, Home, Mint } from './views';
import {
ComponentsTest,
CreateAP,
ExploreView,
IndexedNFAView,
Mint,
} from './views';
export const App: React.FC = () => {
themeGlobals();
@ -13,10 +19,10 @@ export const App: React.FC = () => {
<ToastProvider />
<AppPage>
<Routes>
<Route path="/" element={<Explore />} />
<Route path="/" element={<ExploreView />} />
<Route path="/mint" element={<Mint />} />
<Route path="/create-ap" element={<CreateAP />} />
<Route path="/create-ap/:id" element={<CreateAP />} />
<Route path="/nfa/:id" element={<IndexedNFAView />} />
{/** TODO remove for release */}
<Route path="/components-test" element={<ComponentsTest />} />
<Route path="*" element={<Navigate to="/" />} />

View File

@ -0,0 +1,10 @@
import { styled } from '@/theme';
export const CardTag = styled('span', {
fontSize: '$sm',
backgroundColor: '$slate4',
color: '$slate11',
p: '$1 $3h',
height: '$7',
borderRadius: '$md',
});

View File

@ -0,0 +1 @@
export * from './card-tag';

View File

@ -96,7 +96,7 @@ const DropdownButton: React.FC<DropdownButtonProps> = ({
{selectedValue && selectedValue.label ? selectedValue.label : 'Select'}
</span>
<span
className={`pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4 ${textColorCss}`}
className={`pointer-events-none absolute top-1 bottom-0 right-0 flex items-center pr-4 ${textColorCss}`}
>
<Icon name="chevron-down" />
</span>
@ -172,7 +172,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
leaveTo="opacity-0"
>
<Listbox.Options
className={`absolute mt-1 max-h-32 ${width} right-0 z-10 overflow-auto rounded-xl bg-black px-3 pt-2 border-solid border-slate6 border text-base focus:outline-none sm:text-sm`}
className={`absolute mt-1 max-h-36 ${width} right-0 z-10 overflow-auto rounded-xl bg-black px-3 pt-2 border-solid border-slate6 border text-base focus:outline-none sm:text-sm`}
>
{items.map((option: DropdownItem) => (
<DropdownOption key={option.value} option={option} />

View File

@ -4,12 +4,15 @@ import { AiOutlineTwitter } from '@react-icons/all-files/ai/AiOutlineTwitter';
import { BiGitBranch } from '@react-icons/all-files/bi/BiGitBranch';
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
import { BsFillSquareFill } from '@react-icons/all-files/bs/BsFillSquareFill';
import { FaChevronRight } from '@react-icons/all-files/fa/FaChevronRight';
import { FaExternalLinkAlt } from '@react-icons/all-files/fa/FaExternalLinkAlt';
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
import { IoCheckmarkCircleSharp } from '@react-icons/all-files/io5/IoCheckmarkCircleSharp';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp';
import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp';
import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
import { MdVerifiedUser } from '@react-icons/all-files/md/MdVerifiedUser';
import {
BetaTag,
@ -28,19 +31,22 @@ export const IconLibrary = Object.freeze({
check: AiOutlineCheck,
'check-circle': IoCheckmarkCircleSharp,
'chevron-down': ChevronDownIcon,
'chevron-right': FaChevronRight,
close: IoClose,
error: ErrorIcon,
ethereum: EthereumIcon,
'external-link': FaExternalLinkAlt,
fleekLogo: FleekLogo,
fleekName: FleekName,
github: IoLogoGithub,
info: IoInformationCircleSharp,
upload: IoCloudUploadSharp,
metamask: MetamaskIcon, //remove if not used
search: BiSearch,
square: BsFillSquareFill,
success: AiFillCheckCircle,
twitter: AiOutlineTwitter,
upload: IoCloudUploadSharp,
verified: MdVerifiedUser,
});
export type IconName = keyof typeof IconLibrary;

View File

@ -24,6 +24,5 @@ export abstract class InputFileStyles {
'&[aria-invalid=true], &[data-invalid]': {
borderColor: '$red9',
},
//TODO add error state
});
}

View File

@ -7,4 +7,5 @@ export * from './toast';
export * from './step';
export * from './nfa-card';
export * from './nfa-preview';
export * from './card-tag';
export * from './resolved-address';

View File

@ -1,11 +1,29 @@
import { Avatar, ConnectKitButton } from 'connectkit';
import { Button, Flex } from '@/components';
import { ENSActions, useAppDispatch, useENSStore } from '@/store';
export const ConnectWalletButton: React.FC = () => {
const { addressMap } = useENSStore();
const dispatch = useAppDispatch();
const setEnsNameStore = (ensName: string, address: string): void => {
const stored = addressMap[address] || {};
if (typeof stored.state !== 'undefined') return;
dispatch(
ENSActions.setAddress({
key: address,
value: { state: 'success', value: ensName },
})
);
};
return (
<ConnectKitButton.Custom>
{({ isConnected, show, truncatedAddress, address, ensName }) => {
if (ensName && address) setEnsNameStore(ensName, address);
return (
<Button onClick={show}>
{isConnected && !!address && !!truncatedAddress ? (

View File

@ -13,6 +13,7 @@ export abstract class NavBarStyles {
backgroundColor: '$black',
zIndex: '$sticky',
height: '$22',
overflow: 'hidden', // TODO: this must be worked on for responsive layout
});
static readonly Content = styled('div', {

View File

@ -9,7 +9,8 @@ export abstract class PageStyles {
public static readonly Content = styled('div', {
width: '100%',
minHeight: '85vh',
maxWidth: '$7xl',
maxWidth: '$6xl',
padding: '0 $6',
margin: '0 auto',
display: 'grid',
});

View File

@ -8,7 +8,7 @@ export const NFACardStyles = {
Container: styled(Link, {
display: 'flex',
flexDirection: 'column',
width: '14.6875rem',
minWidth: '12.5rem',
padding: 0,
overflow: 'hidden',
cursor: 'pointer',
@ -60,6 +60,11 @@ export const NFACardStyles = {
fontSize: '$xl',
fontWeight: '$medium',
lineHeight: 1.4,
maxWidth: 'auto',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
Content: styled('span', {
all: 'unset',
@ -79,7 +84,7 @@ export const NFACardStyles = {
Skeleton: {
Preview: styled(Skeleton, {
width: '100%',
height: 'calc(14.6875rem - 2px)',
aspectRatio: 1,
}),
Title: styled(Skeleton, {

View File

@ -32,8 +32,7 @@ export type NFACardProps = Omit<
export const NFACard: React.FC<NFACardProps> = forwardStyledRef<
HTMLAnchorElement,
NFACardProps
// TODO: Set default path to NFA page
>(({ data, to = `/create-ap/${data.tokenId}`, ...props }, ref) => {
>(({ data, to = `/nfa/${data.tokenId}`, ...props }, ref) => {
const { name, color, ENS, logo, accessPoints } = data;
const apCounter = useMemo(() => accessPoints?.length ?? 0, [accessPoints]);
@ -61,8 +60,7 @@ export const NFACard: React.FC<NFACardProps> = forwardStyledRef<
alignItems: 'center',
}}
>
{/* TODO: treat names bigger than space in layout when designs are done */}
<S.Title>{data.name}</S.Title>
<S.Title title={data.name}>{data.name}</S.Title>
{/* TODO: set correct value when it gets available on contract side */}
<Badge verified={Math.random() > 0.5} />
</Flex>

View File

@ -9,19 +9,20 @@ export type ResolvedAddressProps = React.ComponentPropsWithRef<
typeof RAS.Container
> & {
children: string;
truncated?: boolean;
};
export const ResolvedAddress = forwardStyledRef<
HTMLSpanElement,
ResolvedAddressProps
>(({ children, ...props }, ref) => {
>(({ children, truncated = false, ...props }, ref) => {
const [resolvedAddress, loading] = useResolvedAddress(children);
const text = useMemo(() => {
if (!resolvedAddress.endsWith('.eth'))
if (!resolvedAddress.endsWith('.eth') && truncated)
return `${resolvedAddress.slice(0, 6)}...${resolvedAddress.slice(-4)}`;
return resolvedAddress;
}, [resolvedAddress]);
}, [resolvedAddress, truncated]);
return (
<RAS.Container {...props} ref={ref} data-loading={loading}>

View File

@ -1 +1,2 @@
export * from './spinner';
export * from './spinner-dot';

View File

@ -0,0 +1,21 @@
import { SpinnerStyles } from './spinner.styles';
export const SpinnerDot: React.FC<SpinnerStyles.ContainerProps> = (props) => {
return (
<SpinnerStyles.Container
{...props}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<circle cx="12" cy="2.5" r="1" opacity=".14" fill="#FFFFFF" />
<circle cx="16.75" cy="3.77" r="1" opacity=".29" fill="#FFFFFF" />
<circle cx="20.23" cy="7.25" r="1" opacity=".43" fill="#FFFFFF" />
<circle cx="21.50" cy="12.00" r="1" opacity=".57" fill="#FFFFFF" />
<circle cx="20.23" cy="16.75" r="1" opacity=".71" fill="#FFFFFF" />
<circle cx="16.75" cy="20.23" r="1" opacity=".86" fill="#FFFFFF" />
<circle cx="12" cy="21.5" r="1" fill="#FFFFFF" />
</g>
</SpinnerStyles.Container>
);
};

View File

@ -1,12 +1,57 @@
import { styled } from '@/theme';
import { keyframes, styled } from '@/theme';
export abstract class SpinnerStyles {
static readonly Container = styled('svg', {
const DotSpinner = keyframes({
'8.3%': {
transform: 'rotate(30deg)',
},
'16.6%': {
transform: 'rotate(60deg)',
},
'25%': {
transform: 'rotate(90deg)',
},
'33.3%': {
transform: 'rotate(120deg)',
},
'41.6%': {
transform: 'rotate(150deg)',
},
'50%': {
transform: 'rotate(180deg)',
},
'58.3%': {
transform: 'rotate(210deg)',
},
'66.6%': {
transform: 'rotate(240deg)',
},
'75%': {
transform: 'rotate(270deg)',
},
'83.3%': {
transform: 'rotate(300deg)',
},
'91.6%': {
transform: 'rotate(330deg)',
},
'100%': {
transform: 'rotate(360deg)',
},
});
export const SpinnerStyles = {
KeyFrames: {},
Container: styled('svg', {
fontSize: '1.5rem',
width: '1em',
height: '1em',
});
}
g: {
transformOrigin: 'center',
animation: `${DotSpinner} 0.75s step-end infinite`,
},
}),
};
export namespace SpinnerStyles {
export type ContainerProps = React.ComponentProps<

21
ui/src/mocks/bunny-cdn.ts Normal file
View File

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
export const createBunnyCDNMock = async (
domain: string,
targetDomain: string
): Promise<{ bunnyURL: string }> => {
return new Promise((resolved, reject) => {
setTimeout(() => {
resolved({
bunnyURL: '8c12c649402442d88b5f.b-cdn.net',
});
}, 3000);
});
};
export const verifyBunnyCDNMock = async (domain: string): Promise<boolean> => {
return new Promise((resolved, reject) => {
setTimeout(() => {
resolved(true);
}, 3000);
});
};

View File

@ -1,3 +1,5 @@
export * from './mint-site';
export * from './detail';
export * from './list';
export * from './bunny-cdn';
export * from './nfa';

26
ui/src/mocks/nfa.ts Normal file
View File

@ -0,0 +1,26 @@
export const NFAMock = {
id: '6',
tokenId: '6',
name: 'Polygon',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sit amet velit dolor. Praesent dapibus euismod molestie. Duis maximus porttitor odio. Duis quis lorem id lacus cursus commodo vel vehicula mauris.',
externalURL: 'https://polygon.com',
ENS: 'polygon.eth',
logo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzOC40IDMzLjUiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDM4LjQgMzMuNTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM4MjQ3RTU7fQo8L3N0eWxlPgo8Zz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yOSwxMC4yYy0wLjctMC40LTEuNi0wLjQtMi40LDBMMjEsMTMuNWwtMy44LDIuMWwtNS41LDMuM2MtMC43LDAuNC0xLjYsMC40LTIuNCwwTDUsMTYuMwoJCWMtMC43LTAuNC0xLjItMS4yLTEuMi0yLjF2LTVjMC0wLjgsMC40LTEuNiwxLjItMi4xbDQuMy0yLjVjMC43LTAuNCwxLjYtMC40LDIuNCwwTDE2LDcuMmMwLjcsMC40LDEuMiwxLjIsMS4yLDIuMXYzLjNsMy44LTIuMlY3CgkJYzAtMC44LTAuNC0xLjYtMS4yLTIuMWwtOC00LjdjLTAuNy0wLjQtMS42LTAuNC0yLjQsMEwxLjIsNUMwLjQsNS40LDAsNi4yLDAsN3Y5LjRjMCwwLjgsMC40LDEuNiwxLjIsMi4xbDguMSw0LjcKCQljMC43LDAuNCwxLjYsMC40LDIuNCwwbDUuNS0zLjJsMy44LTIuMmw1LjUtMy4yYzAuNy0wLjQsMS42LTAuNCwyLjQsMGw0LjMsMi41YzAuNywwLjQsMS4yLDEuMiwxLjIsMi4xdjVjMCwwLjgtMC40LDEuNi0xLjIsMi4xCgkJTDI5LDI4LjhjLTAuNywwLjQtMS42LDAuNC0yLjQsMGwtNC4zLTIuNWMtMC43LTAuNC0xLjItMS4yLTEuMi0yLjFWMjFsLTMuOCwyLjJ2My4zYzAsMC44LDAuNCwxLjYsMS4yLDIuMWw4LjEsNC43CgkJYzAuNywwLjQsMS42LDAuNCwyLjQsMGw4LjEtNC43YzAuNy0wLjQsMS4yLTEuMiwxLjItMi4xVjE3YzAtMC44LTAuNC0xLjYtMS4yLTIuMUwyOSwxMC4yeiIvPgo8L2c+Cjwvc3ZnPgo=',
color: 8668388,
accessPointAutoApproval: true,
owner: {
id: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
collection: true,
},
mintedBy: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
controllers: [],
gitRepository: {
id: '',
},
commitHash: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
accessPoints: [],
verifier: {
id: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
},
};

View File

@ -0,0 +1,35 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { createBunnyCDNMock } from '@/mocks';
import { RootState } from '@/store';
import { AppLog } from '@/utils';
import { bunnyCDNActions } from '../bunny-cdn-slice';
type CNAMERecord = {
domain: string;
targetDomain: string;
};
export const createBunnyCDN = createAsyncThunk<void, CNAMERecord>(
'BunnyCDN/CreateCDN',
async ({ domain, targetDomain }, { dispatch, getState }) => {
const { state } = (getState() as RootState).bunnyCDN;
if (state === 'loading') return;
try {
dispatch(bunnyCDNActions.setState('loading'));
const CDNRecord = await createBunnyCDNMock(domain, targetDomain);
dispatch(bunnyCDNActions.setCDNRecordData(CDNRecord.bunnyURL));
} catch (error) {
AppLog.errorToast(
'Failed to create the CDN record. Please, try again',
error
);
dispatch(bunnyCDNActions.setState('failed'));
}
}
);

View File

@ -0,0 +1,2 @@
export * from './create-cdn';
export * from './verify-pullzone';

View File

@ -0,0 +1,31 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { verifyBunnyCDNMock } from '@/mocks';
import { RootState } from '@/store';
import { AppLog } from '@/utils';
import { bunnyCDNActions } from '../bunny-cdn-slice';
export const verifyBunnyPullzone = createAsyncThunk<void, string>(
'BunnyCDN/VerifyPullzone',
async (domain, { dispatch, getState }): Promise<void> => {
const { state } = (getState() as RootState).bunnyCDN;
if (state === 'loading') return;
try {
dispatch(bunnyCDNActions.setState('loading'));
const verifyAPState = await verifyBunnyCDNMock(domain);
if (verifyAPState) dispatch(bunnyCDNActions.setState('success'));
else throw new Error('Invalid AP state');
} catch (error) {
AppLog.errorToast(
'There was an error trying to verify the domain. Please, try again',
error
);
dispatch(bunnyCDNActions.setState('failed'));
}
}
);

View File

@ -0,0 +1,51 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '@/store';
import { useAppSelector } from '@/store/hooks';
import * as asyncThunk from './async-thunk';
export namespace BunnyCDNState {
export type CreateCDNState =
| undefined
| 'loading'
| 'unferified'
| 'failed'
| 'success';
}
export interface BunnyCDNState {
state: BunnyCDNState.CreateCDNState;
bunnyURL: string;
}
const initialState: BunnyCDNState = {
state: undefined,
bunnyURL: '',
};
export const bunnyCDNSlice = createSlice({
name: 'BunnyCDNSlice',
initialState,
reducers: {
setState: (state, action: PayloadAction<BunnyCDNState.CreateCDNState>) => {
state.state = action.payload;
},
setCDNRecordData: (state, action: PayloadAction<string>) => {
state.bunnyURL = action.payload;
state.state = 'success';
},
},
});
export const bunnyCDNActions = {
...bunnyCDNSlice.actions,
...asyncThunk,
};
const selectENSState = (state: RootState): BunnyCDNState => state.bunnyCDN;
export const useBunnyCDNStore = (): BunnyCDNState =>
useAppSelector(selectENSState);
export default bunnyCDNSlice.reducer;

View File

@ -0,0 +1 @@
export * from './bunny-cdn-slice';

View File

@ -2,3 +2,4 @@ export * from './fleek-erc721';
export * from './github';
export * from './toasts';
export * from './ens';
export * from './bunny-cdn';

View File

@ -1,5 +1,6 @@
import { configureStore } from '@reduxjs/toolkit';
import bunnyCDNReducer from './features/bunny-cdn/bunny-cdn-slice';
import ENSReducer from './features/ens/ens-slice';
import fleekERC721Reducer from './features/fleek-erc721/fleek-erc721-slice';
import githubReducer from './features/github/github-slice';
@ -7,10 +8,11 @@ import toastsReducer from './features/toasts/toasts-slice';
export const store = configureStore({
reducer: {
bunnyCDN: bunnyCDNReducer,
ENS: ENSReducer,
fleekERC721: fleekERC721Reducer,
github: githubReducer,
toasts: toastsReducer,
ENS: ENSReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({

15
ui/src/utils/color.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* Converts a hex color string to a number.
*/
export const parseColorToNumber = (color: string): number => {
const hexColor = color.replace('#', '');
return parseInt(hexColor, 16);
};
/**
* Converts string number to hex color string.
*/
export const parseNumberToHexColor = (color: number): string => {
const hexColor = color.toString(16);
return hexColor;
};

View File

@ -54,12 +54,23 @@ const hasSpecialCharacters: StringValidator = {
message: 'This field has special characters',
};
const isValidDomain: StringValidator = {
name: 'isValidDomain',
validate: (value = '') => {
const regex =
/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/;
return regex.test(value);
},
message: 'This field is not a valid domain',
};
export const StringValidators = {
required,
maxLength,
isUrl,
maxFileSize,
hasSpecialCharacters,
isValidDomain,
};
export const hasValidator = <

View File

@ -0,0 +1,156 @@
import { useQuery } from '@apollo/client';
import { ethers } from 'ethers';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useAccount } from 'wagmi';
import {
Button,
CardTag,
Flex,
Form,
Spinner,
Stepper,
Text,
} from '@/components';
import { getNFADocument } from '@/graphclient';
import { useAppDispatch } from '@/store';
import { bunnyCDNActions, useBunnyCDNStore } from '@/store/features/bunny-cdn';
import { AppLog } from '@/utils';
import { CreateAccessPoint } from '../create-ap.context';
import { NFAIconFragment } from '../nfa-icon';
import { useAccessPointFormContext } from './create-ap.form.context';
export const SelectedNFA: React.FC = () => {
const { nfa } = CreateAccessPoint.useContext();
return (
<Flex
css={{
justifyContent: 'space-between',
}}
>
<Flex css={{ alignItems: 'center', maxWidth: '65%' }}>
<NFAIconFragment image={nfa.logo} color={nfa.color} />
<Text
css={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
{nfa.name}
</Text>
</Flex>
<CardTag css={{ minWidth: '$28' }}>Selected NFA</CardTag>
</Flex>
);
};
export const CreateAccessPointFormBody: React.FC = () => {
const { id } = useParams();
const { address } = useAccount();
const { nextStep } = Stepper.useContext();
const { nfa, setNfa, billing } = CreateAccessPoint.useContext();
const { setArgs } = CreateAccessPoint.useTransactionContext();
const { state } = useBunnyCDNStore();
const dispatch = useAppDispatch();
const {
form: {
domain: {
value: [domain],
},
isValid: [isValid],
},
} = useAccessPointFormContext();
const {
form: { domain: domainContext },
} = useAccessPointFormContext();
const { loading: nfaLoading } = useQuery(getNFADocument, {
skip: id === undefined,
variables: {
id: ethers.utils.hexlify(Number(id)),
},
onCompleted(data) {
if (data.token && id) {
const { name, tokenId, logo, color, externalURL: domain } = data.token;
setNfa({ name, tokenId, logo, color, domain });
} else {
AppLog.errorToast("We couldn't find the NFA you are looking for");
}
},
onError(error) {
AppLog.errorToast('Error fetching NFA', error);
},
});
useEffect(() => {
if (state === 'success') {
nextStep();
dispatch(bunnyCDNActions.setState(undefined));
}
}, [state, nextStep, dispatch]);
if (nfaLoading) {
return (
<Flex
css={{
justifyContent: 'center',
alignItems: 'center',
height: '$48',
}}
>
<Spinner />
</Flex>
);
}
const handleContinueClick = (): void => {
if (!address) {
AppLog.errorToast('No address found. Please connect your wallet.');
return;
}
if (nfa && domain) {
try {
setArgs([Number(nfa.tokenId), domain, { value: billing }]);
dispatch(
bunnyCDNActions.createBunnyCDN({
domain: 'domain',
targetDomain: domain,
})
);
} catch (e) {
AppLog.errorToast('Error setting transaction arguments');
}
}
};
return (
<Flex css={{ flexDirection: 'column', gap: '$6' }}>
<SelectedNFA />
<Text css={{ fontSize: '$sm', color: '$slate11' }}>
Enter the domain you want to host the NFA. You will need access to the
DNS settings in the next step.
</Text>
<Form.Field context={domainContext}>
<Form.Label>Domain</Form.Label>
<Form.Input placeholder="mydomain.com" />
<Form.Overline />
</Form.Field>
<Button
disabled={!isValid || nfa.tokenId === ''}
isLoading={state === 'loading'}
colorScheme="blue"
variant="solid"
onClick={handleContinueClick}
>
Continue
</Button>
</Flex>
);
};

View File

@ -1,6 +1,6 @@
import { Card, Grid, Icon, IconButton, Stepper } from '@/components';
import { Card, Flex, Icon, IconButton, Stepper } from '@/components';
import { CreateAccessPointFormBody } from './create-ap.form-body';
import { CreateAccessPointFormBody } from './create-ap-form-body';
export const CreateAccessPointForm: React.FC = () => {
const { prevStep } = Stepper.useContext();
@ -8,7 +8,7 @@ export const CreateAccessPointForm: React.FC = () => {
return (
<Card.Container css={{ width: '$107h' }}>
<Card.Heading
title="Create Access Point"
title="Enter Domain"
leftIcon={
<IconButton
aria-label="Add"
@ -29,13 +29,14 @@ export const CreateAccessPointForm: React.FC = () => {
}
/>
<Card.Body>
<Grid
<Flex
css={{
rowGap: '$6',
flexDirection: 'column',
gap: '$6',
}}
>
<CreateAccessPointFormBody />
</Grid>
</Flex>
</Card.Body>
</Card.Container>
);

View File

@ -5,7 +5,7 @@ import { createContext, StringValidators } from '@/utils';
export type CreateAccessPointFormContext = {
form: {
appName: FormField;
domain: FormField;
isValid: ReactState<boolean>;
};
};
@ -20,9 +20,9 @@ export const [CreateAccessPointFormProvider, useAccessPointFormContext] =
export const useAccessPointFormContextInit =
(): CreateAccessPointFormContext => ({
form: {
appName: useFormField('appName', [
domain: useFormField('domain', [
StringValidators.required,
StringValidators.maxLength(50),
StringValidators.isValidDomain,
]),
isValid: useState(false),
},

View File

@ -0,0 +1,2 @@
export * from './create-ap.form.context';
export * from './create-ap-form';

View File

@ -0,0 +1,80 @@
import { useEffect, useMemo } from 'react';
import { Button, Card, Grid, SpinnerDot, Stepper, Text } from '@/components';
import { bunnyCDNActions, useAppDispatch, useBunnyCDNStore } from '@/store';
import { useAccessPointFormContext } from '../ap-form-step';
import { CreateAccessPoint } from '../create-ap.context';
import { DisplayText } from '../display-text';
import { isSubdomain } from './record-step.utils';
export const APRecordCardBody: React.FC = () => {
const dispatch = useAppDispatch();
const { bunnyURL, state } = useBunnyCDNStore();
const {
nfa: { domain: nfaDomain },
} = CreateAccessPoint.useContext();
const {
form: {
domain: {
value: [accesPointDomain],
},
},
} = useAccessPointFormContext();
const { nextStep } = Stepper.useContext();
const isSudomain = useMemo(
() => isSubdomain(accesPointDomain),
[accesPointDomain]
);
useEffect(() => {
if (state === 'success') {
dispatch(bunnyCDNActions.setState(undefined));
nextStep();
}
}, [state, nextStep, dispatch]);
const handleContinueClick = (): void => {
dispatch(bunnyCDNActions.verifyBunnyPullzone(nfaDomain));
};
return (
<Card.Body>
{state === 'loading' ? (
<Card.Text css={{ p: '$12 $10', gap: '$7' }}>
<SpinnerDot css={{ fontSize: '$7xl' }} />
<Text css={{ fontSize: '$md' }}>
Waiting for DNS propagation, allow a few minutes.
</Text>
</Card.Text>
) : (
<Grid
css={{
rowGap: '$6',
}}
>
<Text>
{`Create a ${
isSudomain ? 'CNAME' : 'ANAME'
} record in your DNS provider pointing to our CDN
endpoint.`}
</Text>
<DisplayText
label="Record Type"
value={isSudomain ? 'CNAME' : 'ANAME'}
/>
<DisplayText label="Host" value={isSudomain ? 'App' : '@'} />
<DisplayText label="Data (Points to)" value={bunnyURL} />
<Button
colorScheme="blue"
variant="solid"
onClick={handleContinueClick}
>
I added the record
</Button>
</Grid>
)}
</Card.Body>
);
};

View File

@ -0,0 +1,29 @@
import { Card, Icon, IconButton, Stepper } from '@/components';
export const APRecordCardHeader: React.FC = () => {
const { prevStep } = Stepper.useContext();
return (
<Card.Heading
title="Create Record"
leftIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
css={{ mr: '$2' }}
onClick={prevStep}
/>
}
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
);
};

View File

@ -0,0 +1,13 @@
import { Card } from '@/components';
import { APRecordCardBody } from './ap-record-body';
import { APRecordCardHeader } from './ap-record-header';
export const APRecordStep: React.FC = () => {
return (
<Card.Container css={{ width: '$107h' }}>
<APRecordCardHeader />
<APRecordCardBody />
</Card.Container>
);
};

View File

@ -0,0 +1 @@
export * from './ap-record-step';

View File

@ -0,0 +1,5 @@
export const isSubdomain = (url: string): boolean => {
const urlParts = url.split('.');
return urlParts.length > 2;
};

View File

@ -1,36 +1,71 @@
import { ethers } from 'ethers';
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { useAccount } from 'wagmi';
import {
Button,
Card,
Flex,
Grid,
Icon,
IconButton,
ResolvedAddress,
Stepper,
Text,
} from '@/components';
import { useTransactionCost } from '@/hooks';
import { FleekERC721 } from '@/integrations';
import { AppLog } from '@/utils';
import { useAccessPointFormContext } from './ap-form-step/create-ap.form.context';
import { SelectedNFA } from './ap-form-step/create-ap-form-body';
import { CreateAccessPoint } from './create-ap.context';
import { useAccessPointFormContext } from './create-ap.form.context';
import { DisplayText } from './display-text';
export const AccessPointDataFragment: React.FC = () => {
const { address, status } = useAccount();
const {
form: {
domain: {
value: [domain],
},
},
} = useAccessPointFormContext();
if (status === 'connecting') return <div>Loading...</div>; //TODO replace with spinner
return (
<>
<SelectedNFA />
<DisplayText
label="Owner"
value={
address ? (
<ResolvedAddress truncated={false}>{address || ''}</ResolvedAddress>
) : (
'Please connect to wallet'
)
}
/>
<DisplayText label="Frontend URL" value={domain} />
</>
);
};
export const CreateAccessPointPreview: React.FC = () => {
const { prevStep } = Stepper.useContext();
const { address } = useAccount();
const {
prepare: { status: prepareStatus, data: prepareData, error: prepareError },
write: { status: writeStatus, write },
transaction: { status: transactionStatus },
} = CreateAccessPoint.useTransactionContext();
const {
form: {
appName: {
value: [appName],
},
isValid: [isValid],
},
} = useAccessPointFormContext();
const { nfa } = CreateAccessPoint.useContext();
const [cost, currency, isCostLoading] = useTransactionCost(
prepareData?.request.value,
@ -66,10 +101,19 @@ export const CreateAccessPointPreview: React.FC = () => {
[prepareStatus, writeStatus, transactionStatus]
);
useEffect(() => {
const error = [writeStatus, transactionStatus].some(
(status) => status === 'error'
);
if (error) {
AppLog.errorToast('An error occurred while minting the NFA');
}
}, [writeStatus, transactionStatus]);
return (
<Card.Container css={{ width: '$107h' }}>
<Card.Heading
title={`Create Access Point ${nfa.label || ''}`}
title="Review Details"
leftIcon={
<IconButton
aria-label="Add"
@ -90,26 +134,19 @@ export const CreateAccessPointPreview: React.FC = () => {
}
/>
<Card.Body>
<Grid
css={{
rowGap: '$6',
}}
>
<Flex css={{ flexDirection: 'column' }}>
<span>NFA: {nfa.value}</span>
<span>{appName}</span>
<span className="text-slate11 text-sm">{message}</span>
</Flex>
<Flex css={{ flexDirection: 'column', gap: '$6' }}>
<AccessPointDataFragment />
<Text>{message}</Text>
<Button
disabled={!!prepareError || !nfa}
isLoading={isLoading}
isDisabled={isLoading || !isValid || !address}
colorScheme="blue"
variant="solid"
onClick={write}
isLoading={isLoading}
>
Create
</Button>
</Grid>
</Flex>
</Card.Body>
</Card.Container>
);

View File

@ -0,0 +1,47 @@
import { Button, Card, Flex, Icon, IconButton, Text } from '@/components';
import { CreateAccessPoint } from './create-ap.context';
import { AccessPointDataFragment } from './create-ap-preview';
export const CreateAccessPointSuccess: React.FC = () => {
const { nfa } = CreateAccessPoint.useContext();
return (
<Card.Container css={{ width: '$107h' }}>
<Card.Heading
title="Hosting Successful"
leftIcon={
<Icon
name="check-circle"
css={{ color: '$green11', fontSize: '$xl', mr: '$2' }}
/>
}
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<Card.Body>
<Flex css={{ flexDirection: 'column', gap: '$6' }}>
<Text css={{ fontSize: '$sm', color: '$slate11' }}>
{`You have successfully hosted a ${nfa.name} frontend on your own domain.`}
</Text>
<AccessPointDataFragment />
<Flex css={{ flexDirection: 'column', gap: '$4' }}>
<Button
colorScheme="blue"
variant="solid"
leftIcon={<Icon name="twitter" />}
>
Tweet about your frontend!
</Button>
<Button>Manage Frontend</Button>
</Flex>
</Flex>
</Card.Body>
</Card.Container>
);
};

View File

@ -1,16 +1,23 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState } from 'react';
import { Token } from '@/graphclient';
import { EthereumHooks } from '@/integrations';
import { useFleekERC721Billing } from '@/store';
import { AppLog, createContext, pushToast } from '@/utils';
import { AppLog, createContext } from '@/utils';
type NFA = Pick<Token, 'id' | 'name'>;
export type NFA = {
tokenId: string;
name: string;
logo: string;
color: number;
domain: string;
};
export type AccessPointContext = {
billing: string | undefined;
nfa: NFA | undefined;
setNfa: ReactState<NFA | undefined>[1];
nfa: NFA;
setNfa: (nfa: NFA) => void;
};
const [CreateAPProvider, useContext] = createContext<AccessPointContext>({
@ -31,7 +38,13 @@ export abstract class CreateAccessPoint {
children,
}) => {
const [billing] = useFleekERC721Billing('AddAccessPoint');
const [nfa, setNfa] = useState<NFA>();
const [nfa, setNfa] = useState<NFA>({
tokenId: '',
name: '',
logo: '',
color: 0,
domain: '',
});
const value = {
billing,
@ -44,12 +57,10 @@ export abstract class CreateAccessPoint {
<TransactionProvider
config={{
transaction: {
onSuccess: (data) => {
onSuccess: (data: any) => {
AppLog.info('Transaction:', data);
pushToast('success', 'Your transaction was successful!');
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onError: (error) => {
onError: (error: any) => {
AppLog.errorToast(
'There was an error trying to create the Access Point. Please try again'
);

View File

@ -1,100 +0,0 @@
import { useQuery } from '@apollo/client';
import { ethers } from 'ethers';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Button, Flex, Form, Spinner, Stepper } from '@/components';
import { getNFADocument } from '@/graphclient';
import { AppLog } from '@/utils';
import { CreateAccessPoint } from './create-ap.context';
import { useAccessPointFormContext } from './create-ap.form.context';
import { NfaPicker } from './nfa-picker';
export const CreateAccessPointFormBody: React.FC = () => {
const { id } = useParams();
const { nextStep } = Stepper.useContext();
const { nfa, setNfa, billing } = CreateAccessPoint.useContext();
const { setArgs } = CreateAccessPoint.useTransactionContext();
const {
form: {
appName: {
value: [appName],
},
isValid: [isValid],
},
} = useAccessPointFormContext();
const {
form: { appName: appNameContext },
} = useAccessPointFormContext();
const {
data: nfaData,
error: nfaError,
loading: nfaLoading,
} = useQuery(getNFADocument, {
skip: id === undefined,
variables: {
id: ethers.utils.hexlify(Number(id)),
},
});
useEffect(() => {
if (nfaError) {
AppLog.errorToast('Error fetching NFA');
}
}, [nfaError]);
useEffect(() => {
if (nfaData) {
if (nfaData.token && id) {
const { name } = nfaData.token;
setNfa({ value: id, label: name });
} else {
AppLog.errorToast("We couldn't find the NFA you are looking for");
}
}
}, [nfaData, id, setNfa]);
if (nfaLoading) {
return (
<Flex
css={{
justifyContent: 'center',
alignItems: 'center',
height: '$48',
}}
>
<Spinner />
</Flex>
);
}
const handleContinueClick = (): void => {
if (nfa && appName) {
setArgs([Number(nfa.value), appName, { value: billing }]);
nextStep();
}
};
return (
<>
{/* TODO will have to do some changes on the Form.Combobox if we use this component for the NFA picker */}
{id === undefined && <NfaPicker />}
<Form.Field context={appNameContext}>
<Form.Label>App Name</Form.Label>
<Form.Input />
</Form.Field>
<Button
disabled={!isValid}
colorScheme="blue"
variant="solid"
onClick={handleContinueClick}
>
Continue
</Button>
</>
);
};

View File

@ -1,16 +1,29 @@
import { Form, Step, Stepper } from '@/components';
import { WalletStep } from '../mint/wallet-step';
import { useAccessPointFormContext } from './create-ap.form.context';
import { CreateAccessPointForm } from './create-ap-form';
import { useAccessPointFormContext } from './ap-form-step/create-ap.form.context';
import { CreateAccessPointForm } from './ap-form-step/create-ap-form';
import { APRecordStep } from './ap-record-step/ap-record-step';
import { isSubdomain } from './ap-record-step/record-step.utils';
import { CreateAccessPoint } from './create-ap.context';
import { CreateAccessPointPreview } from './create-ap-preview';
import { CreateAccessPointSuccess } from './create-ap-success';
export const CreateApStepper: React.FC = () => {
const {
transaction: { isSuccess },
} = CreateAccessPoint.useTransactionContext();
const {
form: {
domain: {
value: [accesPointDomain],
},
isValid: [, setIsValid],
},
} = useAccessPointFormContext();
if (isSuccess) return <CreateAccessPointSuccess />;
return (
<Stepper.Root initialStep={1}>
<Form.Root onValidationChange={setIsValid}>
@ -22,13 +35,23 @@ export const CreateApStepper: React.FC = () => {
</Stepper.Step>
<Stepper.Step>
<Step header="Set Access Point">
<Step header="Enter the domain you want to host the NFA">
<CreateAccessPointForm />
</Step>
</Stepper.Step>
<Stepper.Step>
<Step header="Create Access Point">
<Step
header={`Add a ${
isSubdomain(accesPointDomain) ? 'CNAME' : 'ANAME'
} record to your DNS provider`}
>
<APRecordStep />
</Step>
</Stepper.Step>
<Stepper.Step>
<Step header="Review your hosted frontend and confirm">
<CreateAccessPointPreview />
</Step>
</Stepper.Step>

View File

@ -1,10 +1,10 @@
import { Flex } from '@/components';
import { CreateAccessPoint } from './create-ap.context';
import {
CreateAccessPointFormProvider,
useAccessPointFormContextInit,
} from './create-ap.form.context';
} from './ap-form-step/create-ap.form.context';
import { CreateAccessPoint } from './create-ap.context';
import { CreateApStepper } from './create-ap.stepper';
export const CreateAP: React.FC = () => {

View File

@ -0,0 +1,25 @@
import { styled } from '@/theme';
import { Flex } from '../../../components/layout';
export const DisplayTextStyles = {
Container: styled(Flex, {
flexDirection: 'column',
}),
Label: styled('label', {
color: '$slate11',
mb: '$1h',
fontSize: '$xs',
//TODO add variants
}),
Input: styled('span', {
backgroundColor: '$slate1',
borderColor: '$slate1',
color: '$slate12',
borderRadius: '$lg',
fontSize: '$sm',
height: '$11',
p: '$3 $3h',
}),
};

View File

@ -0,0 +1,17 @@
import { DisplayTextStyles as S } from './display-text.styles';
type DisplayTextProps = {
label: string;
value: string | React.ReactNode;
};
export const DisplayText: React.FC<DisplayTextProps> = ({
label,
value,
}: DisplayTextProps) => {
return (
<S.Container>
<S.Label>{label}</S.Label>
<S.Input>{value}</S.Input>
</S.Container>
);
};

View File

@ -0,0 +1 @@
export * from './display-text';

View File

@ -0,0 +1 @@
export * from './nfa-icon';

View File

@ -0,0 +1,15 @@
import { styled } from '@/theme';
export const NFAIconStyles = {
Container: styled('span', {
p: '$1h',
borderRadius: '$full',
width: '$7',
height: '$7',
mr: '$2',
}),
Image: styled('img', {
width: '100%',
height: '100%',
}),
};

View File

@ -0,0 +1,21 @@
import { parseNumberToHexColor } from '@/utils/color';
import { NFAIconStyles as NS } from './nfa-icon.styles';
type NFAIconProps = {
image: string;
color: number;
};
export const NFAIconFragment: React.FC<NFAIconProps> = ({
image,
color,
}: NFAIconProps) => {
return (
<NS.Container
css={{ backgroundColor: `#${parseNumberToHexColor(color)}57` }}
>
<NS.Image src={image} />
</NS.Container>
);
};

View File

@ -1,36 +0,0 @@
import { useQuery } from '@apollo/client';
import { useMemo } from 'react';
import { Combobox } from '@/components';
import { getLatestNFAsDocument } from '@/graphclient';
import { AppLog } from '@/utils';
import { CreateAccessPoint } from './create-ap.context';
export const NfaPicker: React.FC = () => {
const { nfa, setNfa } = CreateAccessPoint.useContext();
const { data, loading, error } = useQuery(getLatestNFAsDocument);
const items = useMemo(() => data?.tokens || [], [data]);
if (error) {
AppLog.errorToast('Error loading NFA list', error);
}
return (
<Combobox
isLoading={loading}
items={items}
selected={[nfa, setNfa]}
queryKey={['name', 'id']}
>
{({ Field, Options }) => (
<>
<Field>{(selected) => selected?.name || 'Select NFA'}</Field>
<Options>{(item) => item.name}</Options>
</>
)}
</Combobox>
);
};

View File

@ -2,13 +2,15 @@ import { Flex, ResolvedAddress } from '@/components';
import { ColorPickerTest } from './color-picker';
import { ComboboxTest } from './combobox-test';
import { SpinnerTest } from './spinner-test';
import { ToastTest } from './toast-test';
export const ComponentsTest: React.FC = () => {
return (
<Flex css={{ flexDirection: 'column' }}>
<SpinnerTest />
<ResolvedAddress css={{ alignSelf: 'center' }}>
{'0x7ed735b7095c05d78df169f991f2b7f1a1f1a049'}
{'0x7ed735b7095c05d78df169f991f2b7f1a1f1a049a'}
</ResolvedAddress>
<ComboboxTest />
<ColorPickerTest />

View File

@ -0,0 +1,18 @@
import { Flex, Spinner, SpinnerDot } from '@/components';
export const SpinnerTest: React.FC = () => {
return (
<>
<Flex css={{ alignItems: 'center' }}>
<SpinnerDot css={{ fontSize: '$6xl' }} />
<SpinnerDot css={{ fontSize: '$4xl' }} />
<SpinnerDot css={{ fontSize: '$lg' }} />
</Flex>
<Flex css={{ alignItems: 'center' }}>
<Spinner css={{ fontSize: '$6xl' }} />
<Spinner css={{ fontSize: '$4xl' }} />
<Spinner css={{ fontSize: '$lg' }} />
</Flex>
</>
);
};

View File

@ -4,7 +4,7 @@ import { Button } from '@/components';
import { ExploreHeaderStyles as S } from './explore-header.styles';
export const ExploreHeader: React.FC = () => (
export const ExploreHeaderFragment: React.FC = () => (
<S.Container>
<S.Text>
<S.GrayText>

View File

@ -0,0 +1 @@
export * from './explore-header.fragment';

View File

@ -1,16 +0,0 @@
import { Flex } from '@/components';
import { Explore } from '../explore.context';
import { NFAListFragment } from './nfa-list';
import { NFASearchFragment } from './nfa-search';
export const ExploreListContainer: React.FC = () => {
return (
<Flex css={{ flexDirection: 'column' }}>
<Explore.Provider>
<NFASearchFragment />
<NFAListFragment />
</Explore.Provider>
</Flex>
);
};

View File

@ -0,0 +1,12 @@
import { Explore } from '../explore.context';
import { NFAListFragment } from './nfa-list.fragment';
import { NFASearchFragment } from './nfa-search.fragment';
export const ExploreListFragment: React.FC = () => {
return (
<Explore.Provider>
<NFASearchFragment />
<NFAListFragment />
</Explore.Provider>
);
};

View File

@ -1 +1 @@
export * from './explore-list-container';
export * from './explore-list.fragment';

View File

@ -1,11 +1,12 @@
import { useQuery } from '@apollo/client';
import { useEffect } from 'react';
import { Flex, NFACard, NFACardSkeleton } from '@/components';
import { NFACard, NFACardSkeleton } from '@/components';
import { lastNFAsPaginatedDocument } from '@/graphclient';
import { useWindowScrollEnd } from '@/hooks';
import { Explore } from '../../explore.context';
import { Explore } from '../explore.context';
import { NFAListFragmentStyles as S } from './nfa-list.styles';
const pageSize = 10; //Set this size to test pagination
@ -15,6 +16,8 @@ const LoadingSkeletons: React.FC = () => (
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
</>
);
@ -61,29 +64,16 @@ export const NFAListFragment: React.FC = () => {
if (queryError) return <div>Error</div>; //TODO handle error
return (
<Flex
css={{
flexDirection: 'column',
gap: '$2',
my: '$6',
minHeight: '50vh',
marginBottom: '30vh', // TODO: remove this if we add page footer
}}
>
<Flex css={{ gap: '$6', flexWrap: 'wrap' }}>
{tokens.map((token) => (
<NFACard data={token} key={token.id} />
))}
{isLoading && <LoadingSkeletons />}
{!isLoading && tokens.length === 0 && (
// TODO: update this after designs are done
<div
className={`relative cursor-default select-none pt-2 px-3.5 pb-4 text-slate11 text-center`}
>
Nothing found.
</div>
)}
</Flex>
</Flex>
<S.Container>
{tokens.map((token) => (
<NFACard data={token} key={token.id} />
))}
{isLoading && <LoadingSkeletons />}
{!isLoading && tokens.length === 0 && (
<S.EmptyMessage>Nothing found.</S.EmptyMessage>
)}
</S.Container>
);
};

View File

@ -0,0 +1,24 @@
import { styled } from '@/theme';
export const NFAListFragmentStyles = {
Container: styled('div', {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(12.5rem, 1fr))',
alignItems: 'flex-start',
flexWrap: 'wrap',
gap: '$6',
my: '$6',
minHeight: '50vh',
marginBottom: '30vh', // TODO: remove this if we add page footer
'@media (min-width: 1080px)': {
gridTemplateColumns: 'repeat(auto-fill, minmax(15rem, 1fr))',
},
}),
EmptyMessage: styled('span', {
padding: '$2 $3 $4 $3',
textAlign: 'center',
color: '$slate11',
width: '100%',
}),
};

View File

@ -1 +0,0 @@
export * from './nfa-list';

View File

@ -1,16 +1,16 @@
import { useState } from 'react';
import { Dropdown, DropdownItem, Flex, Input } from '@/components';
import { Dropdown, DropdownItem, Input } from '@/components';
import { useDebounce } from '@/hooks';
import { Explore } from '../explore.context';
import { ResultsContainer, ResultsNumber, ResultsText } from './results.styles';
import { NFASearchFragmentStyles as S } from './nfa-search.styles';
const orderResults: DropdownItem[] = [
{ value: 'newest', label: 'Newest' },
{ value: 'oldest', label: 'Oldest' },
{ value: 'a-z', label: 'Name A-Z' },
{ value: 'z-a', label: 'Name Z-A' },
{ value: 'a-z', label: 'Sort A-Z' },
{ value: 'z-a', label: 'Sort Z-A' },
];
export const NFASearchFragment: React.FC = () => {
@ -62,17 +62,18 @@ export const NFASearchFragment: React.FC = () => {
};
return (
<Flex css={{ justifyContent: 'space-between' }}>
<ResultsContainer>
<ResultsText>All NFAs </ResultsText>
<ResultsNumber>(3,271)</ResultsNumber>
</ResultsContainer>
<Flex css={{ gap: '$3' }}>
<S.Container>
<S.Data.Wrapper>
<S.Data.Text>All NFAs&nbsp;</S.Data.Text>
<S.Data.Number>(3,271)</S.Data.Number>
</S.Data.Wrapper>
<S.Input.Wrapper>
<Input
placeholder="Search"
leftIcon="search"
css={{ width: '23rem' }}
onChange={handleSearchChange}
wrapperClassName="flex-1"
/>
<Dropdown
items={orderResults}
@ -82,7 +83,7 @@ export const NFASearchFragment: React.FC = () => {
textColor="slate11"
optionsWidth="40"
/>
</Flex>
</Flex>
</S.Input.Wrapper>
</S.Container>
);
};

View File

@ -0,0 +1,40 @@
import { Flex } from '@/components';
import { styled } from '@/theme';
export const NFASearchFragmentStyles = {
Container: styled(Flex, {
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: '$3',
}),
Data: {
Wrapper: styled('div', {
fontSize: '$xl',
fontWeight: '$bold',
display: 'flex',
alignItems: 'center',
}),
Text: styled('span', {
color: '$slate12',
}),
Number: styled('span', {
color: '$slate11',
}),
},
Input: {
Wrapper: styled(Flex, {
gap: '$3',
width: '100%',
maxWidth: '30rem',
justifySelf: 'center',
button: {
minWidth: '$28',
},
}),
},
};

View File

@ -1,16 +0,0 @@
import { styled } from '@/theme';
export const ResultsContainer = styled('div', {
fontSize: '$xl',
fontWeight: '$bold',
display: 'flex',
alignItems: 'center',
});
export const ResultsText = styled('span', {
color: '$slate12',
});
export const ResultsNumber = styled('span', {
color: '$slate11',
});

View File

@ -4,7 +4,5 @@ import { styled } from '@/theme';
export abstract class Explore {
static readonly Container = styled(Flex, {
flexDirection: 'column',
width: '64.75rem', //TODO replace for max-width
margin: '0 auto',
});
}

View File

@ -1,12 +1,12 @@
import { Explore as ES } from './explore.styles';
import { ExploreHeader } from './explore-header';
import { ExploreListContainer } from './explore-list';
import { ExploreHeaderFragment } from './explore-header';
import { ExploreListFragment } from './explore-list';
export const Explore: React.FC = () => {
export const ExploreView: React.FC = () => {
return (
<ES.Container>
<ExploreHeader />
<ExploreListContainer />
<ExploreHeaderFragment />
<ExploreListFragment />
</ES.Container>
);
};

View File

@ -3,3 +3,4 @@ export * from './mint';
export * from './components-test';
export * from './explore';
export * from './access-point';
export * from './indexed-nfa';

View File

@ -0,0 +1,75 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, Flex, Icon, NFAPreview } from '@/components';
import { IndexedNFA } from '../indexed-nfa.context';
import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
const Preview: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
const color = useMemo(
// TODO: replace with util function
() => `#${`000000${nfa.color.toString(16)}`.slice(-6)}`,
[nfa]
);
return (
<NFAPreview
color={color}
logo={nfa.logo}
ens={nfa.ENS}
name={nfa.name}
size="100%"
css={{
borderRadius: '$lg',
border: '1px solid $slate6',
}}
/>
);
};
const CreateAccessPoint: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
return (
<S.Aside.CreateAccessPoint.Container>
<S.Aside.CreateAccessPoint.Heading>
Host NFA Frontend
</S.Aside.CreateAccessPoint.Heading>
{/* TODO: replace with correct text */}
<S.Aside.CreateAccessPoint.Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vitae
ante erat. Sed quis finibus diam.
</S.Aside.CreateAccessPoint.Text>
<Flex css={{ gap: '$3' }}>
<Button as={Link} to={`/create-ap/${nfa.tokenId}`} colorScheme="blue">
Host NFA Frontend
</Button>
<S.Aside.CreateAccessPoint.Extra href="">
{/* TODO: place correct href */}
Learn more
<Icon name="chevron-right" />
</S.Aside.CreateAccessPoint.Extra>
</Flex>
</S.Aside.CreateAccessPoint.Container>
);
};
export const IndexedNFAAsideFragment: React.FC = () => {
const ref = useRef<HTMLDivElement>(null);
const [top, setTop] = useState<number>();
useEffect(() => {
setTop(ref.current?.getBoundingClientRect().top);
}, [ref]);
return (
<S.Aside.Container ref={ref} css={{ top }}>
<Preview />
<CreateAccessPoint />
</S.Aside.Container>
);
};

View File

@ -0,0 +1,3 @@
export * from './aside.fragment';
export * from './main.fragment';
export * from './skeleton.fragment';

View File

@ -0,0 +1,285 @@
import React, { useMemo } from 'react';
import { Flex, Icon, IconName, ResolvedAddress, Text } from '@/components';
import { IndexedNFA } from '../indexed-nfa.context';
import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
type HeaderDataProps = {
label: string;
children: React.ReactNode;
};
const HeaderData: React.FC<HeaderDataProps> = ({
label,
children,
}: HeaderDataProps) => (
<Flex css={{ gap: '$2' }}>
<Text css={{ color: '$slate11' }}>{label}</Text>
<Text css={{ color: '$slate12' }}>{children}</Text>
</Flex>
);
const Header: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
return (
<>
<S.Main.Heading>{nfa.name}</S.Main.Heading>
<Flex css={{ justifyContent: 'space-between', alignItems: 'center' }}>
<HeaderData label="Owner">
<ResolvedAddress>{nfa.owner.id}</ResolvedAddress>
</HeaderData>
<S.Main.Divider.Elipse />
<HeaderData label="Created">
{/* TODO: place correct data */}
12/12/22
</HeaderData>
<S.Main.Divider.Elipse />
<HeaderData label="Access Points">
{nfa.accessPoints?.length ?? 0}
</HeaderData>
</Flex>
<S.Main.Divider.Line />
</>
);
};
const Description: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
return (
<>
<S.Main.SectionHeading css={{ marginTop: 0 }}>
Description
</S.Main.SectionHeading>
<S.Main.DataContainer as={S.Main.Paragraph}>
{nfa.description}
</S.Main.DataContainer>
</>
);
};
type DataWrapperProps = React.PropsWithChildren<{
label: string | number;
}>;
const DataWrapper: React.FC<DataWrapperProps> = ({
children,
label,
}: DataWrapperProps) => (
<S.Main.DataContainer key={label} css={{ flex: 1, minWidth: '45%' }}>
<Text css={{ color: '$slate12', fontWeight: 700 }}>{children || '-'}</Text>
<Text css={{ color: '$slate11' }}>{label}</Text>
</S.Main.DataContainer>
);
const Traits: React.FC = () => {
const { nfa } = IndexedNFA.useContext();
// TODO: place correct data
const traitsToShow = useMemo(() => {
return [
[nfa.ENS, 'ENS'],
[nfa.gitRepository.id, 'Repository'],
[10, 'Version'],
[nfa.externalURL, 'Domain'],
[nfa.externalURL, 'Domain 2'],
];
}, [nfa]);
return (
<>
<S.Main.SectionHeading>Traits</S.Main.SectionHeading>
<S.Main.DataList>
{traitsToShow.map(([value, label]) => (
<DataWrapper key={label} label={label}>
{value}
</DataWrapper>
))}
</S.Main.DataList>
</>
);
};
type VerificationBannerProps = {
verified: boolean;
};
const VerificationBanner: React.FC<VerificationBannerProps> = ({
verified,
}: VerificationBannerProps) => {
const [text, icon] = useMemo<[string, IconName]>(() => {
if (verified)
return ['This Non Fungible Application is Verified.', 'verified'];
return ['This Non Fungible Application is not Verified.', 'error'];
}, [verified]);
return (
<S.Main.VerificationBanner verified={verified}>
{text}
<Icon
name={icon}
css={{
fontSize: '3.5rem',
color: '$black',
position: 'absolute',
right: 'calc(8% - 1.75rem)',
zIndex: 1,
}}
/>
</S.Main.VerificationBanner>
);
};
const Verification: React.FC = () => {
return (
<>
<S.Main.SectionHeading>Verification</S.Main.SectionHeading>
{/* TODO: Get verified from context */}
<VerificationBanner verified={Math.random() > 0.5} />
<S.Main.DataList>
{/* TODO: place correct data */}
<DataWrapper label="Verifier">polygon.eth</DataWrapper>
<DataWrapper label="Repository">polygon/fe</DataWrapper>
</S.Main.DataList>
</>
);
};
// TODO: replace mocks with fetched data
const apMocks = new Array(10).fill(0).map((_, index) => ({
approved: Math.random() > 0.5,
domain: `domain${index}.com`,
owner: '0x7ED735b7095C05d78dF169F991f2b7f1A1F1A049',
createdAt: `${Math.floor(Math.random() * 30)}m ago`,
}));
const AccessPoints: React.FC = () => {
return (
<>
<S.Main.SectionHeading>Frontends</S.Main.SectionHeading>
<S.Main.Table.Container>
<S.Main.Table.Root>
<colgroup>
<col span={1} style={{ width: '9.5%' }} />
<col span={1} style={{ width: '32.5%' }} />
<col span={1} style={{ width: '32.5%' }} />
<col span={1} style={{ width: '16%' }} />
<col span={1} style={{ width: '9.5%' }} />
</colgroup>
<S.Main.Table.Head>
<S.Main.Table.Row>
<S.Main.Table.Data>
<S.Main.Table.Marker />
</S.Main.Table.Data>
<S.Main.Table.Data>Domain</S.Main.Table.Data>
<S.Main.Table.Data>Owner</S.Main.Table.Data>
<S.Main.Table.Data>Created</S.Main.Table.Data>
<S.Main.Table.Data />
</S.Main.Table.Row>
</S.Main.Table.Head>
<S.Main.Table.Body>
{apMocks.map((item) => (
<S.Main.Table.Row key={item.domain}>
<S.Main.Table.Data align="center">
<S.Main.Table.Marker
variant={item.approved ? 'active' : 'inactive'}
/>
</S.Main.Table.Data>
<S.Main.Table.Data>{item.domain}</S.Main.Table.Data>
<S.Main.Table.Data>
<ResolvedAddress>{item.owner}</ResolvedAddress>
</S.Main.Table.Data>
<S.Main.Table.Data>{item.createdAt}</S.Main.Table.Data>
<S.Main.Table.Data>
<Icon name="external-link" />
</S.Main.Table.Data>
</S.Main.Table.Row>
))}
</S.Main.Table.Body>
</S.Main.Table.Root>
</S.Main.Table.Container>
</>
);
};
// TODO: replace mocks with fetched data
const versionsMock = new Array(10).fill(0).map((_, index) => ({
live: index === 0,
commit: (Math.random() * 0xfffffffff).toString(16),
preview: `test: subgraph matchstick tests for access points and acl refactor (#150
)
* fix: errors from deprecated entities.`,
time: `${Math.floor(Math.random() * 30)}m ago`,
}));
const Versions: React.FC = () => {
return (
<>
<S.Main.SectionHeading>Versions</S.Main.SectionHeading>
<S.Main.Table.Container>
<S.Main.Table.Root>
<colgroup>
<col span={1} style={{ width: '9.5%' }} />
<col span={1} style={{ width: '15%' }} />
<col span={1} style={{ width: '50%' }} />
<col span={1} style={{ width: '16%' }} />
<col span={1} style={{ width: '9.5%' }} />
</colgroup>
<S.Main.Table.Head>
<S.Main.Table.Row>
<S.Main.Table.Data>
<S.Main.Table.Marker />
</S.Main.Table.Data>
<S.Main.Table.Data>Commit</S.Main.Table.Data>
<S.Main.Table.Data>Preview</S.Main.Table.Data>
<S.Main.Table.Data>Time</S.Main.Table.Data>
<S.Main.Table.Data />
</S.Main.Table.Row>
</S.Main.Table.Head>
<S.Main.Table.Body>
{versionsMock.map((item) => (
<S.Main.Table.Row key={item.commit}>
<S.Main.Table.Data>
<S.Main.Table.Marker
variant={item.live ? 'active' : 'inactive'}
text={item.live}
>
{item.live && 'Live'}
</S.Main.Table.Marker>
</S.Main.Table.Data>
<S.Main.Table.Data>{item.commit.slice(0, 6)}</S.Main.Table.Data>
<S.Main.Table.Data title={item.preview}>
{item.preview}
</S.Main.Table.Data>
<S.Main.Table.Data>{item.time}</S.Main.Table.Data>
<S.Main.Table.Data>
<Icon name="external-link" />
</S.Main.Table.Data>
</S.Main.Table.Row>
))}
</S.Main.Table.Body>
</S.Main.Table.Root>
</S.Main.Table.Container>
</>
);
};
export const IndexedNFAMainFragment: React.FC = () => {
return (
<S.Main.Container>
<Header />
<Description />
<Traits />
<Verification />
<AccessPoints />
<Versions />
</S.Main.Container>
);
};

View File

@ -0,0 +1,16 @@
import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
export const IndexedNFASkeletonFragment: React.FC = () => (
<S.Grid>
<S.Aside.Container>
<S.Skeleton css={{ aspectRatio: 1, width: '100%' }} />
</S.Aside.Container>
<S.Main.Container css={{ justifyContent: 'stretch' }}>
<S.Skeleton css={{ height: '2.875rem' }} />
<S.Skeleton css={{ height: '1.5rem' }} />
<S.Main.Divider.Line />
<S.Skeleton css={{ height: '10rem' }} />
<S.Skeleton css={{ height: '15rem' }} />
</S.Main.Container>
</S.Grid>
);

View File

@ -0,0 +1 @@
export * from './indexed-nfa';

View File

@ -0,0 +1,28 @@
import { Owner, Token } from '@/graphclient';
import { createContext } from '@/utils';
const [Provider, useContext] = createContext<IndexedNFA.Context>({
name: 'IndexedNFA.Context',
hookName: 'IndexedNFA.useContext',
providerName: 'IndexedNFA.Provider',
});
export const IndexedNFA = {
useContext,
Provider: ({ children, nfa }: IndexedNFA.ProviderProps): JSX.Element => {
return <Provider value={{ nfa }}>{children}</Provider>;
},
};
export namespace IndexedNFA {
export type Context = {
nfa: Omit<Token, 'mintTransaction' | 'id' | 'owner'> & {
owner: Pick<Owner, 'id'>;
};
};
export type ProviderProps = {
children: React.ReactNode | React.ReactNode[];
nfa: Context['nfa'];
};
}

View File

@ -0,0 +1,241 @@
import { Skeleton } from '@/components';
import { styled } from '@/theme';
const Spacing = '$5';
export const IndexedNFAStyles = {
Grid: styled('div', {
display: 'grid',
gridTemplateAreas: '"aside main"',
gridTemplateColumns: '24.0625rem 1fr',
gridTemplateRows: 'fit-content',
gap: `calc(2 * ${Spacing})`,
padding: Spacing,
'@media (max-width: 1080px)': {
gridTemplateColumns: '20rem 1fr',
},
'@media (max-width: 580px)': {
gridTemplateAreas: '"aside" "main"',
gridTemplateColumns: '1fr',
},
}),
Aside: {
Container: styled('aside', {
gridArea: 'aside',
position: 'sticky',
display: 'flex',
flexDirection: 'column',
gap: Spacing,
height: 'fit-content',
'@media (max-width: 580px)': {
position: 'static',
},
}),
CreateAccessPoint: {
Container: styled('div', {
display: 'flex',
flexDirection: 'column',
gap: Spacing,
padding: Spacing,
backgroundColor: '$blue1',
borderRadius: '$lg',
}),
Heading: styled('h2', {
fontSize: '$md',
color: '$slate12',
}),
Text: styled('p', {
fontSize: '$sm',
color: '$slate11',
}),
Extra: styled('a', {
display: 'flex',
alignItems: 'center',
color: '$slate11',
fontSize: '$sm',
gap: '$2',
}),
},
},
Main: {
Container: styled('main', {
gridArea: 'main',
display: 'flex',
flexDirection: 'column',
gap: Spacing,
}),
Heading: styled('h1', {
fontSize: '2.125rem',
lineHeight: 1.35,
fontWeight: 700,
}),
SectionHeading: styled('h2', {
fontSize: '$xl',
lineHeight: 1.2,
fontWeight: 700,
marginTop: Spacing,
}),
Divider: {
Line: styled('span', {
width: '100%',
borderBottom: '1px solid $slate6',
}),
Elipse: styled('span', {
width: '0.375rem',
height: '0.375rem',
backgroundColor: '$slate4',
borderRadius: '100%',
}),
},
Paragraph: styled('p', {
color: '$slate11',
lineHeight: 1.43,
}),
DataContainer: styled('div', {
display: 'flex',
flexDirection: 'column',
border: '1px solid $slate6',
borderRadius: '$lg',
padding: Spacing,
gap: `$1`,
}),
DataList: styled('div', {
display: 'flex',
flexWrap: 'wrap',
gap: '$5',
}),
VerificationBanner: styled('div', {
position: 'relative',
display: 'flex',
alignItems: 'center',
border: '1px solid $slate6',
borderRadius: '$lg',
padding: '$8 $5',
fontWeight: 700,
overflow: 'hidden',
'&:after': {
content: '""',
position: 'absolute',
right: '-$5',
top: '-$10',
bottom: '-$10',
left: '84%',
borderRadius: '80% 0 0 80%',
},
variants: {
verified: {
true: {
borderColor: '$green11',
color: '$green11',
'&:after': {
backgroundColor: '$green11',
},
},
false: {
borderColor: '$red11',
color: '$red11',
'&:after': {
backgroundColor: '$red11',
},
},
},
},
}),
Table: {
Container: styled('div', {
border: '1px solid $slate6',
borderRadius: '10px',
padding: '0 $5',
maxHeight: '15.125rem',
overflow: 'auto',
}),
Root: styled('table', {
width: 'calc(100% + 2 * $space$5)',
margin: '0 -$5',
}),
Head: styled('thead', {
position: 'sticky',
top: 0,
backgroundColor: '$black',
'&:after': {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: 0,
borderBottom: '1px solid $slate6',
},
}),
Row: styled('tr'),
Data: styled('td', {
padding: '$3',
maxWidth: '10rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
Body: styled('tbody', {
tr: {
'&:hover': {
backgroundColor: '$slate6',
cursor: 'pointer',
},
},
}),
Marker: styled('span', {
display: 'block',
margin: 'auto',
width: '0.5625rem',
height: '0.5625rem',
borderRadius: '$full',
backgroundColor: '$slate6',
variants: {
variant: {
active: {
backgroundColor: '$green11',
},
inactive: {
backgroundColor: '$slate8',
},
},
text: {
true: {
fontSize: '$xs',
padding: '0 $2',
width: 'fit-content',
height: 'fit-content',
},
},
},
compoundVariants: [
{
variant: 'active',
text: true,
css: {
color: '$green11',
backgroundColor: '$green3',
},
},
],
}),
},
},
Skeleton: styled(Skeleton, {
borderRadius: '$lg',
}),
};

View File

@ -0,0 +1,55 @@
import { useQuery } from '@apollo/client';
import { ethers } from 'ethers';
import { useNavigate, useParams } from 'react-router-dom';
import { getNFADocument } from '@/graphclient';
import { NFAMock } from '@/mocks';
import { AppLog } from '@/utils';
import {
IndexedNFAAsideFragment,
IndexedNFAMainFragment,
IndexedNFASkeletonFragment,
} from './fragments';
import { IndexedNFA } from './indexed-nfa.context';
import { IndexedNFAStyles as S } from './indexed-nfa.styles';
export const IndexedNFAView: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const handleError = (error: unknown): void => {
AppLog.errorToast(
`It was not possible to find the NFA with id "${id}"`,
error
);
navigate('/', { replace: true });
};
const { loading, data = { token: {} } } = useQuery(getNFADocument, {
skip: id === undefined,
variables: {
id: ethers.utils.hexlify(Number(id)),
},
onCompleted(data) {
if (!data.token) handleError(new Error('Token not found'));
},
onError(error) {
handleError(error);
},
});
if (loading) {
return <IndexedNFASkeletonFragment />;
}
// TODO: replace NFAMock with real data from useQuery
return (
<IndexedNFA.Provider nfa={{ ...NFAMock, ...data.token }}>
<S.Grid>
<IndexedNFAAsideFragment />
<IndexedNFAMainFragment />
</S.Grid>
</IndexedNFA.Provider>
);
};

View File

@ -1,4 +1,4 @@
import { Button, Card, Flex, Stepper } from '@/components';
import { Button, Card, CardTag, Flex, Stepper } from '@/components';
import { Mint } from '@/views/mint/mint.context';
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
@ -25,17 +25,8 @@ export const RepoConfigurationBody: React.FC = () => {
<Flex css={{ rowGap: '$6', flexDirection: 'column' }}>
<RepoRow
repo={repositoryName.name}
css={{ mb: '0' }}
button={
<Button
colorScheme="gray"
disabled
variant="outline"
css={{ py: '$1', height: '$5', borderRadius: '$md' }}
>
Use for NFA
</Button>
}
css={{ mb: '0', cursor: 'default' }}
button={<CardTag>Use for NFA</CardTag>}
/>
<RepoBranchCommitFields />
<Button

View File

@ -31,6 +31,7 @@ export const Repository: React.FC<RepositoryProps> = ({
<RepoRow
onClick={handleSelectRepo}
repo={repository.name}
css={{ cursor: 'pointer' }}
button={
<Button
colorScheme="blue"

View File

@ -16,7 +16,6 @@ export const RepoRow = forwardRef<HTMLDivElement, RepoRowProps>(
justifyContent: 'space-between',
my: '$4',
...props.css,
cursor: 'pointer',
}}
>
<Flex css={{ alignItems: 'center' }}>

View File

@ -18,39 +18,37 @@ export const MintStepper: React.FC = () => {
},
} = useMintFormContext();
if (!isSuccess) {
return (
<Stepper.Root initialStep={1}>
<Form.Root onValidationChange={setIsValid}>
<Stepper.Container>
<Stepper.Step>
<Step header="Connect your Ethereum Wallet to mint an NFA">
<WalletStep />
</Step>
</Stepper.Step>
if (isSuccess) return <NftMinted />;
<Stepper.Step>
<Step header="Connect GitHub and select repository">
<GithubStep />
</Step>
</Stepper.Step>
return (
<Stepper.Root initialStep={1}>
<Form.Root onValidationChange={setIsValid}>
<Stepper.Container>
<Stepper.Step>
<Step header="Connect your Ethereum Wallet to mint an NFA">
<WalletStep />
</Step>
</Stepper.Step>
<Stepper.Step>
<Step header="Finalize a few key things for your NFA">
<NFAStep />
</Step>
</Stepper.Step>
<Stepper.Step>
<Step header="Connect GitHub and select repository">
<GithubStep />
</Step>
</Stepper.Step>
<Stepper.Step>
<Step header="Review your NFA and mint it on Ethereum">
<MintPreview />
</Step>
</Stepper.Step>
</Stepper.Container>
</Form.Root>
</Stepper.Root>
);
} else {
return <NftMinted />;
}
<Stepper.Step>
<Step header="Finalize a few key things for your NFA">
<NFAStep />
</Step>
</Stepper.Step>
<Stepper.Step>
<Step header="Review your NFA and mint it on Ethereum">
<MintPreview />
</Step>
</Stepper.Step>
</Stepper.Container>
</Form.Root>
</Stepper.Root>
);
};

View File

@ -8,11 +8,3 @@ export const fileToBase64 = (file: File): Promise<string> =>
reader.onload = () => resolve(reader.result?.toString() || '');
reader.onerror = reject;
});
/**
* Converts a hex color string to a number.
*/
export const parseColorToNumber = (color: string): number => {
const hexColor = color.replace('#', '');
return parseInt(hexColor, 16);
};

View File

@ -2,6 +2,7 @@ import { useAccount } from 'wagmi';
import { Button, Card, Grid, Stepper } from '@/components';
import { AppLog } from '@/utils';
import { parseColorToNumber } from '@/utils/color';
import { Mint } from '../../mint.context';
import { MintCardHeader } from '../../mint-card';
@ -11,7 +12,6 @@ import {
EnsDomainField,
LogoField,
} from './fields';
import { parseColorToNumber } from './form.utils';
import { useMintFormContext } from './mint-form.context';
export const MintFormStep: React.FC = () => {
@ -54,7 +54,7 @@ export const MintFormStep: React.FC = () => {
AppLog.errorToast('No address found. Please connect your wallet.');
return;
}
// setting the args otherwise mint may fail
setArgs([
address,
appName,
@ -62,7 +62,7 @@ export const MintFormStep: React.FC = () => {
domainURL,
ens,
gitCommit,
`${repositoryName.url}/tree/${gitBranch}`,
`${repositoryName?.url}/tree/${gitBranch}`,
appLogo,
parseColorToNumber(logoColor),
verifyNFA,