feat: mint site view (#15)
* wip: add routes * refactor: add config for hot reload con vite.config.js * wip: added chakra-ui. start with the form * feat: add formik for form validation * feat: added validation for addresses * feat: add success/failure message. add onSubmit handler * feat: add setSubmitting false * feat: update metadata fields * wip: add mocked function * feat: mocked onSubmit funciton * Apply suggestions from code review Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com> * Apply suggestions from code review Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com> * refactor: PR review changes * feat: add validation for image urls. remove controller address * reafctor: refactor fields validation * refactor: create input field component * style: add responsive styles. change bg color and font. change back button * refactor: apply PR comments Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com>
This commit is contained in:
parent
94c364836e
commit
09d24f9723
|
|
@ -11,9 +11,16 @@
|
|||
"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-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
|
|
@ -28,6 +35,7 @@
|
|||
"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",
|
||||
"ts-loader": "^9.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.main {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
import React from 'react';
|
||||
import './App.css';
|
||||
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
|
||||
import { Home, MintSite } from './views';
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<div className="main">
|
||||
<h1>Welcome to Sites as NFTs by Fleek</h1>
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/mint-site" element={<MintSite />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,8 @@
|
|||
import React from 'react';
|
||||
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';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
|
|
@ -9,7 +10,9 @@ const root = ReactDOM.createRoot(
|
|||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ChakraProvider theme={theme} resetCSS>
|
||||
<App />
|
||||
</ChakraProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './mint-site';
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { SiteNFT } from '@/types';
|
||||
|
||||
export const mintSiteNFT = async (props: SiteNFT) => {
|
||||
const { name, description, owner, externalUrl, ens, commitHash, repo } =
|
||||
props;
|
||||
console.log('mintSiteNFT', 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,11 @@
|
|||
export type SiteNFT = {
|
||||
name: string;
|
||||
description: string;
|
||||
owner: string;
|
||||
externalUrl: string;
|
||||
image: string;
|
||||
ens?: string;
|
||||
commitHash: string;
|
||||
repo: 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,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,3 @@
|
|||
export * from './home';
|
||||
export * from './mint-site';
|
||||
|
||||
|
|
@ -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,2 @@
|
|||
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;
|
||||
};
|
||||
|
||||
1782
ui/yarn.lock
1782
ui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue