diff --git a/README.md b/README.md index 962db2a..9405e6b 100644 --- a/README.md +++ b/README.md @@ -32,19 +32,22 @@ Secret phrase `brief outside human axis reveal boat warm amateur dish sample enr and then, post your account to the node ``` -$ curl -X POST 'http://localhost:9933' -H "Content-Type:application/json;charset=utf-8" \ - -d '{ - "jsonrpc":2.0, - "id":1, - "method":"author_insertKey", - "params": [ +curl -X POST -H "Content-type: application/json" http://localhost:9933 -d ' +{ + "method": "author_insertKey", + "jsonrpc": "2.0", + "id": 0, + "params": [ "fdot", "brief outside human axis reveal boat warm amateur dish sample enroll moment", "0x0676a4b19c66b31e12d15fe31ccbc775d3d2cda6e1c8686e395118f808eaa118" - ] - }' + ] +} +' ``` +Make sure you transfer some funds to this account. E.g using polkadot.js/apps and using some funds from the dev accounts. + #### 2. set filecoin rpc endpoint to your node @@ -96,15 +99,15 @@ Or with [@polkadot/api](https://polkadot.js.org/docs/), you can use the code bel ```typescript import { ApiPromise, WsProvider } from "@polkadot/api"; -import { rpc, types } from "@filecoindot/types"; +import { rpc, types } from "@chainsafe/fileconidot-types"; import { Keyring } from "@polkadot/keyring"; (async () => { - // setup api + // setup the api const provider = new WsProvider("http://0.0.0.0:9944"); const api = await ApiPromise.create({ provider, types, rpc }); - // setup singer + // setup the signer const keyring = new Keyring({ type: "sr25519" }); const signer = keyring.addFromUri("//Alice"); diff --git a/filecoindot-cli/README.md b/filecoindot-cli/README.md index 8868693..88df7ab 100644 --- a/filecoindot-cli/README.md +++ b/filecoindot-cli/README.md @@ -5,7 +5,13 @@ Filecoindot Command Line Interface. ## Installation ``` -cargo +nightly install --git https://github.com/ChainSafe/filecoindot --branch main filecoindot-cli +cargo +nightly install --git https://github.com/ChainSafe/filecoindot --branch main filecoindot-cli +``` + +or from the root of the project + +``` +cargo +nightly install --path ./ filecoindot-cli ``` ## Usage diff --git a/ui/.env.example b/ui/.env.example new file mode 100644 index 0000000..0014042 --- /dev/null +++ b/ui/.env.example @@ -0,0 +1 @@ +REACT_APP_WS_PROVIDER="ws://localhost:9944" \ No newline at end of file diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json new file mode 100644 index 0000000..7ae8224 --- /dev/null +++ b/ui/.eslintrc.json @@ -0,0 +1,75 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features + "sourceType": "module", // Allows for the use of imports + "ecmaFeatures": { + "jsx": true // Allows for the parsing of JSX + } + }, + "plugins": ["@typescript-eslint", "ternary"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:ternary/recommended" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "comma-spacing": ["error", { "before": false, "after": true }], + "@typescript-eslint/indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "double"], + "semi": ["error", "never"], + "max-len": [ + "error", + { + "code": 140 + } + ], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "react/prop-types": 0, + "react/jsx-max-props-per-line": ["error", { "maximum": 1, "when": "always" }], + "react/jsx-first-prop-new-line": ["error", "multiline-multiprop"], + "react/jsx-fragments": "error", + "arrow-spacing": "error", + "space-infix-ops": "error", + "no-trailing-spaces": ["error", { "ignoreComments": true }], + "comma-dangle": ["error", "never"], + "object-curly-spacing": ["error", "always"], + "space-in-parens": ["error", "never"], + "ternary/no-unreachable": "off", + "ternary/nesting": "off", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/member-delimiter-style" : ["warn", { + "multiline": { + "delimiter": "none", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + }] + }, + "ignorePatterns": [ + ".github/**", + ".vscode/**", + ".yarn/**", + "**/dist/*", + "**/node_modules/*" + ] + } \ No newline at end of file diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..834b352 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +.env + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/ui/.nvmrc b/ui/.nvmrc new file mode 100644 index 0000000..b08ffc7 --- /dev/null +++ b/ui/.nvmrc @@ -0,0 +1 @@ +v16.0.0 \ No newline at end of file diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..905cb1e --- /dev/null +++ b/ui/README.md @@ -0,0 +1,48 @@ +# Getting Started with Filecoindot UI + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +You need to setup an environment variable `REACT_APP_WS_PROVIDER` or rename `.env.exemple` into `.env` to use the default localhost value: "ws://localhost:9944" + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..424a043 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,59 @@ +{ + "name": "filecoin-substrate-demo", + "version": "0.1.0", + "private": true, + "dependencies": { + "@emotion/react": "^11.7.0", + "@emotion/styled": "^11.6.0", + "@mui/icons-material": "^5.2.0", + "@mui/material": "^5.2.2", + "@mui/styled-engine-sc": "^5.1.0", + "@chainsafe/filecoindot-types": "^0.1.12", + "@polkadot/api": "^7.2.1", + "@polkadot/extension-dapp": "^0.42.4", + "@polkadot/react-identicon": "^0.88.1", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "@types/jest": "^26.0.15", + "@types/node": "^12.0.0", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router-dom": "^6.0.2", + "react-scripts": "4.0.3", + "styled-components": "^5.3.3", + "typescript": "^4.1.2", + "web-vitals": "^1.0.1" + }, + "devDependencies": { + "eslint": "^6.8.0", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-ternary": "^1.0.4", + "@typescript-eslint/eslint-plugin": "^4.15.2", + "@typescript-eslint/parser": "^4.15.2" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "lint": "eslint 'src/**/*.{js,ts,tsx}'", + "lint:types": "tsc --pretty", + "lint:fix": "yarn run lint --fix" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/ui/public/favicon.ico differ diff --git a/ui/public/index.html b/ui/public/index.html new file mode 100644 index 0000000..a29ae3d --- /dev/null +++ b/ui/public/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + Filecoin Substrate pallet demo + + + +
+ + + diff --git a/ui/public/manifest.json b/ui/public/manifest.json new file mode 100644 index 0000000..913649c --- /dev/null +++ b/ui/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Filecoindot demo", + "name": "Filecoin Substrate pallet demo", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/ui/public/robots.txt b/ui/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/ui/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/ui/src/App.css b/ui/src/App.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/ui/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx new file mode 100644 index 0000000..744673e --- /dev/null +++ b/ui/src/App.test.tsx @@ -0,0 +1,9 @@ +import React from "react" +import { render, screen } from "@testing-library/react" +import App from "./App" + +test("renders learn react link", () => { + render() + const linkElement = screen.getByText(/learn react/i) + expect(linkElement).toBeInTheDocument() +}) diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..451bcdb --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,42 @@ +import React from "react" +import "./App.css" +import { BrowserRouter, Route, Routes } from "react-router-dom" +import { NFTList } from "./pages/NFTList" +// import { Settings } from './containers/Settings'; +import { Header } from "./containers/Header" +import Container from "@mui/material/Container" +import { MintNFT } from "./pages/MintNFT" +import { UserSpace } from "./containers/UserSpace" +import { AccountContextProvider } from "./contexts/AccountsContext" +import { ApiContextProvider } from "./contexts/ApiContext" + +function App() { + return ( + + + +
+ + + + } + /> + } + /> + + + + {/* */} + + + + ) +} + +export default App diff --git a/ui/src/components/layout/Center.tsx b/ui/src/components/layout/Center.tsx new file mode 100644 index 0000000..c41ada9 --- /dev/null +++ b/ui/src/components/layout/Center.tsx @@ -0,0 +1,18 @@ +import React from "react" +import { Grid } from "@mui/material" + +export const Center: React.FC = ({ children }) => ( + + + {children} + + +) diff --git a/ui/src/constants/substrate.ts b/ui/src/constants/substrate.ts new file mode 100644 index 0000000..d86f7f9 --- /dev/null +++ b/ui/src/constants/substrate.ts @@ -0,0 +1 @@ +export const DAPP_NAME = "filecoindot" diff --git a/ui/src/containers/AccountSelect/index.tsx b/ui/src/containers/AccountSelect/index.tsx new file mode 100644 index 0000000..5341198 --- /dev/null +++ b/ui/src/containers/AccountSelect/index.tsx @@ -0,0 +1,51 @@ +import React from "react" +import { CardHeader, IconButton, Menu, MenuItem } from "@mui/material" +import { Identicon } from "@polkadot/react-identicon" +import { MenuProps } from "@mui/material/Menu/Menu" +import { useAccountList } from "../../contexts/AccountsContext" + +interface Props extends Omit { + anchorEl: null | HTMLElement + onClose: () => void +} + +export const AccountSelect: React.FC = ({ anchorEl, onClose, ...props }) => { + + const { accountList, selectAccount } = useAccountList() + + if (!accountList) { + return null + } + + const handleSelect = (address: string) => () => { + selectAccount(address) + onClose() + } + + return ( + + {accountList.map((account) => ( + + + + + } + title={account.meta.name} + subheader={account.address} + /> + + ))} + + ) +} diff --git a/ui/src/containers/Header/index.tsx b/ui/src/containers/Header/index.tsx new file mode 100644 index 0000000..58754f5 --- /dev/null +++ b/ui/src/containers/Header/index.tsx @@ -0,0 +1,119 @@ +import AppBar from "@mui/material/AppBar" +import React, { useEffect } from "react" +import { Avatar, Box, Button, Container, IconButton, Toolbar, Tooltip, Typography } from "@mui/material" +import { AccountSelect } from "../AccountSelect" +import { Identicon } from "@polkadot/react-identicon" +import { Link } from "react-router-dom" +import { useAccountList } from "../../contexts/AccountsContext" +import { useApi } from "../../contexts/ApiContext" + +export const Header: React.FC = () => { + const { selected } = useAccountList() + const [anchorElUser, setAnchorElUser] = React.useState(null) + const { api, isApiReady } = useApi() + const handleOpenUserMenu = (event: React.MouseEvent) => { + setAnchorElUser(event.currentTarget) + } + + useEffect(() => { + if (!isApiReady) return + + let unsubscribe: () => void + + // use the api + api.derive.chain.bestNumber((number) => { + console.log(number.toNumber()) + }) + .then(unsub => {unsubscribe = unsub}) + .catch(console.error) + + return () => unsubscribe && unsubscribe() + }) + + const handleCloseUserMenu = () => { + setAnchorElUser(null) + } + + return ( + + + + + Filecoindot pallet demo + + {selected && ( + <> + + + {/* */} + + + + + + + + + + + + + )} + + + + ) +} diff --git a/ui/src/containers/Settings/index.tsx b/ui/src/containers/Settings/index.tsx new file mode 100644 index 0000000..0000e9b --- /dev/null +++ b/ui/src/containers/Settings/index.tsx @@ -0,0 +1,19 @@ +import React from "react" +import { Box, Fab, Tooltip } from "@mui/material" +import SettingsIcon from "@mui/icons-material/Settings" + +export const Settings: React.FC = () => { + return ( + + + + + + + + ) +} diff --git a/ui/src/containers/UserSpace/index.tsx b/ui/src/containers/UserSpace/index.tsx new file mode 100644 index 0000000..b084d5e --- /dev/null +++ b/ui/src/containers/UserSpace/index.tsx @@ -0,0 +1,101 @@ +import React from "react" +import { Center } from "../../components/layout/Center" +import { Box, Button, CircularProgress } from "@mui/material" +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown" +import { AccountSelect } from "../AccountSelect" +import { useAccountList } from "../../contexts/AccountsContext" +import { useApi } from "../../contexts/ApiContext" + +export const UserSpace: React.FC = ({ children }) => { + const { isApiReady } = useApi() + const { extensionNotFound, isAccountListEmpty, isAccountLoading, selected } = useAccountList() + const [anchorElUser, setAnchorElUser] = React.useState(null) + const handleOpenUserMenu = (event: React.MouseEvent) => { + setAnchorElUser(event.currentTarget) + } + const handleCloseUserMenu = () => { + setAnchorElUser(null) + } + + if(!isApiReady || isAccountLoading) + return ( + + + Connecting to the node + + ) + + if (selected) return <>{children} + + if(extensionNotFound) + return ( +
+

Please install the + Polkadot.js extension + +

+
+ ) + + if(isAccountListEmpty) + return ( +
+

Please create at least an account in the extension

+
+ ) + + // if (state === PluginState.INITIALIZATION) return null; + + // if (state === PluginState.UNAUTHORIZED) + // return ( + //
+ //

Please Authorise page

+ //
+ // ); + + // if (state === PluginState.NONE) + // return ( + //
+ //

There is no plugin :sad:

+ //
+ // ); + + // if (state === PluginState.INJECTED) + // return ( + //
+ //

Please Allow Access

+ //
+ // ); + + // if (!accounts.length) + // return ( + //
+ //

Please Add Account

+ //
+ // ); + + return ( +
+ + +
+ ) +} diff --git a/ui/src/contexts/AccountsContext.tsx b/ui/src/contexts/AccountsContext.tsx new file mode 100644 index 0000000..f9922de --- /dev/null +++ b/ui/src/contexts/AccountsContext.tsx @@ -0,0 +1,97 @@ +import React, { useState, useEffect, createContext, useContext } from "react" +import { web3Accounts, web3Enable } from "@polkadot/extension-dapp" +import { InjectedAccountWithMeta } from "@polkadot/extension-inject/types" +import { DAPP_NAME } from "../constants/substrate" + +type AccountContextProps = { + children: React.ReactNode | React.ReactNode[] +} + +export interface IAccountContext { + selected: string + accountList?: InjectedAccountWithMeta[] + selectAccount: (address: string) => void + isAccountLoading: boolean + extensionNotFound: boolean + isAccountListEmpty: boolean +} + +const AccountContext = createContext(undefined) + + +const AccountContextProvider = ({ children }: AccountContextProps) => { + const [selected, setSelected] = useState("") + const [accountList, setAccountList] = useState([]) + const [isAccountLoading, setIsAccountLoading] = useState(false) + const [extensionNotFound, setExtensionNotFound] = useState(false) + const [isAccountListEmpty, setIsAccountListEmpty] = useState(false) + + useEffect(() => { + if (!accountList.length) { + getaccountList() + } + }, [accountList.length]) + + const getaccountList = async (): Promise => { + const extensions = await web3Enable(DAPP_NAME) + + if (extensions.length === 0) { + setExtensionNotFound(true) + setIsAccountLoading(false) + return + } else { + setExtensionNotFound(false) + } + + const accountList = await web3Accounts() + + if (accountList.length === 0) { + setIsAccountListEmpty(true) + setIsAccountLoading(false) + return + } + + // if addresses need to be encoded + // accountList.forEach((account) => { + // account.address = encodeAddress(account.address) || account.address; + // }); + + setAccountList(accountList) + + if (accountList.length > 0) { + setSelected(accountList[0].address) + } + + setIsAccountLoading(false) + return + } + + const selectAccount = (address: string) => { + setSelected(address) + } + + return ( + + {children} + + ) +} + +const useAccountList = () => { + const context = useContext(AccountContext) + if (context === undefined) { + throw new Error("useAccountList must be used within a AccountContextProvider") + } + return context +} + +export { AccountContextProvider, useAccountList } diff --git a/ui/src/contexts/ApiContext.tsx b/ui/src/contexts/ApiContext.tsx new file mode 100644 index 0000000..d5f7d0d --- /dev/null +++ b/ui/src/contexts/ApiContext.tsx @@ -0,0 +1,82 @@ +import React from "react" +import { ApiPromise, WsProvider } from "@polkadot/api" +import { ApiOptions } from "@polkadot/api/types" +import { TypeRegistry } from "@polkadot/types" +import { useState, useEffect, createContext, useContext } from "react" +import { useDidUpdateEffect } from "../hooks/useDidUpdateEffect" +import { rpc } from "@chainsafe/filecoindot-types" + +type ApiContextProps = { + children: React.ReactNode | React.ReactNode[] + types?: ApiOptions["types"] +} + +const registry = new TypeRegistry() + +export interface IApiContext { + api: ApiPromise // From @polkadot/api\ + isApiReady: boolean +} + +const ApiContext = createContext(undefined) + + +const ApiContextProvider = ({ children, types }: ApiContextProps) => { + const WS_PROVIDER = process.env.REACT_APP_WS_PROVIDER + const provider = new WsProvider(WS_PROVIDER) + const [apiPromise, setApiPromise] = useState( + new ApiPromise({ provider, types, rpc }) + ) + const [isReady, setIsReady] = useState(false) + + useDidUpdateEffect(() => { + // We want to fetch all the information again each time we reconnect. We + // might be connecting to a different node, or the node might have changed + // settings. + setApiPromise(new ApiPromise({ provider, types, rpc })) + + setIsReady(false) + }) + + useEffect(() => { + // We want to fetch all the information again each time we reconnect. We + // might be connecting to a different node, or the node might have changed + // settings. + apiPromise.isReady + .then(() => { + if (types) { + registry.register(types) + } + + setIsReady(true) + }) + .catch(e => console.error(e)) + }, [apiPromise.isReady, types]) + + + if (!WS_PROVIDER) { + console.error("REACT_APP_WS_PROVIDER not set") + return null + } + + return ( + + {children} + + ) +} + +const useApi = () => { + const context = useContext(ApiContext) + if (context === undefined) { + throw new Error("useApi must be used within a ApiContextProvider") + } + return context +} + +export { ApiContextProvider, useApi } diff --git a/ui/src/hooks/useDidUpdateEffect.tsx b/ui/src/hooks/useDidUpdateEffect.tsx new file mode 100644 index 0000000..d777409 --- /dev/null +++ b/ui/src/hooks/useDidUpdateEffect.tsx @@ -0,0 +1,21 @@ +/* eslint-disable max-len */ +import { EffectCallback, useEffect, useRef } from "react" + +/** + * Exactly like React's `useEffect`, but skips initial render. Tries to + * reproduce `componentDidUpdate` behavior. + * + * @see https://stackoverflow.com/questions/53179075/with-useeffect-how-can-i-skip-applying-an-effect-upon-the-initial-render/53180013#53180013 + */ +export function useDidUpdateEffect( + fn: EffectCallback + // inputs?: DependencyList +): void { + const didMountRef = useRef(false) + + return useEffect(() => { + if (didMountRef.current) fn() + else didMountRef.current = true + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) +} \ No newline at end of file diff --git a/ui/src/index.css b/ui/src/index.css new file mode 100644 index 0000000..0e2f4ef --- /dev/null +++ b/ui/src/index.css @@ -0,0 +1,15 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; +} + +#root { + min-height: 100vh; +} diff --git a/ui/src/index.tsx b/ui/src/index.tsx new file mode 100644 index 0000000..e6332fb --- /dev/null +++ b/ui/src/index.tsx @@ -0,0 +1,19 @@ +import React from "react" +import ReactDOM from "react-dom" +import "./index.css" +import App from "./App" +import reportWebVitals from "./reportWebVitals" +import { CssBaseline } from "@mui/material" + +ReactDOM.render( + + + + , + document.getElementById("root") +) + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals() diff --git a/ui/src/logo.svg b/ui/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/ui/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/pages/MintNFT.tsx b/ui/src/pages/MintNFT.tsx new file mode 100644 index 0000000..e234b32 --- /dev/null +++ b/ui/src/pages/MintNFT.tsx @@ -0,0 +1,141 @@ +import { Box, Button, TextField, Typography } from "@mui/material" +import React, { ChangeEvent, useCallback, useState } from "react" +import { Center } from "../components/layout/Center" +import { useApi } from "../contexts/ApiContext" +import CheckCircleIcon from "@mui/icons-material/CheckCircle" +import CancelIcon from "@mui/icons-material/Cancel" + +export const MintNFT: React.FC = () => { + const [cid, setCid] = useState("") + const [proof, setProof] = useState("") + const { api, isApiReady } = useApi() + const [isLoading, setIsLoading] = useState(false) + const [isValid, setIsValid] = useState(null) + const [error, setError] = useState("") + + const onChangeCid = useCallback((cid: ChangeEvent) => { + setCid(cid.currentTarget.value) + setIsValid(null) + setError("") + }, []) + + const onChangeProof = useCallback((proof: ChangeEvent) => { + setProof(proof.currentTarget.value) + setIsValid(null) + setError("") + }, []) + + const onVerify = useCallback(() => { + if (!isApiReady) { + console.error("Api is not connected") + setError("Api not connected to your node") + return + } + + setIsLoading(true); + + (api.rpc as any).filecoindot.verifyState(proof, cid) + .then((res: any) => {console.log(Boolean(res)); setIsValid(!!res.toHuman())}) + .catch((e: any) => { + setError(e.message) + console.error(e) + }) + .finally(() => setIsLoading(false)) + }, [api, cid, isApiReady, proof]) + + return ( +
+

Mint new token

+ + + + {error && ( + + {error} + + )} + {!error && isValid === null + ? ( + + ) + : + { + isValid + ? ( + <> + + This proof is valid for this cid! + + ) + : ( + <> + + This proof is not valid for this cid! + + ) + } + + + + } + +
+ ) +} diff --git a/ui/src/pages/NFTList.tsx b/ui/src/pages/NFTList.tsx new file mode 100644 index 0000000..76135f3 --- /dev/null +++ b/ui/src/pages/NFTList.tsx @@ -0,0 +1,55 @@ +import { Card, CardContent, CardMedia, Grid, Typography } from "@mui/material" +import React, { useState } from "react" + +export const NFTList: React.FC = () => { + const [selected, setSelected] = useState(-1) + const handleClick = (index: number) => () => { + if (selected === index) setSelected(-1) + else setSelected(index) + } + + return ( + + {Array.from(Array(9)).map((_, index) => ( + + + + + + f24f3asdfsdvsdv + + + NFT name + + + zb2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA + + + + + ))} + + ) +} diff --git a/ui/src/proof.ts b/ui/src/proof.ts new file mode 100644 index 0000000..89312e8 --- /dev/null +++ b/ui/src/proof.ts @@ -0,0 +1,6 @@ +/* eslint-disable max-len */ + +export const proofJSON = { + "cid": "bafy2bzaceashznt6nk5rtr7gwvsnngsnhzwrcclycxgw6ztaezh3vffsnzcis", + "proof": "0899ac825820ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff990100d82a5827000171a0e402208d4e2379f5b610c53f7bc9d8263ca796633bfac309e73426f57a1036c2196ec8d82a5827000171a0e4022017d32efa3c4ece234a612c1bbf1258a1a83078573165f5f610a55bdef9f9bbf4d82a5827000171a0e40220893eea6ca5aed5d7f2aa7430a091c02307d86ba0fbe8693ca55f1ad5dd7ec59dd82a5827000171a0e4022013a13d02b8f5260c065d79370c3ed82b6b9b5761d4e2cedd1c861717e68e63fed82a5827000171a0e40220ddbd3897eca983db80c61639f7871d89cdfa2bbba9d295c4ef121f8117fb557ad82a5827000171a0e402203cf4f9257d7b6f84e71f70df46fb1edef03dc260dc69c5401e44e4e74aeb7e82d82a5827000171a0e40220514bfce373edd0000ce34b00de5b25b57e83f8234304c023d6669757c94ea80bd82a5827000171a0e40220b0b5789d23bac8e4b078e376515f4329eab17c9d549e686b6f53c2f50ccfe8e0d82a5827000171a0e40220af9043090367fc67d4779bbe20bb89a7b837affc7abbafbd000480d9c60bccdcd82a5827000171a0e402207f41fdc4cbd1030cd654ca7ba6a3b04c5c5bb0d109dde0aadfa0db9e12ecbad6d82a5827000171a0e40220900f151fa82b30cb7659fa56c8d0a251991b8387b05a6066ab1689e7178ed940d82a5827000171a0e4022016cf22ea82208f5065b171cac237bf7a0c4042d84a6488d7152d77464c535470d82a5827000171a0e40220ac2d002850ed7f7d27738b6d3a4afc37953f9fc94a927c07df4eff5f3b8da770d82a5827000171a0e402200143f3bf7d17a08b77d035c36dc2f46a6d997bcd7426ef5ed2f12a36ad6aa067d82a5827000171a0e40220cfa8e7b73f0d1dffac4a7900f720931f8c640d144755509f25b54008283e36f3d82a5827000171a0e40220423e5db5d0af909561849eca002d7c65101cc1a2b1c85f65ee9e21db93bb2fc0d82a5827000171a0e40220197176db727dce10413212d00e8a1069ea96cd75f030785a154a90c5ded53f98d82a5827000171a0e4022090e83682877b1a7b629a3632ebe1a179aa6cab30d32ed961d6aff78570d2597cd82a5827000171a0e40220511d953e60264e3c284be072aa56cdcf85c2a5700d8b1fc27a7895e66ab7d380d82a5827000171a0e40220cda613e4f08c2988b0852c775c98bc9e33ac5f0169c3a52244139f3a00ead1fad82a5827000171a0e402201ddedfb6284e4fe2c4051916f6c9b3ff2d823bdde624614c8827c78ee17449b7d82a5827000171a0e40220f2569157f277e7b99c2124847d70e7167619a4fcaf4c001d5bcdcc87a9e40293d82a5827000171a0e40220908ec792379416e013dd3b5763cc122cd8e4ca13954e61a7beb259f76c670811d82a5827000171a0e402202f4ad9173d5b2dcafd8c1f7605196fe070eda217bb116380ca392a3f6729e3a7d82a5827000171a0e4022057de75299d6fbf5a422488f93e5ec99e5caaf82e828ebcdf7224d38e1ddb35b0d82a5827000171a0e402201f9c02141e2774edb9727411afd0677d8c95ad02c5b65329fd5807d92eb5019cd82a5827000171a0e402201b27a0c8156fb1e684270ff08e30bcb18fbd394117614216538efbb38c20a2b9d82a5827000171a0e402203a2551fbbb10513e2caab9864a53604a8512b02a60199f5bc96c737914b9bff2d82a5827000171a0e40220300fd51302e7649baf33ea34149ada78b50562b343070d80eafdbb31c81322b5d82a5827000171a0e40220a07051bef8de414ae771461483cbc3271cafbfd242de9f433a518d7b08818875d82a5827000171a0e402200079f924b78ae32533ea24e2c17eefad7e5164481218814953f900594c553b8bd82a5827000171a0e40220a721e271b3437739b7e8f98a7b2ce84ad8aa0a2c7872b65d397686af00fb1de8d82a5827000171a0e40220bcebde91c88c6183198287d831a47f1b1656d7ada4d3515bcaa8c269200e80ced82a5827000171a0e402207dbaf7352210a3fdf5c627dccd0f72ce750e0f77469939083f53bed27c3b3d41d82a5827000171a0e4022068bdc72ff40acfa139d0f82e59411e865a85ebfbf6f35edc738a805b4794cccfd82a5827000171a0e40220955e54053c541bd2e0b0263f58ab88017a2df3fa4cf0e6062de78159aba994f3d82a5827000171a0e40220fd205e51a809902d5f9bc64a13124b6a433d872dd7a0e4bcffbb5ef32cb3b656d82a5827000171a0e40220df3e63a9cc4ddceea61f31077e67e4582e9ccd2d47429b0ffb7293d43d166fb9d82a5827000171a0e40220b89ca3d99e8d9d4abee7407a495092d4a0e8635c8e568d5af93672587f6ca44cd82a5827000171a0e40220c7c305d318e7ff6d06780a55431596415200a1ab607e2a2d4fe4a86d9759a162d82a5827000171a0e40220ea9250a53f6c4813b21f11b867027f2552d7dd5ab75ab0a1756a714c94f8472bd82a5827000171a0e40220b719d940febceae87bf24d7d97ce93175c6dec59f202dd80b25df767b3348279d82a5827000171a0e40220ae687ccbfe2b88a2a7480c19e2993a1e9afcb8ae40a386a370d10a759e7ade29d82a5827000171a0e402203e14ed3af7503df23902951ca8a98e19c701b6c02b49a4b397f723a4d5e09876d82a5827000171a0e4022023fba093644f182c6f5ae49af4c1c000313b0c8ec60ff8c4da1e83a965a5c91fd82a5827000171a0e40220d35a1c41194999f1d9556db53653c6e4e1d3da1fc577f3f52d0ae124f1dd9561d82a5827000171a0e402205fd628e59942a368de183d3860d87a791757545e416370df0afb61e31ba09883d82a5827000171a0e40220170268d0b3f045cf4c4db138f87457dbff599b13cebc7d4bb5e60214823f09d0d82a5827000171a0e40220bcf6dd69a60c2d55d5f0f89634fa4521db407f4924eb21e919ca8163e6fa39b3d82a5827000171a0e402205f4728844c59d27e5217c11b0e4215e1d35f3715dd2ddbc7ea809232ebccd6b4d82a5827000171a0e40220b77f0d5e035512bcb2888bc3ec90a8df6c704a75b8dd1e49372090f9ed993f94d82a5827000171a0e40220a3eb5743105d0dcf56db191fad0d54446924ffb35e12e56eed1db6ea38d2c8a5d82a5827000171a0e40220486466aac3900efdb8d23473008a5c522977fe3ce0cb999fc5c6079a26d456f2d82a5827000171a0e402202346d25dec00d20b9623c4e1e84dc94fc8969075d98970d3cb31a9b77855c469d82a5827000171a0e40220158a1c315c15bc40c4b9c716aeb6d7309ab4f2051e81aae4d5c7154d34860ecad82a5827000171a0e402209c2683bbb0053d5e7278152f42e9d94e79704957d6bdbf7b8cf67c6c45de040ad82a5827000171a0e40220f7b81770a22fbdde4c9feba4a3505e67a887353e92855090e9d78c09596e1f44d82a5827000171a0e4022026b7495d72004d583431bdfd3d49e4ebd3ae0a9bad0c4818881273690954f997d82a5827000171a0e402205db5f8ec9670957496d2f33bf5681d17f81b38f59f07044c12e7e7869f84db4ad82a5827000171a0e402206e67c121a21bcaf8c0761c8e59047030e3ae9a29d420ded39fa7652ceecba593d82a5827000171a0e40220ad0bf77b95a945b17fde1dae29cde0c63ccfc00331e9dd94afb62653563aeca6d82a5827000171a0e40220bc4e439ba2d4c5945c067f5dfea16aa16546dac835d1a815fdbb1f125b721646d82a5827000171a0e4022085c315e200d7f64710b6af584b90383b0f847f7e5400ecfb7afd82092a2c6dfbd82a5827000171a0e4022081b378c0b92f6ff92c99030dc960c5f8d12e47ff76cb8fc451acd6613606141bd82a5827000171a0e402208cb22f443c857552dcac5ba22fb351b59d96a89d79666b3d216bd94b8f85bf5cd82a5827000171a0e4022046ddab1270c1a66b6eed6a0e2e1493c69a389ec4c4f6db332c8b5ab437535c66d82a5827000171a0e40220d46ed4a362b17c8372004935cd37c4c116aa4e38ba73c065ac5d25a85cab0454d82a5827000171a0e4022073b99fe0cdc06f934bb3e3808d2090755c42812965441003c19c1aec3d12fa7cd82a5827000171a0e4022049caba5113b4808848d8db9d0f3a906e684daa54a24003f3a596447b52331dd3d82a5827000171a0e40220e886f97155946bb28791657fd6bab2fa145de5b85b026f2548a97fc0309eb24dd82a5827000171a0e40220d49bb27dd5a08f0f17b404f1b72418ee2e254c5abc26998547fe5da6de7390e8d82a5827000171a0e402208f974a539ccf6f09cd86abb24d9827f29912952f373779dac285d2e1bcbdb767d82a5827000171a0e40220b07769a3e9ea8d8ae296bc5865fbb02334612cecfdc3d218676143e18f178634d82a5827000171a0e402200e58c765f9df61828b6b9fbeae5fe1597c60dcf5bb65c3b316d00319b768b5c9d82a5827000171a0e40220b4ce78aee0c4e092e2ce6b1df418fd074b05bbc21860f2b9befe371036742ca9d82a5827000171a0e40220b8625fc5a9a3f0f5984b4d80791d27e115ffea2817ad5b3b0995b59e2a2f1e91d82a5827000171a0e402209109aaf928aa8a39edf66bafa1736f739572807214a230a3a09913a5056d5ac4d82a5827000171a0e40220fdca90dd5210e78efb4f800d5443dfedbe19df84f06dcd6fc6af0be1931e94b8d82a5827000171a0e40220b6ab4e67e97592242572f508994deccb3c1e335fdb862dce267d7a6441671c2fd82a5827000171a0e402208d5da9928de31fe9a89ff9be8c92f281c81d019d9fc56dc77f74cf141fe1e485d82a5827000171a0e40220fe8b3c936c3c7a43b7ba0617d43081ddfc9df33253bfbfb601ac0aa87059c652d82a5827000171a0e402207b2856da429940ca49f44b506414401242b3a8d01e8638778b17be1641df01d4d82a5827000171a0e4022004d0ce34cbac34b3323e1d0ff566fe6c11fc09381aa26cc5d240578bdcb75a42d82a5827000171a0e40220ed64dc75d78a3077bf6a64860f493e1ec9133017573757a3cf10824eacd96781d82a5827000171a0e40220f5fa7bb5ed4e5b5efed7b310dcf47b21b0a944107b1e0f9d7758f191b8191bc7d82a5827000171a0e40220ab9f27cb87bf40259910ef1e6b9681439b66d5528e2430f59b72b15e99240becd82a5827000171a0e4022080481e556f9e171501e002797ed7d75e557435629627fc2452994954f9270949d82a5827000171a0e4022011e714db28f285d425571f9f397cd94f34d73008a4c01aeac0ba749b47a0020fd82a5827000171a0e40220fdfd3f170264afa0f5c0bcbb485b5935cbe55b1baf245486174b15108567ba25d82a5827000171a0e4022077684fe55d70691a46f8cc1dc84ffde0cdf531d15f85b1c55c136bbd548d4208d82a5827000171a0e40220cd4d93f0e2d9db5b25f1fd7cd71da7e1eed700c8f0e294dd0cbf526f97ec3e4ed82a5827000171a0e40220a8b2efebd65af07f49121def4c7e57bdf5bef64406f1425dfb2fc42f3b04268bd82a5827000171a0e40220112755b02270864f51de698e907a50e6b31ccb29a611a2dfc13cd98ad147eca4d82a5827000171a0e40220eae8463abf7ce2dbec865aa5fcfef6e5ddb5cdec0f677ca3723a0856f3fb3555d82a5827000171a0e40220a77fb984bd12abf9fa5cfc9d442fc1dae5369dcc2e91baba6b1cba3b88504d0fd82a5827000171a0e4022074591b81f42bac464ef2efaf47c6cc3b32a052dd8305b4865e23ae50d64c4a8bd82a5827000171a0e402201084d776b2b9136e09d4d76429f006d6d715fd740e78fd9502d1f3b8a2691386d82a5827000171a0e40220c001c3d896b0d83d57df425a02006911bc84a9dc84ea0162c0b356aa654faec3d82a5827000171a0e4022059beb398535e48b9e4ec9563b042c98a9d62a12a13f543d7ce83dfc3bebc0cccd82a5827000171a0e402206880f67f11603c28fb54d03c561b44504f419db65ea10d7fd12ba6c7b17bc002d82a5827000171a0e402209758873e03ebe7957ebf5369c554505e548c33573b8a3b41c51e9e8c797934d8d82a5827000171a0e4022029a782065452ccd5d43e44e9ed5699fd5dce42f8c743e9398be530c1ca605dded82a5827000171a0e40220d020546c4989d8f9efcfbb404b287999aab0e4fb4109c874a04c5ff464ab8243d82a5827000171a0e402203ea2d1f1146ff9495ef58cfb47dc1efa42e8c6a1a55223bc02da69d9f7800305d82a5827000171a0e402205aec8fe354897ec8149c0ba17a1871c8a7d03d8baf0a61444e7ce818a7b3bf0ed82a5827000171a0e4022064f3b2e0c2704a4c709aae7c935b80779265ff631fcca865c664728d044c4de0d82a5827000171a0e402207ef51d35046d6c71b1205ea880765dd85cdefd057409f272f8cea255b7d2ef51d82a5827000171a0e4022028afc19d1915bdaaaafb60ac158c5862ea3c3ddbf8d5af1d9eece808c8640b43d82a5827000171a0e4022063150404cf0429aaf3836325ca7e8e758d4460e1a089cb1e4ed1c03854714a07d82a5827000171a0e402209f249a9646e0243d4f0ff99876b2a8ad20e7d88deafdd4306a67015f03794b11d82a5827000171a0e40220172c02d24008ebb804a08eb0a76e98b5dee1aa25c7fcc42707c786f52f846571d82a5827000171a0e402206247077be9f7d5ac4119a5f60767b2d6be8b3f464f05843cdcc75480d080f2cfd82a5827000171a0e40220539c7c3b0174688f7d99aebef159e5f216d99feda1c705d888370cf64bb4c5d8d82a5827000171a0e40220da2f3142820735a32e4848a43061f94df8362907c03484d4d2d5691d6a6c8845d82a5827000171a0e4022072357b919ade70951584544c18a3603d3beab1b72be648ab2a61812a848c3096d82a5827000171a0e40220df4c877455a71edc38a17e32f5a9568ed363769388c7132171d53f8a1dac855cd82a5827000171a0e4022081fe1b1b9260c43b5642f8307f5580831d82e20bbe2cc40104244ff15b2b84ccd82a5827000171a0e402203449ef5c8c9d480044d7819359a81fc70bad6d9e0d63f1a56a07cf267494c7cad82a5827000171a0e40220842d0b55c5b2fd427b873f5254c774e52dcaa572ca1f070fbb2b29d5a965b910d82a5827000171a0e40220e2fe58f51e31da7440bea09d5fe69929975691ec1a35e19d63a701e6346e8614d82a5827000171a0e40220759f98d2014b71b33f16f1207a933d5ad3ceec4c06ea8211ec46bbf616078526d82a5827000171a0e4022047a5a50c1144999aebdab738fea1a95461901727883498859a9a2ae11f5b2ffed82a5827000171a0e402203d30c3f28571821ef48723d8ba3d991c35699d044e96f8ad5725b0b7386eeb25d82a5827000171a0e40220c24548f73bbcd4ce91b8349a99f34ee5a41f11af0a3a5714b98d540f703fa179d82a5827000171a0e40220fd58bf0637274db0f196b7df96d6da79e2c7335797f445a2c8514c711359f1b6d82a5827000171a0e402202ed4ac274ee7cef7a44adc4fb631aee65ba4019aba3be2da85f9a143d433d331d82a5827000171a0e4022078476bd5a628f08a6536ab6307dcf130e38b2c34b8e0ca3a69ed3531992a1240d82a5827000171a0e40220a9bf76725f3b9dac1f0f280dfd2d815b04be09e7f27b879e87367c22a02cad86d82a5827000171a0e402203e7de8123bf0c91b9764bcdbffb293981297f25397abe8f9694ce2ef6fa06a95d82a5827000171a0e40220c186e16f31aa616b98d2f799f8027e9001de1551ef08bd47ce48edce4cec926ed82a5827000171a0e40220dd40b8d91b7c463bcfed465528bcb86a9f4f9dde7aa904379633d201fecdcaedd82a5827000171a0e4022002629f8bea99f0dd0bcf8081909eac3ea137a61209e820212c503c33d3fdef36d82a5827000171a0e402200ffa650a77b5f44e9ffa1edb67e5cb64cca2ffbd003fe82275fdf84779e443b6d82a5827000171a0e40220ce1e6a1f988f1ae4bf6962ee6d51f0ad299d20a9c13c35da41e5beaa73cbac4dd82a5827000171a0e40220247bf9087726188f29344d8c7ec84fa1934b1318ebbb9e6e1d2d750644b93303d82a5827000171a0e40220175a1c8d0a57b7798a3c9abe13eade64b2091e97667271bfb25f72a06fed7911d82a5827000171a0e402207c0db461a59f25856db10feb6fc4a8a359830cc904fb8e4cf8d50bda69e92937d82a5827000171a0e40220e02b101c40cb97c3a81ff0139920235799602652bd06e40946b99b152ad70f37d82a5827000171a0e4022045336bab6b14bac7e6b6fb07629d42af288b9ce6df3196e60ed67098a992dc92d82a5827000171a0e40220f8126e9ddcdda8c3d3bcb07eef9a65a32b60c674c67741dccdabc66f6bd716d5d82a5827000171a0e402203b1a0770ef5fffca9d83cfcb417d6ea965e83639359c779725448ee4169fcc2fd82a5827000171a0e40220a03f6d0b06213f6654481a800a191b481019e6b86e5911ffee41066af4122c8dd82a5827000171a0e4022045eb749c29bb3cee4323237e7e9613daafe5b7c55ca748d83dbe1b5bc38f384fd82a5827000171a0e402204dd6dc7f37486315df974038ade1b192639100554934336515b8c35e547702cdd82a5827000171a0e402204a7a7c9081d0e2e1752b26e1bc55cb7ef5ea6bd0800822e7eb1c153d78e25604d82a5827000171a0e40220ffdc540f1a4bfe02ac302dbefa275fe3484d5a97f4773131b4a1ab74aacef5b6d82a5827000171a0e402201afbf2c79e629501ee3dfb4b33d15fc556de78d9354cd75c7f343495a74ad4dcd82a5827000171a0e402200c327e440fe61eb376edb31586642e4e596066efe0d51ead0b0f2798b70c36bdd82a5827000171a0e402203ea2d30580805248e995b3848241fed59e827a58fa01687f65565da9c00f8280d82a5827000171a0e4022020ad5875e31f771ae17b516480e1330df6cc134fdc0396179263f6e7bc8cc816d82a5827000171a0e40220cf89a6e7165acebae47938c3e315c6fe0d08e079ede4fade51167b7cfd3a1b43d82a5827000171a0e40220063eb19974ee90011751249d8817c633eb0cb943e9db9e5a63eb0e3035adf742d82a5827000171a0e40220057d88a8b5511a4b83b9b3d40f68ca9b39c6cc46b1a765c419efc9c7f82e8381d82a5827000171a0e402209b90f4b5423815d08d629a6e1f10dc072047d6017a89d0e523565f95b430b55ad82a5827000171a0e402201d938dbfa6e4c77b5cc078b26bf52c79089e163d8bd463a28170dd1aa86c8536d82a5827000171a0e40220e622d224ef73c7385df86317d722e83119ef1163535177ee10d8090b73f140c0d82a5827000171a0e40220d977c5f9adab970d188f392d852ef3139f81624ec9129c085e90ce4d0221d4f6d82a5827000171a0e402203510ef91a3387872a3c59ef1185d5965f77bcc50ff9335a762a7b28fb2b9bb88d82a5827000171a0e40220e2c53781c0acfbc4033a5e023fa224b4b4e47ebc21c9cb1658e712c989726df6d82a5827000171a0e402203172b4e627a0ab7f4d98b02937dbbc370505bb7da0ebefa38e5697df2e31da3cd82a5827000171a0e40220c7f1f6ac290b031e6025e0c1661fcc3465c1288108c108bb6755ffb66d7f3a5ad82a5827000171a0e40220520fabfc587dd893ea8afbae00ff799f1732a802a94c845cf3c602433c142389d82a5827000171a0e4022010661ca3daa47f188b7e42898c0f439f4cfc7ea2fe4ac34721f164c0d5908692d82a5827000171a0e4022015392491de5df19bf452525081036a9db146aef1109c8e8235611e2f1911ecb7d82a5827000171a0e402205bb6adced85c03cd50dd4f21fa4f252332a5b2f4de39d15116abaa2a7f21d839d82a5827000171a0e4022019763271ee53833501a1dcb4afad54b0f7813169ca761278ba064f67afca6b74d82a5827000171a0e40220222d2b49a2e6791472f11c7ff2c9a1695afdbf6d2f643e423d0634d919f34966d82a5827000171a0e4022059117eb33dbd4e901274da4cd4275c64d055d407d95d7d033dbe83e54880037ed82a5827000171a0e4022020f123f202fa1f15fdc373e13184bf02369f17c6746968e723f81a8bafe44311d82a5827000171a0e402202b73cc8038d592f654567b4605671d54af4d6b515a486ec4d6ae4d9235b83deed82a5827000171a0e402203f070ac146c505313a2d6320b72abcaa5d79d4ff6fc578cae5cf09c1845dc555d82a5827000171a0e40220179b960a0f53dc32b2c2e770565aeba85e1bf9f46eb41bff4711eaa97eb6644bd82a5827000171a0e402208aaf294e2e872cc23dd8de423fc7f2f4643d9d4a96b526d1800c1def3ae47b92d82a5827000171a0e40220e544c9baf800b949f3b5d2a9565abc3411d8ca8513c167f8abd703463c2ab2ecd82a5827000171a0e40220ca21191c339d1ec58726eef1ec5a6c996ad30a10cc0848b22ffe320bfdc6bc84d82a5827000171a0e402202384e5ef03bad0aae32b4b55b0b53e00088e0d1af26dbced2ecaf2e97097343dd82a5827000171a0e402202fd6b4d08fc0203a392bb6eef0712d5b31a9e739f3599fe2fc623ceda2e5b359d82a5827000171a0e40220240a17dad97112c03234583dec7d677a78b6058af07c4029c5e05c8ed856d1cad82a5827000171a0e402201173cc1d55480598c5f20bcc9dd6429841fc3266ffe3ecba37a435e7a8f39c04d82a5827000171a0e40220e721c79435d54d58987f1f8eee8151a43a3f2bd45a8113a0e078c3799af3c6e3d82a5827000171a0e40220166b5adcc1ea5fed7c257170e6e95427115d7910af6a3d99fe1210722d706141d82a5827000171a0e40220f1493982c32b699c8886481490c1d283ba6689853ceee517d5cb4892f12a643ed82a5827000171a0e40220e4aa26b5a972b7814ac41b874dae1d38aa5ff6f9d664cf7eec7c57e01309512dd82a5827000171a0e4022054e14be1dd95585e58d3d5a055d17898c6664e526d3e25e605eb13d917cd0e6bd82a5827000171a0e40220429a25649370da425373c675f5ce6c9eed33da0909487ed2e12a55597c181a8dd82a5827000171a0e4022019295932c3535e9da00ee3795afbb65065e2559c55949d6a6bcfca3feb6bd046d82a5827000171a0e402205ff74cebdaf60a646efd1d6e2da93ac8d769b71fab119f15e1f89d679eb52574d82a5827000171a0e40220cd41fdc7d1a78eac551b4dd7422b8f0c0409679966c5c979627f45ccae2d2eb0d82a5827000171a0e40220d6494e90537d11f5cf6883697f26094919b0428490b97b2da5ce6e5523a08ecbd82a5827000171a0e402206439a443543312141a8f443cf26d129678c8ee347ed3f3672e495c1e23d2e2f3d82a5827000171a0e40220097b0d511823e9c0d72ad8df2a16e177494fab54504e2589978b9476d031f0e0d82a5827000171a0e40220d4bbb14797d5e6baf9e7704a9e6e74429b86015bf2cda097e5f156477ac724cdd82a5827000171a0e40220a97add1abe4f40efb48138e9143cf36766144526db4d4caf03aad9ed7a7c21ebd82a5827000171a0e402203b79fadb225bb666a356bd18c9d5340e6f489e5e7e852980406958b434a3049dd82a5827000171a0e402200b455567f4f860c5c562c8ece02dd17e5baf80de742bec2242d5862c9ec4e524d82a5827000171a0e4022043be433fe2036af5d7024663399da5443a631a218d9d40a2e2d5d53d8b595c40d82a5827000171a0e40220c927b663f22e74a6e6062cf45d74dd76e5429bf877d1544c4495e3b9cca8061dd82a5827000171a0e40220c77f58d59b3476d9124dadb441999a215a368f1a8fa70a50bb0f99d5dda36771d82a5827000171a0e402208470ef429ccc6af99f546a240120d54d54d4e955f67b1c1ac042f4bb25877783d82a5827000171a0e40220e31086fb92028ad021507706873288abfbc9dee3b1b2c17993be2227a7768d22d82a5827000171a0e40220b9b315011f0ec66a5d1df5d035dc34cc77506cddef6d79c61f93712d421730a8d82a5827000171a0e40220b785d2815753e61a4a8592b841db727709e66da6efd954c28b39d1288fe7a567d82a5827000171a0e402209eb45c9d9e61cca80d2ea6b7f61e5b6f8ecfa4b9b901740a14e764e89cb1dc7cd82a5827000171a0e40220daf0ade00b9d035e2c51b4cbceaccfc0417aa921010aa61f9d223486c58fa802d82a5827000171a0e4022029942f956f088c45c0eb6036acba40136df1d602300a717ef7d949f90c00408ed82a5827000171a0e402201c276eb7239db4b33b032ed594045c6c147c850871d5db481faf5c6b14100b23d82a5827000171a0e402205dd1ed68ea872a335e8285af89ef0a8f98f3e55d8d98e439c9d3a5bba879f3bad82a5827000171a0e40220382620ddb4bd9eb5fb7c5b1c04543cc6f7d6451679bdac01fc1cbbeb0d07102cd82a5827000171a0e402202f9404cb9982f90cb78c6ee44f22b657469493b98aaaa2d08d0da4a71217f82ad82a5827000171a0e40220a33f59f0e14425b6fa49fd184d7f33b8bc21502afca0cbbb20d88b99dcd812c1d82a5827000171a0e40220088f5086c11028b5146721af56da3337c21d4918f7373ca6b884e019f4693b66d82a5827000171a0e4022048d7e5e8133a9d65cfc7313c095e77bddb02fe215c85d3c6ead7cfd708f03584d82a5827000171a0e40220801c46dd6502d297d9fbd6f30e704592c3ce8eb8d6c0ba0195cbe95543027579d82a5827000171a0e402202e82fbfb1f1625caf7d8fac046dbb7f74a9506cf237aed5189284081e3d35da8d82a5827000171a0e40220b1821f353bf9c3a1b5ec3ad9c3771b62eae5f2999c5e9a474e0e5404004970ffd82a5827000171a0e402204ec2a0910578597ec386bb05aba30fc38641a457a2a0567a0079438283e4e681d82a5827000171a0e40220a7649290d8560f6c2ab7043948a338607d110d0bef6c906cadd18a18a8263f41d82a5827000171a0e402208495e6ef8ab73a4025f1ce1beb3167efde26baa5c45278e312e9c3992d73ceccd82a5827000171a0e40220eeed08c2717157639d3031cc4d6658b13dc719a42e2c8d9b3fceeb05fe1a5690d82a5827000171a0e40220fd263118a02d908d09d8e9bbaf017444922d809ce61d6b2c58f6ef124ad02fbad82a5827000171a0e40220b57c7d100429efd4869b6bfb2712a31ab411ae0e6b12515253478a0d596ae345d82a5827000171a0e4022044eac43358de7f1381a03d7054b1b4d8f231976993d4132567d44baacd4b49a5d82a5827000171a0e40220025dd365d9f3a2387db63dd9b24abd0af5ec8ff8dfe39b707574514e7e6dac8ad82a5827000171a0e40220812be249dabd519c4c799cfb81a1c38838c86420531f3e839f29d5fae915bf98d82a5827000171a0e40220cb76bdadf088ae35e1deef9e191585586fb75d156f7ee69421b1a89da64a185ad82a5827000171a0e40220e1677a3f6332a6c0561f544d6c592ba67114d544e3c1c2a4360078ffbb475fbcd82a5827000171a0e402200b20f496353cbc8c8aabf2f9c0ca1d44fcd290be2cdd65ee88bfbcf7bdced4b5d82a5827000171a0e40220f1d12a9ccba2da83abe1ad2d0b0157703286dba393fdfa4578d09adb2ff06450d82a5827000171a0e40220cfdfd919a9465f62d0ee3e8487f6eaed34aa094d74623ea2e012b3477f0c5ea7d82a5827000171a0e40220a9bf9d218adac8d14107c15c07352867ef36fd3b9e36a0f3ce619d465a794787d82a5827000171a0e4022013753d3c0a2d2ff3fa2ef603e624f0002aba993cbf7c436cf32fbe8bff8c4954d82a5827000171a0e40220901ee825ff09aed9596c11d03995fdb969ed61ec60b5cc4e4df674274d31fdebd82a5827000171a0e40220052b0b77fd890c5f25666540d3a081180eb96b75d2d8ee4b9de2003dec1e285bd82a5827000171a0e40220a0df40489bf6566579f12d80af580f8cefbe0114666acd4317ff447d129a83a8d82a5827000171a0e4022047751ddb02925cf224177b6540d0e2f2afcdac3a91ff1c435fe15acd8b7cc99dd82a5827000171a0e40220f6aeb0c3a4ea2edb8ab6017f678b7c217a950991f9d0aab28765d1b6974b8ff7d82a5827000171a0e402203fefc4f202ed3488c34e514e236d8604d2fb2d1605a08099406057bec2777690d82a5827000171a0e40220165053103f3c76222f983733b40e29ef5e3a84d035c3c80c4b752ee28005f5f5d82a5827000171a0e402206ad96a3b24a88b46488d5d926aa24db235205ba390b1821fad52f56e8beeb3bed82a5827000171a0e40220ca6f5d2dd48c5755da0a09f0876e11c083ec0842ae28df435323e8108337c320d82a5827000171a0e402207ce2e5ac4e1332f824d3474b5b595521b92c27458e35afc2aab377902d1bf7e6d82a5827000171a0e40220d350d67cab7e20f77eae9bbc9e1c7ca1b670e799822f8e586dd68600844e04f8d82a5827000171a0e40220f221e69d09c571268193418f14a1cb9e6717c8af2376e832533bb761c7d33b24d82a5827000171a0e402202aa755d8a87e1a2425fb33785afd0b8658049778b601a71e825e82414365be30d82a5827000171a0e402203a434edbd8ae6875509f92b6c52f8d21c6e3dc81c80c4d8a79a9d38ae67e3901d82a5827000171a0e402201d13fa3f2c0e260f45f09744a406fb692d298d5e07e3d2e53341979cdfedb588d82a5827000171a0e402209947b53e041a6125b94f831d64e37c2dfe270d6cdf749ebe08812a8747f20e3fd82a5827000171a0e40220247cb67e6abb19c7e6b564d69a4d3e6d11097815cd6f6660264fba94b26e4489d82a5827000171a0e402200d2fae35a6c1aa5f5c020c474b795dff3f623a5312a0f977fcc17751c195e089d82a5827000171a0e402205244c114c65eec420442c77d3cd6028d627dae314ab01416279b30b2cc0dce84d82a5827000171a0e402205c542a1eb3698ed3cae5d6be86a5b1df0d0e836d207b7b8769663ea8b63614c5d82a5827000171a0e40220de65bb0c04259102f8bea2af8a82ec505313f6b50abd1f0ce491a54a4e09e997d82a5827000171a0e40220786944a128b2b3761a5c4d62677580d58f2abdc842608aed9e0bff36c1301483d82a5827000171a0e4022030a68cb0bd7d1fc60a6a623720960251980bab5c04b74238d79871d7f257a2a2d82a5827000171a0e402209e14a15f8dd20fd034dae429bad57215b51c89af70eec8ad5c00f726ae6c2d37d82a5827000171a0e40220589188d477a8363cf5ff7ba5bb118ba4f4bd1a1c84630ab6da0b0fbe9623b39d550782581f5042080008403108110127840202000002000880cc0418480001010089090098298182190a4c64323633368182187a6331323281821903966339313881821909b464323438348182190fe464343036388182191e3364373733318282190a69643236363582190c15643330393381821908fd643233303181821915c16435353639818219136e64343937348182191619643536353781821922c36438383939818219192364363433358182190d29643333363981821906d364313734378182190809643230353781821914136435313339818219138864353030308182190acb64323736338182191002643430393881821915ac64353534388182191b1d6436393431818219258064393630308182190e2b64333632378182191bc06437313034818219197764363531398182191fec6438313732818219125f64343730338182192523643935303781821919fa6436363530818219086c643231353681821909c46432353030818219026563363133818219257064393538348282191f3d6437393937821921aa6438363138818219222564383734318182191521643534303981821908a264323231308282191dce64373633308219267e643938353481821906c6643137333481821921516438353239" +} diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/ui/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/src/reportWebVitals.ts b/ui/src/reportWebVitals.ts new file mode 100644 index 0000000..8c8400b --- /dev/null +++ b/ui/src/reportWebVitals.ts @@ -0,0 +1,15 @@ +import { ReportHandler } from "web-vitals" + +const reportWebVitals = (onPerfEntry?: ReportHandler) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry) + getFID(onPerfEntry) + getFCP(onPerfEntry) + getLCP(onPerfEntry) + getTTFB(onPerfEntry) + }) + } +} + +export default reportWebVitals diff --git a/ui/src/setupTests.ts b/ui/src/setupTests.ts new file mode 100644 index 0000000..6a0fd12 --- /dev/null +++ b/ui/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import "@testing-library/jest-dom" diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..9d379a3 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +}