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:
Camila Sosa Morales 2022-12-13 09:32:55 -03:00 committed by GitHub
parent 94c364836e
commit 09d24f9723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2190 additions and 43 deletions

View File

@ -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",

View File

@ -1,3 +0,0 @@
.main {
text-align: center;
}

View File

@ -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>
);
};

View File

@ -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;
}

View File

@ -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>
);

1
ui/src/mocks/index.ts Normal file
View File

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

26
ui/src/mocks/mint-site.ts Normal file
View File

@ -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);
});
};

23
ui/src/theme/index.ts Normal file
View File

@ -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);

1
ui/src/types/index.ts Normal file
View File

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

11
ui/src/types/mint-site.ts Normal file
View File

@ -0,0 +1,11 @@
export type SiteNFT = {
name: string;
description: string;
owner: string;
externalUrl: string;
image: string;
ens?: string;
commitHash: string;
repo: string;
};

8
ui/src/utils/format.ts Normal file
View File

@ -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 };
};

2
ui/src/utils/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './format';
export * from './validation';

View File

@ -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);
};

View File

@ -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>
);
};

View File

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

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

@ -0,0 +1,3 @@
export * from './home';
export * from './mint-site';

View File

@ -0,0 +1 @@
export * from './input-field-form';

View File

@ -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>
)
);

View File

@ -0,0 +1,2 @@
export * from './mint-site';
export * from './mint-site.utils';

View File

@ -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>
</>
);
};

View File

@ -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;
};

File diff suppressed because it is too large Load Diff