feat: UI metamask integration (#41)

* feat: add redux and metamask slice

* refactor: move from metamask to generic wallet using ethers

* feat: add wallet button base

* feat: add getContract function and mint functionality

* refactor: move functions to ethereum file

* feat: wallet menus and disconnect function

* refactor: Ethereum object typings

* feat: add FleekERC721 contract interaction abstraction

* refactor: remove token detail fetch mock using

* refactor: add ethereum mint function to mint site component

* feat: add wallet initialize

* wip: add signature for lastTokenId function

* feat: integrate list of tokens

* refactor: mint params construct

* fix: global window.ethereum type definition

* fix: remove console log

* fix: remove todo comment

* fix: list view items displaying
This commit is contained in:
Felipe Mendes 2022-12-20 14:24:41 -03:00 committed by GitHub
parent 0e67867560
commit 8b88cf2881
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2249 additions and 581 deletions

View File

@ -152,15 +152,7 @@ contract FleekTest is Test {
"}"
);
assertEq(
tokenURI,
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode((dataURI))
)
)
);
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
}
function testCallingTokenURIAfterChangingAllPossibleFields() public {
@ -178,18 +170,11 @@ contract FleekTest is Test {
assertEq(mint, 0);
fleekContract.setTokenName(mint, "Foundry Test App 2");
fleekContract.setTokenDescription(
mint,
"This is a test application submitted by foundry tests. 2"
);
fleekContract.setTokenDescription(mint, "This is a test application submitted by foundry tests. 2");
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
fleekContract.setTokenENS(mint, "fleek_xyz2");
fleekContract.setTokenBuild(
mint,
"afff3f62",
"https://github.com/fleekxyz/contracts2"
);
fleekContract.setTokenBuild(mint, "afff3f62", "https://github.com/fleekxyz/contracts2");
string memory tokenURI = fleekContract.tokenURI(mint);
@ -211,20 +196,10 @@ contract FleekTest is Test {
"}"
);
assertEq(
tokenURI,
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode((dataURI))
)
)
);
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
}
function testFailChangingAllPossibleFieldsOnAnotherUsersTokenWithoutAccess()
public
{
function testFailChangingAllPossibleFieldsOnAnotherUsersTokenWithoutAccess() public {
uint256 mint = fleekContract.mint(
DEPLOYER,
"Foundry Test App",
@ -241,18 +216,11 @@ contract FleekTest is Test {
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
fleekContract.setTokenName(mint, "Foundry Test App 2");
fleekContract.setTokenDescription(
mint,
"This is a test application submitted by foundry tests. 2"
);
fleekContract.setTokenDescription(mint, "This is a test application submitted by foundry tests. 2");
fleekContract.setTokenImage(mint, "https://fleek2.xyz");
fleekContract.setTokenExternalURL(mint, "https://fleek2.xyz");
fleekContract.setTokenENS(mint, "fleek_xyz2");
fleekContract.setTokenBuild(
mint,
"afff3f62",
"https://github.com/fleekxyz/contracts2"
);
fleekContract.setTokenBuild(mint, "afff3f62", "https://github.com/fleekxyz/contracts2");
string memory tokenURI = fleekContract.tokenURI(mint);
@ -274,15 +242,7 @@ contract FleekTest is Test {
"}"
);
assertEq(
tokenURI,
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode((dataURI))
)
)
);
assertEq(tokenURI, string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI)))));
}
function testFailCallingTokenURIOnNonExistantToken() public {
@ -306,12 +266,7 @@ contract FleekTest is Test {
assertEq(
fleekContract.tokenURI(0),
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode((dataURI))
)
)
string(abi.encodePacked("data:application/json;base64,", Base64.encode((dataURI))))
);
}
@ -433,9 +388,7 @@ contract FleekTest is Test {
fleekContract.setTokenDescription(mint, "NEW TOKEN NAME!");
}
function testFailSetTokenDescriptionOnAnotherUsersTokenWithoutAccess()
public
{
function testFailSetTokenDescriptionOnAnotherUsersTokenWithoutAccess() public {
uint256 mint = fleekContract.mint(
DEPLOYER,
"Foundry Test App",
@ -507,9 +460,7 @@ contract FleekTest is Test {
fleekContract.setTokenExternalURL(mint, "https://ethereum.org");
}
function testFailSetTokenExternalURLOnAnotherUsersTokenWithoutAccess()
public
{
function testFailSetTokenExternalURLOnAnotherUsersTokenWithoutAccess() public {
uint256 mint = fleekContract.mint(
DEPLOYER,
"Foundry Test App",
@ -542,11 +493,7 @@ contract FleekTest is Test {
assertEq(mint, 0);
fleekContract.setTokenBuild(
mint,
"aaaaaaa",
"https://github.com/fleekxyz/test_contracts"
);
fleekContract.setTokenBuild(mint, "aaaaaaa", "https://github.com/fleekxyz/test_contracts");
}
function testFailSetTokenBuildOnAnotherUsersTokenWithoutAccess() public {
@ -565,11 +512,7 @@ contract FleekTest is Test {
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
fleekContract.setTokenBuild(
mint,
"aaaaaaa",
"https://github.com/fleekxyz/test_contracts"
);
fleekContract.setTokenBuild(mint, "aaaaaaa", "https://github.com/fleekxyz/test_contracts");
}
function testSetTokenENS() public {
@ -629,9 +572,7 @@ contract FleekTest is Test {
);
}
function testFailAddTokenControllerOnAnotherUsersTokenWithoutAccess()
public
{
function testFailAddTokenControllerOnAnotherUsersTokenWithoutAccess() public {
uint256 mint = fleekContract.mint(
DEPLOYER,
"Foundry Test App",
@ -706,9 +647,7 @@ contract FleekTest is Test {
);
}
function testFailRemoveTokenControllerOnAnotherUsersTokenWithoutAccess()
public
{
function testFailRemoveTokenControllerOnAnotherUsersTokenWithoutAccess() public {
uint256 mint = fleekContract.mint(
DEPLOYER,
"Foundry Test App",
@ -815,11 +754,7 @@ contract FleekTest is Test {
0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
);
vm.prank(address(0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84));
fleekContract.revokeTokenRole(
mint,
FleekAccessControl.Roles.Controller,
DEPLOYER
);
fleekContract.revokeTokenRole(mint, FleekAccessControl.Roles.Controller, DEPLOYER);
}
function testBalanceOfDeployerAfterAndBeforeMinting() public {

View File

@ -1,46 +1,49 @@
{
"name": "sites-as-nfts",
"version": "0.0.1",
"description": "Minimal UI for sites as NFTs",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"author": "Fleek",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.13",
"@chakra-ui/react": "^2.4.2",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"formik": "^2.2.9",
"framer-motion": "^7.6.17",
"path": "^0.12.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.4"
},
"devDependencies": {
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vitejs/plugin-react": "^2.2.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"ethers": "^5.7.2",
"prettier": "^2.8.0",
"react-query": "^3.39.2",
"ts-loader": "^9.4.1",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"vite-tsconfig-paths": "^3.6.0"
}
}
{
"name": "sites-as-nfts",
"version": "0.0.1",
"description": "Minimal UI for sites as NFTs",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"author": "Fleek",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.13",
"@chakra-ui/react": "^2.4.2",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@ethersproject/providers": "^5.7.2",
"@reduxjs/toolkit": "^1.9.1",
"formik": "^2.2.9",
"framer-motion": "^7.6.17",
"path": "^0.12.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.4"
},
"devDependencies": {
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vitejs/plugin-react": "^2.2.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"ethers": "^5.7.2",
"prettier": "^2.8.0",
"react-query": "^3.39.2",
"ts-loader": "^9.4.1",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"vite-tsconfig-paths": "^3.6.0"
}
}

View File

@ -1,17 +1,23 @@
import React from 'react';
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
import { WalletButton } from './components';
import { initializeWallet } from './store';
import { Home, MintSite, MintedSiteDetail } from './views';
initializeWallet();
export const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/mint-site" element={<MintSite />} />
<Route path="/home" element={<Home />} />
<Route path="/detail" element={<MintedSiteDetail />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</BrowserRouter>
<>
<WalletButton />
<BrowserRouter>
<Routes>
<Route path="/mint-site" element={<MintSite />} />
<Route path="/home" element={<Home />} />
<Route path="/detail" element={<MintedSiteDetail />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</BrowserRouter>
</>
);
};

View File

@ -11,10 +11,11 @@ import {
Stack,
} from '@chakra-ui/react';
import React from 'react';
import { FleekERC721 } from '@/integrations';
import { useQuery } from 'react-query';
interface CardSiteProps {
site: SiteNFTDetail;
tokenId?: string; // TODO add param and remove optional
tokenId: number;
}
type InfoContainerProps = {
@ -35,9 +36,15 @@ const InfoContainer = ({ heading, info, width }: InfoContainerProps) => (
/>
);
export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
const { name, owner, image, externalUrl } = site;
export const SiteCard: React.FC<CardSiteProps> = ({ tokenId }) => {
const navigate = useNavigate();
const { data, isLoading } = useQuery<SiteNFTDetail>(
`fetchDetail${tokenId}`,
() => FleekERC721.tokenMetadata(tokenId)
);
if (!data || isLoading) return null;
const { name, owner, image, external_url: externalUrl } = data as any;
return (
<Card
borderColor="#f3f3f36b !important"
@ -47,10 +54,7 @@ export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
borderRadius="10px"
width="350px"
height="350px"
// TODO add token id param
onClick={() => {
navigate(`/detail?tokenId=${1}`);
}}
onClick={() => navigate(`/detail?tokenId=${tokenId}`)}
>
<CardBody width="350px" height="350px" paddingTop="10px">
<Heading size="md" textAlign="center" marginBottom="10px">
@ -78,8 +82,7 @@ export const SiteCard: React.FC<CardSiteProps> = ({ site, tokenId }) => {
</Link>
<Stack mt="10px" spacing="3" overflowY="scroll">
<InfoContainer heading="Owner" info={owner} width="auto" />
{/* TODO add param */}
<InfoContainer heading="Token ID" info="1" width="100px" />
<InfoContainer heading="Token ID" info={tokenId} width="100px" />
<InfoContainer
heading="External url"
width="100px"

View File

@ -1,8 +1,10 @@
export * from './loading';
export * from './home-button';
export * from './image-preview';
export * from './tile-info';
export * from './card';
export * from './accordion-item';
export * from './input-field-form';
export * from './attributes-detail';
export * from './wallet-button';
export * from './loading';
export * from './home-button';
export * from './image-preview';
export * from './tile-info';
export * from './card';
export * from './accordion-item';
export * from './input-field-form';
export * from './attributes-detail';

View File

@ -0,0 +1 @@
export * from './wallet-button';

View File

@ -0,0 +1,65 @@
import { useCallback } from 'react';
import { useAppDispatch, useWalletStore, walletActions } from '@/store';
import { contractAddress } from '@/utils';
import { Menu, MenuButton, MenuList, MenuItem, Button } from '@chakra-ui/react';
const WalletMenu: React.FC = () => {
const { account = '', provider } = useWalletStore();
const dispatch = useAppDispatch();
const handleCopyAccount = useCallback(() => {
navigator.clipboard.writeText(account);
}, [account]);
const handleDisconnect = useCallback(() => {
dispatch(walletActions.disconnect());
}, [dispatch]);
return (
<Menu>
<MenuButton as={Button}>
{`${provider} (${contractAddress(account)})`}
</MenuButton>
<MenuList>
<MenuItem onClick={handleCopyAccount}>Copy Account</MenuItem>
<MenuItem onClick={handleDisconnect}>Disconnect</MenuItem>
</MenuList>
</Menu>
);
};
const ConnectionMenu: React.FC = () => {
const { state } = useWalletStore();
const dispatch = useAppDispatch();
const handleConnectWallet = useCallback(() => {
dispatch(walletActions.connect('metamask'));
}, [dispatch]);
return (
<Menu>
<MenuButton
as={Button}
isLoading={state === 'loading'}
disabled={state === 'loading'}
>
Connect Wallet
</MenuButton>
<MenuList>
<MenuItem onClick={handleConnectWallet}>Metamask</MenuItem>
</MenuList>
</Menu>
);
};
export const WalletButton: React.FC = () => {
const { state } = useWalletStore();
if (state === 'connected') {
return <WalletMenu />;
}
return <ConnectionMenu />;
};

7
ui/src/global.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { ExternalProvider } from '@ethersproject/providers';
declare global {
interface Window {
ethereum: ExternalProvider;
}
}

View File

@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client';
import { App } from './app';
import { ChakraProvider } from '@chakra-ui/react';
import { theme } from './theme';
import { Provider as ReduxProvider } from 'react-redux';
import { store } from './store';
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
@ -13,11 +15,13 @@ const root = ReactDOM.createRoot(
root.render(
<React.StrictMode>
<ChakraProvider theme={theme} resetCSS>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</ChakraProvider>
<ReduxProvider store={store}>
<ChakraProvider theme={theme} resetCSS>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</ChakraProvider>
</ReduxProvider>
</React.StrictMode>
);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
export { default as FleekERC721 } from './FleekERC721.json';

View File

@ -0,0 +1,40 @@
import { JsonRpcProvider, Networkish } from '@ethersproject/providers';
import { ethers } from 'ethers';
import * as Contracts from './contracts';
export const Ethereum: Ethereum.Core = {
defaultNetwork: 'https://rpc-mumbai.maticvigil.com', // TODO: make it environment variable
provider: {
metamask:
window.ethereum && new ethers.providers.Web3Provider(window.ethereum),
},
getContract(contractName, providerName) {
const contract = Contracts[contractName];
const provider =
providerName && providerName in this.provider
? this.provider[providerName].getSigner()
: ethers.getDefaultProvider(this.defaultNetwork);
return new ethers.Contract(contract.address, contract.abi, provider);
},
};
export namespace Ethereum {
export type Providers = 'metamask';
export type Core = {
defaultNetwork: Networkish;
provider: {
[key in Providers]: JsonRpcProvider;
};
getContract: (
contractName: keyof typeof Contracts,
providerName?: Providers
) => ethers.Contract;
};
}

View File

@ -0,0 +1,2 @@
export * from './ethereum';
export * from './lib';

View File

@ -0,0 +1,63 @@
import { Ethereum } from '../ethereum';
export const FleekERC721 = {
async mint(
params: FleekERC721.MintParams,
provider: Ethereum.Providers
): Promise<void> {
const contract = Ethereum.getContract('FleekERC721', provider);
const response = await contract.mint(
params.owner,
params.name,
params.description,
params.image,
params.externalUrl,
params.ens,
params.commitHash,
params.repo,
'author' // TODO: remove author after contract update
);
return response;
},
async tokenMetadata(tokenId: number): Promise<FleekERC721.Metadata> {
const contract = Ethereum.getContract('FleekERC721');
const response = await contract.tokenURI(Number(tokenId));
const parsed = JSON.parse(
Buffer.from(response.slice(29), 'base64').toString('utf-8')
);
return parsed;
},
async lastTokenId(): Promise<number> {
// TODO: fetch last token id
return 6;
},
};
export namespace FleekERC721 {
export type MintParams = {
name: string;
description: string;
owner: string;
externalUrl: string;
image: string;
ens?: string;
commitHash: string;
repo: string;
};
export type Metadata = Omit<MintParams, 'ens' | 'commitHash' | 'repo'> & {
attributes: [
{
trait_type: string;
value: string;
}
];
};
}

View File

@ -0,0 +1 @@
export * from './fleek-erc721';

View File

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

View File

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

View File

@ -0,0 +1,34 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { walletActions, WalletState } from '../wallet-slice';
import { RootState } from '@/store/store';
import { Ethereum } from '@/integrations';
export const connect = createAsyncThunk<
void,
Exclude<WalletState.Provider, null>
>('wallet/connect', async (providerName, { dispatch, getState }) => {
if ((getState() as RootState).wallet.state === 'loading') return;
try {
dispatch(walletActions.setState('loading'));
dispatch(walletActions.setProvider(providerName));
const response = await Ethereum.provider[providerName].send(
'eth_requestAccounts',
[]
);
if (Array.isArray(response)) {
const [account] = response;
if (typeof account !== 'string') throw Error('Invalid account type');
dispatch(walletActions.setAccount(account));
return;
}
throw Error('Invalid response type');
} catch (e) {
console.error('Could not connect to Wallet', e);
dispatch(walletActions.setState('disconnected'));
}
});

View File

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

View File

@ -0,0 +1,2 @@
export * from './wallet-slice';
export * from './utils';

View File

@ -0,0 +1,18 @@
import { Ethereum } from '@/integrations';
import { store } from '../../store';
import { walletActions } from './wallet-slice';
export const initializeWallet = async (): Promise<void> => {
// metamask
try {
const metamask = Ethereum.provider.metamask;
const accounts = await metamask.listAccounts();
if (accounts && accounts.length > 0) {
store.dispatch(walletActions.setAccount(accounts[0]));
}
metamask.on('accountsChanged', (accounts: string[]) => {
store.dispatch(walletActions.setAccount(accounts[0]));
});
store.dispatch(walletActions.setProvider('metamask'));
} catch {}
};

View File

@ -0,0 +1,64 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import * as asyncThunk from './async-thunk';
import { RootState } from '@/store';
import { useAppSelector } from '@/store/hooks';
import { Ethereum } from '@/integrations';
export namespace WalletState {
export type Provider = Ethereum.Providers | null;
export type State = 'disconnected' | 'loading' | 'connected';
export type Account = string;
}
export interface WalletState {
provider: WalletState.Provider;
state: WalletState.State;
account?: WalletState.Account;
}
const initialState: WalletState = {
provider: null,
state: 'disconnected',
account: undefined,
};
export const walletSlice = createSlice({
name: 'wallet',
initialState,
reducers: {
setProvider: (state, action: PayloadAction<WalletState.Provider>) => {
state.provider = action.payload;
},
setAccount: (state, action: PayloadAction<string>) => {
state.state = 'connected';
state.account = action.payload;
},
setState: (
state,
action: PayloadAction<Exclude<WalletState.State, 'connected'>>
) => {
state.state = action.payload;
state.provider = null;
state.account = undefined;
},
disconnect: (state) => {
state.state = 'disconnected';
state.provider = null;
state.account = undefined;
},
},
});
export const walletActions = {
...walletSlice.actions,
...asyncThunk,
};
const selectWalletState = (state: RootState): WalletState => state.wallet;
export const useWalletStore = (): WalletState =>
useAppSelector(selectWalletState);
export default walletSlice.reducer;

5
ui/src/store/hooks.ts Normal file
View File

@ -0,0 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store';
export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

3
ui/src/store/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './store';
export * from './hooks';
export * from './features';

16
ui/src/store/store.ts Normal file
View File

@ -0,0 +1,16 @@
import { configureStore } from '@reduxjs/toolkit';
import walletReducer from './features/wallet/wallet-slice';
export const store = configureStore({
reducer: {
wallet: walletReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@ -1,8 +1,13 @@
export const getRepoAndCommit = (url: string) => {
//TODO validate is a github url
url = url.replace('/commit', '');
const lastIndexSlash = url.lastIndexOf('/');
const repo = url.substring(0, lastIndexSlash + 1).slice(0, lastIndexSlash);
const commit_hash = url.substring(lastIndexSlash + 1, url.length);
return { repo, commit_hash };
};
export const getRepoAndCommit = (url: string) => {
//TODO validate is a github url
url = url.replace('/commit', '');
const lastIndexSlash = url.lastIndexOf('/');
const repo = url.substring(0, lastIndexSlash + 1).slice(0, lastIndexSlash);
const commit_hash = url.substring(lastIndexSlash + 1, url.length);
return { repo, commit_hash };
};
export const contractAddress = (address: string): string => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};

View File

@ -18,16 +18,17 @@ import {
AttributesDetail,
} from '@/components';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { fetchSiteDetail } from '@/mocks';
import { ErrorScreen } from '@/views';
import { SiteNFTDetail } from '@/types';
import { FleekERC721 } from '@/integrations';
export const MintedSiteDetail = () => {
const [searchParams] = useSearchParams();
const tokenIdParam = searchParams.get('tokenId');
//TODO handle response type
const { data, status } = useQuery('fetchDetail', () =>
fetchSiteDetail(tokenIdParam as string)
const { data, status } = useQuery<SiteNFTDetail>(
`fetchDetail${tokenIdParam}`,
async () => FleekERC721.tokenMetadata(Number(tokenIdParam))
);
if (status === 'loading') {
@ -39,7 +40,8 @@ export const MintedSiteDetail = () => {
}
const { owner, name, description, image, externalUrl, attributes } =
data.data as SiteNFTDetail;
data as SiteNFTDetail;
return (
<>
<Flex width="full" align="center" justifyContent="center">

View File

@ -1,31 +1,22 @@
import React from 'react';
import { Loading } from '@/components';
import { fetchMintedSites } from '@/mocks';
import { SiteNFTDetails } from '@/types';
import { Grid, GridItem } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { useQuery } from 'react-query';
import { SiteCard } from '@/components';
import { FleekERC721 } from '@/integrations';
export const ListSites = () => {
const { data, isLoading } = useQuery<Array<SiteNFTDetails>, Error>(
'fetchSites',
fetchMintedSites
const { data, isLoading } = useQuery('fetchLastTokenId', () =>
FleekERC721.lastTokenId()
);
if (isLoading) return <Loading />;
if (!data || isLoading) return <Loading />;
return (
<Grid
templateColumns={{ base: 'repeat(4, 1fr)', md: 'repeat(5, 1fr)' }}
gap={10}
mt="40px"
>
{data &&
data.listSites.map((site: SiteNFTDetails) => (
<GridItem key={site.tokenId}>
<SiteCard site={site} />
</GridItem>
))}
</Grid>
<Flex gap={10} mt="40px" flexWrap="wrap" justifyContent="center">
{new Array(data).fill(0).map((_, index) => {
const id = data - index - 1;
return <SiteCard tokenId={id} />;
})}
</Flex>
);
};

View File

@ -15,10 +15,11 @@ import {
import { Formik, Field } from 'formik';
import { ArrowBackIcon } from '@chakra-ui/icons';
import { Link } from 'react-router-dom';
import { mintSiteNFT } from '@/mocks';
import { getRepoAndCommit } from '@/utils';
import { validateFields } from './mint-site.utils';
import { InputFieldForm } from '@/components';
import { FleekERC721 } from '@/integrations';
import { useWalletStore } from '@/store';
import { useToast } from '@/hooks';
interface FormValues {
@ -43,46 +44,53 @@ const initialValues = {
export const MintSite = () => {
const setToastInfo = useToast();
const { provider } = useWalletStore();
const handleSubmitForm = useCallback(async (values: FormValues) => {
const {
name,
description,
githubCommit,
ownerAddress,
externalUrl,
image,
ens,
} = values;
const { repo, commit_hash } = getRepoAndCommit(githubCommit);
try {
await mintSiteNFT({
const handleSubmitForm = useCallback(
async (values: FormValues) => {
const {
name,
description,
owner: ownerAddress,
githubCommit,
ownerAddress,
externalUrl,
image,
ens,
commitHash: commit_hash,
repo,
});
//TODO connect with the integration
setToastInfo({
title: 'Success!',
description: 'Your site has been minted.',
status: 'success',
});
} catch (err) {
setToastInfo({
title: 'Error!',
description:
'We had an error while minting your site. Please try again later',
status: 'error',
});
}
}, []);
} = values;
const { repo, commit_hash } = getRepoAndCommit(githubCommit);
try {
if (!provider) throw new Error('No provider found');
await FleekERC721.mint(
{
name,
description,
owner: ownerAddress,
externalUrl,
image,
ens,
commitHash: commit_hash,
repo,
},
provider
);
setToastInfo({
title: 'Success!',
description: 'Your site has been minted.',
status: 'success',
});
} catch (err) {
setToastInfo({
title: 'Error!',
description:
'We had an error while minting your site. Please try again later',
status: 'error',
});
}
},
[provider]
);
return (
<>
@ -198,7 +206,8 @@ export const MintSite = () => {
!values.githubCommit ||
!values.ownerAddress ||
!values.image ||
!values.externalUrl
!values.externalUrl ||
!provider
}
>
Mint

File diff suppressed because it is too large Load Diff