Merge branch 'develop' of github.com:fleekxyz/contracts into test/foundry
This commit is contained in:
commit
0c96f1db9d
|
|
@ -11,9 +11,16 @@
|
||||||
"author": "Fleek",
|
"author": "Fleek",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"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",
|
"path": "^0.12.7",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.2.3",
|
"@types/jest": "^29.2.3",
|
||||||
|
|
@ -28,7 +35,9 @@
|
||||||
"eslint-plugin-jest": "^27.1.6",
|
"eslint-plugin-jest": "^27.1.6",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.31.11",
|
"eslint-plugin-react": "^7.31.11",
|
||||||
|
"ethers": "^5.7.2",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
|
"react-query": "^3.39.2",
|
||||||
"ts-loader": "^9.4.1",
|
"ts-loader": "^9.4.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
"vite": "^3.2.4",
|
"vite": "^3.2.4",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.main {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './App.css';
|
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
|
||||||
|
import { Home, MintSite, MintedSiteDetail } from './views';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
return (
|
return (
|
||||||
<div className="main">
|
<BrowserRouter>
|
||||||
<h1>Welcome to Sites as NFTs by Fleek</h1>
|
<Routes>
|
||||||
</div>
|
<Route path="/mint-site" element={<MintSite />} />
|
||||||
|
<Route path="/home" element={<Home />} />
|
||||||
|
<Route path="/detail" element={<MintedSiteDetail />} />
|
||||||
|
<Route path="*" element={<Navigate to="/home" />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {
|
||||||
|
AccordionItem as AccordionItemChakra,
|
||||||
|
AccordionButton,
|
||||||
|
Box,
|
||||||
|
Heading,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionPanel,
|
||||||
|
AccordionPanelProps,
|
||||||
|
forwardRef,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type AccordionProps = AccordionPanelProps & {
|
||||||
|
children: React.ReactNode;
|
||||||
|
heading: string;
|
||||||
|
};
|
||||||
|
export const AccordionItem = forwardRef<AccordionProps, 'div'>(
|
||||||
|
({ children, heading, ...panelProps }, ref) => {
|
||||||
|
return (
|
||||||
|
<AccordionItemChakra>
|
||||||
|
<AccordionButton borderBottomWidth="1px">
|
||||||
|
<Box flex="1" textAlign="left">
|
||||||
|
<Heading size="md"> {heading}</Heading>
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel ref={ref} {...panelProps} pb={4} overflowY="scroll">
|
||||||
|
{children}
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItemChakra>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './accordion-item';
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { SiteNFTDetail } from '@/types';
|
||||||
|
import { HStack } from '@chakra-ui/react';
|
||||||
|
import { CardAttributes } from '../card';
|
||||||
|
|
||||||
|
type AttributesDetailProps = {
|
||||||
|
owner: string;
|
||||||
|
attributes: SiteNFTDetail['attributes'];
|
||||||
|
tokendId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AttributesDetail = ({
|
||||||
|
owner,
|
||||||
|
attributes,
|
||||||
|
tokendId,
|
||||||
|
}: AttributesDetailProps) => {
|
||||||
|
return (
|
||||||
|
<HStack shouldWrapChildren display="inline" spacing="0px">
|
||||||
|
<CardAttributes heading="Owner" info={owner} />
|
||||||
|
{attributes.map((attribute) => (
|
||||||
|
<CardAttributes
|
||||||
|
key={attribute.trait_type}
|
||||||
|
heading={attribute.trait_type}
|
||||||
|
info={attribute.value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<CardAttributes heading="Token ID" info={tokendId} />
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './attributes-detail';
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Card, CardBody } from '@chakra-ui/react';
|
||||||
|
import { TileInfo } from '../tile-info';
|
||||||
|
|
||||||
|
type CardAttributesProps = {
|
||||||
|
heading: string;
|
||||||
|
info: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardAttributes = ({ heading, info }: CardAttributesProps) => (
|
||||||
|
<Card
|
||||||
|
mr="10px"
|
||||||
|
mb="5px"
|
||||||
|
direction={{ base: 'column', sm: 'row' }}
|
||||||
|
overflow="hidden"
|
||||||
|
variant="outline"
|
||||||
|
width="200px"
|
||||||
|
>
|
||||||
|
<CardBody width="200px">
|
||||||
|
<TileInfo size="sm" heading={heading} info={info} width={160} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './card-attributes';
|
||||||
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { ArrowBackIcon } from '@chakra-ui/icons';
|
||||||
|
import { IconButton } from '@chakra-ui/react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const HomeButton = () => {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
as={Link}
|
||||||
|
to="/home"
|
||||||
|
aria-label="back home"
|
||||||
|
icon={<ArrowBackIcon />}
|
||||||
|
variant="link"
|
||||||
|
size={'xl'}
|
||||||
|
textDecoration={'none'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './home-button';
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { forwardRef, Image, ImageProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
type ImagePreviewProps = ImageProps & {
|
||||||
|
image: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImagePreview = forwardRef<ImagePreviewProps, 'img'>(
|
||||||
|
({ image, ...imageProps }, ref) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* TODO add fallback Image */}
|
||||||
|
<Image
|
||||||
|
ref={ref}
|
||||||
|
src={image}
|
||||||
|
{...imageProps}
|
||||||
|
fallbackSrc="https://via.placeholder.com/150"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './image-preview';
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './input-field-form';
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormControlProps,
|
||||||
|
FormErrorMessage,
|
||||||
|
FormLabel,
|
||||||
|
forwardRef,
|
||||||
|
Input,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Field } from 'formik';
|
||||||
|
|
||||||
|
type InputFieldFormProps = FormControlProps & {
|
||||||
|
label: string;
|
||||||
|
fieldName: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
export const InputFieldForm = forwardRef<InputFieldFormProps, 'div'>(
|
||||||
|
({ label, fieldName, error, ...formControlProps }, ref) => (
|
||||||
|
<FormControl ref={ref} {...formControlProps}>
|
||||||
|
<FormLabel htmlFor={fieldName}>{label}</FormLabel>
|
||||||
|
<Field as={Input} name={fieldName} id={fieldName} type="text" />
|
||||||
|
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './loading';
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Flex, Spinner } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const Loading = () => {
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="center" height="80vh" alignItems="center">
|
||||||
|
<Spinner
|
||||||
|
thickness="3px"
|
||||||
|
speed="0.65s"
|
||||||
|
emptyColor="gray.200"
|
||||||
|
color="blue.500"
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './tile-info';
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
forwardRef,
|
||||||
|
Heading,
|
||||||
|
HeadingProps,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
type TileInfoProps = HeadingProps & {
|
||||||
|
heading: string;
|
||||||
|
info: string;
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TileInfo = forwardRef<TileInfoProps, 'h2'>(
|
||||||
|
({ heading, info, width = 250, ...headingProps }, ref) => (
|
||||||
|
<Flex direction="column" alignItems="center">
|
||||||
|
<Heading ref={ref} {...headingProps}>
|
||||||
|
{heading}
|
||||||
|
</Heading>
|
||||||
|
<Text
|
||||||
|
width={width}
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
overflow="hidden"
|
||||||
|
textOverflow="ellipsis"
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
{info}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
:root {
|
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import { App } from './app';
|
||||||
import { App } from './App';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
|
import { theme } from './theme';
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
|
|
@ -9,7 +13,11 @@ const root = ReactDOM.createRoot(
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ChakraProvider theme={theme} resetCSS>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</ChakraProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
const MINT_PARAMS = {
|
||||||
|
name: 'Fleek Test App',
|
||||||
|
description: 'Fleek Test App Description',
|
||||||
|
image: 'https://storageapi.fleek.co/fleek-team-bucket/site/fleek-logo.png',
|
||||||
|
ens: 'fleek.eth',
|
||||||
|
externalUrl: 'https://fleek.co',
|
||||||
|
commitHash: 'b72e47171746b6a9e29b801af9cb655ecf4d665c',
|
||||||
|
gitRepository: 'https://github.com/fleekxyz/contracts',
|
||||||
|
author: 'author',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDetail = {
|
||||||
|
owner: '0x8f7b9e1b5f1f2c3c1f8b0b1b2e1b2f1f2c3c1f8b',
|
||||||
|
name: MINT_PARAMS.name,
|
||||||
|
description: MINT_PARAMS.description,
|
||||||
|
image: MINT_PARAMS.image,
|
||||||
|
external_url: MINT_PARAMS.externalUrl,
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
trait_type: 'ENS',
|
||||||
|
value: MINT_PARAMS.ens,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trait_type: 'Commit Hash',
|
||||||
|
value: MINT_PARAMS.commitHash,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trait_type: 'Repository',
|
||||||
|
value: MINT_PARAMS.gitRepository,
|
||||||
|
},
|
||||||
|
//As we're not showing this on the UI, we can remove it
|
||||||
|
// {
|
||||||
|
// trait_type: 'Author',
|
||||||
|
// value: MINT_PARAMS.author,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// trait_type: 'Version',
|
||||||
|
// value: '0',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSiteDetail = async (tokenId: string) => {
|
||||||
|
//TODO get site detail from api
|
||||||
|
return new Promise((resolved, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolved({
|
||||||
|
data: { ...mockDetail, externalUrl: mockDetail.external_url },
|
||||||
|
});
|
||||||
|
}, 2500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './mint-site';
|
||||||
|
export * from './detail';
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { SiteNFT } from '@/types';
|
||||||
|
|
||||||
|
export const mintSiteNFT = async (props: SiteNFT) => {
|
||||||
|
const { name, description, owner, externalUrl, ens, commitHash, repo } =
|
||||||
|
props;
|
||||||
|
return new Promise((resolved, rejected) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// returning data of the site for now
|
||||||
|
// just leave rejected for testing purposes
|
||||||
|
resolved({
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
owner,
|
||||||
|
externalUrl,
|
||||||
|
ens,
|
||||||
|
commitHash,
|
||||||
|
repo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { extendTheme } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const appTheme = {
|
||||||
|
styles: {
|
||||||
|
global: {
|
||||||
|
body: {
|
||||||
|
color: 'rgba(255, 255, 255)',
|
||||||
|
bg: '#161616',
|
||||||
|
margin: '50px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
heading: 'Nunito Sans,Helvetica,Arial,Lucida,sans-serif',
|
||||||
|
body: 'Nunito Sans,Helvetica,Arial,Lucida,sans-serif',
|
||||||
|
},
|
||||||
|
sizes: {
|
||||||
|
modalHeight: '345px',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const theme = extendTheme(appTheme);
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './mint-site';
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
export type SiteNFT = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
owner: string;
|
||||||
|
externalUrl: string;
|
||||||
|
image: string;
|
||||||
|
ens?: string;
|
||||||
|
commitHash: string;
|
||||||
|
repo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SiteNFTDetail = Omit<SiteNFT, 'ens' | 'commitHash' | 'repo'> & {
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
trait_type: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './format';
|
||||||
|
export * from './validation';
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const isValidUrl = (url: string) => {
|
||||||
|
const regex =
|
||||||
|
/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
|
||||||
|
return regex.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidImageUrl = (url: string) => {
|
||||||
|
const regex = /^https?:\/\/.+\.(jpg|jpeg|png|gif|svg)$/;
|
||||||
|
return regex.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Link,
|
||||||
|
VStack,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
HomeButton,
|
||||||
|
ImagePreview,
|
||||||
|
AccordionItem,
|
||||||
|
Loading,
|
||||||
|
AttributesDetail,
|
||||||
|
} from '@/components';
|
||||||
|
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||||
|
import { fetchSiteDetail } from '@/mocks';
|
||||||
|
import { ErrorScreen } from '@/views';
|
||||||
|
import { SiteNFTDetail } from '@/types';
|
||||||
|
|
||||||
|
export const MintedSiteDetail = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const tokenIdParam = searchParams.get('tokenId');
|
||||||
|
//TODO handle response type
|
||||||
|
const { data, status } = useQuery('fetchDetail', () =>
|
||||||
|
fetchSiteDetail(tokenIdParam as string)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'error') {
|
||||||
|
return <ErrorScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { owner, name, description, image, externalUrl, attributes } =
|
||||||
|
data.data as SiteNFTDetail;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex width="full" align="center" justifyContent="center">
|
||||||
|
<Box width={{ base: '100%' }}>
|
||||||
|
<HomeButton />
|
||||||
|
<Box
|
||||||
|
flexDirection="row"
|
||||||
|
display="flex"
|
||||||
|
justifyContent="space-evenly"
|
||||||
|
mt={10}
|
||||||
|
>
|
||||||
|
<Box mr={5}>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="flex-end"
|
||||||
|
mb={5}
|
||||||
|
>
|
||||||
|
<Heading mr={5}>{name}</Heading>
|
||||||
|
</Box>
|
||||||
|
<Card backgroundColor="transparent" border="1px">
|
||||||
|
<CardBody padding="1px 8px 10px 8px">
|
||||||
|
<Accordion defaultIndex={[0, 1]} allowMultiple width="45vw">
|
||||||
|
<AccordionItem
|
||||||
|
heading="Description"
|
||||||
|
minH={120}
|
||||||
|
maxH="auto"
|
||||||
|
children={<p>{description}</p>}
|
||||||
|
/>
|
||||||
|
<AccordionItem
|
||||||
|
heading="Attributes"
|
||||||
|
children={
|
||||||
|
<AttributesDetail
|
||||||
|
owner={owner}
|
||||||
|
attributes={attributes}
|
||||||
|
tokendId={tokenIdParam as string}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
padding="16px"
|
||||||
|
/>
|
||||||
|
</Accordion>
|
||||||
|
<Box ml={5} mt={2}>
|
||||||
|
<Link href={externalUrl} isExternal>
|
||||||
|
Visit site <ExternalLinkIcon mx="2px" />
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
<VStack alignItems="flex-start">
|
||||||
|
<Box
|
||||||
|
border="1px"
|
||||||
|
width="-webkit-fill-available"
|
||||||
|
padding="5px 10px"
|
||||||
|
borderTopRadius={10}
|
||||||
|
>
|
||||||
|
<Heading size="md">Preview</Heading>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
mt="0px !important"
|
||||||
|
boxSize="md"
|
||||||
|
border="1px"
|
||||||
|
padding={10}
|
||||||
|
borderRadius={20}
|
||||||
|
borderTopRadius={0}
|
||||||
|
boxShadow="12px 10px 14px 6px #868686d1"
|
||||||
|
>
|
||||||
|
<ImagePreview
|
||||||
|
image={image}
|
||||||
|
width="auto"
|
||||||
|
height="auto"
|
||||||
|
maxW="100%"
|
||||||
|
maxH="100%"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './detail';
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Flex, Heading } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const ErrorScreen = () => {
|
||||||
|
return (
|
||||||
|
<Flex justifyContent="center" height="80vh" alignItems="center">
|
||||||
|
<Heading size="md">Something went wrong</Heading>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './error-screen';
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Heading, Button } from '@chakra-ui/react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const Home = () => {
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column" alignItems="center">
|
||||||
|
<Heading>Welcome to Sites as NFTs by Fleek</Heading>
|
||||||
|
{/* TODO add list sites */}
|
||||||
|
<Button as={Link} to="/mint-site" mt={10}>
|
||||||
|
Mint your site
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './home';
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './home';
|
||||||
|
export * from './mint-site';
|
||||||
|
export * from './detail';
|
||||||
|
export * from './error-screen';
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './mint-site';
|
||||||
|
export * from './mint-site.utils';
|
||||||
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Button,
|
||||||
|
FormErrorMessage,
|
||||||
|
IconButton,
|
||||||
|
useToast,
|
||||||
|
UseToastOptions,
|
||||||
|
Textarea,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
githubCommit: string;
|
||||||
|
ownerAddress: string;
|
||||||
|
externalUrl: string;
|
||||||
|
image: string;
|
||||||
|
ens?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
githubCommit: '',
|
||||||
|
ownerAddress: '',
|
||||||
|
externalUrl: '',
|
||||||
|
image: '',
|
||||||
|
ens: '',
|
||||||
|
} as FormValues;
|
||||||
|
|
||||||
|
export const MintSite = () => {
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
//TODO add hook to show the toast
|
||||||
|
const showToast = (
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
status: UseToastOptions['status']
|
||||||
|
) => {
|
||||||
|
toast({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitForm = useCallback(async (values: FormValues) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
githubCommit,
|
||||||
|
ownerAddress,
|
||||||
|
externalUrl,
|
||||||
|
image,
|
||||||
|
ens,
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
const { repo, commit_hash } = getRepoAndCommit(githubCommit);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mintSiteNFT({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
owner: ownerAddress,
|
||||||
|
externalUrl,
|
||||||
|
image,
|
||||||
|
ens,
|
||||||
|
commitHash: commit_hash,
|
||||||
|
repo,
|
||||||
|
});
|
||||||
|
//TODO connect with the integration
|
||||||
|
showToast('Success!', 'Your site has been minted.', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
showToast(
|
||||||
|
'Error!',
|
||||||
|
'We had an error while minting your site. Please try again later',
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex width="full" align="center" justifyContent="center" mt="50px">
|
||||||
|
<Box width={{ base: '100%', md: '80%' }}>
|
||||||
|
<IconButton
|
||||||
|
as={Link}
|
||||||
|
to="/home"
|
||||||
|
aria-label="back home"
|
||||||
|
icon={<ArrowBackIcon />}
|
||||||
|
variant="link"
|
||||||
|
size={'xl'}
|
||||||
|
textDecoration={'none'}
|
||||||
|
/>
|
||||||
|
<Box textAlign="center" mt={2}>
|
||||||
|
<Heading>Mint your Site</Heading>
|
||||||
|
</Box>
|
||||||
|
<Box my={4} textAlign="left">
|
||||||
|
<Formik
|
||||||
|
validate={validateFields}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmitForm}
|
||||||
|
>
|
||||||
|
{({ values, touched, handleSubmit, isSubmitting, errors }) => (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection={{ base: 'column', md: 'row' }}
|
||||||
|
>
|
||||||
|
<InputFieldForm
|
||||||
|
label="Name"
|
||||||
|
fieldName="name"
|
||||||
|
mr={5}
|
||||||
|
error={errors.name}
|
||||||
|
isInvalid={!!errors.name && touched.name}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<InputFieldForm
|
||||||
|
label="Owner address"
|
||||||
|
fieldName="ownerAddress"
|
||||||
|
error={errors.ownerAddress}
|
||||||
|
isInvalid={!!errors.ownerAddress && touched.ownerAddress}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<FormControl
|
||||||
|
mt={6}
|
||||||
|
isRequired
|
||||||
|
isInvalid={!!errors.description && touched.description}
|
||||||
|
>
|
||||||
|
<FormLabel htmlFor="description">Description</FormLabel>
|
||||||
|
<Field as={Textarea} name="description" id="description" />
|
||||||
|
{errors.description && (
|
||||||
|
<FormErrorMessage>{errors.description}</FormErrorMessage>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection={{ base: 'column', md: 'row' }}
|
||||||
|
mt={6}
|
||||||
|
>
|
||||||
|
<InputFieldForm
|
||||||
|
label="Image (IPFS Link)"
|
||||||
|
fieldName="image"
|
||||||
|
mr={5}
|
||||||
|
error={errors.image}
|
||||||
|
isInvalid={!!errors.image && touched.image}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<InputFieldForm
|
||||||
|
label="External url"
|
||||||
|
fieldName="externalUrl"
|
||||||
|
error={errors.externalUrl}
|
||||||
|
isInvalid={!!errors.externalUrl && touched.externalUrl}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Grid
|
||||||
|
templateColumns={{
|
||||||
|
md: 'repeat(3, 1fr)',
|
||||||
|
}}
|
||||||
|
gap={4}
|
||||||
|
mt={6}
|
||||||
|
>
|
||||||
|
<GridItem colSpan={2}>
|
||||||
|
<InputFieldForm
|
||||||
|
label="Github commit url"
|
||||||
|
fieldName="githubCommit"
|
||||||
|
mr={5}
|
||||||
|
error={errors.githubCommit}
|
||||||
|
isInvalid={
|
||||||
|
!!errors.githubCommit && touched.githubCommit
|
||||||
|
}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem colSpan={{ base: 2, md: 1 }}>
|
||||||
|
<InputFieldForm label="ENS" fieldName="ens" />
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
backgroundColor="#1d4ed8"
|
||||||
|
width="full"
|
||||||
|
mt={4}
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
loadingText="Minting..."
|
||||||
|
disabled={
|
||||||
|
isSubmitting ||
|
||||||
|
!values.name ||
|
||||||
|
!values.description ||
|
||||||
|
!values.githubCommit ||
|
||||||
|
!values.ownerAddress ||
|
||||||
|
!values.image ||
|
||||||
|
!values.externalUrl
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Mint
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { isValidImageUrl, isValidUrl } from '@/utils';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { FormikValues } from 'formik';
|
||||||
|
|
||||||
|
export const validateFields = (values: FormikValues) => {
|
||||||
|
const errors: FormikValues = {};
|
||||||
|
if (!values.name) {
|
||||||
|
errors.name = 'Name cannot be empty';
|
||||||
|
}
|
||||||
|
if (!values.description) {
|
||||||
|
errors.description = 'Description cannot be empty';
|
||||||
|
}
|
||||||
|
if (!values.githubCommit) {
|
||||||
|
errors.githubCommit = 'Github commit cannot be empty';
|
||||||
|
} else if (!isValidUrl(values.githubCommit)) {
|
||||||
|
errors.githubCommit = 'Github commit is not a valid url';
|
||||||
|
}
|
||||||
|
if (!values.ownerAddress) {
|
||||||
|
errors.ownerAddress = 'Owner address cannot be empty';
|
||||||
|
} else if (!ethers.utils.isAddress(values.ownerAddress)) {
|
||||||
|
errors.ownerAddress = 'Owner address is not a valid address';
|
||||||
|
}
|
||||||
|
if (!values.externalUrl) {
|
||||||
|
errors.externalUrl = 'External url cannot be empty';
|
||||||
|
} else if (!isValidUrl(values.externalUrl)) {
|
||||||
|
errors.externalUrl = 'External url is not a valid url';
|
||||||
|
}
|
||||||
|
if (!values.image) {
|
||||||
|
errors.image = 'Image cannot be empty';
|
||||||
|
} else if (!isValidImageUrl(values.image)) {
|
||||||
|
errors.image = 'Image url is not a valid url';
|
||||||
|
}
|
||||||
|
//TODO check if ENS is a valid ens name
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
1855
ui/yarn.lock
1855
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue